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.
pull/188/head
Maxime Quandalle 10 years ago
parent 1b4fcc67f4
commit 40c2411f2a
  1. 7
      .jshintrc
  2. 8
      client/components/boards/router.js
  3. 3
      client/components/cards/details.jade
  4. 22
      client/components/forms/inlinedform.js
  5. 32
      client/components/main/editor.js
  6. 8
      client/components/main/events.js
  7. 2
      client/components/main/templates.html
  8. 6
      client/components/sidebar/sidebar.jade
  9. 29
      client/components/sidebar/sidebar.js
  10. 4
      client/config/router.js
  11. 18
      client/lib/filter.js
  12. 58
      client/lib/keyboard.js
  13. 4
      client/lib/popup.js

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

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

@ -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

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

@ -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
{
match: /\B:([\-+\w]*)$/,
@ -37,4 +41,26 @@ Template.editor.rendered = function() {
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() {}
);

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

@ -12,7 +12,7 @@
</template>
<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 name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template>

@ -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

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

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

@ -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() {

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

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

Loading…
Cancel
Save