mirror of
https://github.com/wekan/wekan.git
synced 2026-03-10 15:42:34 +01:00
Merge pull request #6082 from harryadel/mousetrap-migration
Replace mousetrap
This commit is contained in:
commit
8586fa8ce0
7 changed files with 236 additions and 147 deletions
|
|
@ -55,9 +55,6 @@ http@2.0.0! # force new http package
|
||||||
# UI components
|
# UI components
|
||||||
ostrio:i18n
|
ostrio:i18n
|
||||||
reactive-var@1.0.12
|
reactive-var@1.0.12
|
||||||
mousetrap:mousetrap
|
|
||||||
mquandalle:jquery-textcomplete
|
|
||||||
mquandalle:mousetrap-bindglobal
|
|
||||||
templates:tabs
|
templates:tabs
|
||||||
meteor-autosize
|
meteor-autosize
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,10 @@ mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
mousetrap:mousetrap@1.4.6_1
|
|
||||||
mquandalle:autofocus@1.0.0
|
mquandalle:autofocus@1.0.0
|
||||||
mquandalle:collection-mutations@0.1.0
|
mquandalle:collection-mutations@0.1.0
|
||||||
mquandalle:jade@0.4.9
|
mquandalle:jade@0.4.9
|
||||||
mquandalle:jade-compiler@0.4.5
|
mquandalle:jade-compiler@0.4.5
|
||||||
mquandalle:jquery-textcomplete@0.8.0_1
|
|
||||||
mquandalle:mousetrap-bindglobal@0.0.1
|
|
||||||
msavin:usercache@1.8.0
|
msavin:usercache@1.8.0
|
||||||
npm-mongo@4.17.2
|
npm-mongo@4.17.2
|
||||||
oauth@2.2.1
|
oauth@2.2.1
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
// Pressing `Escape` should close the last opened “element” and only the last
|
const hotkeys = require('hotkeys-js').default;
|
||||||
|
|
||||||
|
// Pressing `Escape` should close the last opened "element" and only the last
|
||||||
// one. Components can register themselves using a label a condition, and an
|
// 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
|
// 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
|
// escape we execute the action which have a valid condition and his the highest
|
||||||
|
|
@ -119,9 +121,9 @@ EscapeActions = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pressing escape to execute one escape action. We use `bindGloabal` vecause
|
// Pressing escape to execute one escape action. ESC is allowed globally
|
||||||
// the shortcut sould work on textarea and inputs as well.
|
// in the hotkeys filter (keyboard.js) so it works in textarea and inputs.
|
||||||
Mousetrap.bindGlobal('esc', () => {
|
hotkeys('escape', () => {
|
||||||
EscapeActions.executeLowest();
|
EscapeActions.executeLowest();
|
||||||
Sidebar.hide();
|
Sidebar.hide();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,42 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||||
|
const hotkeys = require('hotkeys-js').default;
|
||||||
|
|
||||||
// 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).
|
||||||
|
|
||||||
|
// Configure hotkeys filter (replaces Mousetrap.stopCallback)
|
||||||
|
// CRITICAL: Return values are INVERTED from Mousetrap's stopCallback
|
||||||
|
// hotkeys filter: true = ALLOW shortcut, false = STOP shortcut
|
||||||
|
hotkeys.filter = (event) => {
|
||||||
|
// Are shortcuts enabled for the user?
|
||||||
|
if (ReactiveCache.getCurrentUser() && !ReactiveCache.getCurrentUser().isKeyboardShortcuts())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Always handle escape
|
||||||
|
if (event.keyCode === 27)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Make sure there are no selected characters
|
||||||
|
if (window.getSelection().type === "Range")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Decide what the current element is
|
||||||
|
const currentElement = event.target || document.activeElement;
|
||||||
|
|
||||||
|
// If the current element is editable, we don't want to trigger an event
|
||||||
|
if (currentElement.isContentEditable)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Make sure we are not in an input element
|
||||||
|
if (currentElement instanceof HTMLInputElement || currentElement instanceof HTMLSelectElement || currentElement instanceof HTMLTextAreaElement)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// We can trigger events!
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle non-Latin keyboards
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// Only handle event if coming from body
|
// Only handle event if coming from body
|
||||||
if (e.target !== document.body) return;
|
if (e.target !== document.body) return;
|
||||||
|
|
@ -11,39 +44,19 @@ window.addEventListener('keydown', (e) => {
|
||||||
// Only handle event if it's in another language
|
// Only handle event if it's in another language
|
||||||
if (String.fromCharCode(e.which).toLowerCase() === e.key) return;
|
if (String.fromCharCode(e.which).toLowerCase() === e.key) return;
|
||||||
|
|
||||||
// Trigger the corresponding action
|
// Trigger the corresponding action by dispatching a new event with the ASCII key
|
||||||
Mousetrap.handleKey(String.fromCharCode(e.which).toLowerCase(), [], {type: "keypress"});
|
const key = String.fromCharCode(e.which).toLowerCase();
|
||||||
|
// Create a synthetic event for hotkeys to handle
|
||||||
|
const syntheticEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: key,
|
||||||
|
keyCode: e.which,
|
||||||
|
which: e.which,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(syntheticEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overwrite the stopCallback to allow for more keyboard shortcut customizations
|
|
||||||
Mousetrap.stopCallback = (event, element) => {
|
|
||||||
// Are shortcuts enabled for the user?
|
|
||||||
if (ReactiveCache.getCurrentUser() && !ReactiveCache.getCurrentUser().isKeyboardShortcuts())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Always handle escape
|
|
||||||
if (event.keyCode === 27)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Make sure there are no selected characters
|
|
||||||
if (window.getSelection().type === "Range")
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Decide what the current element is
|
|
||||||
const currentElement = event.target || document.activeElement;
|
|
||||||
|
|
||||||
// If the current element is editable, we don't want to trigger an event
|
|
||||||
if (currentElement.isContentEditable)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Make sure we are not in an input element
|
|
||||||
if (currentElement instanceof HTMLInputElement || currentElement instanceof HTMLSelectElement || currentElement instanceof HTMLTextAreaElement)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// We can trigger events!
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHoveredCardId() {
|
function getHoveredCardId() {
|
||||||
const card = $('.js-minicard:hover').get(0);
|
const card = $('.js-minicard:hover').get(0);
|
||||||
if (!card) return null;
|
if (!card) return null;
|
||||||
|
|
@ -54,11 +67,13 @@ function getSelectedCardId() {
|
||||||
return Session.get('currentCard') || Session.get('selectedCard') || getHoveredCardId();
|
return Session.get('currentCard') || Session.get('selectedCard') || getHoveredCardId();
|
||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind('?', () => {
|
hotkeys('?', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
FlowRouter.go('shortcuts');
|
FlowRouter.go('shortcuts');
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('w', () => {
|
hotkeys('w', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
if (Sidebar.isOpen() && Sidebar.getView() === 'home') {
|
if (Sidebar.isOpen() && Sidebar.getView() === 'home') {
|
||||||
Sidebar.toggle();
|
Sidebar.toggle();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -66,7 +81,8 @@ Mousetrap.bind('w', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('q', () => {
|
hotkeys('q', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const currentBoardId = Session.get('currentBoard');
|
const currentBoardId = Session.get('currentBoard');
|
||||||
const currentUserId = Meteor.userId();
|
const currentUserId = Meteor.userId();
|
||||||
if (currentBoardId && currentUserId) {
|
if (currentBoardId && currentUserId) {
|
||||||
|
|
@ -74,7 +90,8 @@ Mousetrap.bind('q', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('a', () => {
|
hotkeys('a', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const currentBoardId = Session.get('currentBoard');
|
const currentBoardId = Session.get('currentBoard');
|
||||||
const currentUserId = Meteor.userId();
|
const currentUserId = Meteor.userId();
|
||||||
if (currentBoardId && currentUserId) {
|
if (currentBoardId && currentUserId) {
|
||||||
|
|
@ -82,13 +99,15 @@ Mousetrap.bind('a', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('x', () => {
|
hotkeys('x', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
if (Filter.isActive()) {
|
if (Filter.isActive()) {
|
||||||
Filter.reset();
|
Filter.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('f', () => {
|
hotkeys('f', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
if (Sidebar.isOpen() && Sidebar.getView() === 'filter') {
|
if (Sidebar.isOpen() && Sidebar.getView() === 'filter') {
|
||||||
Sidebar.toggle();
|
Sidebar.toggle();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -96,7 +115,8 @@ Mousetrap.bind('f', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('/', () => {
|
hotkeys('/', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
if (Sidebar.isOpen() && Sidebar.getView() === 'search') {
|
if (Sidebar.isOpen() && Sidebar.getView() === 'search') {
|
||||||
Sidebar.toggle();
|
Sidebar.toggle();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -104,12 +124,13 @@ Mousetrap.bind('/', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind(['down', 'up'], (evt, key) => {
|
hotkeys('down,up', (event, handler) => {
|
||||||
|
event.preventDefault();
|
||||||
if (!Utils.getCurrentCardId()) {
|
if (!Utils.getCurrentCardId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextFunc = key === 'down' ? 'next' : 'prev';
|
const nextFunc = handler.key === 'down' ? 'next' : 'prev';
|
||||||
const nextCard = $('.js-minicard.is-selected')
|
const nextCard = $('.js-minicard.is-selected')
|
||||||
[nextFunc]('.js-minicard')
|
[nextFunc]('.js-minicard')
|
||||||
.get(0);
|
.get(0);
|
||||||
|
|
@ -119,49 +140,47 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
numbArray = _.range(1,10).map(x => 'shift+'+String(x))
|
// Shift + number keys to remove labels in multiselect
|
||||||
Mousetrap.bind(numbArray, (evt, key) => {
|
const shiftNums = _.range(1, 10).map(x => `shift+${x}`).join(',');
|
||||||
num = parseInt(key.substr(6, key.length));
|
hotkeys(shiftNums, (event, handler) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const num = parseInt(handler.key.split('+')[1]);
|
||||||
const currentUserId = Meteor.userId();
|
const currentUserId = Meteor.userId();
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentBoardId = Session.get('currentBoard');
|
const currentBoardId = Session.get('currentBoard');
|
||||||
board = ReactiveCache.getBoard(currentBoardId);
|
const board = ReactiveCache.getBoard(currentBoardId);
|
||||||
labels = board.labels;
|
const labels = board.labels;
|
||||||
if(MultiSelection.isActive())
|
if (MultiSelection.isActive()) {
|
||||||
{
|
|
||||||
const cardIds = MultiSelection.getSelectedCardIds();
|
const cardIds = MultiSelection.getSelectedCardIds();
|
||||||
for (const cardId of cardIds)
|
for (const cardId of cardIds) {
|
||||||
{
|
const card = Cards.findOne(cardId);
|
||||||
card = Cards.findOne(cardId);
|
if (num <= board.labels.length) {
|
||||||
if(num <= board.labels.length)
|
card.removeLabel(labels[num - 1]["_id"]);
|
||||||
{
|
|
||||||
card.removeLabel(labels[num-1]["_id"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
numArray = _.range(1,10).map(x => String(x))
|
// Number keys to toggle labels
|
||||||
Mousetrap.bind(numArray, (evt, key) => {
|
const nums = _.range(1, 10).join(',');
|
||||||
num = parseInt(key);
|
hotkeys(nums, (event, handler) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const num = parseInt(handler.key);
|
||||||
const currentUserId = Meteor.userId();
|
const currentUserId = Meteor.userId();
|
||||||
const currentBoardId = Session.get('currentBoard');
|
const currentBoardId = Session.get('currentBoard');
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
board = ReactiveCache.getBoard(currentBoardId);
|
const board = ReactiveCache.getBoard(currentBoardId);
|
||||||
labels = board.labels;
|
const labels = board.labels;
|
||||||
if(MultiSelection.isActive() && ReactiveCache.getCurrentUser().isBoardMember())
|
if (MultiSelection.isActive() && ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||||
{
|
|
||||||
const cardIds = MultiSelection.getSelectedCardIds();
|
const cardIds = MultiSelection.getSelectedCardIds();
|
||||||
for (const cardId of cardIds)
|
for (const cardId of cardIds) {
|
||||||
{
|
const card = Cards.findOne(cardId);
|
||||||
card = Cards.findOne(cardId);
|
if (num <= board.labels.length) {
|
||||||
if(num <= board.labels.length)
|
card.addLabel(labels[num - 1]["_id"]);
|
||||||
{
|
|
||||||
card.addLabel(labels[num-1]["_id"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -173,14 +192,16 @@ Mousetrap.bind(numArray, (evt, key) => {
|
||||||
}
|
}
|
||||||
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||||
const card = Cards.findOne(cardId);
|
const card = Cards.findOne(cardId);
|
||||||
if(num <= board.labels.length)
|
if (num <= board.labels.length) {
|
||||||
{
|
card.toggleLabel(labels[num - 1]["_id"]);
|
||||||
card.toggleLabel(labels[num-1]["_id"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind(_.range(1, 10).map(x => `ctrl+alt+${x}`), (evt, key) => {
|
// Ctrl+Alt + number keys to toggle assignees
|
||||||
|
const ctrlAltNums = _.range(1, 10).map(x => `ctrl+alt+${x}`).join(',');
|
||||||
|
hotkeys(ctrlAltNums, (event, handler) => {
|
||||||
|
event.preventDefault();
|
||||||
// Make sure the current user is defined
|
// Make sure the current user is defined
|
||||||
if (!ReactiveCache.getCurrentUser())
|
if (!ReactiveCache.getCurrentUser())
|
||||||
return;
|
return;
|
||||||
|
|
@ -189,7 +210,7 @@ Mousetrap.bind(_.range(1, 10).map(x => `ctrl+alt+${x}`), (evt, key) => {
|
||||||
if (!ReactiveCache.getCurrentUser().isBoardMember())
|
if (!ReactiveCache.getCurrentUser().isBoardMember())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const memberIndex = parseInt(key.split("+").pop()) - 1;
|
const memberIndex = parseInt(handler.key.split("+").pop()) - 1;
|
||||||
const currentBoard = Utils.getCurrentBoard();
|
const currentBoard = Utils.getCurrentBoard();
|
||||||
const validBoardMembers = currentBoard.memberUsers().filter(member => member.isBoardMember());
|
const validBoardMembers = currentBoard.memberUsers().filter(member => member.isBoardMember());
|
||||||
|
|
||||||
|
|
@ -211,7 +232,8 @@ Mousetrap.bind(_.range(1, 10).map(x => `ctrl+alt+${x}`), (evt, key) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('m', evt => {
|
hotkeys('m', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const cardId = getSelectedCardId();
|
const cardId = getSelectedCardId();
|
||||||
if (!cardId) {
|
if (!cardId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -225,13 +247,11 @@ Mousetrap.bind('m', evt => {
|
||||||
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||||
const card = Cards.findOne(cardId);
|
const card = Cards.findOne(cardId);
|
||||||
card.toggleAssignee(currentUserId);
|
card.toggleAssignee(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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind('space', evt => {
|
hotkeys('space', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const cardId = getSelectedCardId();
|
const cardId = getSelectedCardId();
|
||||||
if (!cardId) {
|
if (!cardId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -245,13 +265,11 @@ Mousetrap.bind('space', evt => {
|
||||||
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
if (ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||||
const card = Cards.findOne(cardId);
|
const card = Cards.findOne(cardId);
|
||||||
card.toggleMember(currentUserId);
|
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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const archiveCard = evt => {
|
const archiveCard = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const cardId = getSelectedCardId();
|
const cardId = getSelectedCardId();
|
||||||
if (!cardId) {
|
if (!cardId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -265,21 +283,19 @@ const archiveCard = evt => {
|
||||||
if (Utils.canModifyBoard()) {
|
if (Utils.canModifyBoard()) {
|
||||||
const card = Cards.findOne(cardId);
|
const card = Cards.findOne(cardId);
|
||||||
card.archive();
|
card.archive();
|
||||||
// We should prevent scrolling in card when spacebar is clicked
|
|
||||||
// This should do it according to Mousetrap docs, but it doesn't
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Archive card has multiple shortcuts
|
// Archive card has multiple shortcuts
|
||||||
Mousetrap.bind('c', archiveCard);
|
hotkeys('c', archiveCard);
|
||||||
Mousetrap.bind('-', archiveCard);
|
hotkeys('-', archiveCard);
|
||||||
|
|
||||||
// Same as above, this time for Persian keyboard.
|
// Same as above, this time for Persian keyboard.
|
||||||
// https://github.com/wekan/wekan/pull/5589#issuecomment-2516776519
|
// https://github.com/wekan/wekan/pull/5589#issuecomment-2516776519
|
||||||
Mousetrap.bind('÷', archiveCard);
|
hotkeys('\xf7', archiveCard);
|
||||||
|
|
||||||
Mousetrap.bind('n', evt => {
|
hotkeys('n', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const cardId = getSelectedCardId();
|
const cardId = getSelectedCardId();
|
||||||
if (!cardId) {
|
if (!cardId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -296,10 +312,6 @@ Mousetrap.bind('n', evt => {
|
||||||
|
|
||||||
// Find the button and click it
|
// Find the button and click it
|
||||||
$(`#js-list-${card.listId} .list-body .minicards .open-minicard-composer`).click();
|
$(`#js-list-${card.listId} .list-body .minicards .open-minicard-composer`).click();
|
||||||
|
|
||||||
// We should prevent scrolling in card when spacebar is clicked
|
|
||||||
// This should do it according to Mousetrap docs, but it doesn't
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -354,7 +366,7 @@ Template.keyboardShortcuts.helpers({
|
||||||
action: 'shortcut-assign-self',
|
action: 'shortcut-assign-self',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: ['c', '÷', '-'],
|
keys: ['c', '\xf7', '-'],
|
||||||
action: 'archive-card',
|
action: 'archive-card',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,80 @@
|
||||||
// We “inherit” the jquery-textcomplete plugin to integrate with our
|
// We use @textcomplete packages to integrate with our EscapeActions system.
|
||||||
// EscapeActions system. You should always use `escapeableTextComplete` instead
|
// You should always use `createEscapeableTextComplete` or the jQuery extension
|
||||||
// of the vanilla `textcomplete`.
|
// `escapeableTextComplete` instead of the vanilla textcomplete.
|
||||||
|
import { Textcomplete } from '@textcomplete/core';
|
||||||
|
import { TextareaEditor } from '@textcomplete/textarea';
|
||||||
|
import { ContenteditableEditor } from '@textcomplete/contenteditable';
|
||||||
|
|
||||||
let dropdownMenuIsOpened = false;
|
let dropdownMenuIsOpened = false;
|
||||||
|
|
||||||
$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
|
/**
|
||||||
// When the autocomplete menu is shown we want both a press of both `Tab`
|
* Create an escapeable textcomplete instance for a textarea or contenteditable element
|
||||||
// or `Enter` to validation the auto-completion. We also need to stop the
|
* @param {HTMLTextAreaElement|HTMLElement} element - The target element
|
||||||
// event propagation to prevent EscapeActions side effect, for instance the
|
* @param {Array} strategies - Array of strategy objects
|
||||||
// minicard submission (on `Enter`) or going on the next column (on `Tab`).
|
* @param {Object} options - Additional options
|
||||||
options = {
|
* @returns {Textcomplete} The textcomplete instance
|
||||||
onKeydown(evt, commands) {
|
*/
|
||||||
if (evt.keyCode === 9 || evt.keyCode === 13) {
|
export function createEscapeableTextComplete(element, strategies, options = {}) {
|
||||||
evt.stopPropagation();
|
// Determine the appropriate editor based on element type
|
||||||
return commands.KEY_ENTER;
|
const isContentEditable = element.isContentEditable || element.contentEditable === 'true';
|
||||||
}
|
const Editor = isContentEditable ? ContenteditableEditor : TextareaEditor;
|
||||||
return null;
|
|
||||||
|
const editor = new Editor(element);
|
||||||
|
|
||||||
|
// Merge default options
|
||||||
|
const mergedOptions = {
|
||||||
|
dropdown: {
|
||||||
|
className: 'textcomplete-dropdown',
|
||||||
|
maxCount: 10,
|
||||||
|
placement: 'bottom',
|
||||||
|
...options.dropdown,
|
||||||
},
|
},
|
||||||
...options,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Proxy to the vanilla jQuery component
|
const textcomplete = new Textcomplete(editor, strategies, mergedOptions);
|
||||||
this.textcomplete(strategies, options, ...otherArgs);
|
|
||||||
|
|
||||||
// Since commit d474017 jquery-textComplete automatically closes a potential
|
// When the autocomplete menu is shown we want both a press of both `Tab`
|
||||||
// opened dropdown menu when the user press Escape. This behavior conflicts
|
// or `Enter` to validate the auto-completion. We also need to stop the
|
||||||
// with our EscapeActions system, but it's too complicated and hacky to
|
// event propagation to prevent EscapeActions side effect, for instance the
|
||||||
// monkey-pach textComplete to disable it -- I tried. Instead we listen to
|
// minicard submission (on `Enter`) or going on the next column (on `Tab`).
|
||||||
// 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
|
element.addEventListener('keydown', (evt) => {
|
||||||
// is opened (and rely on textComplete to execute the actual action).
|
if (dropdownMenuIsOpened && (evt.keyCode === 9 || evt.keyCode === 13)) {
|
||||||
this.on({
|
evt.stopPropagation();
|
||||||
'textComplete:show'() {
|
}
|
||||||
dropdownMenuIsOpened = true;
|
});
|
||||||
},
|
|
||||||
'textComplete:select'() {
|
// Track dropdown state for EscapeActions integration
|
||||||
EscapeActions.preventNextClick();
|
// Since @textcomplete automatically closes when Escape is pressed, we
|
||||||
},
|
// integrate with our EscapeActions system by tracking open/close state.
|
||||||
'textComplete:hide'() {
|
textcomplete.on('show', () => {
|
||||||
Tracker.afterFlush(() => {
|
dropdownMenuIsOpened = true;
|
||||||
// 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
|
textcomplete.on('selected', () => {
|
||||||
// next item in the stack (for example the minicard editor) which we
|
EscapeActions.preventNextClick();
|
||||||
// don't want.
|
});
|
||||||
setTimeout(() => {
|
|
||||||
dropdownMenuIsOpened = false;
|
textcomplete.on('hidden', () => {
|
||||||
}, 100);
|
Tracker.afterFlush(() => {
|
||||||
});
|
// XXX Hack. We unfortunately need to set a setTimeout here to make the
|
||||||
},
|
// `noClickEscapeOn` work below, 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return textcomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
// jQuery extension for backward compatibility
|
||||||
|
$.fn.escapeableTextComplete = function(strategies, options = {}) {
|
||||||
|
return this.each(function() {
|
||||||
|
createEscapeableTextComplete(this, strategies, options);
|
||||||
});
|
});
|
||||||
return this;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EscapeActions.register(
|
EscapeActions.register(
|
||||||
|
|
|
||||||
51
package-lock.json
generated
51
package-lock.json
generated
|
|
@ -118,6 +118,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@textcomplete/contenteditable": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/contenteditable/-/contenteditable-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-O2BNqtvP0I1lL8WIwJ/ilCVi6rEJu2Jtj7Nnx8+XSN66aoBV5pdl0c1IXFfNvGU5kJh+6EOxkDEmm2NhYCIXlw==",
|
||||||
|
"requires": {
|
||||||
|
"@textcomplete/utils": "^0.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@textcomplete/core": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/core/-/core-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-C4S+ihQU5HsKQ/TbsmS0e7hfPZtLZbEXj5NDUgRnhu/1Nezpu892bjNZGeErZm+R8iyDIT6wDu6EgIhng4M8eQ==",
|
||||||
|
"requires": {
|
||||||
|
"eventemitter3": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@textcomplete/textarea": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/textarea/-/textarea-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-GNathnXpV361YuZrBVXvVqFYZ5NQZsjGC7Bt2sCUA/RTWlIgxHxC0ruDChYyRDx4siQZiZZOO5pWz+z1x8pZFQ==",
|
||||||
|
"requires": {
|
||||||
|
"@textcomplete/utils": "^0.1.13",
|
||||||
|
"textarea-caret": "^3.1.0",
|
||||||
|
"undate": "^0.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@textcomplete/utils": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@textcomplete/utils/-/utils-0.1.13.tgz",
|
||||||
|
"integrity": "sha512-5UW9Ee0WEX1s9K8MFffo5sfUjYm3YVhtqRhAor/ih7p0tnnpaMB7AwMRDKwhSIQL6O+g1fmEkxCeO8WqjPzjUA=="
|
||||||
|
},
|
||||||
"@tokenizer/token": {
|
"@tokenizer/token": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
|
|
@ -724,6 +755,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||||
},
|
},
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="
|
||||||
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
|
@ -907,6 +943,11 @@
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hotkeys-js": {
|
||||||
|
"version": "3.13.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.15.tgz",
|
||||||
|
"integrity": "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg=="
|
||||||
|
},
|
||||||
"htmlparser2": {
|
"htmlparser2": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||||
|
|
@ -2812,6 +2853,11 @@
|
||||||
"readable-stream": "^3.1.1"
|
"readable-stream": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"textarea-caret": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q=="
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||||
|
|
@ -2884,6 +2930,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||||
},
|
},
|
||||||
|
"undate": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undate/-/undate-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ssH8QTNBY6B+2fRr3stSQ+9m2NT8qTaun3ExTx5ibzYQvP7yX4+BnX0McNxFCvh6S5ia/DYu6bsCKQx/U4nb/Q=="
|
||||||
|
},
|
||||||
"unzipper": {
|
"unzipper": {
|
||||||
"version": "0.10.14",
|
"version": "0.10.14",
|
||||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
|
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@
|
||||||
"@mapbox/node-pre-gyp": "^2.0.3",
|
"@mapbox/node-pre-gyp": "^2.0.3",
|
||||||
"@meteorjs/reify": "^0.25.4",
|
"@meteorjs/reify": "^0.25.4",
|
||||||
"@rwap/jquery-ui-touch-punch": "^1.0.11",
|
"@rwap/jquery-ui-touch-punch": "^1.0.11",
|
||||||
|
"@textcomplete/contenteditable": "^0.1.13",
|
||||||
|
"@textcomplete/core": "^0.1.13",
|
||||||
|
"@textcomplete/textarea": "^0.1.13",
|
||||||
"@wekanteam/dragscroll": "^0.0.9",
|
"@wekanteam/dragscroll": "^0.0.9",
|
||||||
"@wekanteam/exceljs": "^4.6.0",
|
"@wekanteam/exceljs": "^4.6.0",
|
||||||
"@wekanteam/html-to-markdown": "^1.0.2",
|
"@wekanteam/html-to-markdown": "^1.0.2",
|
||||||
"@wekanteam/meteor-globals": "^1.1.6",
|
"@wekanteam/meteor-globals": "^1.1.6",
|
||||||
"@wekanteam/meteor-reactive-cache": "^1.0.7",
|
"@wekanteam/meteor-reactive-cache": "^1.0.7",
|
||||||
"meteor-node-stubs": "npm:@wekanteam/meteor-node-stubs@^1.2.7",
|
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^6.12.6",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bson": "^4.7.2",
|
"bson": "^4.7.2",
|
||||||
|
|
@ -37,6 +39,7 @@
|
||||||
"fibers": "^5.0.3",
|
"fibers": "^5.0.3",
|
||||||
"file-type": "^16.5.4",
|
"file-type": "^16.5.4",
|
||||||
"filesize": "^8.0.7",
|
"filesize": "^8.0.7",
|
||||||
|
"hotkeys-js": "^3.13.15",
|
||||||
"i18next": "^21.10.0",
|
"i18next": "^21.10.0",
|
||||||
"i18next-sprintf-postprocessor": "^0.2.2",
|
"i18next-sprintf-postprocessor": "^0.2.2",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
|
|
@ -47,6 +50,7 @@
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"markdown-it-emoji": "^2.0.0",
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"meteor-accounts-t9n": "^2.6.0",
|
"meteor-accounts-t9n": "^2.6.0",
|
||||||
|
"meteor-node-stubs": "npm:@wekanteam/meteor-node-stubs@^1.2.7",
|
||||||
"os": "^0.1.2",
|
"os": "^0.1.2",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pretty-ms": "^7.0.1",
|
"pretty-ms": "^7.0.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue