Merge "Preliminary work for supporting keyboard-shortcut diff navigation"

This commit is contained in:
Andrew Bonventre 2016-05-16 23:16:38 +00:00 committed by Gerrit Code Review
commit 3f7c75fd6e
4 changed files with 345 additions and 0 deletions

View File

@ -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>

View File

@ -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));
},
});
})();

View File

@ -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>

View File

@ -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',