Kaynağa Gözat

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
Maxime Quandalle 9 yıl önce
ebeveyn
işleme
5d77ad4f6b

+ 1 - 1
client/components/cards/cardDetails.js

@@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
   },
 
   reachNextPeak() {
-    const activitiesComponent = this.childrenComponents('activities')[0];
+    const activitiesComponent = this.childComponents('activities')[0];
     activitiesComponent.loadNextPage();
   },
 

+ 2 - 2
client/components/forms/forms.styl

@@ -621,11 +621,11 @@ button
         width: 11px
         height: @width
         border-radius: 2px
-        margin-right: 3px
+        margin: 2px 7px -2px -2px
         display: inline-block
 
     &.active
       background: #005377
 
-      a
+      a, .quiet
         color: white

+ 1 - 1
client/components/lists/list.js

@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
 
   // Proxy
   openForm(options) {
-    this.childrenComponents('listBody')[0].openForm(options);
+    this.childComponents('listBody')[0].openForm(options);
   },
 
   onCreated() {

+ 13 - 2
client/components/lists/listBody.jade

@@ -22,9 +22,20 @@ template(name="listBody")
 
 template(name="addCardForm")
   .minicard.minicard-composer.js-composer
-    .minicard-detailss.clearfix
-      textarea.minicard-composer-textarea.js-card-title(autofocus)
+    if getLabels
+      .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
+        each members.get
+          +userAvatar(userId=this)
+
   .add-controls.clearfix
     button.primary.confirm(type="submit") {{_ 'add'}}
     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

+ 71 - 85
client/components/lists/listBody.js

@@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
     options = options || {};
     options.position = options.position || 'top';
 
-    const forms = this.childrenComponents('inlinedForm');
+    const forms = this.childComponents('inlinedForm');
     let form = forms.find((component) => {
       return component.data().position === options.position;
     });
@@ -27,7 +27,9 @@ BlazeComponent.extendComponent({
     const lastCardDom = this.find('.js-minicard:last');
     const textarea = $(evt.currentTarget).find('textarea');
     const position = this.currentData().position;
-    let title = textarea.val().trim();
+    const title = textarea.val().trim();
+
+    const formComponent = this.childComponents('addCardForm')[0];
     let sortIndex;
     if (position === 'top') {
       sortIndex = Utils.calculateIndex(null, firstCardDom).base;
@@ -35,40 +37,16 @@ BlazeComponent.extendComponent({
       sortIndex = Utils.calculateIndex(lastCardDom, null).base;
     }
 
-    // Parse for @user and #label mentions, stripping them from the title
-    // and applying the appropriate users and labels to the card instead.
-    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}`, '');
-      }
-    });
+    const members = formComponent.members.get();
+    const labelIds = formComponent.labels.get();
 
     if (title) {
       const _id = Cards.insert({
         title,
+        members,
+        labelIds,
         listId: this.data()._id,
         boardId: this.data().board()._id,
-        labelIds: foundLabelIds,
-        members: foundUserIds,
         sort: sortIndex,
       });
       // In case the filter is active we need to add the newly inserted card in
@@ -82,6 +60,8 @@ BlazeComponent.extendComponent({
       if (position === 'bottom') {
         this.scrollToBottom();
       }
+
+      formComponent.reset();
     }
   },
 
@@ -129,18 +109,40 @@ BlazeComponent.extendComponent({
   },
 }).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({
   template() {
     return 'addCardForm';
   },
 
-  pressKey(evt) {
-    // Don't do anything if the drop down is showing
-    if (dropdownMenuIsOpened) {
-      return;
-    }
+  onCreated() {
+    this.labels = new ReactiveVar([]);
+    this.members = new ReactiveVar([]);
+  },
 
+  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
     if (evt.keyCode === 13) {
       evt.preventDefault();
@@ -176,28 +178,25 @@ BlazeComponent.extendComponent({
     }];
   },
 
-  onCreated() {
-    dropdownMenuIsOpened = false;
-  },
-
   onRendered() {
-    const $textarea = this.$('textarea');
-    const currentBoard = Boards.findOne(Session.get('currentBoard'));
-    $textarea.textcomplete([
+    const editor = this;
+    this.$('textarea').escapeableTextComplete([
       // User mentions
       {
         match: /\B@(\w*)$/,
         search(term, callback) {
+          const currentBoard = Boards.findOne(Session.get('currentBoard'));
           callback($.map(currentBoard.members, (member) => {
-            const username = Users.findOne(member.userId).username;
-            return username.indexOf(term) === 0 ? username : null;
+            const user = Users.findOne(member.userId);
+            return user.username.indexOf(term) === 0 ? user : null;
           }));
         },
-        template(value) {
-          return value;
+        template(user) {
+          return user.username;
         },
-        replace(username) {
-          return `@${username} `;
+        replace(user) {
+          toggleValueInReactiveArray(editor.members, user._id);
+          return '';
         },
         index: 1,
       },
@@ -206,51 +205,38 @@ BlazeComponent.extendComponent({
       {
         match: /\B#(\w*)$/,
         search(term, callback) {
+          const currentBoard = Boards.findOne(Session.get('currentBoard'));
           callback($.map(currentBoard.labels, (label) => {
-            const labelName = (!label.name || label.name === '')
-                              ? label.color
-                              : label.name;
-            return labelName.indexOf(term) === 0 ? labelName : null;
+            if (label.name.indexOf(term) > -1 ||
+                label.color.indexOf(term) > -1) {
+              return label;
+            }
           }));
         },
-        template(value) {
-          // XXX the following is duplicated from editor.js and should be
-          // abstracted to keep things DRY
-          // add a "colour badge" in front of the label name
-          // but first, get the colour's name from its value
-          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;
+        template(label) {
+          return Blaze.toHTMLWithData(Template.autocompleteLabelLine, {
+            hasNoName: !Boolean(label.name),
+            colorName: label.color,
+            labelName: label.name || label.color,
+          });
         },
         replace(label) {
-          return `#${label} `;
+          toggleValueInReactiveArray(editor.labels, label._id);
+          return '';
         },
         index: 1,
       },
-    ]);
-
-    // customize hooks for dealing with the dropdowns
-    $textarea.on({
-      'textComplete:show'() {
-        dropdownMenuIsOpened = true;
-      },
-      'textComplete:hide'() {
-        Tracker.afterFlush(() => {
-          dropdownMenuIsOpened = false;
-        });
+    ], {
+      // 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 the card from submitting (on `Enter`) or
+      // going on the next column (on `Tab`).
+      onKeydown(evt, commands) {
+        if (evt.keyCode === 9 || evt.keyCode === 13) {
+          evt.stopPropagation();
+          return commands.KEY_ENTER;
+        }
       },
     });
-
-    EscapeActions.register('textcomplete',
-      () => {},
-      () => dropdownMenuIsOpened
-    );
   },
 }).register('addCardForm');

+ 1 - 1
client/components/lists/listHeader.js

@@ -5,7 +5,7 @@ BlazeComponent.extendComponent({
 
   editTitle(evt) {
     evt.preventDefault();
-    const newTitle = this.childrenComponents('inlinedForm')[0].getValue().trim();
+    const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
     const list = this.currentData();
     if (newTitle) {
       list.rename(newTitle.trim());

+ 1 - 1
client/components/sidebar/sidebar.js

@@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
   },
 
   reachNextPeak() {
-    const activitiesComponent = this.childrenComponents('activities')[0];
+    const activitiesComponent = this.childComponents('activities')[0];
     activitiesComponent.loadNextPage();
   },
 

+ 28 - 4
client/lib/textComplete.js

@@ -3,8 +3,23 @@
 // of the vanilla `textcomplete`.
 let dropdownMenuIsOpened = false;
 
-$.fn.escapeableTextComplete = function(...args) {
-  this.textcomplete(...args);
+$.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
@@ -18,7 +33,14 @@ $.fn.escapeableTextComplete = function(...args) {
     },
     'textComplete:hide'() {
       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',
   () => {},
-  () => dropdownMenuIsOpened
+  () => dropdownMenuIsOpened, {
+    noClickEscapeOn: '.textcomplete-dropdown',
+  }
 );