mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Finish the minicard editor auto-completion feature
This commit stands on the initial support implemented in #342. We now avoid error-prone parsing step by adding the member or the label directly to the card object. We also added support for `Tab` to completion on our textComplete component. Closes #342
This commit is contained in:
parent
2b134ff7a9
commit
5d77ad4f6b
8 changed files with 118 additions and 97 deletions
|
|
@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
reachNextPeak() {
|
reachNextPeak() {
|
||||||
const activitiesComponent = this.childrenComponents('activities')[0];
|
const activitiesComponent = this.childComponents('activities')[0];
|
||||||
activitiesComponent.loadNextPage();
|
activitiesComponent.loadNextPage();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -621,11 +621,11 @@ button
|
||||||
width: 11px
|
width: 11px
|
||||||
height: @width
|
height: @width
|
||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
margin-right: 3px
|
margin: 2px 7px -2px -2px
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
||||||
&.active
|
&.active
|
||||||
background: #005377
|
background: #005377
|
||||||
|
|
||||||
a
|
a, .quiet
|
||||||
color: white
|
color: white
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
// Proxy
|
// Proxy
|
||||||
openForm(options) {
|
openForm(options) {
|
||||||
this.childrenComponents('listBody')[0].openForm(options);
|
this.childComponents('listBody')[0].openForm(options);
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreated() {
|
onCreated() {
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,20 @@ template(name="listBody")
|
||||||
|
|
||||||
template(name="addCardForm")
|
template(name="addCardForm")
|
||||||
.minicard.minicard-composer.js-composer
|
.minicard.minicard-composer.js-composer
|
||||||
.minicard-detailss.clearfix
|
if getLabels
|
||||||
textarea.minicard-composer-textarea.js-card-title(autofocus)
|
.minicard-labels
|
||||||
|
each getLabels
|
||||||
|
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||||
|
textarea.minicard-composer-textarea.js-card-title(autofocus)
|
||||||
|
if members.get
|
||||||
.minicard-members.js-minicard-composer-members
|
.minicard-members.js-minicard-composer-members
|
||||||
|
each members.get
|
||||||
|
+userAvatar(userId=this)
|
||||||
|
|
||||||
.add-controls.clearfix
|
.add-controls.clearfix
|
||||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||||
a.fa.fa-times-thin.js-close-inlined-form
|
a.fa.fa-times-thin.js-close-inlined-form
|
||||||
|
|
||||||
|
template(name="autocompleteLabelLine")
|
||||||
|
.minicard-label(class="card-label-{{colorName}}" title=labelName)
|
||||||
|
span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
|
||||||
options = options || {};
|
options = options || {};
|
||||||
options.position = options.position || 'top';
|
options.position = options.position || 'top';
|
||||||
|
|
||||||
const forms = this.childrenComponents('inlinedForm');
|
const forms = this.childComponents('inlinedForm');
|
||||||
let form = forms.find((component) => {
|
let form = forms.find((component) => {
|
||||||
return component.data().position === options.position;
|
return component.data().position === options.position;
|
||||||
});
|
});
|
||||||
|
|
@ -27,7 +27,9 @@ BlazeComponent.extendComponent({
|
||||||
const lastCardDom = this.find('.js-minicard:last');
|
const lastCardDom = this.find('.js-minicard:last');
|
||||||
const textarea = $(evt.currentTarget).find('textarea');
|
const textarea = $(evt.currentTarget).find('textarea');
|
||||||
const position = this.currentData().position;
|
const position = this.currentData().position;
|
||||||
let title = textarea.val().trim();
|
const title = textarea.val().trim();
|
||||||
|
|
||||||
|
const formComponent = this.childComponents('addCardForm')[0];
|
||||||
let sortIndex;
|
let sortIndex;
|
||||||
if (position === 'top') {
|
if (position === 'top') {
|
||||||
sortIndex = Utils.calculateIndex(null, firstCardDom).base;
|
sortIndex = Utils.calculateIndex(null, firstCardDom).base;
|
||||||
|
|
@ -35,40 +37,16 @@ BlazeComponent.extendComponent({
|
||||||
sortIndex = Utils.calculateIndex(lastCardDom, null).base;
|
sortIndex = Utils.calculateIndex(lastCardDom, null).base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse for @user and #label mentions, stripping them from the title
|
const members = formComponent.members.get();
|
||||||
// and applying the appropriate users and labels to the card instead.
|
const labelIds = formComponent.labels.get();
|
||||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
|
||||||
|
|
||||||
// Find all @-mentioned usernames, collect a list of their IDs and strip
|
|
||||||
// their mention out of the title.
|
|
||||||
let foundUserIds = []; // eslint-disable-line prefer-const
|
|
||||||
currentBoard.members.forEach((member) => {
|
|
||||||
const username = Users.findOne(member.userId).username;
|
|
||||||
if (title.indexOf(`@${username}`) !== -1) {
|
|
||||||
foundUserIds.push(member.userId);
|
|
||||||
title = title.replace(`@${username}`, '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find all #-mentioned labels (based on their colour or name), collect a
|
|
||||||
// list of their IDs, and strip their mention out of the title.
|
|
||||||
let foundLabelIds = []; // eslint-disable-line prefer-const
|
|
||||||
currentBoard.labels.forEach((label) => {
|
|
||||||
const labelName = (!label.name || label.name === '')
|
|
||||||
? label.color : label.name;
|
|
||||||
if (title.indexOf(`#${labelName}`) !== -1) {
|
|
||||||
foundLabelIds.push(label._id);
|
|
||||||
title = title.replace(`#${labelName}`, '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
const _id = Cards.insert({
|
const _id = Cards.insert({
|
||||||
title,
|
title,
|
||||||
|
members,
|
||||||
|
labelIds,
|
||||||
listId: this.data()._id,
|
listId: this.data()._id,
|
||||||
boardId: this.data().board()._id,
|
boardId: this.data().board()._id,
|
||||||
labelIds: foundLabelIds,
|
|
||||||
members: foundUserIds,
|
|
||||||
sort: sortIndex,
|
sort: sortIndex,
|
||||||
});
|
});
|
||||||
// In case the filter is active we need to add the newly inserted card in
|
// In case the filter is active we need to add the newly inserted card in
|
||||||
|
|
@ -82,6 +60,8 @@ BlazeComponent.extendComponent({
|
||||||
if (position === 'bottom') {
|
if (position === 'bottom') {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formComponent.reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -129,18 +109,40 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('listBody');
|
}).register('listBody');
|
||||||
|
|
||||||
let dropdownMenuIsOpened = false;
|
function toggleValueInReactiveArray(reactiveValue, value) {
|
||||||
|
const array = reactiveValue.get();
|
||||||
|
const valueIndex = array.indexOf(value);
|
||||||
|
if (valueIndex === -1) {
|
||||||
|
array.push(value);
|
||||||
|
} else {
|
||||||
|
array.splice(valueIndex, 1);
|
||||||
|
}
|
||||||
|
reactiveValue.set(array);
|
||||||
|
}
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
template() {
|
template() {
|
||||||
return 'addCardForm';
|
return 'addCardForm';
|
||||||
},
|
},
|
||||||
|
|
||||||
pressKey(evt) {
|
onCreated() {
|
||||||
// Don't do anything if the drop down is showing
|
this.labels = new ReactiveVar([]);
|
||||||
if (dropdownMenuIsOpened) {
|
this.members = new ReactiveVar([]);
|
||||||
return;
|
},
|
||||||
}
|
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.labels.set([]);
|
||||||
|
this.members.set([]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getLabels() {
|
||||||
|
const currentBoardId = Session.get('currentBoard');
|
||||||
|
return Boards.findOne(currentBoardId).labels.filter((label) => {
|
||||||
|
return this.labels.get().indexOf(label._id) > -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pressKey(evt) {
|
||||||
// Pressing Enter should submit the card
|
// Pressing Enter should submit the card
|
||||||
if (evt.keyCode === 13) {
|
if (evt.keyCode === 13) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
@ -176,28 +178,25 @@ BlazeComponent.extendComponent({
|
||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreated() {
|
|
||||||
dropdownMenuIsOpened = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
onRendered() {
|
onRendered() {
|
||||||
const $textarea = this.$('textarea');
|
const editor = this;
|
||||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
this.$('textarea').escapeableTextComplete([
|
||||||
$textarea.textcomplete([
|
|
||||||
// User mentions
|
// User mentions
|
||||||
{
|
{
|
||||||
match: /\B@(\w*)$/,
|
match: /\B@(\w*)$/,
|
||||||
search(term, callback) {
|
search(term, callback) {
|
||||||
|
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||||
callback($.map(currentBoard.members, (member) => {
|
callback($.map(currentBoard.members, (member) => {
|
||||||
const username = Users.findOne(member.userId).username;
|
const user = Users.findOne(member.userId);
|
||||||
return username.indexOf(term) === 0 ? username : null;
|
return user.username.indexOf(term) === 0 ? user : null;
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
template(value) {
|
template(user) {
|
||||||
return value;
|
return user.username;
|
||||||
},
|
},
|
||||||
replace(username) {
|
replace(user) {
|
||||||
return `@${username} `;
|
toggleValueInReactiveArray(editor.members, user._id);
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -206,51 +205,38 @@ BlazeComponent.extendComponent({
|
||||||
{
|
{
|
||||||
match: /\B#(\w*)$/,
|
match: /\B#(\w*)$/,
|
||||||
search(term, callback) {
|
search(term, callback) {
|
||||||
|
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||||
callback($.map(currentBoard.labels, (label) => {
|
callback($.map(currentBoard.labels, (label) => {
|
||||||
const labelName = (!label.name || label.name === '')
|
if (label.name.indexOf(term) > -1 ||
|
||||||
? label.color
|
label.color.indexOf(term) > -1) {
|
||||||
: label.name;
|
return label;
|
||||||
return labelName.indexOf(term) === 0 ? labelName : null;
|
}
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
template(value) {
|
template(label) {
|
||||||
// XXX the following is duplicated from editor.js and should be
|
return Blaze.toHTMLWithData(Template.autocompleteLabelLine, {
|
||||||
// abstracted to keep things DRY
|
hasNoName: !Boolean(label.name),
|
||||||
// add a "colour badge" in front of the label name
|
colorName: label.color,
|
||||||
// but first, get the colour's name from its value
|
labelName: label.name || label.color,
|
||||||
const colorName = currentBoard.labels.find((label) => {
|
});
|
||||||
return value === label.name || value === label.color;
|
|
||||||
}).color;
|
|
||||||
const valueSpan = (colorName === value)
|
|
||||||
? `<span class="quiet">${value}</span>`
|
|
||||||
: value;
|
|
||||||
return (colorName && colorName !== '')
|
|
||||||
? `<div class="minicard-label card-label-${colorName}"
|
|
||||||
title="${value}"></div> ${valueSpan}`
|
|
||||||
: value;
|
|
||||||
},
|
},
|
||||||
replace(label) {
|
replace(label) {
|
||||||
return `#${label} `;
|
toggleValueInReactiveArray(editor.labels, label._id);
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
]);
|
], {
|
||||||
|
// When the autocomplete menu is shown we want both a press of both `Tab`
|
||||||
// customize hooks for dealing with the dropdowns
|
// or `Enter` to validation the auto-completion. We also need to stop the
|
||||||
$textarea.on({
|
// event propagation to prevent the card from submitting (on `Enter`) or
|
||||||
'textComplete:show'() {
|
// going on the next column (on `Tab`).
|
||||||
dropdownMenuIsOpened = true;
|
onKeydown(evt, commands) {
|
||||||
},
|
if (evt.keyCode === 9 || evt.keyCode === 13) {
|
||||||
'textComplete:hide'() {
|
evt.stopPropagation();
|
||||||
Tracker.afterFlush(() => {
|
return commands.KEY_ENTER;
|
||||||
dropdownMenuIsOpened = false;
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
EscapeActions.register('textcomplete',
|
|
||||||
() => {},
|
|
||||||
() => dropdownMenuIsOpened
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}).register('addCardForm');
|
}).register('addCardForm');
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
editTitle(evt) {
|
editTitle(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const newTitle = this.childrenComponents('inlinedForm')[0].getValue().trim();
|
const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
|
||||||
const list = this.currentData();
|
const list = this.currentData();
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
list.rename(newTitle.trim());
|
list.rename(newTitle.trim());
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
reachNextPeak() {
|
reachNextPeak() {
|
||||||
const activitiesComponent = this.childrenComponents('activities')[0];
|
const activitiesComponent = this.childComponents('activities')[0];
|
||||||
activitiesComponent.loadNextPage();
|
activitiesComponent.loadNextPage();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,23 @@
|
||||||
// of the vanilla `textcomplete`.
|
// of the vanilla `textcomplete`.
|
||||||
let dropdownMenuIsOpened = false;
|
let dropdownMenuIsOpened = false;
|
||||||
|
|
||||||
$.fn.escapeableTextComplete = function(...args) {
|
$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
|
||||||
this.textcomplete(...args);
|
// 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
|
// Since commit d474017 jquery-textComplete automatically closes a potential
|
||||||
// opened dropdown menu when the user press Escape. This behavior conflicts
|
// opened dropdown menu when the user press Escape. This behavior conflicts
|
||||||
|
|
@ -18,7 +33,14 @@ $.fn.escapeableTextComplete = function(...args) {
|
||||||
},
|
},
|
||||||
'textComplete:hide'() {
|
'textComplete:hide'() {
|
||||||
Tracker.afterFlush(() => {
|
Tracker.afterFlush(() => {
|
||||||
dropdownMenuIsOpened = false;
|
// 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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -26,5 +48,7 @@ $.fn.escapeableTextComplete = function(...args) {
|
||||||
|
|
||||||
EscapeActions.register('textcomplete',
|
EscapeActions.register('textcomplete',
|
||||||
() => {},
|
() => {},
|
||||||
() => dropdownMenuIsOpened
|
() => dropdownMenuIsOpened, {
|
||||||
|
noClickEscapeOn: '.textcomplete-dropdown',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue