Kaynağa Gözat

Click on the page to escape the last action

This is a generalization of what we had for closing a popup by
clicking outside of it. It now works for inlinedForms and detailsPane
as well.
Maxime Quandalle 10 yıl önce
ebeveyn
işleme
92dd05d06d

+ 4 - 4
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}}
 

+ 1 - 6
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); }
-);

+ 8 - 0
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'
+  }
+);

+ 1 - 1
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

+ 4 - 14
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'
+  }
 );

+ 1 - 1
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) {

+ 4 - 8
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

+ 0 - 5
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();
   },

+ 1 - 1
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}}"

+ 1 - 1
client/components/sidebar/sidebar.js

@@ -111,7 +111,7 @@ BlazeComponent.extendComponent({
           snap: false,
           snapMode: 'both',
           start: function() {
-            EscapeActions.executeLowerThan('popup');
+            EscapeActions.executeUpTo('popup');
           }
         });
       });

+ 3 - 3
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")

+ 1 - 1
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

+ 157 - 0
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');
+  }
+});

+ 0 - 69
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();
-});

+ 3 - 6
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(); }
-);

+ 7 - 18
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'
+  }
 );

+ 3 - 7
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