summaryrefslogtreecommitdiffstats
path: root/client/lib
diff options
context:
space:
mode:
Diffstat (limited to 'client/lib')
-rw-r--r--client/lib/accessibility.js41
-rw-r--r--client/lib/dropImage.js62
-rw-r--r--client/lib/filter.js4
-rw-r--r--client/lib/keyboard.js34
-rw-r--r--client/lib/modal.js4
-rw-r--r--client/lib/multiSelection.js9
-rw-r--r--client/lib/pasteImage.js57
-rw-r--r--client/lib/popup.js4
-rw-r--r--client/lib/textComplete.js54
-rw-r--r--client/lib/unsavedEdits.js2
-rw-r--r--client/lib/utils.js14
11 files changed, 260 insertions, 25 deletions
diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js
new file mode 100644
index 00000000..52b771d4
--- /dev/null
+++ b/client/lib/accessibility.js
@@ -0,0 +1,41 @@
+// In this file we define a set of DOM transformations that are specifically
+// intended for blind screen readers.
+//
+// See https://github.com/wekan/wekan/issues/337 for the general accessibility
+// considerations.
+
+// Without an href, links are non-keyboard-focusable and are not presented on
+// blind screen readers. We default to the empty anchor `#` href.
+function enforceHref(attributes) {
+ if (!_.has(attributes, 'href')) {
+ attributes.href = '#';
+ }
+ return attributes;
+}
+
+// `title` is inconsistently used on the web, and is thus inconsistently
+// presented by screen readers. `aria-label`, on the other hand, is specific to
+// accessibility and is presented in ways that title shouldn't be.
+function copyTitleInAriaLabel(attributes) {
+ if (!_.has(attributes, 'aria-label') && _.has(attributes, 'title')) {
+ attributes['aria-label'] = attributes.title;
+ }
+ return attributes;
+}
+
+// XXX Our implementation relies on overwriting Blaze virtual DOM functions,
+// which is a little bit hacky -- but still reasonable with our ES6 usage. If we
+// end up switching to React we will probably create lower level small
+// components to handle that without overwriting any build-in function.
+const {
+ A: superA,
+ I: superI,
+} = HTML;
+
+HTML.A = (attributes, ...others) => {
+ return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others);
+};
+
+HTML.I = (attributes, ...others) => {
+ return superI(copyTitleInAriaLabel(attributes), ...others);
+};
diff --git a/client/lib/dropImage.js b/client/lib/dropImage.js
new file mode 100644
index 00000000..592d5c8f
--- /dev/null
+++ b/client/lib/dropImage.js
@@ -0,0 +1,62 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/drop.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('drag') === 0 || event.type.indexOf('drop') === 0) {
+ event.dataTransfer = event.originalEvent.dataTransfer;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.dropImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ const stopFn = function(event) {
+ event.stopPropagation();
+ return event.preventDefault();
+ };
+ return this.each(function() {
+ const element = this;
+ $(element).bind('dragenter dragover dragleave', stopFn);
+ return $(element).bind('drop', function(event) {
+ stopFn(event);
+ const files = event.dataTransfer.files;
+ for(let i=0; i<files.length; i++) {
+ const f = files[i];
+ if(f.type.match(options.matchType)) {
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/filter.js b/client/lib/filter.js
index f7baf480..74305284 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -95,7 +95,7 @@ Filter = {
return {};
const filterSelector = {};
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
if (filter._isActive())
filterSelector[fieldName] = filter._getMongoSelector();
@@ -116,7 +116,7 @@ Filter = {
},
reset() {
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
filter.reset();
});
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index af5fb7a2..f8212c9b 100644
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -23,6 +23,14 @@ Mousetrap.bind('x', () => {
}
});
+Mousetrap.bind('f', () => {
+ if (Sidebar.isOpen() && Sidebar.getView() === 'filter') {
+ Sidebar.toggle();
+ } else {
+ Sidebar.setView('filter');
+ }
+});
+
Mousetrap.bind(['down', 'up'], (evt, key) => {
if (!Session.get('currentCard')) {
return;
@@ -36,6 +44,26 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
}
});
+// XXX This shortcut should also work when hovering over a card in board view
+Mousetrap.bind('space', (evt) => {
+ if (!Session.get('currentCard')) {
+ return;
+ }
+
+ const currentUserId = Meteor.userId();
+ if (currentUserId === null) {
+ return;
+ }
+
+ if (Meteor.user().isBoardMember()) {
+ const card = Cards.findOne(Session.get('currentCard'));
+ card.toggleMember(currentUserId);
+ // We should prevent scrolling in card when spacebar is clicked
+ // This should do it according to Mousetrap docs, but it doesn't
+ evt.preventDefault();
+ }
+});
+
Template.keyboardShortcuts.helpers({
mapping: [{
keys: ['W'],
@@ -44,6 +72,9 @@ Template.keyboardShortcuts.helpers({
keys: ['Q'],
action: 'shortcut-filter-my-cards',
}, {
+ keys: ['F'],
+ action: 'shortcut-toggle-filterbar',
+ }, {
keys: ['X'],
action: 'shortcut-clear-filters',
}, {
@@ -58,5 +89,8 @@ Template.keyboardShortcuts.helpers({
}, {
keys: [':'],
action: 'shortcut-autocomplete-emojies',
+ }, {
+ keys: ['SPACE'],
+ action: 'shortcut-assign-self',
}],
});
diff --git a/client/lib/modal.js b/client/lib/modal.js
index 5b3392b2..e6301cb5 100644
--- a/client/lib/modal.js
+++ b/client/lib/modal.js
@@ -21,9 +21,9 @@ window.Modal = new class {
}
}
- open(modalName, options) {
+ open(modalName, { onCloseGoTo = ''} = {}) {
this._currentModal.set(modalName);
- this._onCloseGoTo = options && options.onCloseGoTo || '';
+ this._onCloseGoTo = onCloseGoTo;
}
};
diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js
index c2bb2bbc..eeb2015d 100644
--- a/client/lib/multiSelection.js
+++ b/client/lib/multiSelection.js
@@ -119,12 +119,13 @@ MultiSelection = {
}
},
- toggle(cardIds, options) {
+ toggle(cardIds, options = {}) {
cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
- options = _.extend({
+ options = {
add: true,
remove: true,
- }, options || {});
+ ...options,
+ };
if (!this.isActive()) {
this.reset();
@@ -133,7 +134,7 @@ MultiSelection = {
const selectedCards = this._selectedCards.get();
- _.each(cardIds, (cardId) => {
+ cardIds.forEach((cardId) => {
const indexOfCard = selectedCards.indexOf(cardId);
if (options.remove && indexOfCard > -1)
diff --git a/client/lib/pasteImage.js b/client/lib/pasteImage.js
new file mode 100644
index 00000000..264d77ac
--- /dev/null
+++ b/client/lib/pasteImage.js
@@ -0,0 +1,57 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/paste.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('copy') === 0 || event.type.indexOf('paste') === 0) {
+ event.clipboardData = event.originalEvent.clipboardData;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.pasteImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ return this.each(function() {
+ const element = this;
+ return $(element).bind('paste', function(event) {
+ const types = event.clipboardData.types;
+ const items = event.clipboardData.items;
+ for(let i=0; i<types.length; i++) {
+ if(types[i].match(options.matchType) || items[i].type.match(options.matchType)) {
+ const f = items[i].getAsFile();
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 3c39af29..7418d938 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -91,7 +91,7 @@ window.Popup = new class {
if (!self.isOpen()) {
self.current = Blaze.renderWithData(self.template, () => {
self._dep.depend();
- return _.extend(self._getTopStack(), { stack: self._stack });
+ return { ...self._getTopStack(), stack: self._stack };
}, document.body);
} else {
@@ -191,7 +191,7 @@ window.Popup = new class {
// We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape.
const escapeActions = ['back', 'close'];
-_.each(escapeActions, (actionName) => {
+escapeActions.forEach((actionName) => {
EscapeActions.register(`popup-${actionName}`,
() => Popup[actionName](),
() => Popup.isOpen(),
diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js
new file mode 100644
index 00000000..3e69d07f
--- /dev/null
+++ b/client/lib/textComplete.js
@@ -0,0 +1,54 @@
+// We “inherit” the jquery-textcomplete plugin to integrate with our
+// EscapeActions system. You should always use `escapeableTextComplete` instead
+// of the vanilla `textcomplete`.
+let dropdownMenuIsOpened = false;
+
+$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
+ // When the autocomplete menu is shown we want both a press of both `Tab`
+ // or `Enter` to validation the auto-completion. We also need to stop the
+ // event propagation to prevent EscapeActions side effect, for instance the
+ // minicard submission (on `Enter`) or going on the next column (on `Tab`).
+ options = {
+ onKeydown(evt, commands) {
+ if (evt.keyCode === 9 || evt.keyCode === 13) {
+ evt.stopPropagation();
+ return commands.KEY_ENTER;
+ }
+ },
+ ...options,
+ };
+
+ // Proxy to the vanilla jQuery component
+ this.textcomplete(strategies, options, ...otherArgs);
+
+ // 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).
+ this.on({
+ 'textComplete:show'() {
+ dropdownMenuIsOpened = true;
+ },
+ 'textComplete:hide'() {
+ Tracker.afterFlush(() => {
+ // XXX Hack. We unfortunately need to set a setTimeout here to make the
+ // `noClickEscapeOn` work bellow, otherwise clicking on a autocomplete
+ // item will close both the autocomplete menu (as expected) but also the
+ // next item in the stack (for example the minicard editor) which we
+ // don't want.
+ setTimeout(() => {
+ dropdownMenuIsOpened = false;
+ }, 100);
+ });
+ },
+ });
+};
+
+EscapeActions.register('textcomplete',
+ () => {},
+ () => dropdownMenuIsOpened, {
+ noClickEscapeOn: '.textcomplete-dropdown',
+ }
+);
diff --git a/client/lib/unsavedEdits.js b/client/lib/unsavedEdits.js
index dc267bfb..17bb29b5 100644
--- a/client/lib/unsavedEdits.js
+++ b/client/lib/unsavedEdits.js
@@ -65,7 +65,7 @@ UnsavedEdits = {
};
Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
- // Workaround some blaze feature that ass a list of keywords arguments as the
+ // Workaround some blaze feature that pass a list of keywords arguments as the
// last parameter (even if the caller didn't specify any).
if (!_.isString(defaultTo)) {
defaultTo = '';
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 0cd93419..6bdd5822 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -22,20 +22,6 @@ Utils = {
return string.charAt(0).toUpperCase() + string.slice(1);
},
- getLabelIndex(boardId, labelId) {
- const board = Boards.findOne(boardId);
- const labels = {};
- _.each(board.labels, (a, b) => {
- labels[a._id] = b;
- });
- return {
- index: labels[labelId],
- key(key) {
- return `labels.${labels[labelId]}.${key}`;
- },
- };
- },
-
// Determine the new sort index
calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
let base, increment;