diff options
-rw-r--r-- | client/components/boards/boardHeader.jade | 8 | ||||
-rw-r--r-- | client/components/boards/router.js | 7 | ||||
-rw-r--r-- | client/components/cards/details.js | 8 | ||||
-rw-r--r-- | client/components/forms/inlinedform.jade | 2 | ||||
-rw-r--r-- | client/components/forms/inlinedform.js | 18 | ||||
-rw-r--r-- | client/components/lists/main.js | 2 | ||||
-rw-r--r-- | client/components/main/header.styl | 12 | ||||
-rw-r--r-- | client/components/main/popup.js | 5 | ||||
-rw-r--r-- | client/components/main/popup.tpl.jade | 2 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.js | 2 | ||||
-rw-r--r-- | client/components/users/userHeader.jade | 6 | ||||
-rw-r--r-- | client/config/router.js | 2 | ||||
-rw-r--r-- | client/lib/escapeActions.js | 157 | ||||
-rw-r--r-- | client/lib/keyboard.js | 69 | ||||
-rw-r--r-- | client/lib/multiSelection.js | 9 | ||||
-rw-r--r-- | client/lib/popup.js | 25 | ||||
-rw-r--r-- | client/styles/main.styl | 10 |
17 files changed, 199 insertions, 145 deletions
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 86fbe255..258fe843 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -1,7 +1,7 @@ template(name="headerBoard") - h1.header-board-menu( - class="{{#if currentUser.isBoardMember}}is-clickable js-edit-board-title{{/if}}") - = title + h1.header-board-menu + a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}") + = title .board-header-btns.left unless isSandstorm @@ -12,7 +12,7 @@ template(name="headerBoard") if showStarCounter span {{_ 'board-nb-stars' stars}} - a.board-header-btn.js-change-visibility(class="{{#unless currentUser.isBoardAdmin}}no-edit{{/unless}}") + a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}") i.fa(class="{{#if isPublic}}fa-globe{{else}}fa-lock{{/if}}") span {{_ permission}} diff --git a/client/components/boards/router.js b/client/components/boards/router.js index e5ccecdb..1c485225 100644 --- a/client/components/boards/router.js +++ b/client/components/boards/router.js @@ -43,6 +43,7 @@ Router.route('/boards/:boardId/:slug/:cardId', { Sidebar.hide(); } }); + EscapeActions.executeUpTo('popup'); var params = this.params; Session.set('currentBoard', params.boardId); Session.set('currentCard', params.cardId); @@ -55,9 +56,3 @@ Router.route('/boards/:boardId/:slug/:cardId', { return Boards.findOne(this.params.boardId); } }); - -// Close the card details pane by pressing escape -EscapeActions.register('detailsPane', - function() { Utils.goBoardId(Session.get('currentBoard')); }, - function() { return ! Session.equals('currentCard', null); } -); diff --git a/client/components/cards/details.js b/client/components/cards/details.js index 6ab7da22..3f141622 100644 --- a/client/components/cards/details.js +++ b/client/components/cards/details.js @@ -94,3 +94,11 @@ Template.moveCardPopup.events({ Popup.close(); } }); + +// Close the card details pane by pressing escape +EscapeActions.register('detailsPane', + function() { Utils.goBoardId(Session.get('currentBoard')); }, + function() { return ! Session.equals('currentCard', null); }, { + noClickEscapeOn: '.js-card-details' + } +); diff --git a/client/components/forms/inlinedform.jade b/client/components/forms/inlinedform.jade index 5ad9039e..40e1c35c 100644 --- a/client/components/forms/inlinedform.jade +++ b/client/components/forms/inlinedform.jade @@ -1,6 +1,6 @@ template(name='inlinedForm') if isOpen.get - form(id=id class=classNames) + form.js-inlined-form(id=id class=classNames) +Template.contentBlock else +Template.elseBlock diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js index e4331892..2988738c 100644 --- a/client/components/forms/inlinedform.js +++ b/client/components/forms/inlinedform.js @@ -36,7 +36,7 @@ BlazeComponent.extendComponent({ open: function() { // Close currently opened form, if any - EscapeActions.executeLowerThan('inlinedForm'); + EscapeActions.executeUpTo('inlinedForm'); this.isOpen.set(true); currentlyOpenedForm.set(this); }, @@ -61,18 +61,6 @@ BlazeComponent.extendComponent({ 'click .js-close-inlined-form': this.close, 'click .js-open-inlined-form': this.open, - // Close the inlined form by pressing escape. - // - // Keydown (and not keypress) in necessary here because the `keyCode` - // property is consistent in all browsers, (there is not keyCode for the - // `keypress` event in firefox) - 'keydown form input, keydown form textarea': function(evt) { - if (evt.keyCode === 27) { - evt.preventDefault(); - EscapeActions.executeLowest(); - } - }, - // Pressing Ctrl+Enter should submit the form 'keydown form textarea': function(evt) { if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { @@ -98,5 +86,7 @@ BlazeComponent.extendComponent({ // Press escape to close the currently opened inlinedForm EscapeActions.register('inlinedForm', function() { currentlyOpenedForm.get().close(); }, - function() { return currentlyOpenedForm.get() !== null; } + function() { return currentlyOpenedForm.get() !== null; }, { + noClickEscapeOn: '.js-inlined-form' + } ); diff --git a/client/components/lists/main.js b/client/components/lists/main.js index bcdba7c4..2b07c3ee 100644 --- a/client/components/lists/main.js +++ b/client/components/lists/main.js @@ -50,7 +50,7 @@ BlazeComponent.extendComponent({ placeholder: 'minicard-wrapper placeholder', start: function(evt, ui) { ui.placeholder.height(ui.helper.height()); - EscapeActions.executeLowerThan('popup'); + EscapeActions.executeUpTo('popup'); boardComponent.setIsDragging(true); }, stop: function(evt, ui) { diff --git a/client/components/main/header.styl b/client/components/main/header.styl index eaf391f7..76a1bc8c 100644 --- a/client/components/main/header.styl +++ b/client/components/main/header.styl @@ -12,16 +12,15 @@ font-size: 12px display: flex - #header-user-bar + #header-user-bar, ul li color: darken(white, 17%) - a, .fa + .fa color: inherit - text-decoration: none - &:hover - color: white + a:hover, a.is-active + color: white ul flex: 1 @@ -76,9 +75,6 @@ float: left border-radius: 3px - &.is-clickable - cursor: pointer - .board-header-btns display: block margin-top: 3px diff --git a/client/components/main/popup.js b/client/components/main/popup.js index 8672d08a..8cb12dd0 100644 --- a/client/components/main/popup.js +++ b/client/components/main/popup.js @@ -18,11 +18,6 @@ function whichTransitionEvent() { var transitionEvent = whichTransitionEvent(); Popup.template.events({ - click: function(evt) { - if (evt.originalEvent) { - evt.originalEvent.clickInPopup = true; - } - }, 'click .js-back-view': function() { Popup.back(); }, diff --git a/client/components/main/popup.tpl.jade b/client/components/main/popup.tpl.jade index be528f46..e6cc982a 100644 --- a/client/components/main/popup.tpl.jade +++ b/client/components/main/popup.tpl.jade @@ -1,4 +1,4 @@ -.pop-over( +.pop-over.js-pop-over( class="{{#unless title}}miniprofile{{/unless}}" class=currentBoard.colorClass class="{{#unless title}}no-title{{/unless}}" diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index f3844fcd..cfd38c89 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -111,7 +111,7 @@ BlazeComponent.extendComponent({ snap: false, snapMode: 'both', start: function() { - EscapeActions.executeLowerThan('popup'); + EscapeActions.executeUpTo('popup'); } }); }); diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index b8201cb6..960264a9 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -1,12 +1,12 @@ template(name="headerUserBar") - a#header-user-bar - .header-user-bar-name.js-open-header-member-menu + #header-user-bar + a.header-user-bar-name.js-open-header-member-menu i.fa.fa-chevron-down if currentUser.profile.name = currentUser.profile.name else = currentUser.username - .header-user-bar-avatar.js-change-avatar + a.header-user-bar-avatar.js-change-avatar +userAvatar(user=currentUser) template(name="memberMenuPopup") diff --git a/client/config/router.js b/client/config/router.js index 8fa74bee..97871e23 100644 --- a/client/config/router.js +++ b/client/config/router.js @@ -24,7 +24,7 @@ Router.configure({ return this.redirect('atSignIn'); } - // We want to execute our EscapeActions.executeLowerThan method any time the + // We want to execute our EscapeActions.executeUpTo method any time the // route is changed, but not if the stays the same but only the parameters // change (eg when a user is navigation from a card A to a card B). Iron- // Router onBeforeAction is a reactive context (which is a bad desig choice diff --git a/client/lib/escapeActions.js b/client/lib/escapeActions.js new file mode 100644 index 00000000..3759f441 --- /dev/null +++ b/client/lib/escapeActions.js @@ -0,0 +1,157 @@ +// Pressing `Escape` should close the last opened “element” and only the last +// one. Components can register themselves using a label a condition, and an +// action. This is used by Popup or inlinedForm for instance. When we press +// escape we execute the action which have a valid condition and his the highest +// in the label hierarchy. +EscapeActions = { + _actions: [], + + // Executed in order + hierarchy: [ + 'textcomplete', + 'popup', + 'inlinedForm', + 'detailsPane', + 'multiselection', + 'sidebarView' + ], + + register: function(label, action, condition, options) { + condition = condition || function() { return true; }; + options = options || {}; + + // XXX Rewrite this with ES6: .push({ priority, condition, action }) + var priority = this.hierarchy.indexOf(label); + if (priority === -1) { + throw Error('You must define the label in the EscapeActions hierarchy'); + } + + this._actions.push({ + priority: priority, + condition: condition, + action: action, + noClickEscapeOn: options.noClickEscapeOn + }); + // XXX Rewrite this with ES6: => function + this._actions = _.sortBy(this._actions, function(a) { return a.priority; }); + }, + + executeLowest: function() { + return this._execute({ + multipleAction: false + }); + }, + + executeAll: function() { + return this._execute({ + multipleActions: true + }); + }, + + executeUpTo: function(maxLabel) { + return this._execute({ + maxLabel: maxLabel, + multipleActions: true + }); + }, + + clickExecute: function(evt, maxLabel) { + return this._execute({ + maxLabel: maxLabel, + multipleActions: false, + evt: evt + }); + }, + + _stopClick: function(action, clickTarget) { + if (! _.isString(action.noClickEscapeOn)) + return false; + else + return $(clickTarget).closest(action.noClickEscapeOn).length > 0; + }, + + _execute: function(options) { + var maxLabel = options.maxLabel; + var evt = options.evt || {}; + var multipleActions = options.multipleActions; + + var maxPriority, currentAction; + var executedAtLeastOne = false; + if (! maxLabel) + maxPriority = Infinity; + else + maxPriority = this.hierarchy.indexOf(maxLabel); + + for (var i = 0; i < this._actions.length; i++) { + currentAction = this._actions[i]; + if (currentAction.priority > maxPriority) + return executedAtLeastOne; + + if (evt.type === 'click' && this._stopClick(currentAction, evt.target)) + return executedAtLeastOne; + + if (currentAction.condition()) { + currentAction.action(evt); + executedAtLeastOne = true; + if (! multipleActions) + return executedAtLeastOne; + } + } + return executedAtLeastOne; + } +}; + +// MouseTrap plugin bindGlobal plugin. 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); +// +// source: +// https://github.com/ccampbell/mousetrap/tree/master/plugins/global-bind +var _globalCallbacks = {}; +var _originalStopCallback = Mousetrap.stopCallback; + +Mousetrap.stopCallback = function(e, element, combo, sequence) { + var self = this; + + if (self.paused) { + return true; + } + + if (_globalCallbacks[combo] || _globalCallbacks[sequence]) { + return false; + } + + return _originalStopCallback.call(self, e, element, combo); +}; + +Mousetrap.bindGlobal = function(keys, callback, action) { + var self = this; + self.bind(keys, callback, action); + + if (keys instanceof Array) { + for (var i = 0; i < keys.length; i++) { + _globalCallbacks[keys[i]] = true; + } + return; + } + + _globalCallbacks[keys] = true; +}; + +// Pressing escape to execute one escape action. We use `bindGloabal` vecause +// the shortcut sould work on textarea and inputs as well. +Mousetrap.bindGlobal('esc', function() { + EscapeActions.executeLowest(); +}); + +// On a left click on the document, we try to exectute one escape action (eg, +// close the popup). We don't execute any action if the user has clicked on a +// link or a button. +$(document).on('click', function(evt) { + if (evt.which === 1 && $(evt.target).closest('a,button').length === 0) { + EscapeActions.clickExecute(evt, 'detailsPane'); + } +}); diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js index 8b105c28..bd78390a 100644 --- a/client/lib/keyboard.js +++ b/client/lib/keyboard.js @@ -33,72 +33,3 @@ 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 themselves using a label a condition, and an -// action. This is used by Popup or inlinedForm for instance. When we press -// escape we execute the action which have a condition is valid and his the the -// highest in the label hierarchy. -EscapeActions = { - _actions: [], - - // Executed in order - hierarchy: [ - 'textcomplete', - 'popup', - 'inlinedForm', - 'multiselection-disable', - 'sidebarView', - 'detailsPane', - 'multiselection-reset' - ], - - register: function(label, action, condition) { - if (_.isUndefined(condition)) - condition = function() { return true; }; - - // XXX Rewrite this with ES6: .push({ priority, condition, action }) - var priority = this.hierarchy.indexOf(label); - if (priority === -1) { - throw Error('You must define the label in the EscapeActions hierarchy'); - } - 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(label) { - var maxPriority, currentAction; - if (! label) - maxPriority = Infinity; - else - maxPriority = this.hierarchy.indexOf(label); - - for (var i = 0; i < this._actions.length; i++) { - currentAction = this._actions[i]; - if (currentAction.priority > maxPriority) - return; - if (!! currentAction.condition()) - currentAction.action(); - } - }, - - executeAll: function() { - return this.executeLowerThan(); - } -}; - -Mousetrap.bind('esc', function() { - EscapeActions.executeLowest(); -}); diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js index 53c16da0..2f96e199 100644 --- a/client/lib/multiSelection.js +++ b/client/lib/multiSelection.js @@ -78,7 +78,7 @@ MultiSelection = { activate: function() { if (! this.isActive()) { - EscapeActions.executeLowerThan('detailsPane'); + EscapeActions.executeUpTo('detailsPane'); this._isActive.set(true); Sidebar.setView(this.sidebarView); Tracker.flush(); @@ -91,6 +91,7 @@ MultiSelection = { if (Sidebar && Sidebar.getView() === this.sidebarView) { Sidebar.setView(); } + this.reset(); } }, @@ -149,11 +150,7 @@ MultiSelection = { Blaze.registerHelper('MultiSelection', MultiSelection); -EscapeActions.register('multiselection-disable', +EscapeActions.register('multiselection', function() { MultiSelection.disable(); }, function() { return MultiSelection.isActive(); } ); - -EscapeActions.register('multiselection-reset', - function() { MultiSelection.reset(); } -); diff --git a/client/lib/popup.js b/client/lib/popup.js index fe8b581b..a1f4def2 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -40,11 +40,8 @@ Popup = { self._stack = []; openerElement = evt.currentTarget; } - $(openerElement).addClass('is-active'); - // We modify the event to prevent the popup being closed when the event - // bubble up to the document element. - evt.originalEvent.clickInPopup = true; + $(openerElement).addClass('is-active'); evt.preventDefault(); // We push our popup data to the stack. The top of the stack is always @@ -201,19 +198,11 @@ Popup = { } }; -// We automatically close a potential opened popup on any left click on the -// document. To avoid closing it unexpectedly we modify the bubbled event in -// case the click event happen in the popup or in a button that open a popup. -$(document).on('click', function(evt) { - if (evt.which === 1 && ! (evt.originalEvent && - evt.originalEvent.clickInPopup)) { - Popup.close(); - } -}); - -// Press escape to go back, or close the popup. -var bindPopup = function(f) { return _.bind(f, Popup); }; +// We close a potential opened popup on any left click on the document, or go +// one step back by pressing escape. EscapeActions.register('popup', - bindPopup(Popup.back), - bindPopup(Popup.isOpen) + function(evt) { Popup[evt.type === 'click' ? 'close' : 'back'](); }, + _.bind(Popup.isOpen, Popup), { + noClickEscapeOn: '.js-pop-over' + } ); diff --git a/client/styles/main.styl b/client/styles/main.styl index cc66576b..e6c0eb13 100644 --- a/client/styles/main.styl +++ b/client/styles/main.styl @@ -63,16 +63,12 @@ h3, h4, h5, h6 color: #aa8f09 a - color: #444 + color: inherit cursor: pointer text-decoration: none - &:hover - color: #111 - - &.disabled, - &.disabled:hover - color: #8c8c8c + &.is-disabled, + &.is-disabled:hover cursor: default text-decoration: none |