From cff99b96035a6cf798668929213748f5decb2f66 Mon Sep 17 00:00:00 2001 From: Giulio Cesare Solaroli Date: Sun, 4 Jan 2015 16:53:08 +0100 Subject: [PATCH] Added key-bindings for searching cards (desktop version) --- .../Clipperz/PM/UI/Components/Selections.js | 25 +- .../delta/js/Clipperz/PM/UI/MainController.js | 95 +- .../js/MouseTrap/mousetrap-bind-dictionary.js | 62 ++ .../js/MouseTrap/mousetrap-global-bind.js | 59 ++ .../delta/js/MouseTrap/mousetrap-pause.js | 52 + frontend/delta/js/MouseTrap/mousetrap.js | 976 ++++++++++++++++++ .../delta/properties/creditsAndCopyrights.txt | 12 + .../delta/properties/delta.properties.json | 13 +- frontend/delta/scss/core/fonts.scss | 5 +- frontend/delta/scss/core/mixin.scss | 1 + frontend/delta/scss/style/selectionPanel.scss | 61 +- 11 files changed, 1300 insertions(+), 61 deletions(-) create mode 100644 frontend/delta/js/MouseTrap/mousetrap-bind-dictionary.js create mode 100644 frontend/delta/js/MouseTrap/mousetrap-global-bind.js create mode 100644 frontend/delta/js/MouseTrap/mousetrap-pause.js create mode 100644 frontend/delta/js/MouseTrap/mousetrap.js diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js b/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js index 1615f0a..e10db79 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js @@ -51,6 +51,21 @@ Clipperz.PM.UI.Components.Selections = React.createClass({ handleSearchChange: function (anEvent) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'search', anEvent.currentTarget.value); }, + + handleKeyDown: function (anEvent) { + if (anEvent.key == 'Escape') { + this.clearSearch(); + } else if (anEvent.key == 'Enter') { + anEvent.stopPropagation(); + anEvent.preventDefault(); + + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'exitSearch', anEvent.currentTarget.value); + } + }, + + clearSearch: function (anEvent) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'search', ""); + }, render: function () { var tagInfo; @@ -78,10 +93,14 @@ Clipperz.PM.UI.Components.Selections = React.createClass({ React.DOM.div({'className':'search'}, [ React.DOM.form({'className':'searchForm'}, [ React.DOM.div({}, [ + React.DOM.input({'type':'text', 'id':'searchValue', 'onFocus':this.handleSearchChange, 'onChange':this.handleSearchChange, 'onKeyDown':this.handleKeyDown, 'name':'search', 'value':this.props['searchTerm']}), React.DOM.label({'htmlFor':'searchValue'}, 'search'), - React.DOM.input({'type':'text', 'id':'searchValue', 'onFocus':this.handleSearchChange, 'onChange':this.handleSearchChange, 'name':'search', 'value':this.props['searchTerm']}) - ]), - React.DOM.div({}, [ React.DOM.span({'className':'count'}, selectedCardCount) ]) + React.DOM.span({'className':'searchClear', 'onClick':this.clearSearch}, "clear") + ]) + ]), + React.DOM.div({'className':'searchResultInfo'}, [ + React.DOM.label({}, "Items found:"), + React.DOM.span({}, selectedCardCount) ]) ]), React.DOM.ul({'className':'tagList'}, MochiKit.Base.map(function (aTag) {return Clipperz.PM.UI.Components.TagIndexItem({'label':aTag, 'count':tagInfo[aTag], 'selected':aTag == filterValue}); }, tags)), diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index 3c02876..4001d81 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -64,7 +64,6 @@ Clipperz.PM.UI.MainController = function() { 'matchMediaQuery', 'unmatchMediaQuery', 'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards', 'refreshCardEditDetail', -// 'refreshCardEditToolbar', 'saveCardEdits', 'cancelCardEdits', 'cardSelected', 'addCardClick', @@ -75,9 +74,14 @@ Clipperz.PM.UI.MainController = function() { 'maskClick', 'downloadOfflineCopy', 'runDirectLogin', + 'exitSearch' ]); -// MochiKit.Signal.connect(MochiKit.DOM.currentDocument(), 'onselectionchange', this, 'selectionChange_handler'); + Mousetrap.bind({ + '/': MochiKit.Base.method(this, 'focusOnSearch'), + 'up': MochiKit.Base.method(this, 'selectPreviousCard'), + 'down': MochiKit.Base.method(this, 'selectNextCard'), + }) return this; } @@ -347,7 +351,7 @@ console.log("THE BROWSER IS OFFLINE"); }, setUser: function (aUser) { -console.log("SET USER", aUser); +//console.log("SET USER", aUser); this._user = aUser; return this._user; }, @@ -496,7 +500,6 @@ console.log("SET USER", aUser); selectedCardReference: function () { return this.pages()['mainPage'].props && this.pages()['mainPage'].props['selectedCard'] && -// this.pages()['mainPage'].props['selectedCard'] && this.pages()['mainPage'].props['selectedCard']['_reference'] ? this.pages()['mainPage'].props['selectedCard']['_reference'] : ''; @@ -600,23 +603,6 @@ console.log("SET USER", aUser); deferredResult.addMethodcaller('sort', sortCriteria); deferredResult.addCallback(rangeFilter); -/* - if (aFilter['type'] == 'ALL') { - deferredResult.addMethodcaller('sort', sortCriteria); - } else if (aFilter['type'] == 'RECENT') { - deferredResult.addMethodcaller('sort', sortCriteria); - deferredResult.addCallback(function (someCards) { return someCards.slice(0, 9)}); - } else if (aFilter['type'] == 'SEARCH') { - deferredResult.addCallback(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForSearch(aFilter['value']))); - deferredResult.addMethodcaller('sort', sortCriteria); - } else if (aFilter['type'] == 'TAG') { - deferredResult.addCallback(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForTag(aFilter['value']))); - deferredResult.addMethodcaller('sort', sortCriteria); - } else if (aFilter['type'] == 'UNTAGGED') { - deferredResult.addCallback(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForNoTag())); - deferredResult.addMethodcaller('sort', sortCriteria); - } -*/ deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'cards'); deferredResult.addCallback(MochiKit.Base.bind(function (someCards) { if (!this.isSelectedCardStillVisible(someCards)) { @@ -638,7 +624,7 @@ console.log("SET USER", aUser); var deferredResult; deferredResult = new Clipperz.Async.Deferred('MainController.refreshUI', {trace:false}); - deferredResult.addMethod(this, 'resetRecordsInfo'), +// deferredResult.addMethod(this, 'resetRecordsInfo'), deferredResult.addMethod(this, 'refreshSelectedCards'); deferredResult.addMethod(this, 'renderTags'); @@ -740,14 +726,6 @@ console.log("SET USER", aUser); }, */ //========================================================================= -/* - searchCards_handler: function (someParameters) { -//console.log("SEARCH CARDS", someParameters); - this.setFilter(someParameters); - this.renderAccountData(); - }, -*/ - //========================================================================= /* showPreferences_handler: function (anEvent) { var deferredResult; @@ -996,15 +974,28 @@ console.log("SET USER", aUser); this._isSettingsPanelOpen = false; }, + isSelectionPanelHidable: function () { + var result; + + if (this.mediaQueryStyle() == 'extra-wide') { + result = false; + } else { + result = true; + } + + return result; + }, + isSelectionPanelOpen: function () { return this._isSelectionPanelOpen; }, - toggleSelectionPanel_handler: function (anEvent) { - this._isSelectionPanelOpen = !this._isSelectionPanelOpen; - this.setCloseMaskAction(MochiKit.Base.method(this, 'toggleSelectionPanel_handler')); - this.refreshCurrentPage(); + if (this.isSelectionPanelHidable() == true) { + this._isSelectionPanelOpen = !this._isSelectionPanelOpen; + this.setCloseMaskAction(MochiKit.Base.method(this, 'toggleSelectionPanel_handler')); + this.refreshCurrentPage(); + } }, @@ -1031,16 +1022,13 @@ console.log("SET USER", aUser); refreshCardEditDetail_handler: function (aRecordReference) { this.updateSelectedCard({'reference':aRecordReference}, false); }, -// refreshCardEditToolbar_handler: function (aRecordReference) { -// -// }, saveChanges: function () { // TODO: handle errors while savings return Clipperz.Async.callbacks("MainController.saveChanges", [ MochiKit.Base.method(this.overlay(), 'show', "saving …", true), MochiKit.Base.method(this.user(), 'saveChanges'), -// MochiKit.Base.method(this, 'resetRecordsInfo'), + MochiKit.Base.method(this, 'resetRecordsInfo'), MochiKit.Base.method(this.overlay(), 'done', "saved", 1), ], {trace:false}); }, @@ -1051,11 +1039,9 @@ console.log("SET USER", aUser); return Clipperz.Async.callbacks("MainController.saveCardEdits_handler", [ MochiKit.Base.method(currentPage, 'setProps', {'showGlobalMask':true}), -// MochiKit.Base.method(this.overlay(), 'show', "saving …", true), MochiKit.Base.method(this, 'saveChanges'), MochiKit.Base.method(currentPage, 'setProps', {'mode':'view', 'showGlobalMask':false}), MochiKit.Base.method(this, 'refreshUI', aRecordReference), -// MochiKit.Base.method(this.overlay(), 'done', "saved", 1), ], {trace:false}); }, @@ -1340,7 +1326,7 @@ console.log("SET USER", aUser); //============================================================================ - 'downloadOfflineCopy_handler': function (anEvent) { + downloadOfflineCopy_handler: function (anEvent) { var downloadHref; var deferredResult; var newWindow; @@ -1359,6 +1345,33 @@ console.log("SET USER", aUser); }, //============================================================================ + + focusOnSearch: function (anEvent) { + anEvent.preventDefault(); + + // TODO: show selection panel, if it is hidden +// if (! this.isSelectionPanelVisible()) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'toggleSelectionPanel'); +// } + MochiKit.DOM.getElement('searchValue').focus(); + }, + + exitSearch_handler: function (anEvent) { + // TODO: hide selection panel, if it is overlaying + + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'toggleSelectionPanel'); + MochiKit.DOM.getElement('searchValue').blur(); + }, + + selectPreviousCard: function () { +console.log("select PREVIOUS card"); + }, + + selectNextCard: function () { +console.log("select NEXT card"); + }, + + //============================================================================ /* wrongAppVersion: function (anError) { // this.pages()['errorPage'].setProps({message:anError.message}); diff --git a/frontend/delta/js/MouseTrap/mousetrap-bind-dictionary.js b/frontend/delta/js/MouseTrap/mousetrap-bind-dictionary.js new file mode 100644 index 0000000..fcece65 --- /dev/null +++ b/frontend/delta/js/MouseTrap/mousetrap-bind-dictionary.js @@ -0,0 +1,62 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz 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 Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +/** + * Overwrites default Mousetrap.bind method to optionally accept + * an object to bind multiple key events in a single call + * + * You can pass it in like: + * + * Mousetrap.bind({ + * 'a': function() { console.log('a'); }, + * 'b': function() { console.log('b'); } + * }); + * + * And can optionally pass in 'keypress', 'keydown', or 'keyup' + * as a second argument + * + */ +/* global Mousetrap:true */ +Mousetrap = (function(Mousetrap) { + var self = Mousetrap, + _oldBind = self.bind, + args; + + self.bind = function() { + args = arguments; + + // normal call + if (typeof args[0] == 'string' || args[0] instanceof Array) { + return _oldBind(args[0], args[1], args[2]); + } + + // object passed in + for (var key in args[0]) { + if (args[0].hasOwnProperty(key)) { + _oldBind(key, args[0][key], args[1]); + } + } + }; + + return self; +}) (Mousetrap); diff --git a/frontend/delta/js/MouseTrap/mousetrap-global-bind.js b/frontend/delta/js/MouseTrap/mousetrap-global-bind.js new file mode 100644 index 0000000..9514485 --- /dev/null +++ b/frontend/delta/js/MouseTrap/mousetrap-global-bind.js @@ -0,0 +1,59 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz 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 Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +/** + * adds a bindGlobal method to Mousetrap that allows you to + * bind specific keyboard shortcuts that will still work + * inside a text input field + * + * usage: + * Mousetrap.bindGlobal('ctrl+s', _saveChanges); + */ +/* global Mousetrap:true */ +Mousetrap = (function(Mousetrap) { + var _globalCallbacks = {}, + _originalStopCallback = Mousetrap.stopCallback; + + Mousetrap.stopCallback = function(e, element, combo, sequence) { + if (_globalCallbacks[combo] || _globalCallbacks[sequence]) { + return false; + } + + return _originalStopCallback(e, element, combo); + }; + + Mousetrap.bindGlobal = function(keys, callback, action) { + Mousetrap.bind(keys, callback, action); + + if (keys instanceof Array) { + for (var i = 0; i < keys.length; i++) { + _globalCallbacks[keys[i]] = true; + } + return; + } + + _globalCallbacks[keys] = true; + }; + + return Mousetrap; +}) (Mousetrap); diff --git a/frontend/delta/js/MouseTrap/mousetrap-pause.js b/frontend/delta/js/MouseTrap/mousetrap-pause.js new file mode 100644 index 0000000..9da0dd9 --- /dev/null +++ b/frontend/delta/js/MouseTrap/mousetrap-pause.js @@ -0,0 +1,52 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz 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 Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +/** + * adds a pause and unpause method to Mousetrap + * this allows you to enable or disable keyboard shortcuts + * without having to reset Mousetrap and rebind everything + */ +/* global Mousetrap:true */ +Mousetrap = (function(Mousetrap) { + var self = Mousetrap, + _originalStopCallback = self.stopCallback, + enabled = true; + + self.stopCallback = function(e, element, combo) { + if (!enabled) { + return true; + } + + return _originalStopCallback(e, element, combo); + }; + + self.pause = function() { + enabled = false; + }; + + self.unpause = function() { + enabled = true; + }; + + return self; +}) (Mousetrap); diff --git a/frontend/delta/js/MouseTrap/mousetrap.js b/frontend/delta/js/MouseTrap/mousetrap.js new file mode 100644 index 0000000..aaf051b --- /dev/null +++ b/frontend/delta/js/MouseTrap/mousetrap.js @@ -0,0 +1,976 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz 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 Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +/*global define:false */ +/** + * Copyright 2013 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.4.6 + * @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} + */ + _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} + */ + _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} + */ + _SPECIAL_ALIASES = { + 'option': 'alt', + 'command': 'meta', + 'return': 'enter', + 'escape': 'esc', + '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} + */ + _REVERSE_MAP, + + /** + * a list of all the callbacks setup via Mousetrap.bind() + * + * @type {Object} + */ + _callbacks = {}, + + /** + * direct map of string combinations to callbacks used for trigger() + * + * @type {Object} + */ + _directMap = {}, + + /** + * keeps track of what level each sequence is at since multiple + * sequences can start out with the same sequence + * + * @type {Object} + */ + _sequenceLevels = {}, + + /** + * variable to store the setTimeout call + * + * @type {null|number} + */ + _resetTimer, + + /** + * temporary state where we will ignore the next keyup + * + * @type {boolean|string} + */ + _ignoreNextKeyup = false, + + /** + * temporary state where we will ignore the next keypress + * + * @type {boolean} + */ + _ignoreNextKeypress = false, + + /** + * are we currently inside of a sequence? + * type of action ("keyup" or "keydown" or "keypress") or false + * + * @type {boolean|string} + */ + _nextExpectedAction = false; + + /** + * 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(','); + } + + /** + * 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, + callback, + matches = [], + action = e.type; + + // if there are no events related to this keycode + if (!_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 < _callbacks[character].length; ++i) { + callback = _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) { + _callbacks[character].splice(i, 1); + } + + matches.push(callback); + } + } + + return matches; + } + + /** + * 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; + } + + /** + * 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 (Mousetrap.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 + */ + function _handleKey(character, modifiers, e) { + var callbacks = _getMatches(character, modifiers, e), + i, + doNotReset = {}, + maxLevel = 0, + 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; + } + + Mousetrap.handleKey(character, _eventModifiers(e), e); + } + + /** + * 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'; + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * 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); + } + } + + /** + * Converts from a string key combination to an array + * + * @param {string} combination like "command+shift+l" + * @return {Array} + */ + function _keysFromString(combination) { + if (combination === '+') { + return ['+']; + } + + 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, + key, + i, + 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 + }; + } + + /** + * 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 + _directMap[combination + ':' + action] = callback; + + // make sure multiple spaces in a row become a single space + combination = combination.replace(/\s+/g, ' '); + + var sequence = combination.split(' '), + 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 + _callbacks[info.key] = _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 + _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 + */ + function _bindMultiple(combinations, callback, action) { + for (var i = 0; i < combinations.length; ++i) { + _bindSingle(combinations[i], callback, action); + } + } + + // start! + _addEvent(document, 'keypress', _handleKeyEvent); + _addEvent(document, 'keydown', _handleKeyEvent); + _addEvent(document, 'keyup', _handleKeyEvent); + + var Mousetrap = { + + /** + * 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 + */ + bind: function(keys, callback, action) { + keys = keys instanceof Array ? keys : [keys]; + _bindMultiple(keys, callback, action); + return this; + }, + + /** + * 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 + */ + unbind: function(keys, action) { + return Mousetrap.bind(keys, function() {}, action); + }, + + /** + * triggers an event that has already been bound + * + * @param {string} keys + * @param {string=} action + * @returns void + */ + trigger: function(keys, action) { + if (_directMap[keys + ':' + action]) { + _directMap[keys + ':' + action]({}, keys); + } + return this; + }, + + /** + * 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 + */ + reset: function() { + _callbacks = {}; + _directMap = {}; + return this; + }, + + /** + * should we stop this event before firing off callbacks + * + * @param {Event} e + * @param {Element} element + * @return {boolean} + */ + stopCallback: function(e, element) { + + // if the element has the class "mousetrap" then no need to stop + if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { + 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 + */ + handleKey: _handleKey + }; + + // expose mousetrap to the global object + window.Mousetrap = Mousetrap; + + // expose mousetrap as an AMD module + if (typeof define === 'function' && define.amd) { + define(Mousetrap); + } +}) (window, document); diff --git a/frontend/delta/properties/creditsAndCopyrights.txt b/frontend/delta/properties/creditsAndCopyrights.txt index 4923672..5413510 100644 --- a/frontend/delta/properties/creditsAndCopyrights.txt +++ b/frontend/delta/properties/creditsAndCopyrights.txt @@ -464,6 +464,18 @@ | THE SOFTWARE. +# mousetrap (http://craig.is/killing/mice) + - repository: @mousetrap.repository@ (version: @mousetrap.version@ - commit: @mousetrap.commit@) + + * Software license: https://github.com/ccampbell/mousetrap + + | […] + | + | It is licensed under the Apache 2.0 license. + | + | […] + + # Add to Home Screen (http://cubiq.org/add-to-home-screen) - repository: @addtohomescreen.repository@ (version: @addtohomescreen.version@ - commit: @addtohomescreen.commit@) diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index 957e693..4867696 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -18,7 +18,11 @@ "addtohomescreen.repository": "https://github.com/cubiq/add-to-homescreen.git", "addtohomescreen.version": "2.0.8", - "addtohomescreen.commit": "4d375840079bcea994cc5795a568802400c7a793" + "addtohomescreen.commit": "4d375840079bcea994cc5795a568802400c7a793", + + "mousetrap.repository": "https://github.com/ccampbell/mousetrap.git", + "mousetrap.version": "1.4.6", + "mousetrap.commit": "dcdf4d85b7d98dc2a141a7418f1bdf8987586b40" }, "html.template": "index_template.html", @@ -39,7 +43,12 @@ "MochiKit/Selector.js", "-- MochiKit/Visual.js", - "React/react-with-addons-0.11.1.js", + "React/react-with-addons-0.12.2.js", + + "MouseTrap/mousetrap.js", + "MouseTrap/mousetrap-bind-dictionary.js", + "-- MouseTrap/mousetrap-global-bind.js", + "-- MouseTrap/mousetrap-pause.js", "-- Hammer/hammer-1.0.5.js", "-- Cubiq/add2home.js", diff --git a/frontend/delta/scss/core/fonts.scss b/frontend/delta/scss/core/fonts.scss index d15a0c0..7ffb4cb 100644 --- a/frontend/delta/scss/core/fonts.scss +++ b/frontend/delta/scss/core/fonts.scss @@ -75,11 +75,10 @@ @font-face { font-family: "clipperz-icons"; - // generator: IcoMoon +// generator: IcoMoon font-style: normal; font-weight: normal; - src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAAMAIAAAwBAR1NVQuUv5xcAAADMAAABzk9TLzIIIvzGAAACnAAAAGBjbWFw52cCOwAAAvwAAAB0Z2FzcAAAABAAAANwAAAACGdseWbJg89gAAADeAAADNRoZWFkAZ02KQAAEEwAAAA2aGhlYQP+AisAABCEAAAAJGhtdHgfygIxAAAQqAAAAKhsb2NhKMAlwgAAEVAAAABWbWF4cAAzANwAABGoAAAAIG5hbWWH7XEQAAARyAAAAYRwb3N0AAMAAAAAE0wAAAAgAAEAAAAKAB4ALAABbGF0bgAIAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAoAAAABACQADwBGAFwAagCUALIAwgDmAPoBCAESASgBOgFmAX4BigABAA8ABAAFAAYABwAIAAkADgAPABEAEgATABQAFQAWACkAAQAEACgACAAHAAcAAwAGAAQAEwAHAAEABAAlAAQABAAGAA0AAgAGABgAJwAIABEADwAPAAQAEAAHABQAHQAIAA4ADAASABIACAATABgAAgAGABAAHAAEABEAEAAIABsABgAIAA4ACAAVAAgAAQAEACEABQAPAAQADAAOAAIABgAWABsABwAEAAwADgAWABMACAAbAAYABAAMAA4ACAAHAAEABAAkAAcAEQAEAAcADAAQAAoAAQAEABoABAAIABAAFgABAAQAHAACAA0AAQAEACIACAAEABQAFAAXABEAEwAHAAEABAAgAAYACAAGAAgAEAAVAAIABgAeACYACwALABEAFwADAAcACAAVAAQADAAOAB8ABgAIAAQAEwAGAAsAAgAGABAAGQAEAAQACgAUAB4AAwAEAAoAAQAEACMAAwATAA4AAQAEACgAAgApAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADmEAHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEAGAAAAAUABAAAwAEAAEAIABpAHAAdQB3AHrmEP/9//8AAAAAACAAYQBrAHIAdwB65gD//f//AAH/4/+j/6L/of+g/54aGQADAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAAC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A6wEJgRyBZYF0gYOBigGYAZqAAAAAQAAACoA2gAIAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABABwAAAABAAAAAAACAA4AeAABAAAAAAADABwAMgABAAAAAAAEABwAhgABAAAAAAAFABYAHAABAAAAAAAGAA4ATgABAAAAAAAKADQAogADAAEECQABABwAAAADAAEECQACAA4AeAADAAEECQADABwAMgADAAEECQAEABwAhgADAAEECQAFABYAHAADAAEECQAGABwAXAADAAEECQAKADQAogBjAGwAaQBwAHAAZQByAHoALQBpAGMAbwBuAHMAVgBlAHIAcwBpAG8AbgAgADEALgAwAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4Ac2NsaXBwZXJ6LWljb25zAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4AcwBSAGUAZwB1AGwAYQByAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4AcwBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('truetype'); - + src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAAMAIAAAwBAR1NVQubK6EkAAADMAAACAE9TLzIIYvzIAAACzAAAAGBjbWFw52kCOwAAAywAAAB0Z2FzcAAAABAAAAOgAAAACGdseWajQiY9AAADqAAAD0RoZWFkAtQ8FwAAEuwAAAA2aGhlYQP+Ai0AABMkAAAAJGhtdHgjygIwAAATSAAAALBsb2NhMgouYAAAE/gAAABabWF4cAA1ANwAABRUAAAAIG5hbWWH7XEQAAAUdAAAAYRwb3N0AAMAAAAAFfgAAAAgAAEAAAAKAB4ALAABbGF0bgAIAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAoAAAABACQADwBGAFwAagCiAMAA0AD0ARgBJgEwAUYBWAGEAZwBvAABAA8ABAAFAAYABwAIAAkADgAPABEAEgATABQAFQAWACsAAQAEACgACAAHAAcAAwAGAAQAEwAHAAEABAAlAAQABAAGAA0AAwAIABoALAAnAAgAEQAPAA8ABAAQAAcAFAAdAAgADgAMABIAEgAIABMAGAAbAAUADgAIAAQAEwACAAYAEAAcAAQAEQAQAAgAGwAGAAgADgAIABUACAABAAQAIQAFAA8ABAAMAA4AAgAGABYAGwAHAAQADAAOABYAEwAIABsABgAEAAwADgAIAAcAAgAGABQAKQAGABEABgANAAgABwAkAAcAEQAEAAcADAAQAAoAAQAEABoABAAIABAAFgABAAQAHAACAA0AAQAEACIACAAEABQAFAAXABEAEwAHAAEABAAgAAYACAAGAAgAEAAVAAIABgAeACYACwALABEAFwADAAcACAAVAAQADAAOAB8ABgAIAAQAEwAGAAsAAgAGABAAGQAEAAQACgAUAB4AAwAEAAoAAgAGAA4AIwADABMADgAqAAgAEAAOABEABgANAAgABwABAAQAKgACACsAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADmEgHg/+AAIAHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEAGAAAAAUABAAAwAEAAEAIABpAHAAdQB3AHrmEv/9//8AAAAAACAAYQBrAHIAdwB65gD//f//AAH/4/+j/6L/of+g/54aGQADAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAMAAAAEAhwBwAAjADwAWQAAEzMyFhceAR8BHgEVFAYPAQ4BIyImLwEuAScuAT0BNDY3PgEzFzI2Nz4BNTQmJy4BIyIGBw4BFRQWFx4BMwUHHgE3PgE/AT4BNTQmLwEuAScuASMXHgEVFAYHMHAKFwsLFAe8BwcHB4wHEgkJEge8BwwFBQUIBgcRCkAKEQcGCAgGBxEKChEHBggIBgcRCgGFrAcPCAcOBowHBwcHvAcUCwsXCvUCAgICAcAFBQUMB7wHEgkJEgeMBwcHB7wHFAsLFwpwChEHBgigCAYHEQoKEQcGCAgGBxEKChEHBghrrAMDAQEHBowHEgkJEge8BwwFBQX1AgYDAwYCAAADACAAIAHgAYAAAwAHAAsAABMhFSEVIRUhFSEVISABwP5AAcD+QAHA/kABgGAgYCBgAAAAAQAA/+ACAAHgAGwAACU4ATEnNzgBMT4BNzY0NS4BLwEuAScqAQcOAQc4ATEHJzgBMS4BJyYiIw4BDwEOAQccARceARc4ATEXBzgBMQ4BBwYUFR4BHwEeARc6ATc+ATc4ATE3FzgBMR4BFxYyMz4BPwE+ATc8AScuAScB+5ubAgEBAQECAkkCBAICBQICAgKbmwICAgIFAgIEAkkCAgEBAQECm5sCAQEBAQICSQIEAgIFAgICApubAgICAgUCAgQCSQICAQEBAQJFm5sCAgICBQICBAJJAgIBAQEBApubAgEBAQECAkkCBAICBQICAgKbmwICAgIFAgIEAkkCAgEBAQECm5sCAQEBAQICSQIEAgIFAgICAgAAAAEAAAAQAgABoAAFAAABBycHFwEBsPBwUMABQAGg8HBQwAFAAAEABf/lAhcB3AAOAAABNSMVJwcXBxc3FzcnNycBToaWLZtkcVxmbmWfLQEzqak9gTSEUoyMUoQ1gQAAAAIAAP/gAgAB4AAjADwAABMzMhYXHgEfAR4BFRQGDwEOASMiJi8BLgEnLgE9ATQ2Nz4BMxcyNjc+ATU0JicuASMiBgcOARUUFhceATMwkAoXCwsUB+AHBwcHrAcSCQoRB+AHDAUFBQgGBxEKQAoRBwYICAYHEQoKEQcGCAgGBxEKAeAFBQUMB+AHEQoJEgesBwcHB+AHFAsLFwqQChEHBgigCAYHEQoKEQcGCAgGBxEKChEHBggAAAACAAD/4AIAAeAAOABRAAAlJy4BJy4BBz4BNz4BNTQmJy4BIyIGBw4BFRQWFx4BMzI2Nz4BNwYWFx4BHwEeARcyNjc+ATUuASclIiYnLgE1NDY3PgEzMhYXHgEVFAYHDgEjAfB5BQkFBQkECxEGBgYeGhpGKChGGhoeHhoaRigSIhAPHQ0BAgICBgVnBhEJCBEHBgYBCAf+0BsuEhEUFBESLhsbLhIRFBQREi4bLGcFBgICAgENHQ8QIhIoRhoaHh4aGkYoKEYaGh4GBgYRCwQJBQUJBXkHCAEGBgcRCAkRBnQUERIuGxsuEhEUFBESLhsbLhIRFAAAAAMAAP/gAgAB4AAFAB4ANwAAJSc1MxUXAyIGBw4BFRQWFx4BMzI2Nz4BNTQmJy4BIxEiJicuATU0Njc+ATMyFhceARUUBgcOASMBSWlAV3c1XSMjKCgjI101NV0jIygoIyNdNShGGhoeHhoaRigoRhoaHh4aGkYoaWqNc1YBSSgjI101NV0jIygoIyNdNTVdIyMo/kAeGhpGKChGGhoeHhoaRigoRhoaHgAAAAAFAAAAAAIAAaAAHAAgACQAKgAuAAABISIGBw4BFREUFhceATMhMjY3PgE1ETQmJy4BIwUHNRcnIQcnHwE3FyE3PwEVJwHQ/mAKEQcGCAgGBxEKAaAKEQcGCAgGBxEK/veHh28BUKiodTMzav7GamyHhwGgCAYHEQr+wAoRBwYICAYHEQoBQAoRBwYI02r7kZN+fpk3N4eHBpH7agADAED/4AHAAeAAKwBEAFQAAAEjNTQmJy4BIyIGBw4BHQEjIgYHDgEdARQWFx4BMyEyNjc+AT0BNCYnLgEjByImJy4BNTQ2Nz4BMzIWFx4BFRQGBw4BIzcjNTQ2Nz4BMzIWFx4BHQEBoCAUERIuGxsuEhEUIAcLBQQFBQQFCwcBQAcLBQQFBQQFCwegBwsFBAUFBAULBwcLBQQFBQQFCwdAgAoJCBgNDRgICQoBAGAbLhIRFBQREi4bYAUEBQsH4AcLBQQFBQQFCwfgBwsFBAXABQQFCwcHCwUEBQUEBQsHBwsFBAXAYA0YCAkKCgkIGA1gAAAAAwAzABoBzQGzABwAIQAuAAABIyIGBw4BHQEUFhceATsBMjY3PgE9ATQmJy4BIxEjNTMVJSMVFBYXHgE7ATUjNQGazgoTBwYICAcHEgvNChMHBwgIBwcTCs3N/swzCAcHEwqamgGzCAcGEwrOChMHBwgIBwcTCs0LEgcHCP8Azc0zmQsSBwcIM5kAAAAIABwAAAHgAeAAGAAxAEoAYwCCAKEAwADZAAATNDY3PgEzMhYXHgEVFAYHDgEjIiYnLgE1FzQ2Nz4BMzIWFx4BFRQGBw4BIyImJy4BNRc0Njc+ATMyFhceARUUBgcOASMiJicuATUHNDY3PgEzMhYXHgEVFAYHDgEjIiYnLgE1BzgBMTQ2Nz4BMzIWFx4BFTgBMRQGBw4BIyImJy4BNSc4ATE0Njc+ATMyFhceARU4ATEUBgcOASMiJicuATUDOAExNDY3PgEzMhYXHgEVOAExFAYHDgEjIiYnLgE1BzQ2Nz4BMzIWFx4BFRQGBw4BIyImJy4BNcAKCQgYDQ0YCAkKCgkIGA0NGAgJCogKCQgYDQ0XCQkKCgkJFw0NGAgJClgFBAULBwcLBQQFBQQFCwcHCwUEBTgFBAQMBwYMBAUFBQUEDAYHDAQEBYgFBAULBwcLBQQFBQQFCwcHCwUEBYgFBQQMBgcMBAQFBQQEDAcGDAQFBRAIBgcRCgoSBgcHBwcGEgoKEQcGCCwGBQQOBwcOBAUGBgUEDgcHDgQFBgGgDRgICQoKCQgYDQ0YCAkKCgkIGA04DRcJCQoKCQkXDQ0YCAkKCgkIGA2IBwsFBAUFBAULBwcLBQQFBQQFCweIBwwEBAUFBAQMBwYMBAUFBQUEDAY4BwsFBAUFBAULBwcLBQQFBQQFCwc4BwwEBAUFBAQMBwYMBAUFBQUEDAYBEAoRBwYICAYHEQoKEgYHBwcHBhIKiAcOBAUGBgUEDgcHDgQFBgYFBA4HAAEAqQBSAVcBewAyAAABDgEHDgExDgEHDgEVFBYXHgEXMBYXHgEXHgEzMjY3PgE1NCYvATc+ATU0JicuASMiBgcBJQQlFRQhAwMBAQEBAQEDAyEUFSUEBAsFBgsEBAUEBWBgBQQFBAQLBgULBAFzBCcVFiIDBAMCBgMCBgIDBQIiFhYmBAQEBAQECgUFCwVkZAYKBgUKBAQEBAQAAAEAqQBSAVcBewAyAAATHgEXHgExHgEXHgEVFAYHDgEHMAYHDgEHDgEjIiYnLgE1NDY/AScuATU0Njc+ATMyFhfbBCUVFCEDAwEBAQEBAQMDIRQVJQQECwUGCwQEBQQFYGAFBAUEBAsGBgoEAXMEJxUWIgMEAwIGAwIGAgMFAiIWFiYEBAQEBAQKBQULBWRkBgoGBQoEBAQEBAAAAAMAAf/hAI0B3wAEAAkADgAANzMVIzURMxUjNRUzFSM1AYyMjIyMjHGQkAFukJC3kJAAAAACACkADwHXAb0AGAAlAAABIgYHDgEVFBYXHgEzMjY3PgE1NCYnLgEjFxUjNSM1MzUzFTMVIwEALU4dHSIiHR1OLS1OHR0iIh0dTi0aNGdnNGdnAb0hHh1OLSxOHh0iIh0eTiwtTh0eIfFnZzVnZzUAAwAA/+ABQAHAACwASABZAAABIzU0JicuASsBIgYHDgEdASMiBgcOAR0BFBYXHgEzITI2Nz4BPQE0JicuASMHIzcuAScuATU0Njc+ATMyFhceARUUBgcOAQcXNyM1NDY3PgE7ATIWFx4BHQEBKAgPDQ0jFEAUIw0NDwgFCQMDBAQDAwkFARAFCQMDBAQDAwkFaEAOAwUCAgIFBAULBwcLBQQFAgICBQMOIIAFBAULB0AHCwUEBQEAYBQjDQ0PDw0NIxRgBAMDCQXwBQkDAwQEAwMJBfAFCQMDBOBGAgUEAwgEBwsFBAUFBAULBwQIAwQFAkbgYAcLBQQFBQQFCwdgAAIAAP/gAeABwAA8AFgAAAEjIgYHDgEdASMiBgcOAR0BFBYXHgEzITI2Nz4BPQE0JicuASsBNTQ2Nz4BOwEyFhceAR0BMzU0JicuASMDIzcuAScuATU0Njc+ATMyFhceARUUBgcOAQcXAYBAFCMNDQ/IBQkDAwQEAwMJBQEQBQkDAwQEAwMJBQgFBAULB0AHCwUEBUAPDQ0jFMBADgMFAgICBQQFCwcHCwUEBQICAgUDDgHADw0NIxRgBAMDCQXwBQkDAwQEAwMJBfAFCQMDBGAHCwUEBQUEBQsHYGAUIw0ND/5gRgIFBAMIBAcLBQQFBQQFCwcECAMEBQJGAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAEAADmXWENfDzz1AAsCAAAAAADQzH2tAAAAANDMfa0AAP/gAhwB4AAAAAgAAgAAAAAAAAABAAAB4P/gAAACIAAAAAACHAABAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIAAAAgAAIAIAAAACAAAAAhwABQIAAAACAAAAAgAAAAIAAAACAABAAgAAMwIAABwCAACpAgAAqQCOAAECAAApAgAAAAIAAAAAAAAAAAAAAAAKABQAHgAoADIAPABGAFAAWgBkAG4AeACCAIwAlgCgAKoAtAC+AMgA0gDcAOYA8AF2AZACHAIuAkwCqAMkA3oDyARCBIgFrAX6BkgGYgaaBxoHmAeiAAAAAQAAACwA2gAIAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABABwAAAABAAAAAAACAA4AeAABAAAAAAADABwAMgABAAAAAAAEABwAhgABAAAAAAAFABYAHAABAAAAAAAGAA4ATgABAAAAAAAKADQAogADAAEECQABABwAAAADAAEECQACAA4AeAADAAEECQADABwAMgADAAEECQAEABwAhgADAAEECQAFABYAHAADAAEECQAGABwAXAADAAEECQAKADQAogBjAGwAaQBwAHAAZQByAHoALQBpAGMAbwBuAHMAVgBlAHIAcwBpAG8AbgAgADEALgAwAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4Ac2NsaXBwZXJ6LWljb25zAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4AcwBSAGUAZwB1AGwAYQByAGMAbABpAHAAcABlAHIAegAtAGkAYwBvAG4AcwBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('truetype'); } //======================================================================================== diff --git a/frontend/delta/scss/core/mixin.scss b/frontend/delta/scss/core/mixin.scss index 6c69735..b36956f 100644 --- a/frontend/delta/scss/core/mixin.scss +++ b/frontend/delta/scss/core/mixin.scss @@ -158,3 +158,4 @@ overflow: scroll; -webkit-overflow-scrolling: touch; } + diff --git a/frontend/delta/scss/style/selectionPanel.scss b/frontend/delta/scss/style/selectionPanel.scss index d7df621..9c6df16 100644 --- a/frontend/delta/scss/style/selectionPanel.scss +++ b/frontend/delta/scss/style/selectionPanel.scss @@ -6,6 +6,7 @@ // height: 100%; $iconColumnWidth: 40px; + $spacing: 5px; ul.defaultSet { font-weight: bold; @@ -24,18 +25,51 @@ } .search { - label { - @include icon-font(); - font-size: 18pt; - padding-left: 5px; - padding-right:5px; - } + form { + input { + font-family: "clipperz-font"; + color: #999; + font-size: 18pt; + padding: 0px 30px; + margin: 0px 5px; +// width: $selectionPanelWidth - $iconColumnWidth - 10px; + width: $selectionPanelWidth - ($spacing * 2); + } + + label { + @include icon-font(); + font-size: 18pt; + padding-left: $spacing; +// padding-right:5px; + margin-left: -$selectionPanelWidth + $spacing; + color: #666; + } - input { - font-family: "clipperz-font"; - color: #999; - font-size: 18pt; - width: $selectionPanelWidth - $iconColumnWidth - 10px; + .searchClear { + @include icon-font(); + @include border-radius(4px); + background-color: #aaa; + color: white; + margin-left: $selectionPanelWidth - $iconColumnWidth - 30px; + cursor: pointer; + padding: 5px; + font-size: 10pt; + + &:hover { + background-color: #666; + } + } + } + + .searchResultInfo { +// color: #999; + font-size: 10pt; + padding: 2px 6px; + display: none; + + label { + padding-right: 3px; + } } } @@ -113,7 +147,10 @@ $selectionColor: yellow; #selections.SEARCH { div.search { - color: $selectionColor; + .searchResultInfo { + color: $selectionColor; + display: block; + } } }