[Rt-commit] rt branch, master, updated. rt-4.2.11-231-g9e8d7e0
Shawn Moore
shawn at bestpractical.com
Tue Oct 6 15:09:07 EDT 2015
The branch, master has been updated
via 9e8d7e0d9fd4343079a7a00e2238c5ded8592d4f (commit)
via ffdc046654227db22f0863c6815fc8b8a0b4eb44 (commit)
from 3cfb7d8f48d1c019e7eb0d740255045b23195a45 (commit)
Summary of changes:
devel/third-party/mousetrap.js | 1021 ++++++++++++++++++++++++++
lib/RT/Interface/Web.pm | 2 +
share/html/Elements/CollectionAsTable/Row | 3 +
share/html/Elements/JavascriptConfig | 1 +
share/html/Helpers/ShortcutHelp | 108 +++
share/html/Search/Bulk.html | 5 +
share/html/Search/Results.html | 7 +
share/static/css/base/keyboard-shortcuts.css | 29 +
share/static/css/base/main.css | 1 +
share/static/js/keyboard-shortcuts.js | 143 ++++
share/static/js/mousetrap.min.js | 11 +
share/static/js/util.js | 17 +
12 files changed, 1348 insertions(+)
create mode 100644 devel/third-party/mousetrap.js
create mode 100644 share/html/Helpers/ShortcutHelp
create mode 100644 share/static/css/base/keyboard-shortcuts.css
create mode 100644 share/static/js/keyboard-shortcuts.js
create mode 100644 share/static/js/mousetrap.min.js
- Log -----------------------------------------------------------------
commit ffdc046654227db22f0863c6815fc8b8a0b4eb44
Author: Dustin Graves <dustin at bestpractical.com>
Date: Thu Sep 17 14:04:17 2015 +0000
add global / page-specific keyboard shortcuts
global shortcuts:
gh - click on home link
gb - browser back
gf - browser forward
/ - highlight quick search
search results shortcuts:
k/j - up/down through search results
o or <Enter> - view highlighted ticket
x - toggle highlighted ticket's checkbox (on bulk update)
t - reply to ticket
c - comment on ticket
Fixes: T#151846
diff --git a/devel/third-party/mousetrap.js b/devel/third-party/mousetrap.js
new file mode 100644
index 0000000..48d2f62
--- /dev/null
+++ b/devel/third-party/mousetrap.js
@@ -0,0 +1,1021 @@
+/*global define:false */
+/**
+ * Copyright 2015 Craig Campbell
+ *
+ * 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.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.5.3
+ * @url craig.is/killing/mice
+ */
+(function(window, document, undefined) {
+
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ };
+
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ var _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ };
+
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ var _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ };
+
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ var _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc',
+ 'plus': '+',
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
+ };
+
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ var _REVERSE_MAP;
+
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
+ }
+
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
+
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ object.addEventListener(type, callback, false);
+ return;
+ }
+
+ object.attachEvent('on' + type, callback);
+ }
+
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
+
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ var character = String.fromCharCode(e.which);
+
+ // if the shift key is not pressed then it is safe to assume
+ // that we want the character to be lowercase. this means if
+ // you accidentally have caps lock on then your key bindings
+ // will continue to work
+ //
+ // the only side effect that might not be desired is if you
+ // bind something like 'A' cause you want to trigger an
+ // event when capital A is pressed caps lock will no longer
+ // trigger the event. shift+a will though.
+ if (!e.shiftKey) {
+ character = character.toLowerCase();
+ }
+
+ return character;
+ }
+
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
+
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
+
+ // if it is not in the special map
+
+ // with keydown and keyup events the character seems to always
+ // come in as an uppercase character whether you are pressing shift
+ // or not. we should make sure it is always lowercase for comparisons
+ return String.fromCharCode(e.which).toLowerCase();
+ }
+
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ }
+
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
+
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
+
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
+
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
+
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
+
+ return modifiers;
+ }
+
+ /**
+ * prevents default for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _preventDefault(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ return;
+ }
+
+ e.returnValue = false;
+ }
+
+ /**
+ * stops propogation for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _stopPropagation(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ return;
+ }
+
+ e.cancelBubble = true;
+ }
+
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ }
+
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
+
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
+
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
+
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ }
+
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
+ }
+
+ return action;
+ }
+
+ /**
+ * Converts from a string key combination to an array
+ *
+ * @param {string} combination like "command+shift+l"
+ * @return {Array}
+ */
+ function _keysFromString(combination) {
+ if (combination === '+') {
+ return ['+'];
+ }
+
+ combination = combination.replace(/\+{2}/g, '+plus');
+ return combination.split('+');
+ }
+
+ /**
+ * Gets info for a specific key combination
+ *
+ * @param {string} combination key combination ("command+s" or "a" or "*")
+ * @param {string=} action
+ * @returns {Object}
+ */
+ function _getKeyInfo(combination, action) {
+ var keys;
+ var key;
+ var i;
+ var modifiers = [];
+
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = _keysFromString(combination);
+
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
+
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
+
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
+
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
+
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
+
+ return {
+ key: key,
+ modifiers: modifiers,
+ action: action
+ };
+ }
+
+ function _belongsTo(element, ancestor) {
+ if (element === null || element === document) {
+ return false;
+ }
+
+ if (element === ancestor) {
+ return true;
+ }
+
+ return _belongsTo(element.parentNode, ancestor);
+ }
+
+ function Mousetrap(targetElement) {
+ var self = this;
+
+ targetElement = targetElement || document;
+
+ if (!(self instanceof Mousetrap)) {
+ return new Mousetrap(targetElement);
+ }
+
+ /**
+ * element to attach key events to
+ *
+ * @type {Element}
+ */
+ self.target = targetElement;
+
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ self._callbacks = {};
+
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ self._directMap = {};
+
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ var _sequenceLevels = {};
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ var _resetTimer;
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ var _ignoreNextKeyup = false;
+
+ /**
+ * temporary state where we will ignore the next keypress
+ *
+ * @type {boolean}
+ */
+ var _ignoreNextKeypress = false;
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ var _nextExpectedAction = false;
+
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} doNotReset
+ * @returns void
+ */
+ function _resetSequences(doNotReset) {
+ doNotReset = doNotReset || {};
+
+ var activeSequences = false,
+ key;
+
+ for (key in _sequenceLevels) {
+ if (doNotReset[key]) {
+ activeSequences = true;
+ continue;
+ }
+ _sequenceLevels[key] = 0;
+ }
+
+ if (!activeSequences) {
+ _nextExpectedAction = false;
+ }
+ }
+
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event|Object} e
+ * @param {string=} sequenceName - name of the sequence we are looking for
+ * @param {string=} combination
+ * @param {number=} level
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
+ var i;
+ var callback;
+ var matches = [];
+ var action = e.type;
+
+ // if there are no events related to this keycode
+ if (!self._callbacks[character]) {
+ return [];
+ }
+
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
+
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < self._callbacks[character].length; ++i) {
+ callback = self._callbacks[character][i];
+
+ // if a sequence name is not specified, but this is a sequence at
+ // the wrong level then move onto the next match
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
+ continue;
+ }
+
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
+
+ // if this is a keypress event and the meta key and control key
+ // are not pressed that means that we need to only look at the
+ // character, otherwise check the modifiers as well
+ //
+ // chrome will not fire a keypress if meta or control is down
+ // safari will fire a keypress if meta or meta+shift is down
+ // firefox will fire a keypress if meta or control is down
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
+
+ // when you bind a combination or sequence a second time it
+ // should overwrite the first one. if a sequenceName or
+ // combination is specified in this call it does just that
+ //
+ // @todo make deleting its own method?
+ var deleteCombo = !sequenceName && callback.combo == combination;
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
+ if (deleteCombo || deleteSequence) {
+ self._callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
+
+ return matches;
+ }
+
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e, combo, sequence) {
+
+ // if this event should not happen stop here
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
+ return;
+ }
+
+ if (callback(e, combo) === false) {
+ _preventDefault(e);
+ _stopPropagation(e);
+ }
+ }
+
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event} e
+ * @returns void
+ */
+ self._handleKey = function(character, modifiers, e) {
+ var callbacks = _getMatches(character, modifiers, e);
+ var i;
+ var doNotReset = {};
+ var maxLevel = 0;
+ var processedSequenceCallback = false;
+
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
+ for (i = 0; i < callbacks.length; ++i) {
+ if (callbacks[i].seq) {
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
+ }
+ }
+
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+
+ // only fire callbacks for the maxLevel to prevent
+ // subsequences from also firing
+ //
+ // for example 'a option b' should not cause 'option b' to fire
+ // even though 'option b' is part of the other sequence
+ //
+ // any sequences that do not match here will be discarded
+ // below by the _resetSequences call
+ if (callbacks[i].level != maxLevel) {
+ continue;
+ }
+
+ processedSequenceCallback = true;
+
+ // keep a list of which sequences were matches for later
+ doNotReset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processedSequenceCallback) {
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ }
+ }
+
+ // if the key you pressed matches the type of sequence without
+ // being a modifier (ie "keyup" or "keypress") then we should
+ // reset all sequences that were not matched by this event
+ //
+ // this is so, for example, if you have the sequence "h a t" and you
+ // type "h e a r t" it does not match. in this case the "e" will
+ // cause the sequence to reset
+ //
+ // modifier keys are ignored because you can have a sequence
+ // that contains modifiers such as "enter ctrl+space" and in most
+ // cases the modifier key will be pressed before the next key
+ //
+ // also if you have a sequence such as "ctrl+b a" then pressing the
+ // "b" key will trigger a "keypress" and a "keydown"
+ //
+ // the "keydown" is expected when there is a modifier, but the
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
+ // after and that causes the sequence to reset
+ //
+ // we ignore keypresses in a sequence that directly follow a keydown
+ // for the same character
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
+ _resetSequences(doNotReset);
+ }
+
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
+ };
+
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKeyEvent(e) {
+
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ if (typeof e.which !== 'number') {
+ e.which = e.keyCode;
+ }
+
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
+
+ // need to use === for the character check because the character can be 0
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
+ _ignoreNextKeyup = false;
+ return;
+ }
+
+ self.handleKey(character, _eventModifiers(e), e);
+ }
+
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_resetTimer);
+ _resetTimer = setTimeout(_resetSequences, 1000);
+ }
+
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
+
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequenceLevels[combo] = 0;
+
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {string} nextAction
+ * @returns {Function}
+ */
+ function _increaseSequence(nextAction) {
+ return function() {
+ _nextExpectedAction = nextAction;
+ ++_sequenceLevels[combo];
+ _resetSequenceTimer();
+ };
+ }
+
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _callbackAndReset(e) {
+ _fireCallback(callback, e, combo);
+
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignoreNextKeyup = _characterFromEvent(e);
+ }
+
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ }
+
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ //
+ // if an action is specified in the original bind call then that will
+ // be used throughout. otherwise we will pass the action that the
+ // next key in the sequence should match. this allows a sequence
+ // to mix and match keypress and keydown events depending on which
+ // ones are better suited to the key provided
+ for (var i = 0; i < keys.length; ++i) {
+ var isFinal = i + 1 === keys.length;
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
+ }
+ }
+
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequenceName - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequenceName, level) {
+
+ // store a direct mapped reference for use with Mousetrap.trigger
+ self._directMap[combination + ':' + action] = callback;
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' ');
+ var info;
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ _bindSequence(combination, sequence, callback, action);
+ return;
+ }
+
+ info = _getKeyInfo(combination, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
+
+ // remove an existing match if there is one
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
+
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: info.modifiers,
+ action: info.action,
+ seq: sequenceName,
+ level: level,
+ combo: combination
+ });
+ }
+
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ self._bindMultiple = function(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ };
+
+ // start!
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
+ }
+
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * an array of keys, or a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ Mousetrap.prototype.bind = function(keys, callback, action) {
+ var self = this;
+ keys = keys instanceof Array ? keys : [keys];
+ self._bindMultiple.call(self, keys, callback, action);
+ return self;
+ };
+
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _directMap dict.
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ Mousetrap.prototype.unbind = function(keys, action) {
+ var self = this;
+ return self.bind.call(self, keys, function() {}, action);
+ };
+
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ Mousetrap.prototype.trigger = function(keys, action) {
+ var self = this;
+ if (self._directMap[keys + ':' + action]) {
+ self._directMap[keys + ':' + action]({}, keys);
+ }
+ return self;
+ };
+
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ Mousetrap.prototype.reset = function() {
+ var self = this;
+ self._callbacks = {};
+ self._directMap = {};
+ return self;
+ };
+
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @param {Element} element
+ * @return {boolean}
+ */
+ Mousetrap.prototype.stopCallback = function(e, element) {
+ var self = this;
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ if (_belongsTo(element, self.target)) {
+ return false;
+ }
+
+ // stop for input, select, and textarea
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
+ };
+
+ /**
+ * exposes _handleKey publicly so it can be overwritten by extensions
+ */
+ Mousetrap.prototype.handleKey = function() {
+ var self = this;
+ return self._handleKey.apply(self, arguments);
+ };
+
+ /**
+ * Init the global mousetrap functions
+ *
+ * This method is needed to allow the global mousetrap functions to work
+ * now that mousetrap is a constructor function.
+ */
+ Mousetrap.init = function() {
+ var documentMousetrap = Mousetrap(document);
+ for (var method in documentMousetrap) {
+ if (method.charAt(0) !== '_') {
+ Mousetrap[method] = (function(method) {
+ return function() {
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
+ };
+ } (method));
+ }
+ }
+ };
+
+ Mousetrap.init();
+
+ // expose mousetrap to the global object
+ window.Mousetrap = Mousetrap;
+
+ // expose as a common js module
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Mousetrap;
+ }
+
+ // expose mousetrap as an AMD module
+ if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return Mousetrap;
+ });
+ }
+}) (window, document);
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 8406762..1bd28ce 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -128,6 +128,8 @@ sub JSFiles {
forms.js
event-registration.js
late.js
+ mousetrap.min.js
+ keyboard-shortcuts.js
/static/RichText/ckeditor.js
dropzone.min.js
}, RT->Config->Get('JSFiles');
diff --git a/share/html/Elements/CollectionAsTable/Row b/share/html/Elements/CollectionAsTable/Row
index deaa312..142b1c6 100644
--- a/share/html/Elements/CollectionAsTable/Row
+++ b/share/html/Elements/CollectionAsTable/Row
@@ -57,6 +57,8 @@ $Class => 'RT__Ticket'
$Classes => ''
</%ARGS>
<%init>
+$m->out( '<tbody class="list-item"' . ( $record->can('id') ? ' data-record-id="'.$record->id.'"' : '' ) . '>' );
+
$m->out( '<tr class="' . $Classes . ' '
. ( $Warning ? 'warnline' : $i % 2 ? 'oddline' : 'evenline' ) . '" >'
. "\n" );
@@ -139,4 +141,5 @@ foreach my $column (@Format) {
$m->out( '</td>' . "\n" );
}
$m->out('</tr>');
+$m->out('</tbody>');
</%init>
diff --git a/share/html/Elements/JavascriptConfig b/share/html/Elements/JavascriptConfig
index 7385dd3..cd2f223 100644
--- a/share/html/Elements/JavascriptConfig
+++ b/share/html/Elements/JavascriptConfig
@@ -65,6 +65,7 @@ if ($session{CurrentUser} and $session{CurrentUser}->id) {
my $Catalog = {
quote_in_filename => "Filenames with double quotes can not be uploaded.", #loc
attachment_warning_regex => "\\battach", #loc
+ shortcut_help_error => "Unable to open shortcut help. Reason: ", #loc
};
$_ = loc($_) for values %$Catalog;
diff --git a/share/html/Helpers/ShortcutHelp b/share/html/Helpers/ShortcutHelp
new file mode 100644
index 0000000..8e6d4f0
--- /dev/null
+++ b/share/html/Helpers/ShortcutHelp
@@ -0,0 +1,108 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="keyboard-shortcuts">
+ <h2><&|/l&>Keyboard Shortcuts</&></h2>
+
+ <div class="titlebox">
+ <div class="titlebox-title">
+ <span class="left"><&|/l&>Global</&></span>
+ <span class="right-empty"></span>
+ </div>
+ <div class="titlebox-content">
+ <hr class="clear">
+ <table>
+ <tr>
+ <td class="key-column"><span class="keyboard-shortcuts-key">/</span></td>
+ <td><&|/l&>Quick search</&></td>
+ </tr>
+ <tr>
+ <td><span class="keyboard-shortcuts-key">gh</span></td>
+ <td><&|/l&>Return home</&></td>
+ </tr>
+ <tr>
+ <td><span class="keyboard-shortcuts-key">gb</span> <span class="keyboard-shortcuts-separator">/</span> <span class="keyboard-shortcuts-key">gf</span></td>
+ <td><&|/l&>Go back / forward</&></td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div class="titlebox">
+ <div class="titlebox-title">
+ <span class="left"><&|/l&>Search</&></span>
+ <span class="right-empty"></span>
+ </div>
+ <div class="titlebox-content">
+ <hr class="clear">
+ <table>
+ <tr>
+ <td class="key-column"><span class="keyboard-shortcuts-key">k</span><span class="keyboard-shortcuts-separator"> / </span><span class="keyboard-shortcuts-key">j</span></td>
+ <td><&|/l&>Move up / down the list of results</&></td>
+ </tr>
+ <tr>
+ <td><span class="keyboard-shortcuts-key">o</span> <span class="keyboard-shortcuts-separator">or</span> <span class="keyboard-shortcuts-key"><<&|/l&>Enter</&>></span></td>
+ <td><&|/l&>View highlighted ticket</&></td>
+ </tr>
+ <tr>
+ <td class="key-column"><span class="keyboard-shortcuts-key">r</span></td>
+ <td><&|/l&>Reply to ticket</&></td>
+ </tr>
+ <tr>
+ <td><span class="keyboard-shortcuts-key">c</span></td>
+ <td><&|/l&>Comment on ticket</&></td>
+ </tr>
+ <tr>
+ <td><span class="keyboard-shortcuts-key">x</span></td>
+ <td><&|/l&>Toggle highlighted ticket's checkbox (for bulk update)</&></td>
+ </tr>
+ </table>
+ </div>
+ </div>
+</div>
+
+% $m->abort;
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index 10799af..8b02c0c 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -190,6 +190,11 @@ $cfs->SetContextObject( values %$seen_queues ) if keys %$seen_queues == 1;
</form>
+%# Keyboard shortcuts info
+<div class="clear"></div>
+<div class="keyboard-shortcuts footer">
+ <p><&|/l_unsafe, '<span class="keyboard-shortcuts-key">?</span>' &>Press [_1] to view keyboard shortcuts.</&></p>
+</div>
<%INIT>
unless ( defined $Rows ) {
diff --git a/share/html/Search/Results.html b/share/html/Search/Results.html
index b6b3379..9f4dd39 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -89,6 +89,13 @@
<input type="submit" class="button" value="<&|/l&>Change</&>" />
</form>
</div>
+
+%# Keyboard shortcuts info
+<div class="clear"></div>
+<div class="keyboard-shortcuts footer">
+ <p><&|/l_unsafe, '<span class="keyboard-shortcuts-key">?</span>' &>Press [_1] to view keyboard shortcuts.</&></p>
+</div>
+
<%INIT>
$m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
diff --git a/share/static/css/base/keyboard-shortcuts.css b/share/static/css/base/keyboard-shortcuts.css
new file mode 100644
index 0000000..690a64b
--- /dev/null
+++ b/share/static/css/base/keyboard-shortcuts.css
@@ -0,0 +1,29 @@
+.keyboard-shortcuts td {
+ text-align: left;
+}
+
+.keyboard-shortcuts td.key-column {
+ width: 8em;
+}
+
+.keyboard-shortcuts h2 {
+ text-align: center;
+}
+
+.keyboard-shortcuts.footer{
+ font-size: 0.9em;
+}
+
+.keyboard-shortcuts .keyboard-shortcuts-key {
+ color: #3858a3;
+ font-family: 'Courier New';
+}
+
+.keyboard-shortcuts .keyboard-shortcuts-separator {
+ font-size: 75%;
+}
+
+.ticket-list .selected-row tr {
+ border-left-color: #3858a3;
+ border-left-width: 3px;
+}
diff --git a/share/static/css/base/main.css b/share/static/css/base/main.css
index 9f03969..c934db1 100644
--- a/share/static/css/base/main.css
+++ b/share/static/css/base/main.css
@@ -30,3 +30,4 @@
@import "print.css";
@import "dropzone.css";
@import "dropzone.customized.css";
+ at import "keyboard-shortcuts.css";
diff --git a/share/static/js/keyboard-shortcuts.js b/share/static/js/keyboard-shortcuts.js
new file mode 100644
index 0000000..ffe05dd
--- /dev/null
+++ b/share/static/js/keyboard-shortcuts.js
@@ -0,0 +1,143 @@
+jQuery(function() {
+ var goBack = function() {
+ window.history.back();
+ };
+
+ var goForward = function() {
+ window.history.forward();
+ };
+
+ var goHome = function() {
+ var homeLink = jQuery('a#home');
+ window.location.href = homeLink.attr('href');
+ };
+
+ var simpleSearch = function() {
+ var searchInput = jQuery('#simple-search').find('input');
+ if (!searchInput.length) return;
+
+ searchInput.focus();
+ searchInput.select();
+
+ return false; // prevent '/' character from being typed in search box
+ };
+
+ var openHelp = function() {
+ var modal = jQuery('.modal');
+ if (modal.length) {
+ jQuery.modal.close();
+ return;
+ }
+
+ jQuery.ajax({
+ url: RT.Config.WebHomePath + "/Helpers/ShortcutHelp",
+ success: showModal,
+ error: function(xhr, reason) {
+ // give the browser a chance to redraw the readout
+ setTimeout(function () {
+ alert(loc_key("shortcut_help_error") + reason);
+ }, 100);
+ }
+ });
+ };
+
+ var showModal = function(html) {
+ jQuery("<div class='modal'></div>")
+ .append(html).appendTo("body")
+ .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+ .modal();
+ };
+
+ Mousetrap.bind('g b', goBack);
+ Mousetrap.bind('g f', goForward);
+ Mousetrap.bind('g h', goHome);
+ Mousetrap.bind('/', simpleSearch);
+ Mousetrap.bind('?', openHelp);
+});
+
+jQuery(function() {
+ // Only load these shortcuts if there is a ticket list on the page
+ var hasTicketList = jQuery('table.ticket-list').length;
+ if (!hasTicketList) return;
+
+ var currentRow;
+
+ var nextTicket = function() {
+ var nextRow;
+ var searchResultsTable = jQuery('.ticket-list.collection-as-table');
+ if (!currentRow || !(nextRow = currentRow.next('tbody.list-item')).length) {
+ nextRow = searchResultsTable.find('tbody.list-item').first();
+ }
+ setNewRow(nextRow);
+ };
+
+ var setNewRow = function(newRow) {
+ if (currentRow) currentRow.removeClass('selected-row');
+ currentRow = newRow;
+ currentRow.addClass('selected-row');
+ scrollToJQueryObject(currentRow);
+ };
+
+ var prevTicket = function() {
+ var prevRow, searchResultsTable = jQuery('.ticket-list.collection-as-table');
+ if (!currentRow || !(prevRow = currentRow.prev('tbody.list-item')).length) {
+ prevRow = searchResultsTable.find('tbody.list-item').last();
+ }
+ setNewRow(prevRow);
+ };
+
+ var generateTicketLink = function(ticketId) {
+ if (!ticketId) return '';
+ return RT.Config.WebHomePath + '/Ticket/Display.html?id=' + ticketId;
+ };
+
+ var generateUpdateLink = function(ticketId, action) {
+ if (!ticketId) return '';
+ return RT.Config.WebHomePath + '/Ticket/Update.html?Action=' + action + '&id=' + ticketId;
+ };
+
+ var navigateToCurrentTicket = function() {
+ if (!currentRow) return;
+
+ var ticketId = currentRow.closest('tbody').data('recordId');
+ var ticketLink = generateTicketLink(ticketId);
+ if (!ticketLink) return;
+
+ window.location.href = ticketLink;
+ };
+
+ var toggleTicketCheckbox = function() {
+ if (!currentRow) return;
+ var ticketCheckBox = currentRow.find('input[type=checkbox]');
+ if (!ticketCheckBox.length) return;
+ ticketCheckBox.prop("checked", !ticketCheckBox.prop("checked"));
+ };
+
+ var replyToTicket = function() {
+ if (!currentRow) return;
+
+ var ticketId = currentRow.closest('tbody').data('recordId');
+ var replyLink = generateUpdateLink(ticketId, 'Respond');
+ if (!replyLink) return;
+
+ window.location.href = replyLink;
+ };
+
+ var commentOnTicket = function() {
+ if (!currentRow) return;
+
+ var ticketId = currentRow.closest('tbody').data('recordId');
+ var commentLink = generateUpdateLink(ticketId, 'Comment');
+ if (!commentLink) return;
+
+ window.location.href = commentLink;
+ };
+
+ Mousetrap.bind('j', nextTicket);
+ Mousetrap.bind('k', prevTicket);
+ Mousetrap.bind(['enter','o'], navigateToCurrentTicket);
+ Mousetrap.bind('r', replyToTicket);
+ Mousetrap.bind('c', commentOnTicket);
+ Mousetrap.bind('x', toggleTicketCheckbox);
+});
+
diff --git a/share/static/js/mousetrap.min.js b/share/static/js/mousetrap.min.js
new file mode 100644
index 0000000..291aff8
--- /dev/null
+++ b/share/static/js/mousetrap.min.js
@@ -0,0 +1,11 @@
+/* mousetrap v1.5.3 craig.is/killing/mice */
+(function(C,r,g){function t(a,b,h){a.addEventListener?a.addEventListener(b,h,!1):a.attachEvent("on"+b,h)}function x(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return l[a.which]?l[a.which]:p[a.which]?p[a.which]:String.fromCharCode(a.which).toLowerCase()}function D(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function u(a){return"shift"==a||"ctrl"==a||"alt"==a||
+"meta"==a}function y(a,b){var h,c,e,g=[];h=a;"+"===h?h=["+"]:(h=h.replace(/\+{2}/g,"+plus"),h=h.split("+"));for(e=0;e<h.length;++e)c=h[e],z[c]&&(c=z[c]),b&&"keypress"!=b&&A[c]&&(c=A[c],g.push("shift")),u(c)&&g.push(c);h=c;e=b;if(!e){if(!k){k={};for(var m in l)95<m&&112>m||l.hasOwnProperty(m)&&(k[l[m]]=m)}e=k[h]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:c,modifiers:g,action:e}}function B(a,b){return null===a||a===r?!1:a===b?!0:B(a.parentNode,b)}function c(a){function b(a){a=
+a||{};var b=!1,n;for(n in q)a[n]?b=!0:q[n]=0;b||(v=!1)}function h(a,b,n,f,c,h){var g,e,l=[],m=n.type;if(!d._callbacks[a])return[];"keyup"==m&&u(a)&&(b=[a]);for(g=0;g<d._callbacks[a].length;++g)if(e=d._callbacks[a][g],(f||!e.seq||q[e.seq]==e.level)&&m==e.action){var k;(k="keypress"==m&&!n.metaKey&&!n.ctrlKey)||(k=e.modifiers,k=b.sort().join(",")===k.sort().join(","));k&&(k=f&&e.seq==f&&e.level==h,(!f&&e.combo==c||k)&&d._callbacks[a].splice(g,1),l.push(e))}return l}function g(a,b,n,f){d.stopCallback(b,
+b.target||b.srcElement,n,f)||!1!==a(b,n)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=x(a);b&&("keyup"==a.type&&w===b?w=!1:d.handleKey(b,D(a),a))}function l(a,c,n,f){function e(c){return function(){v=c;++q[a];clearTimeout(k);k=setTimeout(b,1E3)}}function h(c){g(n,c,a);"keyup"!==f&&(w=x(c));setTimeout(b,10)}for(var d=q[a]=0;d<c.length;++d){var p=d+1===c.length?h:e(f||
+y(c[d+1]).action);m(c[d],p,f,a,d)}}function m(a,b,c,f,e){d._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var g=a.split(" ");1<g.length?l(a,g,b,c):(c=y(a,c),d._callbacks[c.key]=d._callbacks[c.key]||[],h(c.key,c.modifiers,{type:c.action},f,a,e),d._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:e,combo:a}))}var d=this;a=a||r;if(!(d instanceof c))return new c(a);d.target=a;d._callbacks={};d._directMap={};var q={},k,w=!1,p=!1,v=!1;d._handleKey=function(a,
+c,e){var f=h(a,c,e),d;c={};var k=0,l=!1;for(d=0;d<f.length;++d)f[d].seq&&(k=Math.max(k,f[d].level));for(d=0;d<f.length;++d)f[d].seq?f[d].level==k&&(l=!0,c[f[d].seq]=1,g(f[d].callback,e,f[d].combo,f[d].seq)):l||g(f[d].callback,e,f[d].combo);f="keypress"==e.type&&p;e.type!=v||u(a)||f||b(c);p=l&&"keydown"==e.type};d._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)m(a[d],b,c)};t(a,"keypress",e);t(a,"keydown",e);t(a,"keyup",e)}var l={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",
+20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},p={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},A={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},z={option:"alt",command:"meta","return":"enter",
+escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},k;for(g=1;20>g;++g)l[111+g]="f"+g;for(g=0;9>=g;++g)l[g+96]=g;c.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};c.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};c.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};c.prototype.reset=function(){this._callbacks={};this._directMap=
+{};return this};c.prototype.stopCallback=function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")||B(b,this.target)?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};c.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};c.init=function(){var a=c(r),b;for(b in a)"_"!==b.charAt(0)&&(c[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};c.init();C.Mousetrap=c;"undefined"!==typeof module&&module.exports&&(module.exports=
+c);"function"===typeof define&&define.amd&&define(function(){return c})})(window,document);
diff --git a/share/static/js/util.js b/share/static/js/util.js
index 77da0e2..3878558 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -517,3 +517,20 @@ jQuery(function() {
return false;
});
});
+
+// focus jquery object in window, only moving the screen when necessary
+function scrollToJQueryObject(obj) {
+ if (!obj.length) return;
+
+ var viewportHeight = jQuery(window).height(),
+ currentScrollPosition = jQuery(window).scrollTop(),
+ currentItemPosition = obj.offset().top,
+ currentItemSize = obj.height() + obj.next().height();
+
+ if (currentScrollPosition + viewportHeight < currentItemPosition + currentItemSize) {
+ jQuery('html, body').scrollTop(currentItemPosition - viewportHeight + currentItemSize);
+ } else if (currentScrollPosition > currentItemPosition) {
+ jQuery('html, body').scrollTop(currentItemPosition);
+ }
+}
+
commit 9e8d7e0d9fd4343079a7a00e2238c5ded8592d4f
Merge: 3cfb7d8 ffdc046
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Oct 6 15:02:11 2015 -0400
Merge branch '4.4/keyboard-shortcuts'
-----------------------------------------------------------------------
More information about the rt-commit
mailing list