From 972c3de417d835a5d3b1766c11b040d7a4818f57 Mon Sep 17 00:00:00 2001 From: Wyatt Allen Date: Mon, 16 May 2016 11:40:44 -0700 Subject: [PATCH] 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 --- .../gr-cursor-manager/gr-cursor-manager.html | 22 ++ .../gr-cursor-manager/gr-cursor-manager.js | 198 ++++++++++++++++++ .../gr-cursor-manager_test.html | 124 +++++++++++ polygerrit-ui/app/test/index.html | 1 + 4 files changed, 345 insertions(+) create mode 100644 polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html create mode 100644 polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js create mode 100644 polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html new file mode 100644 index 0000000000..f213312e2d --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js new file mode 100644 index 0000000000..fe1605c386 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js @@ -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)); + }, + }); +})(); diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html new file mode 100644 index 0000000000..1ad014d0c2 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html @@ -0,0 +1,124 @@ + + + + +gr-cursor-manager + + + + + + + + + + + + diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index efdbaef910..da2072b5c2 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -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',