From 40c2411f2a1ce0bbd177f377828f9d6700112b06 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 26 May 2015 20:30:01 +0200 Subject: Implement a new system to handle "escape actions" The new EscapeActions object decide what to do when the user press the Escape key (such as closing a opened popup or inlined form). This commit also re-introduced the sidebar current view as a sidebar component local state. --- client/components/boards/router.js | 8 ++++- client/components/cards/details.jade | 3 +- client/components/forms/inlinedform.js | 22 ++++++++---- client/components/main/editor.js | 66 ++++++++++++++++++++++++++++++++++ client/components/main/events.js | 8 ----- client/components/main/rendered.js | 40 --------------------- client/components/main/templates.html | 2 +- client/components/sidebar/sidebar.jade | 6 +--- client/components/sidebar/sidebar.js | 29 ++++++++++++++- client/config/router.js | 4 ++- client/lib/filter.js | 18 +++++----- client/lib/keyboard.js | 58 ++++++++++++++++++++++-------- client/lib/popup.js | 4 +++ 13 files changed, 181 insertions(+), 87 deletions(-) create mode 100644 client/components/main/editor.js delete mode 100644 client/components/main/events.js delete mode 100644 client/components/main/rendered.js (limited to 'client') diff --git a/client/components/boards/router.js b/client/components/boards/router.js index 9c5bee35..80fadd9e 100644 --- a/client/components/boards/router.js +++ b/client/components/boards/router.js @@ -39,7 +39,7 @@ Router.route('/boards/:boardId/:slug/:cardId', { template: 'board', onAfterAction: function() { Tracker.nonreactive(function() { - if (! Session.get('currentCard') && typeof Sidebar !== 'undefined') { + if (! Session.get('currentCard') && Sidebar) { Sidebar.hide(); } }); @@ -55,3 +55,9 @@ Router.route('/boards/:boardId/:slug/:cardId', { return Boards.findOne(this.params.boardId); } }); + +// Close the card details pane by pressing escape +EscapeActions.register(50, + function() { return ! Session.equals('currentCard', null); }, + function() { Utils.goBoardId(Session.get('currentBoard')); } +); diff --git a/client/components/cards/details.jade b/client/components/cards/details.jade index 55cc4b9e..b77c3961 100644 --- a/client/components/cards/details.jade +++ b/client/components/cards/details.jade @@ -26,7 +26,8 @@ template(name="cardDetails") h3 Description +inlinedForm(classNames="js-card-description") i.fa.fa-times.js-close-inlined-form - textarea(autofocus)= description + +editor(autofocus=true) + = description button(type="submit") {{_ 'edit'}} else .js-open-inlined-form diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js index 2e2b2eba..200a6f9d 100644 --- a/client/components/forms/inlinedform.js +++ b/client/components/forms/inlinedform.js @@ -15,7 +15,9 @@ // We can only have one inlined form element opened at a time // XXX Could we avoid using a global here ? This is used in Mousetrap // keyboard.js -currentlyOpenedForm = new ReactiveVar(null); +var currentlyOpenedForm = new ReactiveVar(null); + +var inlinedFormEscapePriority = 30; BlazeComponent.extendComponent({ template: function() { @@ -32,9 +34,10 @@ BlazeComponent.extendComponent({ open: function() { // Close currently opened form, if any - if (currentlyOpenedForm.get() !== null) { - currentlyOpenedForm.get().close(); - } + // if (currentlyOpenedForm.get() !== null) { + // currentlyOpenedForm.get().close(); + // } + EscapeActions.executeLowerThan(inlinedFormEscapePriority); this.isOpen.set(true); currentlyOpenedForm.set(this); }, @@ -46,7 +49,8 @@ BlazeComponent.extendComponent({ }, getValue: function() { - return this.isOpen.get() && this.find('textarea,input[type=text]').value; + var input = this.find('textarea,input[type=text]'); + return this.isOpen.get() && input && input.value; }, saveValue: function() { @@ -66,7 +70,7 @@ BlazeComponent.extendComponent({ 'keydown form input, keydown form textarea': function(evt) { if (evt.keyCode === 27) { evt.preventDefault(); - this.close(); + EscapeActions.executeLowest(); } }, @@ -91,3 +95,9 @@ BlazeComponent.extendComponent({ }]; } }).register('inlinedForm'); + +// Press escape to close the currently opened inlinedForm +EscapeActions.register(inlinedFormEscapePriority, + function() { return currentlyOpenedForm.get() !== null; }, + function() { currentlyOpenedForm.get().close(); } +); diff --git a/client/components/main/editor.js b/client/components/main/editor.js new file mode 100644 index 00000000..95a8dc5d --- /dev/null +++ b/client/components/main/editor.js @@ -0,0 +1,66 @@ +var dropdownMenuIsOpened = false; + +Template.editor.onRendered(function() { + var $textarea = this.$('textarea'); + + $textarea.textcomplete([ + // Emojies + { + match: /\B:([\-+\w]*)$/, + search: function(term, callback) { + callback($.map(Emoji.values, function(emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function(value) { + var image = ''; + return image + value; + }, + replace: function(value) { + return ':' + value + ':'; + }, + index: 1 + }, + + // User mentions + { + match: /\B@(\w*)$/, + search: function(term, callback) { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.members, function(member) { + var username = Users.findOne(member.userId).username; + return username.indexOf(term) === 0 ? username : null; + })); + }, + template: function(value) { + return value; + }, + replace: function(username) { + return '@' + username + ' '; + }, + index: 1 + } + ]); + + // Since commit d474017 jquery-textComplete automatically closes a potential + // opened dropdown menu when the user press Escape. This behavior conflicts + // with our EscapeActions system, but it's too complicated and hacky to + // monkey-pach textComplete to disable it -- I tried. Instead we listen to + // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown + // is opened (and rely on textComplete to execute the actual action). + $textarea.on({ + 'textComplete:show': function() { + dropdownMenuIsOpened = true; + }, + 'textComplete:hide': function() { + Tracker.afterFlush(function() { + dropdownMenuIsOpened = false; + }); + } + }); +}); + +EscapeActions.register(10, + function() { return dropdownMenuIsOpened; }, + function() {} +); diff --git a/client/components/main/events.js b/client/components/main/events.js deleted file mode 100644 index beb90c5e..00000000 --- a/client/components/main/events.js +++ /dev/null @@ -1,8 +0,0 @@ -Template.editor.events({ - // Pressing Ctrl+Enter should submit the form. - 'keydown textarea': function(event) { - if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) { - $(event.currentTarget).parents('form:first').submit(); - } - } -}); diff --git a/client/components/main/rendered.js b/client/components/main/rendered.js deleted file mode 100644 index 787e8225..00000000 --- a/client/components/main/rendered.js +++ /dev/null @@ -1,40 +0,0 @@ -Template.editor.rendered = function() { - this.$('textarea').textcomplete([ - // Emojies - { - match: /\B:([\-+\w]*)$/, - search: function(term, callback) { - callback($.map(Emoji.values, function(emoji) { - return emoji.indexOf(term) === 0 ? emoji : null; - })); - }, - template: function(value) { - var image = ''; - return image + value; - }, - replace: function(value) { - return ':' + value + ':'; - }, - index: 1 - }, - - // User mentions - { - match: /\B@(\w*)$/, - search: function(term, callback) { - var currentBoard = Boards.findOne(Session.get('currentBoard')); - callback($.map(currentBoard.members, function(member) { - var username = Users.findOne(member.userId).username; - return username.indexOf(term) === 0 ? username : null; - })); - }, - template: function(value) { - return value; - }, - replace: function(username) { - return '@' + username + ' '; - }, - index: 1 - } - ]); -}; diff --git a/client/components/main/templates.html b/client/components/main/templates.html index e9be0f93..4828663a 100644 --- a/client/components/main/templates.html +++ b/client/components/main/templates.html @@ -12,7 +12,7 @@ diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 07cd777c..07d6bbcf 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -4,11 +4,7 @@ template(name="sidebar") class="{{#if isTongueHidden}}is-hidden{{/if}}") i.fa.fa-chevron-left .sidebar-content.js-board-sidebar-content.js-perfect-scrollbar - //- XXX https://github.com/peerlibrary/meteor-blaze-components/issues/30 - if Filter.isActive - +filterSidebar - else - +homeSidebar + +Template.dynamic(template=getViewTemplate) template(name='homeSidebar') +membersWidget diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 6e45b5cf..729bc42b 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -1,3 +1,7 @@ +var defaultView = 'home'; + +Sidebar = null; + BlazeComponent.extendComponent({ template: function() { return 'sidebar'; @@ -9,9 +13,14 @@ BlazeComponent.extendComponent({ onCreated: function() { this._isOpen = new ReactiveVar(! Session.get('currentCard')); + this._view = new ReactiveVar(defaultView); Sidebar = this; }, + onDestroyed: function() { + Sidebar = null; + }, + isOpen: function() { return this._isOpen.get(); }, @@ -43,7 +52,20 @@ BlazeComponent.extendComponent({ }, isTongueHidden: function() { - return this.isOpen() && Filter.isActive(); + return this.isOpen() && this.getView() !== defaultView; + }, + + getView: function() { + return this._view.get(); + }, + + setView: function(view) { + view = view || defaultView; + this._view.set(view); + }, + + getViewTemplate: function() { + return this.getView() + 'Sidebar'; }, onRendered: function() { @@ -74,3 +96,8 @@ BlazeComponent.extendComponent({ }]); } }).register('sidebar'); + +EscapeActions.register(40, + function() { return Sidebar && Sidebar.getView() !== defaultView; }, + function() { Sidebar.setView(defaultView); } +); diff --git a/client/config/router.js b/client/config/router.js index 2fa1908d..d4bc3c4f 100644 --- a/client/config/router.js +++ b/client/config/router.js @@ -20,7 +20,9 @@ Router.configure({ // Reset default sessions Session.set('error', false); - Popup.close(); + Tracker.nonreactive(function() { + EscapeActions.executeLowerThan(40); + }); this.next(); } diff --git a/client/lib/filter.js b/client/lib/filter.js index 507a2bb7..d96fa89c 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -4,6 +4,10 @@ // goal is to filter complete documents by using the local filters for each // fields. +var showFilterSidebar = function() { + Sidebar.setView('filter'); +}; + // Use a "set" filter for a field that is a set of documents uniquely // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`. var SetFilter = function() { @@ -18,29 +22,27 @@ _.extend(SetFilter.prototype, { }, add: function(val) { - if (this.indexOfVal(val) === -1) { + if (this._indexOfVal(val) === -1) { this._selectedElements.push(val); this._dep.changed(); + showFilterSidebar(); } }, remove: function(val) { var indexOfVal = this._indexOfVal(val); - if (this.indexOfVal(val) !== -1) { + if (this._indexOfVal(val) !== -1) { this._selectedElements.splice(indexOfVal, 1); this._dep.changed(); } }, toogle: function(val) { - var indexOfVal = this._indexOfVal(val); - if (indexOfVal === -1) { - this._selectedElements.push(val); + if (this._indexOfVal(val) === -1) { + this.add(val); } else { - this._selectedElements.splice(indexOfVal, 1); + this.remove(val); } - - this._dep.changed(); }, reset: function() { diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js index 8601e623..723d498b 100644 --- a/client/lib/keyboard.js +++ b/client/lib/keyboard.js @@ -3,21 +3,6 @@ // XXX There is no reason to define these shortcuts globally, they should be // attached to a template (most of them will go in the `board` template). -// Pressing `Escape` should close the last opened “element” and only the last -// one -- curently we handle popups and the card detailed view of the sidebar. -Mousetrap.bind('esc', function() { - if (currentlyOpenedForm.get() !== null) { - currentlyOpenedForm.get().close(); - - } else if (Popup.isOpen()) { - Popup.back(); - - // XXX We should have a higher level API - } else if (Session.get('currentCard')) { - Utils.goBoardId(Session.get('currentBoard')); - } -}); - Mousetrap.bind('w', function() { Sidebar.toogle(); }); @@ -48,3 +33,46 @@ Mousetrap.bind(['down', 'up'], function(evt, key) { Utils.goCardId(nextCardId); } }); + +// Pressing `Escape` should close the last opened “element” and only the last +// one. Components can register themself using a priority number (smaller is +// closed first), a condition, and an action.This is used by Popup or +// inlinedForm for instance. When we press escape we execute the action which +// condition is valid with the highest priority. +EscapeActions = { + _actions: [], + + register: function(priority, condition, action) { + // XXX Rewrite this with ES6: .push({ priority, condition, action }) + this._actions.push({ + priority: priority, + condition: condition, + action: action + }); + // XXX Rewrite this with ES6: => function + this._actions = _.sortBy(this._actions, function(a) { return a.priority; }); + }, + + executeLowest: function() { + var topActiveAction = _.find(this._actions, function(a) { + return !! a.condition(); + }); + return topActiveAction && topActiveAction.action(); + }, + + executeLowerThan: function(maxPriority) { + maxPriority = maxPriority || Infinity; + var currentAction; + for (var i = 0; i < this._actions.length; i++) { + currentAction = this._actions[i]; + if (currentAction.priority > maxPriority) + return; + if (!! currentAction.condition()) + currentAction.action(); + } + } +}; + +Mousetrap.bind('esc', function() { + EscapeActions.executeLowest(); +}); diff --git a/client/lib/popup.js b/client/lib/popup.js index 04f7dbf6..70f2660f 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -201,3 +201,7 @@ $(document).on('click', function(evt) { Popup.close(); } }); + +// Press escape to close the popup. +var bindPopup = function(f) { return _.bind(f, Popup); }; +EscapeActions.register(20, bindPopup(Popup.isOpen), bindPopup(Popup.close)); -- cgit v1.2.3-1-g7c22