From d48542796e21ec4d674f4b9508f85c58f26ddbaa Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 22 Mar 2018 11:29:55 +0100 Subject: [PATCH] FIX: select-kit refactoring - improve mini-tag-chooser keyboard behavior - all multil select now respond to select all and left/right arrows - improve events handling - many minor tweaks --- .../admin-agree-flag-dropdown.js.es6 | 2 +- .../admin-delete-flag-dropdown.js.es6 | 5 +- .../components/admin-group-selector.js.es6 | 6 +- .../components/category-chooser.js.es6 | 2 +- .../components/category-drop.js.es6 | 2 +- .../category-drop/category-drop-header.js.es6 | 4 +- .../category-notifications-button.js.es6 | 7 +- .../select-kit/components/combo-box.js.es6 | 11 +- .../combo-box/combo-box-header.js.es6 | 11 +- .../components/composer-actions.js.es6 | 2 +- .../components/dropdown-select-box.js.es6 | 2 +- .../dropdown-select-box-header.js.es6 | 2 +- .../future-date-input-selector.js.es6 | 12 +- .../select-kit/components/list-setting.js.es6 | 7 +- .../components/mini-tag-chooser.js.es6 | 165 +++++------- .../mini-tag-chooser-header.js.es6 | 2 +- .../select-kit/components/multi-select.js.es6 | 252 ++++++++---------- .../multi-select/multi-select-filter.js.es6 | 14 + .../multi-select/multi-select-header.js.es6 | 4 +- .../multi-select/selected-name.js.es6 | 33 +-- .../components/none-category-row.js.es6 | 4 - .../components/notifications-button.js.es6 | 2 +- .../components/period-chooser.js.es6 | 6 + .../period-chooser-header.js.es6 | 8 +- .../components/pinned-options.js.es6 | 2 +- .../select-kit/components/select-kit.js.es6 | 200 ++++++++------ .../select-kit/select-kit-filter.js.es6 | 5 +- .../select-kit/select-kit-header.js.es6 | 7 +- .../select-kit/select-kit-none-row.js.es6 | 6 +- .../select-kit/select-kit-row.js.es6 | 16 +- .../components/single-select.js.es6 | 153 ++++++----- .../select-kit/components/tag-chooser.js.es6 | 8 +- .../select-kit/components/tag-drop.js.es6 | 2 +- .../components/tag-group-chooser.js.es6 | 10 +- .../toolbar-popup-menu-options.js.es6 | 2 +- .../topic-footer-mobile-dropdown.js.es6 | 4 +- .../select-kit/mixins/dom-helpers.js.es6 | 31 ++- .../select-kit/mixins/events.js.es6 | 172 +++++++++--- .../javascripts/select-kit/mixins/tags.js.es6 | 9 +- .../select-kit/mixins/utils.js.es6 | 12 +- .../components/combo-box/combo-box-header.hbs | 2 +- .../dropdown-select-box-header.hbs | 2 +- .../future-date-input-selector-header.hbs | 2 +- .../mini-tag-chooser-header.hbs | 4 +- .../templates/components/multi-select.hbs | 18 +- .../multi-select/multi-select-header.hbs | 7 +- .../select-kit/select-kit-collection.hbs | 18 +- .../select-kit/select-kit-filter.hbs | 2 +- .../templates/components/single-select.hbs | 16 +- .../common/select-kit/category-chooser.scss | 5 + .../future-date-input-selector.scss | 6 + .../components/category-selector-test.js.es6 | 1 + .../components/list-setting-test.js.es6 | 2 +- .../components/multi-select-test.js.es6 | 1 + 54 files changed, 717 insertions(+), 573 deletions(-) create mode 100644 app/assets/javascripts/select-kit/components/multi-select/multi-select-filter.js.es6 diff --git a/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 index 3020529ef65..51fa7317d0d 100644 --- a/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 +++ b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 @@ -10,7 +10,7 @@ export default DropdownSelectBox.extend({ headerIcon: "thumbs-o-up", computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.name = `${I18n.t("admin.flags.agree")}...`; return content; }, diff --git a/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 index a6f87d26066..a8c64523be4 100644 --- a/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 +++ b/app/assets/javascripts/select-kit/components/admin-delete-flag-dropdown.js.es6 @@ -1,5 +1,6 @@ import DropdownSelectBox from "select-kit/components/dropdown-select-box"; import computed from "ember-addons/ember-computed-decorators"; +const { get } = Ember; export default DropdownSelectBox.extend({ classNames: ["delete-flag", "admin-delete-flag-dropdown"], @@ -8,7 +9,7 @@ export default DropdownSelectBox.extend({ headerIcon: "trash-o", computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.name = `${I18n.t("admin.flags.delete")}...`; return content; }, @@ -55,7 +56,7 @@ export default DropdownSelectBox.extend({ mutateValue(value) { const computedContentItem = this.get("computedContent").findBy("value", value); - Ember.get(computedContentItem, "originalContent.action")(); + get(computedContentItem, "originalContent.action")(); }, actions: { diff --git a/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 b/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 index be3352b376b..3cab568f60b 100644 --- a/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/admin-group-selector.js.es6 @@ -18,9 +18,9 @@ export default MultiSelectComponent.extend({ }, computeContentItem(contentItem, name) { - let computedContent = this.baseComputedContentItem(contentItem, name); - computedContent.locked = contentItem.automatic; - return computedContent; + let computedContentItem = this._super(contentItem, name); + computedContentItem.locked = contentItem.automatic; + return computedContentItem; }, mutateValues(values) { diff --git a/app/assets/javascripts/select-kit/components/category-chooser.js.es6 b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 index 82c9f27d091..069ba1a3299 100644 --- a/app/assets/javascripts/select-kit/components/category-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-chooser.js.es6 @@ -59,7 +59,7 @@ export default ComboBoxComponent.extend({ }, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); if (this.get("hasSelection")) { const category = Category.findById(content.value); diff --git a/app/assets/javascripts/select-kit/components/category-drop.js.es6 b/app/assets/javascripts/select-kit/components/category-drop.js.es6 index b8c4a8724c1..debffeb9668 100644 --- a/app/assets/javascripts/select-kit/components/category-drop.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-drop.js.es6 @@ -68,7 +68,7 @@ export default ComboBoxComponent.extend({ }, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); if (this.get("hasSelection")) { const category = Category.findById(content.value); diff --git a/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 b/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 index 84dec7d4d9e..cc952c4dc4b 100644 --- a/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-drop/category-drop-header.js.es6 @@ -6,8 +6,8 @@ export default ComboBoxSelectBoxHeaderComponent.extend({ layoutName: "select-kit/templates/components/category-drop/category-drop-header", classNames: "category-drop-header", - classNameBindings: ['categoryStyleClass'], - categoryStyleClass: Ember.computed.alias('site.category_style'), + classNameBindings: ["categoryStyleClass"], + categoryStyleClass: Ember.computed.alias("site.category_style"), @computed("computedContent.value", "computedContent.name") category(value, name) { diff --git a/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 index 0115add4d3e..ff97c4b12db 100644 --- a/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-notifications-button.js.es6 @@ -1,20 +1,15 @@ import NotificationOptionsComponent from "select-kit/components/notifications-button"; -import computed from "ember-addons/ember-computed-decorators"; export default NotificationOptionsComponent.extend({ pluginApiIdentifiers: ["category-notifications-button"], classNames: "category-notifications-button", isHidden: Ember.computed.or("category.deleted", "site.isMobileDevice"), + headerIcon: Ember.computed.alias("iconForSelectedDetails"), i18nPrefix: "category.notifications", showFullTitle: false, allowInitialValueMutation: false, mutateValue(value) { this.get("category").setNotification(value); - }, - - @computed("iconForSelectedDetails") - headerIcon(iconForSelectedDetails) { - return [iconForSelectedDetails]; } }); diff --git a/app/assets/javascripts/select-kit/components/combo-box.js.es6 b/app/assets/javascripts/select-kit/components/combo-box.js.es6 index 0a576eae9af..8dd4756c5d9 100644 --- a/app/assets/javascripts/select-kit/components/combo-box.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box.js.es6 @@ -1,5 +1,5 @@ import SingleSelectComponent from "select-kit/components/single-select"; -import { on } from "ember-addons/ember-computed-decorators"; +import { on, default as computed } from "ember-addons/ember-computed-decorators"; export default SingleSelectComponent.extend({ pluginApiIdentifiers: ["combo-box"], @@ -12,16 +12,19 @@ export default SingleSelectComponent.extend({ clearable: false, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.hasSelection = this.get("hasSelection"); return content; }, + @computed("isExpanded", "caretUpIcon", "caretDownIcon") + caretIcon(isExpanded, caretUpIcon, caretDownIcon) { + return isExpanded ? caretUpIcon : caretDownIcon; + }, + @on("didReceiveAttrs") _setComboBoxOptions() { this.get("headerComponentOptions").setProperties({ - caretUpIcon: this.get("caretUpIcon"), - caretDownIcon: this.get("caretDownIcon"), clearable: this.get("clearable"), }); } diff --git a/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 index 6cc5ecd21f7..49d4e72fc49 100644 --- a/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 @@ -8,14 +8,5 @@ export default SelectKitHeaderComponent.extend({ clearable: Ember.computed.alias("options.clearable"), caretUpIcon: Ember.computed.alias("options.caretUpIcon"), caretDownIcon: Ember.computed.alias("options.caretDownIcon"), - - @computed("isExpanded", "caretUpIcon", "caretDownIcon") - caretIcon(isExpanded, caretUpIcon, caretDownIcon) { - return isExpanded === true ? caretUpIcon : caretDownIcon; - }, - - @computed("clearable", "computedContent.hasSelection") - shouldDisplayClearableButton(clearable, hasSelection) { - return clearable === true && hasSelection === true; - } + shouldDisplayClearableButton: Ember.computed.and("clearable", "computedContent.hasSelection") }); diff --git a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 index a95d688053f..ff587c2bdc9 100644 --- a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 +++ b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 @@ -45,7 +45,7 @@ export default DropdownSelectBoxComponent.extend({ }, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); switch (this.get("action")) { case PRIVATE_MESSAGE: diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 index 5f76542aac3..0b540b47885 100644 --- a/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box.js.es6 @@ -21,7 +21,7 @@ export default SingleSelectComponent.extend({ }, didClickOutside() { - if (this.get("isExpanded") === false) return; + if (!this.get("isExpanded")) return; this.close(); }, diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 index d7e08764309..e1939e3214a 100644 --- a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 @@ -10,6 +10,6 @@ export default SelectKitHeaderComponent.extend({ @computed("options.showFullTitle") btnClassName(showFullTitle) { - return `btn ${showFullTitle ? 'btn-icon-text' : 'no-text btn-icon'}`; + return `btn ${showFullTitle ? "btn-icon-text" : "no-text btn-icon"}`; } }); diff --git a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 index 8597cb9668b..296da5a1fd9 100644 --- a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 @@ -120,19 +120,19 @@ export default ComboBoxComponent.extend(DatetimeMixin, { headerComponent: "future-date-input-selector/future-date-input-selector-header", computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.datetime = this._computeDatetimeForValue(this.get("computedValue")); - content.name = this.get("selectedComputedContent.name") || content.name; + content.name = this.get("selection.name") || content.name; content.hasSelection = this.get("hasSelection"); content.icons = this._computeIconsForValue(this.get("computedValue")); return content; }, computeContentItem(contentItem, name) { - let item = this.baseComputedContentItem(contentItem, name); - item.datetime = this._computeDatetimeForValue(contentItem.id); - item.icons = this._computeIconsForValue(contentItem.id); - return item; + let computedContentItem = this._super(contentItem, name); + computedContentItem.datetime = this._computeDatetimeForValue(contentItem.id); + computedContentItem.icons = this._computeIconsForValue(contentItem.id); + return computedContentItem; }, computeContent() { diff --git a/app/assets/javascripts/select-kit/components/list-setting.js.es6 b/app/assets/javascripts/select-kit/components/list-setting.js.es6 index 0872aab9510..cc1c4f94fb7 100644 --- a/app/assets/javascripts/select-kit/components/list-setting.js.es6 +++ b/app/assets/javascripts/select-kit/components/list-setting.js.es6 @@ -1,4 +1,5 @@ import MultiSelectComponent from "select-kit/components/multi-select"; +const { isNone, makeArray } = Ember; export default MultiSelectComponent.extend({ pluginApiIdentifiers: ["list-setting"], @@ -11,7 +12,7 @@ export default MultiSelectComponent.extend({ init() { this._super(); - if (!Ember.isNone(this.get("settingName"))) { + if (!isNone(this.get("settingName"))) { this.set("nameProperty", this.get("settingName")); } @@ -24,13 +25,13 @@ export default MultiSelectComponent.extend({ computeContent() { let content; - if (Ember.isNone(this.get("choices"))) { + if (isNone(this.get("choices"))) { content = this.get("settingValue").split(this.get("tokenSeparator"));; } else { content = this.get("choices"); } - return Ember.makeArray(content).filter(c => c); + return makeArray(content).filter(c => c); }, mutateValues(values) { diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 23113a0a8fa..a48d1620f0a 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -13,7 +13,7 @@ export default ComboBox.extend(Tags, { classNameBindings: ["noTags"], verticalOffset: 3, filterable: true, - noTags: Ember.computed.empty("selectedTags"), + noTags: Ember.computed.empty("selection"), allowAny: true, caretUpIcon: Ember.computed.alias("caretIcon"), caretDownIcon: Ember.computed.alias("caretIcon"), @@ -24,6 +24,7 @@ export default ComboBox.extend(Tags, { this._super(); this.set("termMatchesForbidden", false); + this.selectionSelector = ".selected-tag"; this.set("templateForRow", (rowComponent) => { const tag = rowComponent.get("computedContent"); @@ -36,22 +37,13 @@ export default ComboBox.extend(Tags, { this.set("limit", parseInt(this.get("limit") || this.get("siteSettings.max_tags_per_topic"))); }, - @computed("limitReached") - caretIcon(limitReached) { - return limitReached ? null : "plus"; - }, - - @computed("selectedTags.[]", "limit") - limitReached(selectedTags, limit) { - if (selectedTags.length >= limit) { - return true; - } - - return false; + @computed("hasReachedLimit") + caretIcon(hasReachedLimit) { + return hasReachedLimit ? null : "plus"; }, @computed("tags") - selectedTags(tags) { + selection(tags) { return makeArray(tags); }, @@ -62,12 +54,12 @@ export default ComboBox.extend(Tags, { didRender() { this._super(); - $(".select-kit-body").on("click.mini-tag-chooser", ".selected-tag", (event) => { + this.$(".select-kit-body").on("click.mini-tag-chooser", ".selected-tag", (event) => { event.stopImmediatePropagation(); - this.send("removeTag", $(event.target).attr("data-value")); + this.destroyTags($(event.target).attr("data-value")); }); - $(".select-kit-header").on("focus.mini-tag-chooser", ".selected-name", (event) => { + this.$(".select-kit-header").on("focus.mini-tag-chooser", ".selected-name", (event) => { event.stopImmediatePropagation(); this.focus(event); }); @@ -76,63 +68,51 @@ export default ComboBox.extend(Tags, { willDestroyElement() { this._super(); - $(".select-kit-body").off("click.mini-tag-chooser"); - $(".select-kit-header").off("focus.mini-tag-chooser"); + this.$(".select-kit-body").off("click.mini-tag-chooser"); + this.$(".select-kit-header").off("focus.mini-tag-chooser"); }, - didPressEscape(event) { - const $lastSelectedTag = $(".selected-tag.selected:last"); - - if ($lastSelectedTag && this.get("isExpanded")) { - $lastSelectedTag.removeClass("selected"); - this._destroyEvent(event); - } else { - this._super(event); - } - }, - - backspaceFromFilter(event) { - this.didPressBackspace(event); - }, - - // we are relying on selectedTags and not on value - // to define the current selection + // we are directly mutatings tags to define the current selection mutateValue() {}, - didPressBackspace() { - if (!this.get("isExpanded")) { - this.expand(); - return; + didPressTab(event) { + if (this.get("isLoading")) { + this._destroyEvent(event); + return false; } - const $lastSelectedTag = $(".selected-tag:last"); - - if (!isEmpty(this.get("filter"))) { - $lastSelectedTag.removeClass("is-highlighted"); - return; + if (isEmpty(this.get("filter")) && !this.get("highlighted")) { + this.$header().focus(); + this.close(event); + return true; } - if (!$lastSelectedTag.length) return; - - if (!$lastSelectedTag.hasClass("is-highlighted")) { - $lastSelectedTag.addClass("is-highlighted"); + if (this.get("highlighted") && this.get("isExpanded")) { + this._destroyEvent(event); + this.focus(); + this.select(this.get("highlighted")); + return false; } else { - this.send("removeTag", $lastSelectedTag.attr("data-value")); + this.close(event); } + + return true; }, - @computed("tags.[]", "filter") - collectionHeader(tags, filter) { + @computed("tags.[]", "filter", "highlightedSelection.[]") + collectionHeader(tags, filter, highlightedSelection) { if (!isEmpty(tags)) { let output = ""; + // if we have more than x tags we will also filter the selection if (tags.length >= 20) { tags = tags.filter(t => t.indexOf(filter) >= 0); } tags.map((tag) => { + const isHighlighted = highlightedSelection.includes(tag); output += ` - `; @@ -143,10 +123,11 @@ export default ComboBox.extend(Tags, { }, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); - const joinedTags = this.get("selectedTags").join(", "); + let content = this._super(); - if (isEmpty(this.get("selectedTags"))) { + const joinedTags = this.get("selection").join(", "); + + if (isEmpty(this.get("selection"))) { content.label = I18n.t("tagging.choose_for_topic"); } else { content.label = joinedTags; @@ -157,45 +138,13 @@ export default ComboBox.extend(Tags, { return content; }, - actions: { - removeTag(tag) { - let tags = this.get("selectedTags"); - delete tags[tags.indexOf(tag)]; - this.set("tags", tags.filter(t => t)); - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 200)); - }, - - onExpand() { - if (isEmpty(this.get("collectionComputedContent"))) { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 200)); - } - }, - - onFilter(filter) { - filter = isEmpty(filter) ? null : filter; - this.set("searchDebounce", run.debounce(this, this.prepareSearch, filter, 200)); - }, - - onSelect(tag) { - if (isEmpty(this.get("selectedTags"))) { - this.set("tags", makeArray(tag)); - } else { - this.set("tags", this.get("selectedTags").concat(tag)); - } - - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 50)); - - this.autoHighlight(); - } - }, - - prepareSearch(query) { + _prepareSearch(query) { const data = { q: query, limit: this.get("siteSettings.max_tag_search_results"), categoryId: this.get("categoryId") }; - if (this.get("selectedTags")) data.selected_tags = this.get("selectedTags").slice(0, 100); + if (this.get("selection")) data.selected_tags = this.get("selection").slice(0, 100); if (!this.get("everyTag")) data.filterForInput = true; this.searchTags("/tags/filter/search", data, this._transformJson); @@ -210,10 +159,42 @@ export default ComboBox.extend(Tags, { results = results.sort((a, b) => a.id > b.id); } - results = results.filter(r => !context.get("selectedTags").includes(r.id)); + results = results.filter(r => !context.get("selection").includes(r.id)); return results.map(result => { return { id: result.text, name: result.text, count: result.count }; }); - } + }, + + destroyTags(tags) { + tags = Ember.makeArray(tags); + this.get("tags").removeObjects(tags); + this._prepareSearch(this.get("filter")); + }, + + didDeselect(tags) { + this.destroyTags(tags); + }, + + actions: { + onSelect(tag) { + this.set("tags", makeArray(this.get("tags")).concat(tag)); + this._prepareSearch(this.get("filter")); + this.autoHighlight(); + }, + + onExpand() { + if (isEmpty(this.get("collectionComputedContent"))) { + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 350)); + } + }, + + onFilter(filter) { + // we start loading right away so we avoid updating createRow multiple times + this.startLoading(); + + filter = isEmpty(filter) ? null : filter; + this.set("searchDebounce", run.debounce(this, this._prepareSearch, filter, 350)); + } + }, }); diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser/mini-tag-chooser-header.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser/mini-tag-chooser-header.js.es6 index 1efc5095a1d..20f13a4b3b7 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser/mini-tag-chooser-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser/mini-tag-chooser-header.js.es6 @@ -2,5 +2,5 @@ import SelectKitHeaderComponent from "select-kit/components/select-kit/select-ki export default SelectKitHeaderComponent.extend({ layoutName: "select-kit/templates/components/mini-tag-chooser/mini-tag-chooser-header", - classNames: "mini-tag-chooser-header", + classNames: "mini-tag-chooser-header" }); diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 index da21f55b07f..0635f8b906a 100644 --- a/app/assets/javascripts/select-kit/components/multi-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -1,7 +1,7 @@ import SelectKitComponent from "select-kit/components/select-kit"; import computed from "ember-addons/ember-computed-decorators"; import { on } from "ember-addons/ember-computed-decorators"; -const { get, isNone, isEmpty, makeArray } = Ember; +const { get, isNone, isEmpty, makeArray, run } = Ember; import { applyOnSelectPluginApiCallbacks } from "select-kit/mixins/plugin-api"; @@ -17,11 +17,15 @@ export default SelectKitComponent.extend({ autoFilterable: true, selectedNameComponent: "multi-select/selected-name", filterIcon: null, + filterComponent: "multi-select/multi-select-filter", + computedValues: null, + values: null, init() { this._super(); this.set("computedValues", []); + if (isNone(this.get("values"))) { this.set("values", []); } this.set("headerComponentOptions", Ember.Object.create({ @@ -37,7 +41,7 @@ export default SelectKitComponent.extend({ @on("didReceiveAttrs") _compute() { - Ember.run.scheduleOnce("afterRender", () => { + run.scheduleOnce("afterRender", () => { this.willComputeAttributes(); let content = this.get("content") || []; let asyncContent = this.get("asyncContent") || []; @@ -51,8 +55,6 @@ export default SelectKitComponent.extend({ values = this.willComputeValues(values); values = this.computeValues(values); values = this._beforeDidComputeValues(values); - this._setHeaderComputedContent(); - this._setCollectionHeaderComputedContent(); this.didComputeContent(content); this.didComputeAsyncContent(asyncContent); this.didComputeValues(values); @@ -62,7 +64,7 @@ export default SelectKitComponent.extend({ @computed("filter", "shouldDisplayCreateRow") createRowComputedContent(filter, shouldDisplayCreateRow) { - if (shouldDisplayCreateRow === true) { + if (shouldDisplayCreateRow) { let content = this.createContentFromInput(filter); return this.computeContentItem(content, { created: true }); } @@ -88,13 +90,11 @@ export default SelectKitComponent.extend({ didComputeValues(values) { return values; }, mutateAttributes() { - if (this.get("isDestroyed") || this.get("isDestroying")) return; + run.next(() => { + if (this.get("isDestroyed") || this.get("isDestroying")) return; - Ember.run.next(() => { this.mutateContent(this.get("computedContent")); this.mutateValues(this.get("computedValues")); - this._setCollectionHeaderComputedContent(); - this._setHeaderComputedContent(); }); }, mutateValues(computedValues) { @@ -139,10 +139,10 @@ export default SelectKitComponent.extend({ return computedContent; }, - baseHeaderComputedContent() { + computeHeaderContent() { return { title: this.get("title"), - selectedComputedContents: this.get("selectedComputedContents") + selection: this.get("selection") }; }, @@ -153,81 +153,12 @@ export default SelectKitComponent.extend({ }; }, - @computed("limit", "computedValues.[]") - limitReached(limit, computedValues) { - if (!limit) return false; - return computedValues.length >= limit; - }, - validateSelect() { - return this._super() && !this.get("limitReached"); - }, - - didPressBackspace(event) { - this.expand(event); - this.keyDown(event); - this._destroyEvent(event); - }, - - didPressEscape(event) { - const $highlighted = this.$(".selected-name.is-highlighted"); - if ($highlighted.length > 0) { - $highlighted.removeClass("is-highlighted"); - } - - this._super(event); - }, - - keyDown(event) { - if (!isEmpty(this.get("filter"))) return; - - const keyCode = event.keyCode || event.which; - const $filterInput = this.$filterInput(); - - // select all choices - if (this.get("hasSelection") && event.metaKey === true && keyCode === 65) { - this.$(".choices .selected-name:not(.is-locked)").addClass("is-highlighted"); - return false; - } - - // clear selection when multiple - if (this.$(".selected-name.is-highlighted").length >= 1 && keyCode === this.keys.BACKSPACE) { - const highlightedComputedContents = []; - $.each(this.$(".selected-name.is-highlighted"), (i, el) => { - const computedContent = this._findComputedContentItemByGuid($(el).attr("data-guid")); - if (!Ember.isNone(computedContent)) { highlightedComputedContents.push(computedContent); } - }); - this.send("deselect", highlightedComputedContents); - return; - } - - // try to remove last item from the list - if (keyCode === this.keys.BACKSPACE) { - let $lastSelectedValue = $(this.$(".choices .selected-name:not(.is-locked)").last()); - - if ($lastSelectedValue.length === 0) { return; } - - if ($filterInput.not(":visible") && $lastSelectedValue.length > 0) { - $lastSelectedValue.trigger("backspace"); - return false; - } - - if ($filterInput.val() === "") { - if ($filterInput.is(":focus")) { - if ($lastSelectedValue.length > 0) { $lastSelectedValue.trigger("backspace"); } - } else { - if ($lastSelectedValue.length > 0) { - $lastSelectedValue.trigger("backspace"); - } else { - $filterInput.focus(); - } - } - } - } + return this._super() && !this.get("hasReachedLimit"); }, @computed("computedValues.[]", "computedContent.[]") - selectedComputedContents(computedValues, computedContent) { + selection(computedValues, computedContent) { const selected = []; computedValues.forEach(v => { @@ -238,88 +169,123 @@ export default SelectKitComponent.extend({ return selected; }, - @computed("selectedComputedContents.[]") - hasSelection(selectedComputedContents) { return !Ember.isEmpty(selectedComputedContents); }, + @computed("selection.[]") + hasSelection(selection) { return !isEmpty(selection); }, + + didPressTab(event) { + if (isEmpty(this.get("filter")) && !this.get("highlighted")) { + this.$header().focus(); + this.close(event); + return true; + } + + if (this.get("highlighted") && this.get("isExpanded")) { + this._destroyEvent(event); + this.focus(); + this.select(this.get("highlighted")); + return false; + } else { + this.close(event); + } + + return true; + }, autoHighlight() { - Ember.run.schedule("afterRender", () => { + run.schedule("afterRender", () => { if (!this.get("isExpanded")) return; if (!this.get("renderedBodyOnce")) return; - if (!isNone(this.get("highlightedValue"))) return; + if (this.get("highlighted")) return; if (isEmpty(this.get("collectionComputedContent"))) { if (this.get("createRowComputedContent")) { - this.send("highlight", this.get("createRowComputedContent")); + this.highlight(this.get("createRowComputedContent")); } else if (this.get("noneRowComputedContent") && this.get("hasSelection")) { - this.send("highlight", this.get("noneRowComputedContent")); + this.highlight(this.get("noneRowComputedContent")); } } else { - this.send("highlight", this.get("collectionComputedContent.firstObject")); + this.highlight(this.get("collectionComputedContent.firstObject")); } }); }, - didSelect() { - this.focus(); - this.autoHighlight(); + select(computedContentItem) { + if (!computedContentItem || computedContentItem.__sk_row_type === "noneRow") { + this.clearSelection(); + return; + } - applyOnSelectPluginApiCallbacks( - this.get("pluginApiIdentifiers"), - this.get("computedValue"), - this - ); - - this._boundaryActionHandler("onSelect", this.get("computedValue")); - }, - - willDeselect() { - this.clearFilter(); - this.set("highlightedValue", null); - }, - - didDeselect(rowComputedContentItems) { - this.focus(); - this.autoHighlight(); - this._boundaryActionHandler("onDeselect", rowComputedContentItems); - }, - - actions: { - clearSelection() { - this.send("deselect", this.get("selectedComputedContents")); - this._boundaryActionHandler("onClearSelection"); - }, - - create(computedContentItem) { + if (computedContentItem.__sk_row_type === "createRow") { if (!this.get("computedValues").includes(computedContentItem.value) && this.validateCreate(computedContentItem.value)) { + this.willCreate(computedContentItem); + + computedContentItem.__sk_row_type = null; this.get("computedContent").pushObject(computedContentItem); - this._boundaryActionHandler("onCreate"); - this.send("select", computedContentItem); + + run.schedule("afterRender", () => { + this.didCreate(computedContentItem); + this._boundaryActionHandler("onCreate"); + }); + + this.select(computedContentItem); + return; } else { this._boundaryActionHandler("onCreateFailure"); + return; } - }, - - select(computedContentItem) { - this.willSelect(computedContentItem); - - if (this.validateSelect(computedContentItem)) { - this.get("computedValues").pushObject(computedContentItem.value); - Ember.run.next(() => this.mutateAttributes()); - Ember.run.schedule("afterRender", () => this.didSelect(computedContentItem)); - } else { - this._boundaryActionHandler("onSelectFailure"); - } - }, - - deselect(rowComputedContentItems) { - rowComputedContentItems = Ember.makeArray(rowComputedContentItems); - const generatedComputedContents = this._filterRemovableComputedContents(makeArray(rowComputedContentItems)); - this.willDeselect(rowComputedContentItems); - this.get("computedValues").removeObjects(rowComputedContentItems.map(r => r.value)); - this.get("computedContent").removeObjects(generatedComputedContents); - Ember.run.next(() => this.mutateAttributes()); - Ember.run.schedule("afterRender", () => this.didDeselect(rowComputedContentItems)); } + + if (this.validateSelect(computedContentItem)) { + this.willSelect(computedContentItem); + this.clearFilter(); + this.setProperties({ highlighted: null }); + this.get("computedValues").pushObject(computedContentItem.value); + + run.next(() => this.mutateAttributes()); + + run.schedule("afterRender", () => { + this.didSelect(computedContentItem); + + applyOnSelectPluginApiCallbacks( + this.get("pluginApiIdentifiers"), + computedContentItem.value, + this + ); + + this.autoHighlight(); + + this._boundaryActionHandler("onSelect", computedContentItem.value); + }); + } else { + this._boundaryActionHandler("onSelectFailure"); + } + }, + + deselect(rowComputedContentItems) { + this.willDeselect(rowComputedContentItems); + rowComputedContentItems = makeArray(rowComputedContentItems); + const generatedComputedContents = this._filterRemovableComputedContents(makeArray(rowComputedContentItems)); + this.set("highlighted", null); + this.set("highlightedSelection", []); + this.get("computedValues").removeObjects(rowComputedContentItems.map(r => r.value)); + this.get("computedContent").removeObjects(generatedComputedContents); + run.next(() => this.mutateAttributes()); + run.schedule("afterRender", () => { + this.didDeselect(rowComputedContentItems); + this.autoHighlight(); + }); + }, + + close(event) { + this.clearHighlightSelection(); + + this._super(event); + }, + + unfocus(event) { + this.clearHighlightSelection(); + + this._super(event); } }); diff --git a/app/assets/javascripts/select-kit/components/multi-select/multi-select-filter.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/multi-select-filter.js.es6 new file mode 100644 index 00000000000..e9b92334484 --- /dev/null +++ b/app/assets/javascripts/select-kit/components/multi-select/multi-select-filter.js.es6 @@ -0,0 +1,14 @@ +import computed from "ember-addons/ember-computed-decorators"; +const { isEmpty } = Ember; +import SelectKitFilterComponent from "select-kit/components/select-kit/select-kit-filter"; + +export default SelectKitFilterComponent.extend({ + layoutName: "select-kit/templates/components/select-kit/select-kit-filter", + classNames: ["multi-select-filter"], + + @computed("placeholder", "hasSelection") + computedPlaceholder(placeholder, hasSelection) { + if (hasSelection) return ""; + return isEmpty(placeholder) ? "" : I18n.t(placeholder); + } +}); diff --git a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 index 2db20196894..bd57cf549e7 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 @@ -34,12 +34,12 @@ export default SelectKitHeaderComponent.extend({ $filter.width(availableSpace - parentRightPadding * 4); }, - @computed("computedContent.selectedComputedContents.[]") + @computed("computedContent.selection.[]") names(selection) { return Ember.makeArray(selection).map(s => s.name).join(","); }, - @computed("computedContent.selectedComputedContents.[]") + @computed("computedContent.selection.[]") values(selection) { return Ember.makeArray(selection).map(s => s.value).join(","); } diff --git a/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 index eff2b9716d4..9b6b288023e 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/selected-name.js.es6 @@ -28,20 +28,6 @@ export default Ember.Component.extend({ return null; }, - didInsertElement() { - this._super(); - - $(this.element).on("backspace.selected-name", () => { - this._handleBackspace(); - }); - }, - - willDestroyElement() { - this._super(); - - $(this.element).off("backspace.selected-name"); - }, - label: Ember.computed.or("computedContent.label", "title", "name"), name: Ember.computed.alias("computedContent.name"), @@ -52,19 +38,14 @@ export default Ember.Component.extend({ return this.getWithDefault("computedContent.locked", false); }), - click() { - if (this.get("isLocked") === true) return false; - this.sendAction("deselect", [this.get("computedContent")]); - return false; + @computed("computedContent", "highlightedSelection.[]") + isHighlighted(computedContent, highlightedSelection) { + return highlightedSelection.includes(this.get("computedContent")); }, - _handleBackspace() { - if (this.get("isLocked") === true) return false; - - if (this.get("isHighlighted")) { - this.sendAction("deselect", [this.get("computedContent")]); - } else { - this.set("isHighlighted", true); - } + click() { + if (this.get("isLocked")) return false; + this.sendAction("onClickSelectionItem", [this.get("computedContent")]); + return false; } }); diff --git a/app/assets/javascripts/select-kit/components/none-category-row.js.es6 b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 index 51b6b4add0f..7e92c2bc85c 100644 --- a/app/assets/javascripts/select-kit/components/none-category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/none-category-row.js.es6 @@ -13,9 +13,5 @@ export default CategoryRowComponent.extend({ allowUncategorized: true, hideParent: true }).htmlSafe(); - }, - - click() { - this.sendAction("clearSelection"); } }); diff --git a/app/assets/javascripts/select-kit/components/notifications-button.js.es6 b/app/assets/javascripts/select-kit/components/notifications-button.js.es6 index 1c91d22a8ec..b7bd8dfa8c9 100644 --- a/app/assets/javascripts/select-kit/components/notifications-button.js.es6 +++ b/app/assets/javascripts/select-kit/components/notifications-button.js.es6 @@ -22,7 +22,7 @@ export default DropdownSelectBoxComponent.extend({ iconForSelectedDetails: Ember.computed.alias("selectedDetails.icon"), computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.name = I18n.t(`${this.get("i18nPrefix")}.${this.get("selectedDetails.key")}.title`); content.hasSelection = this.get("hasSelection"); return content; diff --git a/app/assets/javascripts/select-kit/components/period-chooser.js.es6 b/app/assets/javascripts/select-kit/components/period-chooser.js.es6 index 939bdedd949..31edf3f7d3c 100644 --- a/app/assets/javascripts/select-kit/components/period-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/period-chooser.js.es6 @@ -1,4 +1,5 @@ import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; export default DropdownSelectBoxComponent.extend({ classNames: ["period-chooser"], @@ -8,6 +9,11 @@ export default DropdownSelectBoxComponent.extend({ value: Ember.computed.alias("period"), isHidden: Ember.computed.alias("showPeriods"), + @computed("isExpanded") + caretIcon(isExpanded) { + return isExpanded ? "caret-up" : "caret-down"; + }, + actions: { onSelect() { this.sendAction("action", this.get("computedValue")); diff --git a/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-header.js.es6 b/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-header.js.es6 index 2c9c927992f..99cd40352e0 100644 --- a/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-header.js.es6 @@ -1,12 +1,6 @@ import DropdownSelectBoxHeaderomponent from "select-kit/components/dropdown-select-box/dropdown-select-box-header"; -import computed from 'ember-addons/ember-computed-decorators'; export default DropdownSelectBoxHeaderomponent.extend({ layoutName: "select-kit/templates/components/period-chooser/period-chooser-header", - classNames: "period-chooser-header", - - @computed("isExpanded") - caretIcon(isExpanded) { - return isExpanded ? "caret-up" : "caret-down"; - } + classNames: "period-chooser-header" }); diff --git a/app/assets/javascripts/select-kit/components/pinned-options.js.es6 b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 index 860e017601a..44a4f16612c 100644 --- a/app/assets/javascripts/select-kit/components/pinned-options.js.es6 +++ b/app/assets/javascripts/select-kit/components/pinned-options.js.es6 @@ -10,7 +10,7 @@ export default DropdownSelectBoxComponent.extend({ autoHighlight() {}, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); const pinnedGlobally = this.get("topic.pinned_globally"); const pinned = this.get("computedValue"); const globally = pinnedGlobally ? "_globally" : ""; diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index 6f14d71b39c..895fbddfc70 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -1,4 +1,4 @@ -const { isNone, run } = Ember; +const { get, isNone, run, isEmpty, makeArray } = Ember; import computed from "ember-addons/ember-computed-decorators"; import UtilsMixin from "select-kit/mixins/utils"; import DomHelpersMixin from "select-kit/mixins/dom-helpers"; @@ -7,7 +7,6 @@ import PluginApiMixin from "select-kit/mixins/plugin-api"; import { applyContentPluginApiCallbacks, applyHeaderContentPluginApiCallbacks, - applyOnSelectPluginApiCallbacks, applyCollectionHeaderCallbacks } from "select-kit/mixins/plugin-api"; @@ -26,6 +25,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi "isLeftAligned", "isRightAligned", "hasSelection", + "hasReachedLimit", ], isDisabled: false, isExpanded: false, @@ -37,13 +37,13 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi renderedFilterOnce: false, tabindex: 0, none: null, - highlightedValue: null, + highlighted: null, valueAttribute: "id", nameProperty: "name", autoFilterable: false, filterable: false, filter: "", - previousFilter: null, + previousFilter: "", filterPlaceholder: "select_kit.filter_placeholder", filterIcon: "search", headerIcon: null, @@ -70,6 +70,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi allowContentReplacement: false, collectionHeader: null, allowAutoSelectFirst: true, + highlightedSelection: null, init() { this._super(); @@ -78,6 +79,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi this.set("headerComponentOptions", Ember.Object.create()); this.set("rowComponentOptions", Ember.Object.create()); this.set("computedContent", []); + this.set("highlightedSelection", []); if (this.site && this.site.isMobileDevice) { this.setProperties({ @@ -99,6 +101,22 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi } }, + keyDown(event) { + if (!isEmpty(this.get("filter"))) return true; + + const keyCode = event.keyCode || event.which; + + if (event.metaKey === true && keyCode === this.keys.A) { + this.didPressSelectAll(); + return false; + } + + if (keyCode === this.keys.BACKSPACE) { + this.didPressBackspace(); + return false; + } + }, + willDestroyElement() { this.removeObserver(`content.@each.${this.get("nameProperty")}`, this, this._compute); this.removeObserver(`content.[]`, this, this._compute); @@ -132,28 +150,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi }, didComputeAsyncContent() {}, - computeHeaderContent() { - return this.baseHeaderComputedContent(); - }, - computeContentItem(contentItem, options) { - return this.baseComputedContentItem(contentItem, options); - }, - - computeAsyncContentItem(contentItem, options) { - return this.computeContentItem(contentItem, options); - }, - - @computed("isAsync", "filteredAsyncComputedContent.[]", "filteredComputedContent.[]") - collectionComputedContent(isAsync, filteredAsyncComputedContent, filteredComputedContent) { - return isAsync ? filteredAsyncComputedContent : filteredComputedContent; - }, - - validateCreate() { return true; }, - - validateSelect() { return true; }, - - baseComputedContentItem(contentItem, options) { let originalContent; options = options || {}; const name = options.name; @@ -166,13 +163,39 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi originalContent = contentItem; } - return { + let computedContentItem = { value: this._castInteger(this.valueForContentItem(contentItem)), name: name || this._nameForContent(contentItem), locked: false, created: options.created || false, + __sk_row_type: options.created ? "createRow" : null, originalContent }; + + return computedContentItem; + }, + + computeAsyncContentItem(contentItem, options) { + return this.computeContentItem(contentItem, options); + }, + + @computed("isAsync", "isLoading", "filteredAsyncComputedContent.[]", "filteredComputedContent.[]") + collectionComputedContent(isAsync, isLoading, filteredAsyncComputedContent, filteredComputedContent) { + if (isAsync) { + return isLoading ? [] : filteredAsyncComputedContent; + } else { + return filteredComputedContent; + } + }, + + validateCreate() { return !this.get("hasReachedLimit"); }, + + validateSelect() { return !this.get("hasReachedLimit"); }, + + @computed("limit", "selection.[]") + hasReachedLimit(limit, selection) { + if (!limit) return false; + return selection.length >= limit; }, @computed("shouldFilter", "allowAny", "filter") @@ -182,16 +205,16 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi return false; }, - @computed("filter", "collectionComputedContent.[]") - noContentRow(filter, collectionComputedContent) { - if (filter.length > 0 && collectionComputedContent.length === 0) { + @computed("filter", "collectionComputedContent.[]", "isLoading") + noContentRow(filter, collectionComputedContent, isLoading) { + if (filter.length > 0 && collectionComputedContent.length === 0 && !isLoading) { return I18n.t("select_kit.no_content"); } }, - @computed("limitReached", "limit") - maxContentRow(limitReached, limit) { - if (limitReached) { + @computed("hasReachedLimit", "limit") + maxContentRow(hasReachedLimit, limit) { + if (hasReachedLimit) { return I18n.t("select_kit.max_content_reached", { count: limit }); } }, @@ -204,9 +227,9 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi return false; }, - @computed("computedValue", "filter", "collectionComputedContent.[]", "limitReached") - shouldDisplayCreateRow(computedValue, filter, collectionComputedContent, limitReached) { - if (limitReached) return false; + @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedLimit", "isLoading") + shouldDisplayCreateRow(computedValue, filter, collectionComputedContent, hasReachedLimit, isLoading) { + if (isLoading || hasReachedLimit) return false; if (collectionComputedContent.map(c => c.value).includes(filter)) return false; if (this.get("allowAny") && filter.length > 0 && this.validateCreate(filter)) return true; return false; @@ -216,7 +239,9 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi createRowComputedContent(filter, shouldDisplayCreateRow) { if (shouldDisplayCreateRow) { let content = this.createContentFromInput(filter); - return this.computeContentItem(content, { created: true }); + let computedContentItem = this.computeContentItem(content, { created: true }); + computedContentItem.__sk_row_type = "createRow"; + return computedContentItem; } }, @@ -238,90 +263,100 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi @computed("none") noneRowComputedContent(none) { if (isNone(none)) { return null; } + let noneRowComputedContent; switch (typeof none) { case "string": - return this.computeContentItem(this.noneValue, { + noneRowComputedContent = this.computeContentItem(this.noneValue, { name: (I18n.t(none) || "").htmlSafe() }); + break; default: - return this.computeContentItem(none); + noneRowComputedContent = this.computeContentItem(none); } + + noneRowComputedContent.__sk_row_type = "noneRow"; + + return noneRowComputedContent; }, createContentFromInput(input) { return input; }, - willSelect() { - this.clearFilter(); - this.set("highlightedValue", null); - }, - didSelect() { - this.collapse(); - this.focus(); - - applyOnSelectPluginApiCallbacks( - this.get("pluginApiIdentifiers"), - this.get("computedValue"), - this - ); - - this._boundaryActionHandler("onSelect", this.get("computedValue")); + highlightSelection(items) { + this.propertyWillChange("highlightedSelection"); + this.set("highlightedSelection", makeArray(items)); + this.propertyDidChange("highlightedSelection"); }, - willDeselect() { - this.clearFilter(); - this.set("highlightedValue", null); - }, - didDeselect(rowComputedContentItem) { - this.collapse(); - this.focus(); - this._boundaryActionHandler("onDeselect", rowComputedContentItem); + clearHighlightSelection() { + this.highlightSelection([]); }, + willSelect() {}, + didSelect() {}, + + willCreate() {}, + didCreate() {}, + + willDeselect() {}, + didDeselect() {}, + clearFilter() { this.$filterInput().val(""); - this.setProperties({ filter: "" }); + this.setProperties({ filter: "", previousFilter: "" }); }, startLoading() { this.set("isLoading", true); + this.set("highlighted", null); this._boundaryActionHandler("onStartLoading"); }, stopLoading() { + this.focus(); this.set("isLoading", false); this._boundaryActionHandler("onStopLoading"); }, - _setCollectionHeaderComputedContent() { - const collectionHeaderComputedContent = applyCollectionHeaderCallbacks( + @computed("selection.[]", "isExpanded", "filter", "highlightedSelection.[]") + collectionHeaderComputedContent() { + return applyCollectionHeaderCallbacks( this.get("pluginApiIdentifiers"), this.get("collectionHeader"), this ); - this.set("collectionHeaderComputedContent", collectionHeaderComputedContent); }, - _setHeaderComputedContent() { - const headerComputedContent = applyHeaderContentPluginApiCallbacks( + @computed("selection.[]", "isExpanded", "headerIcon") + headerComputedContent() { + return applyHeaderContentPluginApiCallbacks( this.get("pluginApiIdentifiers"), this.computeHeaderContent(), this ); - this.set("headerComputedContent", headerComputedContent); }, _boundaryActionHandler(actionName, ...params) { - if (Ember.get(this.actions, actionName)) { + if (get(this.actions, actionName)) { run.next(() => this.send(actionName, ...params)); } else if (this.get(actionName)) { run.next(() => this.get(actionName)()); } }, + highlight(computedContent) { + this.set("highlighted", computedContent); + this._boundaryActionHandler("onHighlight", computedContent); + }, + + clearSelection() { + this.deselect(this.get("selection")); + this.focus(); + }, + actions: { - toggle() { - this._boundaryActionHandler("onToggle", this); + onToggle() { + this.clearHighlightSelection(); if (this.get("isExpanded")) { this._boundaryActionHandler("onCollapse", this); @@ -332,16 +367,29 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi } }, - highlight(rowComputedContent) { - this.set("highlightedValue", rowComputedContent.value); - this._boundaryActionHandler("onHighlight", rowComputedContent); + onClickRow(computedContentItem) { + this.didClickRow(computedContentItem); }, - filterComputedContent(filter) { + onClickSelectionItem(computedContentItem) { + this.didClickSelectionItem(computedContentItem); + }, + + onClearSelection() { + this.clearSelection(); + }, + + onMouseoverRow(computedContentItem) { + this.highlight(computedContentItem); + }, + + onFilterComputedContent(filter) { if (filter === this.get("previousFilter")) return; + this.clearHighlightSelection(); + this.setProperties({ - highlightedValue: null, + highlighted: null, renderedFilterOnce: true, previousFilter: filter, filter diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 index 60e7ec98199..426261f4bfb 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 @@ -7,9 +7,8 @@ export default Ember.Component.extend({ classNameBindings: ["isFocused", "isHidden"], isHidden: Ember.computed.not("shouldDisplayFilter"), - @computed("placeholder", "hasSelection") - computedPlaceholder(placeholder, hasSelection) { - if (hasSelection) return ""; + @computed("placeholder") + computedPlaceholder(placeholder) { return isEmpty(placeholder) ? "" : I18n.t(placeholder); } }); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 index d5e76b4a767..75e3eab0b1d 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 @@ -1,4 +1,5 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; +const { isEmpty, makeArray } = Ember; export default Ember.Component.extend({ layoutName: "select-kit/templates/components/select-kit/select-kit-header", @@ -33,10 +34,10 @@ export default Ember.Component.extend({ @computed("computedContent.icon", "computedContent.icons") icons(icon, icons) { - return Ember.makeArray(icon).concat(icons).filter(i => !Ember.isEmpty(i)); + return makeArray(icon).concat(icons).filter(i => !isEmpty(i)); }, click() { - this.sendAction("toggle"); + this.sendAction("onToggle"); } }); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 index b2d488f4992..49b922d14b9 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-none-row.js.es6 @@ -2,9 +2,5 @@ import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-r export default SelectKitRowComponent.extend({ layoutName: "select-kit/templates/components/select-kit/select-kit-row", - classNames: "none", - - click() { - this.sendAction("clearSelection"); - } + classNames: "none" }); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 index 3ac3a2ce8e5..ab08156148d 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 @@ -13,7 +13,8 @@ export default Ember.Component.extend(UtilsMixin, { "title", "value:data-value", "name:data-name", - "ariaLabel:aria-label" + "ariaLabel:aria-label", + "guid:data-guid" ], classNameBindings: ["isHighlighted", "isSelected"], @@ -27,6 +28,9 @@ export default Ember.Component.extend(UtilsMixin, { return null; }, + @computed("computedContent") + guid(computedContent) { return Ember.guidFor(computedContent); }, + label: Ember.computed.or("computedContent.label", "title", "name"), name: Ember.computed.alias("computedContent.name"), @@ -39,7 +43,7 @@ export default Ember.Component.extend(UtilsMixin, { @on("didReceiveAttrs") _setSelectionState() { this.set("isSelected", this.get("computedValue") === this.get("value")); - this.set("isHighlighted", this.get("highlightedValue") === this.get("value")); + this.set("isHighlighted", this.get("highlighted.value") === this.get("value")); }, @on("willDestroyElement") @@ -57,14 +61,14 @@ export default Ember.Component.extend(UtilsMixin, { }, mouseEnter() { - this.set("hoverDebounce", run.debounce(this, this._sendHighlightAction, 32)); + this.set("hoverDebounce", run.debounce(this, this._sendMouseoverAction, 32)); }, click() { - this.sendAction("select", this.get("computedContent")); + this.sendAction("onClickRow", this.get("computedContent")); }, - _sendHighlightAction() { - this.sendAction("highlight", this.get("computedContent")); + _sendMouseoverAction() { + this.sendAction("onMouseoverRow", this.get("computedContent")); } }); diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 index a9e2d32979a..ca6878d8130 100644 --- a/app/assets/javascripts/select-kit/components/single-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -1,6 +1,10 @@ import SelectKitComponent from "select-kit/components/select-kit"; import { default as computed, on } from 'ember-addons/ember-computed-decorators'; -const { get, isNone, isEmpty, isPresent, run } = Ember; +const { get, isNone, isEmpty, isPresent, run, makeArray } = Ember; + +import { + applyOnSelectPluginApiCallbacks, +} from "select-kit/mixins/plugin-api"; export default SelectKitComponent.extend({ pluginApiIdentifiers: ["single-select"], @@ -32,20 +36,15 @@ export default SelectKitComponent.extend({ this.didComputeAttributes(); if (this.get("allowInitialValueMutation")) this.mutateAttributes(); - - this._setCollectionHeaderComputedContent(); - this._setHeaderComputedContent(); }); }, mutateAttributes() { - if (this.get("isDestroyed") || this.get("isDestroying")) return; - run.next(() => { + if (this.get("isDestroyed") || this.get("isDestroying")) return; + this.mutateContent(this.get("computedContent")); this.mutateValue(this.get("computedValue")); - this._setCollectionHeaderComputedContent(); - this._setHeaderComputedContent(); }); }, mutateContent() {}, @@ -84,12 +83,12 @@ export default SelectKitComponent.extend({ }); }, - baseHeaderComputedContent() { + computeHeaderContent() { return { title: this.get("title"), - icons: Ember.makeArray(this.getWithDefault("headerIcon", [])), - value: this.get("selectedComputedContent.value"), - name: this.get("selectedComputedContent.name") || this.get("noneRowComputedContent.name") + icons: makeArray(this.getWithDefault("headerIcon", [])), + value: this.get("selection.value"), + name: this.get("selection.name") || this.get("noneRowComputedContent.name") }; }, @@ -120,84 +119,118 @@ export default SelectKitComponent.extend({ }, @computed("computedValue", "computedContent.[]") - selectedComputedContent(computedValue, computedContent) { + selection(computedValue, computedContent) { return computedContent.findBy("value", computedValue); }, - @computed("selectedComputedContent") - hasSelection(selectedComputedContent) { - return selectedComputedContent !== this.get("noneRowComputedContent") && - !Ember.isNone(selectedComputedContent); + @computed("selection") + hasSelection(selection) { + return selection !== this.get("noneRowComputedContent") && !isNone(selection); }, - @computed("computedValue", "filter", "collectionComputedContent.[]", "limitReached") + @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedLimit") shouldDisplayCreateRow(computedValue, filter) { return this._super() && computedValue !== filter; }, autoHighlight() { run.schedule("afterRender", () => { - if (!isNone(this.get("highlightedValue"))) return; - - const filteredComputedContent = this.get("filteredComputedContent"); - const displayCreateRow = this.get("shouldDisplayCreateRow"); - const none = this.get("noneRowComputedContent"); - - if (this.get("hasSelection") && isEmpty(this.get("filter"))) { - this.send("highlight", this.get("selectedComputedContent")); + if (this.get("shouldDisplayCreateRow")) { + this.highlight(this.get("createRowComputedContent")); return; } - if (isNone(this.get("highlightedValue")) && !isEmpty(filteredComputedContent)) { - this.send("highlight", get(filteredComputedContent, "firstObject")); + if (!isEmpty(this.get("filter")) && !isEmpty(this.get("collectionComputedContent"))) { + this.highlight(this.get("collectionComputedContent.firstObject")); return; } - if (displayCreateRow && isEmpty(filteredComputedContent)) { - this.send("highlight", this.get("createRowComputedContent")); + if (!this.get("isAsync") && this.get("hasSelection") && isEmpty(this.get("filter"))) { + this.highlight(get(makeArray(this.get("selection")), "firstObject")); + return; } - else if (!isEmpty(filteredComputedContent)) { - this.send("highlight", get(filteredComputedContent, "firstObject")); + + if (!this.get("isAsync") && !this.get("hasSelection") && isEmpty(this.get("filter")) && !isEmpty(this.get("collectionComputedContent"))) { + this.highlight(this.get("collectionComputedContent.firstObject")); + return; } - else if (isEmpty(filteredComputedContent) && isPresent(none) && !displayCreateRow) { - this.send("highlight", none); + + if (isPresent(this.get("noneRowComputedContent"))) { + this.highlight(this.get("noneRowComputedContent")); + return; } }); }, - actions: { - clearSelection() { - this.send("deselect", this.get("selectedComputedContent")); - this._boundaryActionHandler("onClearSelection"); - }, + select(computedContentItem) { + if (!computedContentItem || computedContentItem.__sk_row_type === "noneRow") { + this.clearSelection(); + return; + } - create(computedContentItem) { + if (computedContentItem.__sk_row_type === "createRow") { if (this.get("computedValue") !== computedContentItem.value && this.validateCreate(computedContentItem.value)) { + this.willCreate(computedContentItem); + computedContentItem.__sk_row_type = null; this.get("computedContent").pushObject(computedContentItem); - this._boundaryActionHandler("onCreate"); - this.send("select", computedContentItem); + + run.schedule("afterRender", () => { + this.didCreate(computedContentItem); + this._boundaryActionHandler("onCreate"); + }); + + this.select(computedContentItem); + return; } else { this._boundaryActionHandler("onCreateFailure"); + return; } - }, - - select(rowComputedContentItem) { - if (this.validateSelect(rowComputedContentItem)) { - this.willSelect(rowComputedContentItem); - this.set("computedValue", rowComputedContentItem.value); - this.mutateAttributes(); - run.schedule("afterRender", () => this.didSelect(rowComputedContentItem)); - } else { - this._boundaryActionHandler("onSelectFailure"); - } - }, - - deselect(rowComputedContentItem) { - this.willDeselect(rowComputedContentItem); - this.set("computedValue", null); - this.mutateAttributes(); - run.schedule("afterRender", () => this.didDeselect(rowComputedContentItem)); } + + if (this.validateSelect(computedContentItem)) { + this.willSelect(computedContentItem); + this.clearFilter(); + this.setProperties({ highlighted: null, computedValue: computedContentItem.value }); + + run.next(() => this.mutateAttributes()); + + run.schedule("afterRender", () => { + this.didSelect(computedContentItem); + + applyOnSelectPluginApiCallbacks( + this.get("pluginApiIdentifiers"), + computedContentItem.value, + this + ); + + this._boundaryActionHandler("onSelect", computedContentItem.value); + + this.autoHighlight(); + }); + } else { + this._boundaryActionHandler("onSelectFailure"); + } + }, + + deselect(computedContentItem) { + makeArray(computedContentItem).forEach((item) => { + this.willDeselect(item); + + this.clearFilter(); + + this.setProperties({ + computedValue: null, + highlighted: null, + highlightedSelection: [] + }); + + run.next(() => this.mutateAttributes()); + run.schedule("afterRender", () => { + this.didDeselect(item); + this._boundaryActionHandler("onDeselect", item); + this.autoHighlight(); + }); + }); } }); diff --git a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 index e50b6f780ef..d8e590eccff 100644 --- a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 @@ -58,7 +58,7 @@ export default MultiSelectComponent.extend(Tags, { actions: { onFilter(filter) { this.expand(); - this.set("searchDebounce", run.debounce(this, this.prepareSearch, filter, 200)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, filter, 200)); }, onExpand() { @@ -66,15 +66,15 @@ export default MultiSelectComponent.extend(Tags, { }, onDeselect() { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 200)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 200)); }, onSelect() { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 50)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 50)); } }, - prepareSearch(query) { + _prepareSearch(query) { const selectedTags = makeArray(this.get("values")).filter(t => t); const data = { diff --git a/app/assets/javascripts/select-kit/components/tag-drop.js.es6 b/app/assets/javascripts/select-kit/components/tag-drop.js.es6 index d30775edb43..47e8a343a96 100644 --- a/app/assets/javascripts/select-kit/components/tag-drop.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-drop.js.es6 @@ -39,7 +39,7 @@ export default ComboBoxComponent.extend({ }, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); if (!content.value) { if (this.get("noTagsSelected")) { diff --git a/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 index 07aa1a6d2b8..2a9713f9d32 100644 --- a/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 @@ -42,25 +42,25 @@ export default MultiSelectComponent.extend(Tags, { actions: { onFilter(filter) { this.expand(); - this.set("searchDebounce", run.debounce(this, this.prepareSearch, filter, 200)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, filter, 200)); }, onExpand() { if (isEmpty(this.get("collectionComputedContent"))) { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 200)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 200)); } }, onDeselect() { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 200)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 200)); }, onSelect() { - this.set("searchDebounce", run.debounce(this, this.prepareSearch, this.get("filter"), 50)); + this.set("searchDebounce", run.debounce(this, this._prepareSearch, this.get("filter"), 50)); } }, - prepareSearch(query) { + _prepareSearch(query) { const data = { q: query, limit: this.get("siteSettings.max_tag_search_results") diff --git a/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 b/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 index 8a54a215f1b..73272fc1ca7 100644 --- a/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 +++ b/app/assets/javascripts/select-kit/components/toolbar-popup-menu-options.js.es6 @@ -14,7 +14,7 @@ export default DropdownSelectBoxComponent.extend({ mutateValue(value) { this.sendAction("onPopupMenuAction", value); - this.setProperties({ value: null, highlightedValue: null }); + this.setProperties({ value: null, highlighted: null }); }, computeContent(content) { diff --git a/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 index 8bde260f5b8..0aa66a1920b 100644 --- a/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 +++ b/app/assets/javascripts/select-kit/components/topic-footer-mobile-dropdown.js.es6 @@ -8,7 +8,7 @@ export default ComboBoxComponent.extend({ allowInitialValueMutation: false, computeHeaderContent() { - let content = this.baseHeaderComputedContent(); + let content = this._super(); content.name = I18n.t("topic.controls"); return content; }, @@ -45,7 +45,7 @@ export default ComboBoxComponent.extend({ return; } - const refresh = () => this.send("deselect", value); + const refresh = () => this.deselect(this.get("selection")); switch(value) { case "invite": diff --git a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 index b867011f359..206e5fb7701 100644 --- a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 @@ -6,6 +6,7 @@ export default Ember.Mixin.create({ this._previousScrollParentOverflow = null; this._previousCSSContext = null; + this.selectionSelector = ".choice"; this.filterInputSelector = ".filter-input"; this.rowSelector = ".select-kit-row"; this.collectionSelector = ".select-kit-collection"; @@ -58,36 +59,42 @@ export default Ember.Mixin.create({ this.setProperties({ isFocused: false }); }, - // try to focus filter and fallback to header if not present focus() { Ember.run.schedule("afterRender", () => { - if ((!this.get("filterable")) || !this.$filterInput().is(":visible")) { - this.$header().focus(); - } else { - this.$filterInput().focus(); - } + this.$header().focus(); + }); + }, + + // try to focus filter and fallback to header if not present + focusFilterOrHeader() { + // next so we are sure it finised expand/collapse + Ember.run.next(() => { + Ember.run.schedule("afterRender", () => { + if (!this.$filterInput().is(":visible")) { + this.$header().focus(); + } else { + this.$filterInput().focus(); + } + }); }); }, expand() { - if (this.get("isExpanded") === true) return; + if (this.get("isExpanded")) return; this.setProperties({ isExpanded: true, renderedBodyOnce: true, isFocused: true }); - this._setCollectionHeaderComputedContent(); - this._setHeaderComputedContent(); - this.focus(); + this.focusFilterOrHeader(); this.autoHighlight(); }, collapse() { this.set("isExpanded", false); Ember.run.schedule("afterRender", () => this._removeFixedPosition() ); - this._setHeaderComputedContent(); }, // lose focus of the component in two steps // first collapse and keep focus and then remove focus unfocus(event) { - if (this.get("isExpanded") === true) { + if (this.get("isExpanded")) { this.collapse(event); this.focus(event); } else { diff --git a/app/assets/javascripts/select-kit/mixins/events.js.es6 b/app/assets/javascripts/select-kit/mixins/events.js.es6 index b5abd5dd03b..0d4492e9824 100644 --- a/app/assets/javascripts/select-kit/mixins/events.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/events.js.es6 @@ -9,6 +9,9 @@ export default Ember.Mixin.create({ UP: 38, DOWN: 40, BACKSPACE: 8, + LEFT: 37, + RIGHT: 39, + A: 65 }; }, @@ -55,7 +58,7 @@ export default Ember.Mixin.create({ this.$header() .on("blur.select-kit", () => { - if (this.get("isExpanded") === false && this.get("isFocused") === true) { + if (!this.get("isExpanded") && this.get("isFocused")) { this.close(); } }) @@ -68,22 +71,27 @@ export default Ember.Mixin.create({ if (document.activeElement !== this.$header()[0]) return event; - if (keyCode === this.keys.TAB) this.tabFromHeader(event); - if (keyCode === this.keys.BACKSPACE) this.backspaceFromHeader(event); + if (keyCode === this.keys.TAB && event.shiftKey) { this.unfocus(event); } + if (keyCode === this.keys.TAB && !event.shiftKey) this.tabFromHeader(event); + if (Ember.isEmpty(this.get("filter")) && keyCode === this.keys.BACKSPACE) this.backspaceFromHeader(event); if (keyCode === this.keys.ESC) this.escapeFromHeader(event); if (keyCode === this.keys.ENTER) this.enterFromHeader(event); if ([this.keys.UP, this.keys.DOWN].includes(keyCode)) this.upAndDownFromHeader(event); + if (Ember.isEmpty(this.get("filter")) && + [this.keys.LEFT, this.keys.RIGHT].includes(keyCode)) { + this.leftAndRightFromHeader(event); + } return event; }) .on("keypress.select-kit", (event) => { const keyCode = event.keyCode || event.which; - if (keyCode === this.keys.ENTER) { return true; } - if (keyCode === this.keys.TAB) { return true; } + if (keyCode === this.keys.ENTER) return true; + if (keyCode === this.keys.TAB) return true; this.expand(event); - if (this.get("filterable") === true || this.get("autoFilterable")) { + if (this.get("filterable") || this.get("autoFilterable")) { this.set("renderedFilterOnce", true); } @@ -100,7 +108,7 @@ export default Ember.Mixin.create({ this.$filterInput() .on("change.select-kit", (event) => { - this.send("filterComputedContent", $(event.target).val()); + this.send("onFilterComputedContent", $(event.target).val()); }) .on("keypress.select-kit", (event) => { event.stopPropagation(); @@ -108,40 +116,83 @@ export default Ember.Mixin.create({ .on("keydown.select-kit", (event) => { const keyCode = event.keyCode || event.which; - if (keyCode === this.keys.BACKSPACE && typeof this.backspaceFromFilter === "function") { - this.backspaceFromFilter(event); + if (Ember.isEmpty(this.get("filter")) && + keyCode === this.keys.BACKSPACE && + typeof this.didPressBackspaceFromFilter === "function") { + this.didPressBackspaceFromFilter(event); }; - if (keyCode === this.keys.TAB) this.tabFromFilter(event); + + if (keyCode === this.keys.TAB && event.shiftKey) { this.unfocus(event); } + if (keyCode === this.keys.TAB && !event.shiftKey) this.tabFromFilter(event); if (keyCode === this.keys.ESC) this.escapeFromFilter(event); if (keyCode === this.keys.ENTER) this.enterFromFilter(event); if ([this.keys.UP, this.keys.DOWN].includes(keyCode)) this.upAndDownFromFilter(event); + + if (Ember.isEmpty(this.get("filter")) && + [this.keys.LEFT, this.keys.RIGHT].includes(keyCode)) { + this.leftAndRightFromFilter(event); + } }); }, didPressTab(event) { - if (this.get("isExpanded") === false) { - this.unfocus(event); - } else if (this.$highlightedRow().length === 1) { - Ember.run.throttle(this, this._rowClick, this.$highlightedRow(), 150, 150, true); - this.unfocus(event); + if (this.$highlightedRow().length && this.get("isExpanded")) { + this.close(event); + this.$header().focus(); + const guid = this.$highlightedRow().attr("data-guid"); + this.select(this._findComputedContentItemByGuid(guid)); + return true; + } + + if (Ember.isEmpty(this.get("filter"))) { + this.close(event); return true; - } else { - this._destroyEvent(event); - this.unfocus(event); } return true; }, + didPressEnter(event) { + if (!this.get("isExpanded")) { + this.expand(event); + } else if (this.$highlightedRow().length) { + this.close(event); + this.$header().focus(); + const guid = this.$highlightedRow().attr("data-guid"); + this.select(this._findComputedContentItemByGuid(guid)); + } + + return true; + }, + + didClickSelectionItem(computedContentItem) { + this.focus(); + this.deselect(computedContentItem); + }, + + didClickRow(computedContentItem) { + this.close(event); + this.focus(); + this.select(computedContentItem); + }, + didPressEscape(event) { this._destroyEvent(event); - this.unfocus(event); + + if (this.get("highlightedSelection").length && this.get("isExpanded")) { + this.clearHighlightSelection(); + } else { + this.unfocus(event); + } }, didPressUpAndDownArrows(event) { this._destroyEvent(event); + this.clearHighlightSelection(); + const keyCode = event.keyCode || event.which; + const $rows = this.$rows(); if (this.get("isExpanded") === false) { @@ -151,9 +202,12 @@ export default Ember.Mixin.create({ this._highlightRow(this.$selectedRow()); return; } + + return; } - if ($rows.length <= 0) { return; } + if (!$rows.length) { return; } + if ($rows.length === 1) { this._rowSelection($rows, 0); return; @@ -164,28 +218,43 @@ export default Ember.Mixin.create({ Ember.run.throttle(this, this._moveHighlight, direction, $rows, 32); }, + didPressBackspaceFromFilter(event) { this.didPressBackspace(event); }, didPressBackspace(event) { - this._destroyEvent(event); + if (!this.get("isExpanded")) { + this.expand(); + if (event) event.stopImmediatePropagation(); + return; + } - this.expand(event); + if (!this.get("selection").length) return; - if (this.$filterInput().is(":visible")) { - this.$filterInput().focus().trigger(event).trigger("change"); + if (!Ember.isEmpty(this.get("filter"))) { + this.clearHighlightSelection(); + return; + } + + if (!this.get("highlightedSelection").length) { + // try to highlight the last non locked item from the current selection + Ember.makeArray(this.get("selection")).slice().reverse().some(selection => { + if (!Ember.get(selection, "locked")) { + this.highlightSelection(selection); + return true; + } + }); + + if (event) event.stopImmediatePropagation(); + } else { + this.deselect(this.get("highlightedSelection")); + if (event) event.stopImmediatePropagation(); } }, - didPressEnter(event) { - this._destroyEvent(event); - - if (this.get("isExpanded") === false) { - this.expand(event); - } else if (this.$highlightedRow().length === 1) { - Ember.run.throttle(this, this._rowClick, this.$highlightedRow(), 150, true); - } + didPressSelectAll() { + this.highlightSelection(Ember.makeArray(this.get("selection"))); }, didClickOutside(event) { - if ($(event.target).parents(".select-kit").length === 1) { + if (this.get("isExpanded") && $(event.target).parents(".select-kit").length) { this.close(event); return false; } @@ -200,6 +269,38 @@ export default Ember.Mixin.create({ this._destroyEvent(event); }, + didPressLeftAndRightArrows(event) { + if (!this.get("isExpanded")) { + this.expand(); + event.stopImmediatePropagation(); + return; + } + + if (Ember.isEmpty(this.get("selection"))) return; + + const keyCode = event.keyCode || event.which; + + if (keyCode === this.keys.LEFT) { + const prev = this.get("highlightedSelection.lastObject"); + const indexOfPrev = this.get("selection").indexOf(prev); + + if (this.get("selection")[indexOfPrev - 1]) { + this.highlightSelection(this.get("selection")[indexOfPrev - 1]); + } else { + this.highlightSelection(this.get("selection.lastObject")); + } + } else { + const prev = this.get("highlightedSelection.firstObject"); + const indexOfNext= this.get("selection").indexOf(prev); + + if (this.get("selection")[indexOfNext + 1]) { + this.highlightSelection(this.get("selection")[indexOfNext + 1]); + } else { + this.highlightSelection(this.get("selection.firstObject")); + } + } + }, + tabFromHeader(event) { this.didPressTab(event); }, tabFromFilter(event) { this.didPressTab(event); }, @@ -209,6 +310,9 @@ export default Ember.Mixin.create({ upAndDownFromHeader(event) { this.didPressUpAndDownArrows(event); }, upAndDownFromFilter(event) { this.didPressUpAndDownArrows(event); }, + leftAndRightFromHeader(event) { this.didPressLeftAndRightArrows(event); }, + leftAndRightFromFilter(event) { this.didPressLeftAndRightArrows(event); }, + backspaceFromHeader(event) { this.didPressBackspace(event); }, enterFromHeader(event) { this.didPressEnter(event); }, @@ -227,8 +331,6 @@ export default Ember.Mixin.create({ this._rowSelection($rows, nextIndex); }, - _rowClick($row) { $row.click(); }, - _rowSelection($rows, nextIndex) { const highlightableValue = $rows.eq(nextIndex).attr("data-value"); const $highlightableRow = this.$findRowByValue(highlightableValue); diff --git a/app/assets/javascripts/select-kit/mixins/tags.js.es6 b/app/assets/javascripts/select-kit/mixins/tags.js.es6 index ab5003616f1..099eeadfb77 100644 --- a/app/assets/javascripts/select-kit/mixins/tags.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/tags.js.es6 @@ -22,17 +22,18 @@ export default Ember.Mixin.create({ data }).then(json => { self.set("asyncContent", callback(self, json)); + self.autoHighlight(); }).catch(error => { popupAjaxError(error); }) .finally(() => { self.stopLoading(); - self.autoHighlight(); + self.focusFilterOrHeader(); }); }, validateCreate(term) { - if (this.get("limitReached") || !this.site.get("can_create_tag")) { + if (this.get("hasReachedLimit") || !this.site.get("can_create_tag")) { return false; } @@ -47,7 +48,9 @@ export default Ember.Mixin.create({ return false; } - if (this.get("asyncContent").map(c => get(c, "id")).includes(term)) { + const inCollection = this.get("collectionComputedContent").map(c => get(c, "id")).includes(term); + const inSelection = this.get("selection").map(s => s.toLowerCase()).includes(term); + if (inCollection || inSelection) { return false; } diff --git a/app/assets/javascripts/select-kit/mixins/utils.js.es6 b/app/assets/javascripts/select-kit/mixins/utils.js.es6 index 54f404b3b9e..11f4418cf4a 100644 --- a/app/assets/javascripts/select-kit/mixins/utils.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/utils.js.es6 @@ -36,12 +36,20 @@ export default Ember.Mixin.create({ }, _findComputedContentItemByGuid(guid) { - return this.get("computedContent").find(c => { + if (guidFor(this.get("createRowComputedContent")) === guid) { + return this.get("createRowComputedContent"); + } + + if (guidFor(this.get("noneRowComputedContent")) === guid) { + return this.get("noneRowComputedContent"); + } + + return this.get("collectionComputedContent").find(c => { return guidFor(c) === guid; }); }, _filterRemovableComputedContents(computedContent) { - return computedContent.filter(c => c.created === true); + return computedContent.filter(c => c.created); } }); diff --git a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs index 622baaaf02f..c3a7aa14fb3 100644 --- a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs @@ -5,7 +5,7 @@ {{#if shouldDisplayClearableButton}} - {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs index 48af8fab951..5a1ba341646 100644 --- a/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/dropdown-select-box/dropdown-select-box-header.hbs @@ -2,6 +2,6 @@ {{#if options.showFullTitle}} - {{label}} + {{{label}}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs index c419312c696..5034fa77bb8 100644 --- a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs @@ -15,7 +15,7 @@ {{/if}} {{#if shouldDisplayClearableButton}} - {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/mini-tag-chooser/mini-tag-chooser-header.hbs b/app/assets/javascripts/select-kit/templates/components/mini-tag-chooser/mini-tag-chooser-header.hbs index 09799d3d576..1bd94d922ce 100644 --- a/app/assets/javascripts/select-kit/templates/components/mini-tag-chooser/mini-tag-chooser-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/mini-tag-chooser/mini-tag-chooser-header.hbs @@ -1 +1,3 @@ -{{input class="selected-name" value=label readonly="readonly"}} +{{input class="selected-name" value=label readonly="readonly" tabindex="-1"}} + +{{d-icon caretIcon class="caret-icon"}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs index 072a4bff866..b3d4f473645 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs @@ -2,10 +2,10 @@ tabindex=tabindex isFocused=isFocused isExpanded=isExpanded + highlightedSelection=highlightedSelection + onClickSelectionItem=(action "onClickSelectionItem") computedContent=headerComputedContent - deselect=(action "deselect") - toggle=(action "toggle") - clearSelection=(action "clearSelection") + onToggle=(action "onToggle") options=headerComponentOptions }} {{component filterComponent @@ -16,12 +16,13 @@ isLoading=isLoading shouldDisplayFilter=shouldDisplayFilter isFocused=isFocused - filterComputedContent=(action "filterComputedContent") + onFilterComputedContent=(action "onFilterComputedContent") }} {{/component}}
{{#if renderedBodyOnce}} + {{#unless isLoading}} {{component collectionComponent collectionHeaderComputedContent=collectionHeaderComputedContent hasSelection=hasSelection @@ -34,16 +35,15 @@ templateForRow=templateForRow templateForNoneRow=templateForNoneRow templateForCreateRow=templateForCreateRow - clearSelection=(action "clearSelection") - select=(action "select") - highlight=(action "highlight") - create=(action "create") - highlightedValue=highlightedValue + onClickRow=(action "onClickRow") + onMouseoverRow=(action "onMouseoverRow") + highlighted=highlighted computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow maxContentRow=maxContentRow }} + {{/unless}} {{/if}}
diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs index 71173acb30d..2627302c4e7 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs @@ -1,8 +1,9 @@
- {{#each computedContent.selectedComputedContents as |selectedComputedContent|}} + {{#each computedContent.selection as |selection|}} {{component selectedNameComponent - deselect=deselect - computedContent=selectedComputedContent}} + onClickSelectionItem=onClickSelectionItem + highlightedSelection=highlightedSelection + computedContent=selection}} {{/each}} {{yield}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index 12f5d053800..6a7089e9ecd 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -9,11 +9,11 @@ {{component noneRowComponent computedContent=noneRowComputedContent templateForRow=templateForNoneRow - highlightedValue=highlightedValue - clearSelection=clearSelection - highlight=highlight + highlighted=highlighted + onMouseoverRow=onMouseoverRow computedValue=computedValue options=rowComponentOptions + onClickRow=onClickRow }} {{/if}} {{/if}} @@ -21,11 +21,11 @@ {{#if createRowComputedContent}} {{component createRowComponent computedContent=createRowComputedContent - highlightedValue=highlightedValue + highlighted=highlighted computedValue=computedValue templateForRow=templateForCreateRow - highlight=highlight - select=create + onMouseoverRow=onMouseoverRow + onClickRow=onClickRow options=rowComponentOptions }} {{/if}} @@ -43,11 +43,11 @@ {{#each collectionComputedContent as |computedContent|}} {{component rowComponent computedContent=computedContent - highlightedValue=highlightedValue + highlighted=highlighted computedValue=computedValue templateForRow=templateForRow - select=select - highlight=highlight + onClickRow=onClickRow + onMouseoverRow=onMouseoverRow options=rowComponentOptions }} {{/each}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs index dfe505f5b1f..4e0a519d58c 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-filter.hbs @@ -2,7 +2,7 @@ tabindex=-1 class="filter-input" placeholder=computedPlaceholder - key-up=filterComputedContent + key-up=onFilterComputedContent autocomplete="off" autocorrect="off" autocapitalize="off" diff --git a/app/assets/javascripts/select-kit/templates/components/single-select.hbs b/app/assets/javascripts/select-kit/templates/components/single-select.hbs index 8f676036031..46128823d3a 100644 --- a/app/assets/javascripts/select-kit/templates/components/single-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/single-select.hbs @@ -1,11 +1,11 @@ {{component headerComponent + caretIcon=caretIcon tabindex=tabindex isFocused=isFocused isExpanded=isExpanded computedContent=headerComputedContent - deselect=(action "deselect") - toggle=(action "toggle") - clearSelection=(action "clearSelection") + onToggle=(action "onToggle") + onClearSelection=(action "onClearSelection") options=headerComponentOptions }} @@ -18,7 +18,7 @@ shouldDisplayFilter=shouldDisplayFilter placeholder=filterPlaceholder isFocused=isFocused - filterComputedContent=(action "filterComputedContent") + onFilterComputedContent=(action "onFilterComputedContent") }} {{#if renderedBodyOnce}} @@ -34,11 +34,9 @@ templateForRow=templateForRow templateForNoneRow=templateForNoneRow templateForCreateRow=templateForCreateRow - clearSelection=(action "clearSelection") - select=(action "select") - highlight=(action "highlight") - create=(action "create") - highlightedValue=highlightedValue + onClickRow=(action "onClickRow") + onMouseoverRow=(action "onMouseoverRow") + highlighted=highlighted computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow diff --git a/app/assets/stylesheets/common/select-kit/category-chooser.scss b/app/assets/stylesheets/common/select-kit/category-chooser.scss index 9102b7cf873..09a20fc434d 100644 --- a/app/assets/stylesheets/common/select-kit/category-chooser.scss +++ b/app/assets/stylesheets/common/select-kit/category-chooser.scss @@ -2,6 +2,11 @@ &.combo-box { &.category-chooser { width: 300px; + + .combo-box-header { + padding: 4px; + } + .select-kit-row { display: -webkit-box; display: -ms-flexbox; diff --git a/app/assets/stylesheets/common/select-kit/future-date-input-selector.scss b/app/assets/stylesheets/common/select-kit/future-date-input-selector.scss index 8ca4efde5bf..1e6385b755a 100644 --- a/app/assets/stylesheets/common/select-kit/future-date-input-selector.scss +++ b/app/assets/stylesheets/common/select-kit/future-date-input-selector.scss @@ -3,6 +3,12 @@ &.future-date-input-selector { min-width: 50%; + .future-date-input-selector-header { + .btn-clear { + line-height: $line-height-large; + } + } + .future-date-input-selector-datetime { color: lighten($primary, 40%); font-size: $font-down-1; diff --git a/test/javascripts/components/category-selector-test.js.es6 b/test/javascripts/components/category-selector-test.js.es6 index ff35b7817b4..befc4a9e166 100644 --- a/test/javascripts/components/category-selector-test.js.es6 +++ b/test/javascripts/components/category-selector-test.js.es6 @@ -72,6 +72,7 @@ componentTest('interactions', { assert.equal(this.get('categories').length, 3); }); + this.get('subject').expand(); this.get('subject').keyboard().backspace(); this.get('subject').keyboard().backspace(); diff --git a/test/javascripts/components/list-setting-test.js.es6 b/test/javascripts/components/list-setting-test.js.es6 index b5592974a07..d45e02429cb 100644 --- a/test/javascripts/components/list-setting-test.js.es6 +++ b/test/javascripts/components/list-setting-test.js.es6 @@ -63,7 +63,7 @@ componentTest('interactions', { assert.equal(listSetting.header().value(), 'bold,italic,underline'); }); - listSetting.fillInFilter('strike'); + listSetting.expand().fillInFilter('strike'); andThen(() => { assert.equal(listSetting.highlightedRow().value(), 'strike'); diff --git a/test/javascripts/components/multi-select-test.js.es6 b/test/javascripts/components/multi-select-test.js.es6 index 454a45dac61..349b26d7c63 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -66,6 +66,7 @@ componentTest('interactions', { }); this.get('subject').selectRowByValue(3); + this.get('subject').expand(); andThen(() => { assert.equal(