summaryrefslogtreecommitdiffstats
path: root/client/lib
diff options
context:
space:
mode:
authorMaxime Quandalle <maxime@quandalle.com>2015-06-07 10:30:27 +0200
committerMaxime Quandalle <maxime@quandalle.com>2015-06-07 10:34:19 +0200
commit92dd05d06ddeb2a9434df6038c432e6b167c1c99 (patch)
treeecb93138c1dd0ab0c1fcf2682a6eae600a4bbca9 /client/lib
parent12919cbfc6c3fd0793624776b3afb70e3a0cdd1a (diff)
downloadwekan-92dd05d06ddeb2a9434df6038c432e6b167c1c99.tar.gz
wekan-92dd05d06ddeb2a9434df6038c432e6b167c1c99.tar.bz2
wekan-92dd05d06ddeb2a9434df6038c432e6b167c1c99.zip
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.
Diffstat (limited to 'client/lib')
-rw-r--r--client/lib/escapeActions.js157
-rw-r--r--client/lib/keyboard.js69
-rw-r--r--client/lib/multiSelection.js9
-rw-r--r--client/lib/popup.js25
4 files changed, 167 insertions, 93 deletions
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'
+ }
);