Preliminary work for supporting keyboard-shortcut diff navigation
Adds the gr-cursor-manager element, which is a generic cursor manager for arbitrary sets of DOM elements. It's inspired by the Chromium Reitveld implementation of cr-cursor-manager. Bug: Issue 4033 Change-Id: I5b3eb8ad39ab9db3c273b14070f888a48b5de6d4
This commit is contained in:
parent
1766703b05
commit
972c3de417
@ -0,0 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||||
|
|
||||||
|
<dom-module id="gr-cursor-manager">
|
||||||
|
<template></template>
|
||||||
|
<script src="gr-cursor-manager.js"></script>
|
||||||
|
</dom-module>
|
@ -0,0 +1,198 @@
|
|||||||
|
// Copyright (C) 2016 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
Polymer({
|
||||||
|
is: 'gr-cursor-manager',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
stops: {
|
||||||
|
type: Array,
|
||||||
|
value: function() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
observer: '_updateIndex',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: Object,
|
||||||
|
notify: true,
|
||||||
|
observer: '_scrollToTarget',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the current target (if any). -1 otherwise.
|
||||||
|
*/
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class to apply to the current target. Use null for no class.
|
||||||
|
*/
|
||||||
|
cursorTargetClass: {
|
||||||
|
type: String,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
scroll: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
detached: function() {
|
||||||
|
this.unsetCursor();
|
||||||
|
},
|
||||||
|
|
||||||
|
next: function(opt_condition) {
|
||||||
|
this._moveCursor(1, opt_condition);
|
||||||
|
},
|
||||||
|
|
||||||
|
previous: function(opt_condition) {
|
||||||
|
this._moveCursor(-1, opt_condition);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cursor to an arbitrary element.
|
||||||
|
* @param {DOMElement}
|
||||||
|
*/
|
||||||
|
setCursor: function(element) {
|
||||||
|
this.unsetCursor();
|
||||||
|
this.target = element;
|
||||||
|
this._updateIndex();
|
||||||
|
this._decorateTarget();
|
||||||
|
},
|
||||||
|
|
||||||
|
unsetCursor: function() {
|
||||||
|
this._unDecorateTarget();
|
||||||
|
this.index = -1;
|
||||||
|
this.target = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
isAtStart: function() {
|
||||||
|
return this.index === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
isAtEnd: function() {
|
||||||
|
return this.index === this.stops.length - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
moveToStart: function() {
|
||||||
|
if (this.stops.length) {
|
||||||
|
this.setCursor(this.stops[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor forward or backward by delta. Noop if moving past either
|
||||||
|
* end of the stop list.
|
||||||
|
* @param {Number} delta: either -1 or 1.
|
||||||
|
* @param {Function} opt_condition Optional stop condition. If a condition
|
||||||
|
* is passed the cursor will continue to move in the specified direction
|
||||||
|
* until the condition is met.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_moveCursor: function(delta, opt_condition) {
|
||||||
|
if (!this.stops.length) {
|
||||||
|
this.unsetCursor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._unDecorateTarget();
|
||||||
|
|
||||||
|
var newIndex = this._getNextindex(delta, opt_condition);
|
||||||
|
|
||||||
|
var newTarget = null;
|
||||||
|
if (newIndex != -1) {
|
||||||
|
newTarget = this.stops[newIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.index = newIndex;
|
||||||
|
this.target = newTarget;
|
||||||
|
|
||||||
|
this._decorateTarget();
|
||||||
|
},
|
||||||
|
|
||||||
|
_decorateTarget: function() {
|
||||||
|
if (this.target && this.cursorTargetClass) {
|
||||||
|
this.target.classList.add(this.cursorTargetClass);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_unDecorateTarget: function() {
|
||||||
|
if (this.target && this.cursorTargetClass) {
|
||||||
|
this.target.classList.remove(this.cursorTargetClass);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next stop index indicated by the delta direction.
|
||||||
|
* @param {Number} delta: either -1 or 1.
|
||||||
|
* @param {Function} opt_condition Optional stop condition.
|
||||||
|
* @return {Number} the new index.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getNextindex: function(delta, opt_condition) {
|
||||||
|
if (!this.stops.length || this.index === -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = this.index;
|
||||||
|
do {
|
||||||
|
newIndex = newIndex + delta;
|
||||||
|
} while(newIndex !== 0 &&
|
||||||
|
newIndex !== this.stops.length - 1 &&
|
||||||
|
opt_condition &&
|
||||||
|
!opt_condition(this.stops[newIndex]));
|
||||||
|
|
||||||
|
newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
|
||||||
|
|
||||||
|
return newIndex;
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateIndex: function() {
|
||||||
|
if (!this.target) {
|
||||||
|
this.index = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = Array.prototype.indexOf.call(this.stops, this.target);
|
||||||
|
if (newIndex === -1) {
|
||||||
|
this.unsetCursor();
|
||||||
|
} else {
|
||||||
|
this.index = newIndex;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_scrollToTarget: function() {
|
||||||
|
if (!this.target || !this.scroll) { return; }
|
||||||
|
|
||||||
|
// Calculate where the element is relative to the window.
|
||||||
|
var top = this.target.offsetTop;
|
||||||
|
for (var offsetParent = this.target.offsetParent;
|
||||||
|
offsetParent;
|
||||||
|
offsetParent = offsetParent.offsetParent) {
|
||||||
|
top += offsetParent.offsetTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll the element to the middle of the window. Dividing by a third
|
||||||
|
// instead of half the inner height feels a bit better otherwise the
|
||||||
|
// element appears to be below the center of the window even when it
|
||||||
|
// isn't.
|
||||||
|
window.scrollTo(0, top - (window.innerHeight / 3) +
|
||||||
|
(this.target.offsetHeight / 2));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
@ -0,0 +1,124 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||||
|
<title>gr-cursor-manager</title>
|
||||||
|
|
||||||
|
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
|
||||||
|
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||||
|
|
||||||
|
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
|
||||||
|
<link rel="import" href="gr-cursor-manager.html">
|
||||||
|
|
||||||
|
<test-fixture id="basic">
|
||||||
|
<template>
|
||||||
|
<gr-cursor-manager
|
||||||
|
cursor-stop-selector="li"
|
||||||
|
cursor-target-class="targeted"></gr-cursor-manager>
|
||||||
|
<ul>
|
||||||
|
<li>A</li>
|
||||||
|
<li>B</li>
|
||||||
|
<li>C</li>
|
||||||
|
<li>D</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</test-fixture>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
suite('gr-cursor-manager tests', function() {
|
||||||
|
var element;
|
||||||
|
var list;
|
||||||
|
|
||||||
|
setup(function() {
|
||||||
|
var fixtureElements = fixture('basic');
|
||||||
|
element = fixtureElements[0];
|
||||||
|
list = fixtureElements[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('core cursor functionality', function() {
|
||||||
|
// The element is initialized into the proper state.
|
||||||
|
assert.isArray(element.stops);
|
||||||
|
assert.equal(element.stops.length, 0);
|
||||||
|
assert.equal(element.index, -1);
|
||||||
|
assert.isNotOk(element.target);
|
||||||
|
|
||||||
|
// Initialize the cursor with its stops.
|
||||||
|
element.stops = list.querySelectorAll('li');
|
||||||
|
|
||||||
|
// It should have the stops but it should not be targeting any of them.
|
||||||
|
assert.isNotNull(element.stops);
|
||||||
|
assert.equal(element.stops.length, 4);
|
||||||
|
assert.equal(element.index, -1);
|
||||||
|
assert.isNotOk(element.target);
|
||||||
|
|
||||||
|
// Select the third stop.
|
||||||
|
element.setCursor(list.children[2]);
|
||||||
|
|
||||||
|
// It should update its internal state and update the element's class.
|
||||||
|
assert.equal(element.index, 2);
|
||||||
|
assert.equal(element.target, list.children[2]);
|
||||||
|
assert.isTrue(list.children[2].classList.contains('targeted'));
|
||||||
|
assert.isFalse(element.isAtStart());
|
||||||
|
assert.isFalse(element.isAtEnd());
|
||||||
|
|
||||||
|
// Progress the cursor.
|
||||||
|
element.next();
|
||||||
|
|
||||||
|
// Confirm that the next stop is selected and that the previous stop is
|
||||||
|
// unselected.
|
||||||
|
assert.equal(element.index, 3);
|
||||||
|
assert.equal(element.target, list.children[3]);
|
||||||
|
assert.isTrue(element.isAtEnd());
|
||||||
|
assert.isFalse(list.children[2].classList.contains('targeted'));
|
||||||
|
assert.isTrue(list.children[3].classList.contains('targeted'));
|
||||||
|
|
||||||
|
// Progress the cursor.
|
||||||
|
element.next();
|
||||||
|
|
||||||
|
// We should still be at the end.
|
||||||
|
assert.equal(element.index, 3);
|
||||||
|
assert.equal(element.target, list.children[3]);
|
||||||
|
assert.isTrue(element.isAtEnd());
|
||||||
|
|
||||||
|
// Wind the cursor all the way back to the first stop.
|
||||||
|
element.previous();
|
||||||
|
element.previous();
|
||||||
|
element.previous();
|
||||||
|
|
||||||
|
// The element state should reflect the end of the list.
|
||||||
|
assert.equal(element.index, 0);
|
||||||
|
assert.equal(element.target, list.children[0]);
|
||||||
|
assert.isTrue(element.isAtStart());
|
||||||
|
assert.isTrue(list.children[0].classList.contains('targeted'));
|
||||||
|
|
||||||
|
var newLi = document.createElement('li');
|
||||||
|
newLi.textContent = 'Z';
|
||||||
|
list.insertBefore(newLi, list.children[0]);
|
||||||
|
element.stops = list.querySelectorAll('li');
|
||||||
|
|
||||||
|
assert.equal(element.index, 1);
|
||||||
|
|
||||||
|
// De-select all targets.
|
||||||
|
element.unsetCursor();
|
||||||
|
|
||||||
|
// There should now be no cursor target.
|
||||||
|
assert.isFalse(list.children[1].classList.contains('targeted'));
|
||||||
|
assert.isNotOk(element.target);
|
||||||
|
assert.equal(element.index, -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
@ -56,6 +56,7 @@ limitations under the License.
|
|||||||
'../elements/shared/gr-avatar/gr-avatar_test.html',
|
'../elements/shared/gr-avatar/gr-avatar_test.html',
|
||||||
'../elements/shared/gr-change-star/gr-change-star_test.html',
|
'../elements/shared/gr-change-star/gr-change-star_test.html',
|
||||||
'../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
|
'../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
|
||||||
|
'../elements/shared/gr-cursor-manager/gr-cursor-manager_test.html',
|
||||||
'../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
|
'../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
|
||||||
'../elements/shared/gr-js-api-interface/gr-js-api-interface_test.html',
|
'../elements/shared/gr-js-api-interface/gr-js-api-interface_test.html',
|
||||||
'../elements/shared/gr-linked-text/gr-linked-text_test.html',
|
'../elements/shared/gr-linked-text/gr-linked-text_test.html',
|
||||||
|
Loading…
Reference in New Issue
Block a user