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-change-star/gr-change-star_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-js-api-interface/gr-js-api-interface_test.html',
|
||||
'../elements/shared/gr-linked-text/gr-linked-text_test.html',
|
||||
|
Loading…
Reference in New Issue
Block a user