소스 검색

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.
Maxime Quandalle 10 년 전
부모
커밋
40c2411f2a

+ 4 - 3
.jshintrc

@@ -67,16 +67,17 @@
     "AccountsTemplates": true,
     "AccountsTemplates": true,
 
 
     // Our objects
     // Our objects
-    "Utils": true,
+    "EscapeActions": true,
+    "Filter": true,
+    "Mixins": true,
     "Popup": true,
     "Popup": true,
     "Filter": true,
     "Filter": true,
     "Sidebar": true,
     "Sidebar": true,
-    "Mixins": true,
+    "Utils": true,
 
 
     // XXX Temp, we should remove these
     // XXX Temp, we should remove these
     "allowIsBoardAdmin": true,
     "allowIsBoardAdmin": true,
     "allowIsBoardMember": true,
     "allowIsBoardMember": true,
-    "currentlyOpenedForm": true,
     "Emoji": true
     "Emoji": true
   }
   }
 }
 }

+ 7 - 1
client/components/boards/router.js

@@ -39,7 +39,7 @@ Router.route('/boards/:boardId/:slug/:cardId', {
   template: 'board',
   template: 'board',
   onAfterAction: function() {
   onAfterAction: function() {
     Tracker.nonreactive(function() {
     Tracker.nonreactive(function() {
-      if (! Session.get('currentCard') && typeof Sidebar !== 'undefined') {
+      if (! Session.get('currentCard') && Sidebar) {
         Sidebar.hide();
         Sidebar.hide();
       }
       }
     });
     });
@@ -55,3 +55,9 @@ Router.route('/boards/:boardId/:slug/:cardId', {
     return Boards.findOne(this.params.boardId);
     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')); }
+);

+ 2 - 1
client/components/cards/details.jade

@@ -26,7 +26,8 @@ template(name="cardDetails")
       h3 Description
       h3 Description
       +inlinedForm(classNames="js-card-description")
       +inlinedForm(classNames="js-card-description")
         i.fa.fa-times.js-close-inlined-form
         i.fa.fa-times.js-close-inlined-form
-        textarea(autofocus)= description
+        +editor(autofocus=true)
+          = description
         button(type="submit") {{_ 'edit'}}
         button(type="submit") {{_ 'edit'}}
       else
       else
         .js-open-inlined-form
         .js-open-inlined-form

+ 16 - 6
client/components/forms/inlinedform.js

@@ -15,7 +15,9 @@
 // We can only have one inlined form element opened at a time
 // 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
 // XXX Could we avoid using a global here ? This is used in Mousetrap
 // keyboard.js
 // keyboard.js
-currentlyOpenedForm = new ReactiveVar(null);
+var currentlyOpenedForm = new ReactiveVar(null);
+
+var inlinedFormEscapePriority = 30;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   template: function() {
   template: function() {
@@ -32,9 +34,10 @@ BlazeComponent.extendComponent({
 
 
   open: function() {
   open: function() {
     // Close currently opened form, if any
     // 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);
     this.isOpen.set(true);
     currentlyOpenedForm.set(this);
     currentlyOpenedForm.set(this);
   },
   },
@@ -46,7 +49,8 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   getValue: function() {
   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() {
   saveValue: function() {
@@ -66,7 +70,7 @@ BlazeComponent.extendComponent({
       'keydown form input, keydown form textarea': function(evt) {
       'keydown form input, keydown form textarea': function(evt) {
         if (evt.keyCode === 27) {
         if (evt.keyCode === 27) {
           evt.preventDefault();
           evt.preventDefault();
-          this.close();
+          EscapeActions.executeLowest();
         }
         }
       },
       },
 
 
@@ -91,3 +95,9 @@ BlazeComponent.extendComponent({
     }];
     }];
   }
   }
 }).register('inlinedForm');
 }).register('inlinedForm');
+
+// Press escape to close the currently opened inlinedForm
+EscapeActions.register(inlinedFormEscapePriority,
+  function() { return currentlyOpenedForm.get() !== null; },
+  function() { currentlyOpenedForm.get().close(); }
+);

+ 29 - 3
client/components/main/rendered.js → client/components/main/editor.js

@@ -1,5 +1,9 @@
-Template.editor.rendered = function() {
-  this.$('textarea').textcomplete([
+var dropdownMenuIsOpened = false;
+
+Template.editor.onRendered(function() {
+  var $textarea = this.$('textarea');
+
+  $textarea.textcomplete([
     // Emojies
     // Emojies
     {
     {
       match: /\B:([\-+\w]*)$/,
       match: /\B:([\-+\w]*)$/,
@@ -37,4 +41,26 @@ Template.editor.rendered = function() {
       index: 1
       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() {}
+);

+ 0 - 8
client/components/main/events.js

@@ -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();
-    }
-  }
-});

+ 1 - 1
client/components/main/templates.html

@@ -12,7 +12,7 @@
 </template>
 </template>
 
 
 <template name="editor">
 <template name="editor">
-    <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" tabindex="1">{{> UI.contentBlock }}</textarea>
+    <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" autofocus="{{autofocus}}">{{> UI.contentBlock}}</textarea>
 </template>
 </template>
 
 
 <template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template>
 <template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template>

+ 1 - 5
client/components/sidebar/sidebar.jade

@@ -4,11 +4,7 @@ template(name="sidebar")
       class="{{#if isTongueHidden}}is-hidden{{/if}}")
       class="{{#if isTongueHidden}}is-hidden{{/if}}")
       i.fa.fa-chevron-left
       i.fa.fa-chevron-left
     .sidebar-content.js-board-sidebar-content.js-perfect-scrollbar
     .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')
 template(name='homeSidebar')
   +membersWidget
   +membersWidget

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

@@ -1,3 +1,7 @@
+var defaultView = 'home';
+
+Sidebar = null;
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   template: function() {
   template: function() {
     return 'sidebar';
     return 'sidebar';
@@ -9,9 +13,14 @@ BlazeComponent.extendComponent({
 
 
   onCreated: function() {
   onCreated: function() {
     this._isOpen = new ReactiveVar(! Session.get('currentCard'));
     this._isOpen = new ReactiveVar(! Session.get('currentCard'));
+    this._view = new ReactiveVar(defaultView);
     Sidebar = this;
     Sidebar = this;
   },
   },
 
 
+  onDestroyed: function() {
+    Sidebar = null;
+  },
+
   isOpen: function() {
   isOpen: function() {
     return this._isOpen.get();
     return this._isOpen.get();
   },
   },
@@ -43,7 +52,20 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   isTongueHidden: function() {
   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() {
   onRendered: function() {
@@ -74,3 +96,8 @@ BlazeComponent.extendComponent({
     }]);
     }]);
   }
   }
 }).register('sidebar');
 }).register('sidebar');
+
+EscapeActions.register(40,
+  function() { return Sidebar && Sidebar.getView() !== defaultView; },
+  function() { Sidebar.setView(defaultView); }
+);

+ 3 - 1
client/config/router.js

@@ -20,7 +20,9 @@ Router.configure({
     // Reset default sessions
     // Reset default sessions
     Session.set('error', false);
     Session.set('error', false);
 
 
-    Popup.close();
+    Tracker.nonreactive(function() {
+      EscapeActions.executeLowerThan(40);
+    });
 
 
     this.next();
     this.next();
   }
   }

+ 10 - 8
client/lib/filter.js

@@ -4,6 +4,10 @@
 // goal is to filter complete documents by using the local filters for each
 // goal is to filter complete documents by using the local filters for each
 // fields.
 // fields.
 
 
+var showFilterSidebar = function() {
+  Sidebar.setView('filter');
+};
+
 // Use a "set" filter for a field that is a set of documents uniquely
 // Use a "set" filter for a field that is a set of documents uniquely
 // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
 // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
 var SetFilter = function() {
 var SetFilter = function() {
@@ -18,29 +22,27 @@ _.extend(SetFilter.prototype, {
   },
   },
 
 
   add: function(val) {
   add: function(val) {
-    if (this.indexOfVal(val) === -1) {
+    if (this._indexOfVal(val) === -1) {
       this._selectedElements.push(val);
       this._selectedElements.push(val);
       this._dep.changed();
       this._dep.changed();
+      showFilterSidebar();
     }
     }
   },
   },
 
 
   remove: function(val) {
   remove: function(val) {
     var indexOfVal = this._indexOfVal(val);
     var indexOfVal = this._indexOfVal(val);
-    if (this.indexOfVal(val) !== -1) {
+    if (this._indexOfVal(val) !== -1) {
       this._selectedElements.splice(indexOfVal, 1);
       this._selectedElements.splice(indexOfVal, 1);
       this._dep.changed();
       this._dep.changed();
     }
     }
   },
   },
 
 
   toogle: function(val) {
   toogle: function(val) {
-    var indexOfVal = this._indexOfVal(val);
-    if (indexOfVal === -1) {
-      this._selectedElements.push(val);
+    if (this._indexOfVal(val) === -1) {
+      this.add(val);
     } else {
     } else {
-      this._selectedElements.splice(indexOfVal, 1);
+      this.remove(val);
     }
     }
-
-    this._dep.changed();
   },
   },
 
 
   reset: function() {
   reset: function() {

+ 43 - 15
client/lib/keyboard.js

@@ -3,21 +3,6 @@
 // XXX There is no reason to define these shortcuts globally, they should be
 // 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).
 // 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() {
 Mousetrap.bind('w', function() {
   Sidebar.toogle();
   Sidebar.toogle();
 });
 });
@@ -48,3 +33,46 @@ Mousetrap.bind(['down', 'up'], function(evt, key) {
     Utils.goCardId(nextCardId);
     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();
+});

+ 4 - 0
client/lib/popup.js

@@ -201,3 +201,7 @@ $(document).on('click', function(evt) {
     Popup.close();
     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));