mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 13:37:09 -04:00
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.
This commit is contained in:
parent
12919cbfc6
commit
92dd05d06d
17 changed files with 199 additions and 145 deletions
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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); }
|
||||
);
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,4 +1,4 @@
|
|||
.pop-over(
|
||||
.pop-over.js-pop-over(
|
||||
class="{{#unless title}}miniprofile{{/unless}}"
|
||||
class=currentBoard.colorClass
|
||||
class="{{#unless title}}no-title{{/unless}}"
|
||||
|
|
|
@ -111,7 +111,7 @@ BlazeComponent.extendComponent({
|
|||
snap: false,
|
||||
snapMode: 'both',
|
||||
start: function() {
|
||||
EscapeActions.executeLowerThan('popup');
|
||||
EscapeActions.executeUpTo('popup');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
client/lib/escapeActions.js
Normal file
157
client/lib/escapeActions.js
Normal file
|
@ -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');
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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(); }
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue