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.
This commit is contained in:
Maxime Quandalle 2015-05-26 20:30:01 +02:00
parent 1b4fcc67f4
commit 40c2411f2a
13 changed files with 148 additions and 53 deletions

View file

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

View file

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

View file

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

View file

@ -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) { // if (currentlyOpenedForm.get() !== null) {
currentlyOpenedForm.get().close(); // 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(); }
);

View file

@ -1,5 +1,9 @@
Template.editor.rendered = function() { var dropdownMenuIsOpened = false;
this.$('textarea').textcomplete([
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() {}
);

View file

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

View file

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

View file

@ -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 +Template.dynamic(template=getViewTemplate)
if Filter.isActive
+filterSidebar
else
+homeSidebar
template(name='homeSidebar') template(name='homeSidebar')
+membersWidget +membersWidget

View file

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

View file

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

View file

@ -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 (this._indexOfVal(val) === -1) {
if (indexOfVal === -1) { this.add(val);
this._selectedElements.push(val);
} else { } else {
this._selectedElements.splice(indexOfVal, 1); this.remove(val);
} }
this._dep.changed();
}, },
reset: function() { reset: function() {

View file

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

View file

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