diff --git a/ghost/admin/app/components/gh-navigation.js b/ghost/admin/app/components/gh-navigation.js deleted file mode 100644 index 8a7454563c..0000000000 --- a/ghost/admin/app/components/gh-navigation.js +++ /dev/null @@ -1,38 +0,0 @@ -import Component from '@ember/component'; -import {run} from '@ember/runloop'; - -export default Component.extend({ - tagName: 'section', - classNames: 'gh-view', - - didInsertElement() { - let navContainer = this.$('.js-gh-blognav'); - let navElements = '.gh-blognav-item:not(.gh-blognav-item:last-child)'; - // needed because jqueryui sortable doesn't trigger babel's autoscoping - let _this = this; - - this._super(...arguments); - - navContainer.sortable({ - handle: '.gh-blognav-grab', - items: navElements, - - start(event, ui) { - run(() => { - ui.item.data('start-index', ui.item.index()); - }); - }, - - update(event, ui) { - run(() => { - _this.sendAction('moveItem', ui.item.data('start-index'), ui.item.index()); - }); - } - }); - }, - - willDestroyElement() { - this._super(...arguments); - this.$('.ui-sortable').sortable('destroy'); - } -}); diff --git a/ghost/admin/app/components/gh-navitem.js b/ghost/admin/app/components/gh-navitem.js index 0c4dc4e72c..3ce66322c9 100644 --- a/ghost/admin/app/components/gh-navitem.js +++ b/ghost/admin/app/components/gh-navitem.js @@ -1,16 +1,14 @@ import Component from '@ember/component'; -import SortableItem from 'ember-sortable/mixins/sortable-item'; import ValidationState from 'ghost-admin/mixins/validation-state'; import {alias, readOnly} from '@ember/object/computed'; import {computed} from '@ember/object'; import {run} from '@ember/runloop'; -export default Component.extend(ValidationState, SortableItem, { +export default Component.extend(ValidationState, { classNames: 'gh-blognav-item', classNameBindings: ['errorClass', 'navItem.isNew::gh-blognav-item--sortable'], new: false, - handle: '.gh-blognav-grab', model: alias('navItem'), errors: readOnly('navItem.errors'), diff --git a/ghost/admin/app/components/gh-post-settings-menu.js b/ghost/admin/app/components/gh-post-settings-menu.js index 8718889157..78b92ed227 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.js +++ b/ghost/admin/app/components/gh-post-settings-menu.js @@ -5,7 +5,6 @@ import formatMarkdown from 'ghost-admin/utils/format-markdown'; import moment from 'moment'; import {alias, or} from '@ember/object/computed'; import {computed} from '@ember/object'; -import {guidFor} from '@ember/object/internals'; import {htmlSafe} from '@ember/string'; import {run} from '@ember/runloop'; import {inject as service} from '@ember/service'; @@ -138,13 +137,6 @@ export default Component.extend(SettingsMenuMixin, { return seoURL; }), - // live-query of all tags for tag input autocomplete - availableTags: computed(function () { - return this.get('store').filter('tag', {limit: 'all'}, () => { - return true; - }); - }), - showError(error) { // TODO: remove null check once ValidationEngine has been removed if (error) { @@ -530,56 +522,6 @@ export default Component.extend(SettingsMenuMixin, { }); }, - addTag(tagName, index) { - let currentTags = this.get('model.tags'); - let currentTagNames = currentTags.map((tag) => { - return tag.get('name').toLowerCase(); - }); - let availableTagNames, - tagToAdd; - - tagName = tagName.trim(); - - // abort if tag is already selected - if (currentTagNames.includes(tagName.toLowerCase())) { - return; - } - - this.get('availableTags').then((availableTags) => { - availableTagNames = availableTags.map((tag) => { - return tag.get('name').toLowerCase(); - }); - - // find existing tag or create new - if (availableTagNames.includes(tagName.toLowerCase())) { - tagToAdd = availableTags.find((tag) => { - return tag.get('name').toLowerCase() === tagName.toLowerCase(); - }); - } else { - tagToAdd = this.get('store').createRecord('tag', { - name: tagName - }); - - // we need to set a UUID so that selectize has a unique value - // it will be ignored when sent to the server - tagToAdd.set('uuid', guidFor(tagToAdd)); - } - - // push tag onto post relationship - if (tagToAdd) { - this.get('model.tags').insertAt(index, tagToAdd); - } - }); - }, - - removeTag(tag) { - this.get('model.tags').removeObject(tag); - - if (tag.get('isNew')) { - tag.destroyRecord(); - } - }, - deletePost() { if (this.get('deletePost')) { this.get('deletePost')(); diff --git a/ghost/admin/app/components/gh-psm-tags-input.js b/ghost/admin/app/components/gh-psm-tags-input.js new file mode 100644 index 0000000000..714cdc4139 --- /dev/null +++ b/ghost/admin/app/components/gh-psm-tags-input.js @@ -0,0 +1,87 @@ +import Component from '@ember/component'; +import {computed} from '@ember/object'; +import {inject as service} from '@ember/service'; + +export default Component.extend({ + + store: service(), + + // public attrs + post: null, + tagName: '', + + // live-query of all tags for tag input autocomplete + availableTags: computed(function () { + return this.get('store').filter('tag', {limit: 'all'}, () => true); + }), + + availableTagNames: computed('availableTags.@each.name', function () { + return this.get('availableTags').map((tag) => { + return tag.get('name').toLowerCase(); + }); + }), + + actions: { + matchTags(tagName, term) { + return tagName.toLowerCase() === term.trim().toLowerCase(); + }, + + hideCreateOptionOnMatchingTag(term) { + return !this.get('availableTagNames').includes(term.toLowerCase()); + }, + + updateTags(newTags) { + let currentTags = this.get('post.tags'); + + // destroy new+unsaved tags that are no longer selected + currentTags.forEach(function (tag) { + if (!newTags.includes(tag) && tag.get('isNew')) { + tag.destroyRecord(); + } + }); + + // update tags + return this.set('post.tags', newTags); + }, + + createTag(tagName) { + let currentTags = this.get('post.tags'); + let currentTagNames = currentTags.map((tag) => { + return tag.get('name').toLowerCase(); + }); + let tagToAdd; + + tagName = tagName.trim(); + + // abort if tag is already selected + if (currentTagNames.includes(tagName.toLowerCase())) { + return; + } + + // add existing tag or create new one + return this._findTagByName(tagName).then((matchedTag) => { + tagToAdd = matchedTag; + + // create new tag if no match + if (!tagToAdd) { + tagToAdd = this.get('store').createRecord('tag', { + name: tagName + }); + } + + // push tag onto post relationship + return currentTags.pushObject(tagToAdd); + }); + } + }, + + // methods + + _findTagByName(name) { + return this.get('availableTags').then((availableTags) => { + return availableTags.find((tag) => { + return tag.get('name').toLowerCase() === name.toLowerCase(); + }); + }); + } +}); diff --git a/ghost/admin/app/components/gh-selectize.js b/ghost/admin/app/components/gh-selectize.js deleted file mode 100644 index 7f5aba4975..0000000000 --- a/ghost/admin/app/components/gh-selectize.js +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable camelcase */ -import EmberSelectizeComponent from 'ember-cli-selectize/components/ember-selectize'; -import {computed} from '@ember/object'; -import {A as emberA, isArray as isEmberArray} from '@ember/array'; -import {get} from '@ember/object'; -import {isBlank} from '@ember/utils'; -import {run} from '@ember/runloop'; - -export default EmberSelectizeComponent.extend({ - - selectizeOptions: computed(function () { - let options = this._super(...arguments); - - options.onChange = run.bind(this, '_onChange'); - - return options; - }), - - /** - * Event callback that is triggered when user creates a tag - * - modified to pass the caret position to the action - */ - _create(input, callback) { - let caret = this._selectize.caretPos; - - // Delete user entered text - this._selectize.setTextboxValue(''); - // Send create action - - // allow the observers and computed properties to run first - run.schedule('actions', this, function () { - this.sendAction('create-item', input, caret); - }); - // We cancel the creation here, so it's up to you to include the created element - // in the content and selection property - callback(null); - }, - - _addSelection(obj) { - let _valuePath = this.get('_valuePath'); - let val = get(obj, _valuePath); - let caret = this._selectize.caretPos; - - // caret position is always 1 more than the desired index as this method - // is called after selectize has inserted the item and the caret has moved - // to the right - caret = caret - 1; - - this.get('selection').insertAt(caret, obj); - - run.schedule('actions', this, function () { - this.sendAction('add-item', obj); - this.sendAction('add-value', val); - }); - }, - - _onChange(args) { - let selection = get(this, 'selection'); - let valuePath = get(this, '_valuePath'); - let reorderedSelection = emberA([]); - - if (!args || !selection || !isEmberArray(selection) || args.length !== get(selection, 'length')) { - return; - } - - // exit if we're not dealing with the same objects as the selection - let objectsHaveChanged = selection.any(function (obj) { - return args.indexOf(get(obj, valuePath)) === -1; - }); - - if (objectsHaveChanged) { - return; - } - - // exit if the order is still the same - let orderIsSame = selection.every(function (obj, idx) { - return get(obj, valuePath) === args[idx]; - }); - - if (orderIsSame) { - return; - } - - // we have a re-order, update the selection - args.forEach((value) => { - let obj = selection.find(function (item) { - return `${get(item, valuePath)}` === value; - }); - - if (obj) { - reorderedSelection.addObject(obj); - } - }); - - this.set('selection', reorderedSelection); - }, - - _preventOpeningWhenBlank() { - let openOnFocus = this.get('openOnFocus'); - - if (!openOnFocus) { - run.schedule('afterRender', this, function () { - let selectize = this._selectize; - if (selectize) { - selectize.on('dropdown_open', function () { - if (isBlank(selectize.$control_input.val())) { - selectize.close(); - } - }); - selectize.on('type', function (filter) { - if (isBlank(filter)) { - selectize.close(); - } - }); - } - }); - } - }, - - didInsertElement() { - this._super(...arguments); - this._preventOpeningWhenBlank(); - } - -}); diff --git a/ghost/admin/app/components/gh-token-input.js b/ghost/admin/app/components/gh-token-input.js new file mode 100644 index 0000000000..48816daf7c --- /dev/null +++ b/ghost/admin/app/components/gh-token-input.js @@ -0,0 +1,189 @@ +/* global key */ +import Component from '@ember/component'; +import Ember from 'ember'; +import {A} from '@ember/array'; +import { + advanceSelectableOption, + defaultMatcher, + filterOptions +} from 'ember-power-select/utils/group-utils'; +import {computed} from '@ember/object'; +import {get} from '@ember/object'; +import {htmlSafe} from '@ember/string'; +import {isBlank} from '@ember/utils'; +import {task} from 'ember-concurrency'; + +const {Handlebars} = Ember; + +const BACKSPACE = 8; +const TAB = 9; + +export default Component.extend({ + + // public attrs + closeOnSelect: false, + labelField: 'name', + matcher: defaultMatcher, + searchField: 'name', + tagName: '', + triggerComponent: 'gh-token-input/trigger', + + optionsWithoutSelected: computed('options.[]', 'selected.[]', function () { + return this.get('optionsWithoutSelectedTask').perform(); + }), + + actions: { + handleKeydown(select, event) { + // On backspace with empty text, remove the last token but deviate + // from default behaviour by not updating search to match last token + if (event.keyCode === BACKSPACE && isBlank(event.target.value)) { + let lastSelection = select.selected[select.selected.length - 1]; + + if (lastSelection) { + this.get('onchange')(select.selected.slice(0, -1), select); + select.actions.search(''); + select.actions.open(event); + } + + // prevent default + return false; + } + + // Tab should work the same as Enter if there's a highlighted option + if (event.keyCode === TAB && !isBlank(event.target.value) && select.highlighted) { + if (!select.selected || select.selected.indexOf(select.highlighted) === -1) { + select.actions.choose(select.highlighted, event); + return false; + } + } + + // fallback to default + return true; + }, + + onfocus() { + key.setScope('gh-token-input'); + + if (this.get('onfocus')) { + this.get('onfocus')(...arguments); + } + }, + + onblur() { + key.setScope('default'); + + if (this.get('onblur')) { + this.get('onblur')(...arguments); + } + } + }, + + optionsWithoutSelectedTask: task(function* () { + let options = yield this.get('options'); + let selected = yield this.get('selected'); + return options.filter((o) => !selected.includes(o)); + }), + + shouldShowCreateOption(term, options) { + if (this.get('showCreateWhen')) { + return this.get('showCreateWhen')(term, options); + } else { + return this.hideCreateOptionOnSameTerm(term, options); + } + }, + + hideCreateOptionOnSameTerm(term, options) { + let searchField = this.get('searchField'); + let existingOption = options.findBy(searchField, term); + return !existingOption; + }, + + addCreateOption(term, options) { + if (this.shouldShowCreateOption(term, options)) { + options.unshift(this.buildSuggestionForTerm(term)); + } + }, + + searchAndSuggest(term, select) { + return this.get('searchAndSuggestTask').perform(term, select); + }, + + searchAndSuggestTask: task(function* (term, select) { + let newOptions = (yield this.get('optionsWithoutSelected')).toArray(); + + if (term.length === 0) { + return newOptions; + } + + let searchAction = this.get('search'); + if (searchAction) { + let results = yield searchAction(term, select); + + if (results.toArray) { + results = results.toArray(); + } + + this.addCreateOption(term, results); + return results; + } + + newOptions = this.filter(A(newOptions), term); + this.addCreateOption(term, newOptions); + + return newOptions; + }), + + selectOrCreate(selection, select) { + let suggestion = selection.find((option) => { + return option.__isSuggestion__; + }); + + if (suggestion) { + this.get('oncreate')(suggestion.__value__, select); + } else { + this.get('onchange')(selection, select); + } + + // clear select search + select.actions.search(''); + }, + + filter(options, searchText) { + let matcher; + if (this.get('searchField')) { + matcher = (option, text) => this.matcher(get(option, this.get('searchField')), text); + } else { + matcher = (option, text) => this.matcher(option, text); + } + return filterOptions(options || [], searchText, matcher); + }, + + buildSuggestionForTerm(term) { + return { + __isSuggestion__: true, + __value__: term, + text: this.buildSuggestionLabel(term) + }; + }, + + buildSuggestionLabel(term) { + let buildSuggestion = this.get('buildSuggestion'); + if (buildSuggestion) { + return buildSuggestion(term); + } + return htmlSafe(`Add "${Handlebars.Utils.escapeExpression(term)}"...`); + }, + + // always select the first item in the list that isn't the "Add x" option + defaultHighlighted(select) { + let {results} = select; + let option = advanceSelectableOption(results, undefined, 1); + + if (results.length > 1 && option.__isSuggestion__) { + option = advanceSelectableOption(results, option, 1); + } + + return option; + } + +}); diff --git a/ghost/admin/app/components/gh-token-input/select-multiple.js b/ghost/admin/app/components/gh-token-input/select-multiple.js new file mode 100644 index 0000000000..69be0f2952 --- /dev/null +++ b/ghost/admin/app/components/gh-token-input/select-multiple.js @@ -0,0 +1,58 @@ +import $ from 'jquery'; +import PowerSelectMultiple from 'ember-power-select/components/power-select-multiple'; +import {bind} from '@ember/runloop'; + +const endActions = 'click.ghToken mouseup.ghToken touchend.ghToken'; + +// triggering focus on the search input within ESA's onfocus event breaks the +// drag-n-drop functionality in ember-drag-drop so we watch for events that +// could be the start of a drag and disable the default focus behaviour until +// we get another event signalling the end of a drag + +export default PowerSelectMultiple.extend({ + + _canFocus: true, + + willDestroyElement() { + this._super(...arguments); + + if (this._allowFocusListener) { + $(window).off(endActions, this._allowFocusListener); + } + }, + + actions: { + optionMouseDown(event) { + if (event.which === 1 && !event.ctrlKey) { + this._denyFocus(event); + } + }, + + optionTouchStart(event) { + this._denyFocus(event); + }, + + handleFocus() { + if (this._canFocus) { + this._super(...arguments); + } + } + }, + + _denyFocus() { + if (this._canFocus) { + this._canFocus = false; + + this._allowFocusListener = bind(this, this._allowFocus); + + $(window).on(endActions, this._allowFocusListener); + } + }, + + _allowFocus() { + this._canFocus = true; + + $(window).off(endActions, this._allowFocusListener); + this._allowFocusListener = null; + } +}); diff --git a/ghost/admin/app/components/gh-token-input/select.js b/ghost/admin/app/components/gh-token-input/select.js new file mode 100644 index 0000000000..e8eee9335a --- /dev/null +++ b/ghost/admin/app/components/gh-token-input/select.js @@ -0,0 +1,9 @@ +// NOTE: This is only here because we wanted to override the `eventType` attr. +// DO NOT add any functionality here, this will hopefully disappear after an +// upstream PR + +import PowerSelect from 'ember-power-select/components/power-select'; + +export default PowerSelect.extend({ + +}); diff --git a/ghost/admin/app/components/gh-token-input/suggested-option.js b/ghost/admin/app/components/gh-token-input/suggested-option.js new file mode 100644 index 0000000000..041ba0ad19 --- /dev/null +++ b/ghost/admin/app/components/gh-token-input/suggested-option.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '' +}); diff --git a/ghost/admin/app/components/gh-token-input/tag-token.js b/ghost/admin/app/components/gh-token-input/tag-token.js new file mode 100644 index 0000000000..900a97681c --- /dev/null +++ b/ghost/admin/app/components/gh-token-input/tag-token.js @@ -0,0 +1,35 @@ +import DraggableObject from 'ember-drag-drop/components/draggable-object'; +import {computed} from '@ember/object'; +import {readOnly} from '@ember/object/computed'; + +export default DraggableObject.extend({ + + attributeBindings: ['title'], + classNames: ['tag-token'], + classNameBindings: [ + 'primary:tag-token--primary', + 'internal:tag-token--internal' + ], + + content: readOnly('option'), + internal: readOnly('option.isInternal'), + + primary: computed('idx', 'internal', function () { + return !this.get('internal') && this.get('idx') === 0; + }), + + title: computed('option.name', 'primary', 'internal', function () { + let name = this.get('option.name'); + + if (this.get('internal')) { + return `${name} (internal)`; + } + + if (this.get('primary')) { + return `${name} (primary tag)`; + } + + return name; + }) + +}); diff --git a/ghost/admin/app/components/gh-token-input/trigger.js b/ghost/admin/app/components/gh-token-input/trigger.js new file mode 100644 index 0000000000..ff9fbe0c15 --- /dev/null +++ b/ghost/admin/app/components/gh-token-input/trigger.js @@ -0,0 +1,28 @@ +import EmberPowerSelectMultipleTrigger from 'ember-power-select/components/power-select-multiple/trigger'; +import {copy} from '@ember/object/internals'; + +export default EmberPowerSelectMultipleTrigger.extend({ + + actions: { + handleOptionMouseDown(event) { + let action = this.get('extra.optionMouseDown'); + if (action) { + return action(event); + } + }, + + handleOptionTouchStart(event) { + let action = this.get('extra.optionTouchStart'); + if (action) { + return action(event); + } + }, + + reorderItems() { + // ember-drag-drop's sortable-objects has two-way bindings and will + // update EPS' selected value directly. We have to create a copy + // after sorting in order to force the onchange action to be triggered + this.get('select').actions.select(copy(this.get('select.selected'))); + } + } +}); diff --git a/ghost/admin/app/controllers/settings/design.js b/ghost/admin/app/controllers/settings/design.js index 61bbec5210..0c074dcc7c 100644 --- a/ghost/admin/app/controllers/settings/design.js +++ b/ghost/admin/app/controllers/settings/design.js @@ -112,10 +112,6 @@ export default Controller.extend({ navItems.removeObject(item); }, - reorderItems(navItems) { - this.set('model.navigation', navItems); - }, - updateUrl(url, navItem) { if (!navItem) { return; diff --git a/ghost/admin/app/models/tag.js b/ghost/admin/app/models/tag.js index 21c69bacca..530bb00413 100644 --- a/ghost/admin/app/models/tag.js +++ b/ghost/admin/app/models/tag.js @@ -1,10 +1,7 @@ -/* eslint-disable camelcase */ import Model from 'ember-data/model'; import ValidationEngine from 'ghost-admin/mixins/validation-engine'; import attr from 'ember-data/attr'; -import {computed} from '@ember/object'; import {equal} from '@ember/object/computed'; -import {guidFor} from '@ember/object/internals'; import {observer} from '@ember/object'; import {inject as service} from '@ember/service'; @@ -30,13 +27,6 @@ export default Model.extend(ValidationEngine, { feature: service(), - // HACK: ugly hack to main compatibility with selectize as used in the - // PSM tags input - // TODO: remove once we've switched over to EPS for the tags input - uuid: computed(function () { - return guidFor(this); - }), - setVisibility() { let internalRegex = /^#.?/; this.set('visibility', internalRegex.test(this.get('name')) ? 'internal' : 'public'); diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js index d0cbed6aa7..d162bf807a 100644 --- a/ghost/admin/app/routes/application.js +++ b/ghost/admin/app/routes/application.js @@ -20,7 +20,7 @@ function K() { let shortcuts = {}; -shortcuts.esc = {action: 'closeMenus', scope: 'all'}; +shortcuts.esc = {action: 'closeMenus', scope: 'default'}; shortcuts[`${ctrlOrCmd}+s`] = {action: 'save', scope: 'all'}; export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, { diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index c1129b8b2f..a6535ec1f8 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -28,7 +28,6 @@ @import "components/pagination.css"; @import "components/badges.css"; @import "components/settings-menu.css"; -@import "components/selectize.css"; @import "components/power-select.css"; @import "components/power-calendar.css"; @import "components/publishmenu.css"; @@ -82,7 +81,6 @@ hr { input, .gh-input, .gh-select, -.selectize-input, .gh-select select { color: var(--darkgrey); border-color: color(var(--lightgrey)); @@ -189,21 +187,6 @@ input, background: var(--lightgrey); } -.selectize-dropdown, -.selectize-input, -.selectize-input input { - color: #fff; -} - -.selectize-dropdown .option { - color: var(--lightgrey); -} - -.selectize-input, -.selectize-control.single .selectize-input.input-active { - background: var(--lightgrey); -} - .gh-select select { color: var(--darkgrey); background: var(--lightgrey); diff --git a/ghost/admin/app/styles/app.css b/ghost/admin/app/styles/app.css index fd4d112755..8c3daf823e 100644 --- a/ghost/admin/app/styles/app.css +++ b/ghost/admin/app/styles/app.css @@ -28,7 +28,6 @@ @import "components/pagination.css"; @import "components/badges.css"; @import "components/settings-menu.css"; -@import "components/selectize.css"; @import "components/power-select.css"; @import "components/power-calendar.css"; @import "components/publishmenu.css"; diff --git a/ghost/admin/app/styles/components/dropdowns.css b/ghost/admin/app/styles/components/dropdowns.css index 52351612c7..7fdcc696c8 100644 --- a/ghost/admin/app/styles/components/dropdowns.css +++ b/ghost/admin/app/styles/components/dropdowns.css @@ -151,11 +151,3 @@ .closed > .dropdown-menu { display: none; } - - -/* Selectize -/* ---------------------------------------------------------- */ - -.selectize-dropdown { - z-index: 200; -} diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index ac00da6c46..4d72f5138a 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -152,3 +152,41 @@ .ember-power-select-option[aria-current="true"] { } + + +/* Multiple */ +.ember-power-select-multiple-trigger { + background: #fff; + padding: 5px; + border: rgb(214, 227, 235) 1px solid; + border-radius: 3px; + outline: none; +} + +.ember-power-select-multiple-option { + margin: 2px; + padding: 2px 4px; + border-radius: 3px; + border: 0; + background: #3eb0ef; + color: white; +} + +.ember-power-select-trigger-multiple-input { + height: 24px; + margin: 2px; +} + +.ember-power-select-status-icon { + right: 10px; +} + +/* Token input */ +.gh-token-input .ember-power-select-options { + max-height: 172px; /* 5.5 options */ +} + +/* Tag input */ +.tag-token--primary { + background: color(#3eb0ef lightness(-20%)); +} diff --git a/ghost/admin/app/styles/components/selectize.css b/ghost/admin/app/styles/components/selectize.css deleted file mode 100644 index 553a02a030..0000000000 --- a/ghost/admin/app/styles/components/selectize.css +++ /dev/null @@ -1,405 +0,0 @@ -.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { - visibility: visible !important; - border: 0 none !important; - background: #f2f2f2 !important; - background: rgba(0, 0, 0, 0.06) !important; -} - -.selectize-control.plugin-drag_drop .ui-sortable-placeholder:after { - content: "!"; - visibility: hidden; -} - -.selectize-control.plugin-drag_drop .ui-sortable-helper { - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.selectize-dropdown-header { - position: relative; - padding: 5px 8px; - border-bottom: 1px solid #d0d0d0; - background: #f8f8f8; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.selectize-dropdown-header-close { - position: absolute; - top: 50%; - right: 8px; - margin-top: -12px; - color: #303030; - font-size: 20px !important; - line-height: 20px; - opacity: 0.4; -} - -.selectize-dropdown-header-close:hover { - color: #000; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup { - float: left; - box-sizing: border-box; - border-top: 0 none; - border-right: 1px solid #f2f2f2; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { - border-right: 0 none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup:before { - display: none; -} - -.selectize-dropdown.plugin-optgroup_columns .optgroup-header { - border-top: 0 none; -} - -.selectize-control.plugin-remove_button [data-value] { - position: relative; - padding-right: 20px !important; -} - -.selectize-control.plugin-remove_button [data-value] .remove { - position: absolute; - top: 0; - right: 0; - bottom: 0; - z-index: 1; - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - width: 17px; - border-radius: 0 2px 2px 0; - color: inherit; - vertical-align: middle; - text-align: center; - text-decoration: none; - font-size: 12px; - font-weight: bold; -} - -.selectize-control.plugin-remove_button [data-value] .remove:hover { - background: rgba(0, 0, 0, 0.05); -} - -.selectize-control.plugin-remove_button [data-value].active .remove { - border-left-color: #00578d; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { - background: none; -} - -.selectize-control.plugin-remove_button .disabled [data-value] .remove { - border-left-color: #aaa; -} - -.selectize-control { - position: relative; -} - -.selectize-dropdown, -.selectize-input, -.selectize-input input { - color: #303030; - font-family: inherit; - font-size: 1.4rem; -} - -.selectize-input, -.selectize-control.single .selectize-input.input-active { - display: inline-block; - background: #fff; - cursor: text; -} - -.selectize-input { - position: relative; - z-index: 1; - display: inline-block; - overflow: hidden; - box-sizing: border-box; - padding: 10px 12px; - width: 100%; - height: 39px; - border: color(var(--lightgrey) l(-5%) s(-10%)) 1px solid; - border-radius: var(--border-radius); - color: color(var(--midgrey) l(-18%)); - transition: border-color 0.15s linear; -} - -.selectize-input.focus { - border-color: color(var(--lightgrey) l(-15%) s(-10%)); -} - -.selectize-control.multi .selectize-input.has-items { - padding: 6px 10px 3px; - height: auto; -} - -.selectize-input.full { - background-color: #fff; -} - -.selectize-input.disabled, -.selectize-input.disabled * { - cursor: default !important; -} - -.selectize-input.dropdown-active { - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.selectize-input > * { - display: -moz-inline-stack; - display: inline-block; - vertical-align: baseline; - zoom: 1; - - *display: inline; -} - -.selectize-control.multi .selectize-input > div { - margin: 0 3px 3px 0; - padding: 1px 4px; - background: var(--blue); - color: #fff; - cursor: pointer; -} - - -/* Active tag - selected state when tag is clicked */ -.selectize-control.multi .selectize-input > div.active { - background: color(var(--blue) lightness(-10%)); - color: #fff; -} - -.selectize-control.multi .selectize-input.disabled > div, -.selectize-control.multi .selectize-input.disabled > div.active { - border: 1px solid #aaa; - background: #d2d2d2; - color: #fff; -} - -.selectize-input > input { - display: inline-block !important; - margin: 0 1px !important; - padding: 0 !important; - min-height: 0 !important; - max-width: 100% !important; - max-height: none !important; - border: 0 none !important; - background: none !important; - box-shadow: none !important; - text-indent: 0 !important; - line-height: inherit !important; -} - -.selectize-input > input:-ms-clear { - display: none; -} - -.selectize-input > input:focus { - outline: none !important; -} - -.selectize-input:after { - content: " "; - display: block; - clear: left; -} -.selectize-input.dropdown-active:before { - content: " "; - position: absolute; - right: 0; - bottom: 0; - left: 0; - display: block; - height: 1px; - background: #f0f0f0; -} - -.selectize-dropdown { - position: absolute; - z-index: 1000; - box-sizing: border-box; - margin: -1px 0 0 0; - border: 1px solid #b1b1b1; - border-top: 0 none; - background: #fff; - border-radius: 0 0 var(--border-radius) var(--border-radius); - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); -} - -.selectize-dropdown [data-selectable] { - overflow: hidden; - cursor: pointer; -} - -.selectize-dropdown [data-selectable] .highlight { - background: #fff3b8; - border-radius: 1px; -} - -.selectize-dropdown [data-selectable], -.selectize-dropdown .optgroup-header, -.selectize-dropdown .dropdown-empty-message { - padding: 7px 8px; -} - -.selectize-dropdown .optgroup-header { - background: #fff; - color: #303030; - cursor: default; -} - -.selectize-dropdown .active { - background: color(var(--blue) alpha(-85%)); - color: var(--darkgrey); -} - -.selectize-dropdown .active.create { - color: #666; -} - -.selectize-dropdown .create { - color: rgba(48, 48, 48, 0.5); -} - -.selectize-dropdown-content { - overflow-x: hidden; - overflow-y: auto; - max-height: 200px; -} - -.selectize-control.single .selectize-input, -.selectize-control.single .selectize-input input { - cursor: pointer; -} - -.selectize-control.single .selectize-input.input-active, -.selectize-control.single .selectize-input.input-active input { - cursor: text; -} - -.selectize-control.single .selectize-input:after { - content: " "; - position: absolute; - top: 50%; - right: 15px; - display: block; - margin-top: -3px; - width: 0; - height: 0; - border-width: 5px 5px 0 5px; - border-style: solid; - border-color: #808080 transparent transparent transparent; -} - -.selectize-control.single .selectize-input.dropdown-active:after { - margin-top: -4px; - border-width: 0 5px 5px 5px; - border-color: transparent transparent #808080 transparent; -} - -.selectize-control.rtl.single .selectize-input:after { - right: auto; - left: 15px; -} - -.selectize-control.rtl .selectize-input > input { - margin: 0 4px 0 -2px !important; -} - -.selectize-control .selectize-input.disabled { - background-color: #fafafa; - opacity: 0.5; -} - -.selectize-control.multi .selectize-input.has-items { - padding-right: 5px; - padding-left: 5px; -} - -.selectize-control.multi .selectize-input.disabled [data-value] { - background: none; - box-shadow: none; - color: #999; - text-shadow: none; -} - -.selectize-control.multi .selectize-input.disabled [data-value], -.selectize-control.multi .selectize-input.disabled [data-value] .remove { - border-color: #e6e6e6; -} - -.selectize-control.multi .selectize-input.disabled [data-value] .remove { - background: none; -} - -.selectize-control.multi .selectize-input [data-value] { - background: var(--blue); - border-radius: 3px; -} - -.selectize-control.multi .selectize-input [data-value].active { - background: color(var(--blue) lightness(-10%)); -} - -.selectize-control.single .selectize-input { - background: #f9f9f9; -} - -.selectize-control.single .selectize-input, -.selectize-dropdown.single { - border-color: #b8b8b8; -} - -.optgroup:first-of-type .optgroup-header { - margin-bottom: 7px; - padding-top: 0; - padding-bottom: 0; -} - -.selectize-dropdown .optgroup-header { - position: relative; - display: inline-block; - padding-top: 7px; - background: #fff; - color: var(--midgrey); - font-size: 0.85em; -} - -.selectize-dropdown .optgroup-header:after { - content: ""; - position: absolute; - top: 52%; - left: calc(100% + 3px); - display: block; - width: calc(189px - 100%); - height: 1px; - border-bottom: #dfe1e3 1px solid; -} -@media (max-width: 800px) { - .selectize-dropdown .optgroup-header:after { - width: calc(224px - 100%); - } -} -@media (max-width: 500px) { - .selectize-dropdown .optgroup-header:after { - width: calc(80vw - 45px - 100%); - } -} - -.selectize-dropdown .option { - line-height: 1.35em; -} - -.dropdown-empty-message { - position: relative; - color: var(--midgrey); - font-size: 0.9em; -} diff --git a/ghost/admin/app/styles/components/settings-menu.css b/ghost/admin/app/styles/components/settings-menu.css index 3ad88a0c98..5a8cef1081 100644 --- a/ghost/admin/app/styles/components/settings-menu.css +++ b/ghost/admin/app/styles/components/settings-menu.css @@ -188,10 +188,6 @@ color: color(var(--red) lightness(-10%)); } -.settings-menu-content .selectize-input { - padding: 7px 12px; -} - .post-setting-custom-excerpt { font-size: 1.5rem; line-height: 1.35em; diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css index 5f17b5bf0f..ebf2f3d832 100644 --- a/ghost/admin/app/styles/layouts/main.css +++ b/ghost/admin/app/styles/layouts/main.css @@ -319,12 +319,6 @@ body > .ember-view:not(.default-liquid-destination) { transform: translate3d(80vw, 0, 0); } - .gh-nav-search-input .selectize-input, - .gh-nav-search-input .selectize-input input, - .gh-nav-search-input .selectize-dropdown { - font-size: 1.5rem; - } - .gh-nav-list { font-size: 1.5rem; } diff --git a/ghost/admin/app/templates/components/gh-post-settings-menu.hbs b/ghost/admin/app/templates/components/gh-post-settings-menu.hbs index f354e31e1c..df2fecd680 100644 --- a/ghost/admin/app/templates/components/gh-post-settings-menu.hbs +++ b/ghost/admin/app/templates/components/gh-post-settings-menu.hbs @@ -67,17 +67,7 @@
- {{gh-selectize - id="tag-input" - multiple=true - selection=model.tags - content=availableTags - optionValuePath="content.uuid" - optionLabelPath="content.name" - openOnFocus=false - create-item="addTag" - remove-item="removeTag" - plugins="remove_button, drag_drop"}} + {{gh-psm-tags-input post=model triggerId="tag-input"}}
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="customExcerpt"}} diff --git a/ghost/admin/app/templates/components/gh-psm-tags-input.hbs b/ghost/admin/app/templates/components/gh-psm-tags-input.hbs new file mode 100644 index 0000000000..aecf3f41c1 --- /dev/null +++ b/ghost/admin/app/templates/components/gh-psm-tags-input.hbs @@ -0,0 +1,12 @@ +{{gh-token-input + extra=(hash + tokenComponent="gh-token-input/tag-token" + ) + onchange=(action "updateTags") + oncreate=(action "createTag") + options=availableTags + renderInPlace=true + selected=post.tags + showCreateWhen=(action "hideCreateOptionOnMatchingTag") + triggerId=triggerId +}} diff --git a/ghost/admin/app/templates/components/gh-token-input.hbs b/ghost/admin/app/templates/components/gh-token-input.hbs new file mode 100644 index 0000000000..4103fe457c --- /dev/null +++ b/ghost/admin/app/templates/components/gh-token-input.hbs @@ -0,0 +1,54 @@ +{{#gh-token-input/select-multiple + afterOptionsComponent=afterOptionsComponent + allowClear=allowClear + ariaDescribedBy=ariaDescribedBy + ariaInvalid=ariaInvalid + ariaLabel=ariaLabel + ariaLabelledBy=ariaLabelledBy + beforeOptionsComponent=beforeOptionsComponent + class=(concat "gh-token-input " class) + closeOnSelect=closeOnSelect + defaultHighlighted=defaultHighlighted + destination=destination + dir=dir + disabled=disabled + dropdownClass=dropdownClass + extra=extra + horizontalPosition=horizontalPosition + initiallyOpened=initiallyOpened + loadingMessage=loadingMessage + matcher=matcher + matchTriggerWidth=matchTriggerWidth + noMatchesMessage=noMatchesMessage + onblur=(action "onblur") + onchange=(action selectOrCreate) + onclose=onclose + onfocus=(action "onfocus") + oninput=oninput + onkeydown=(action "handleKeydown") + onopen=onopen + options=optionsWithoutSelected + optionsComponent=(or optionsComponent "power-select-vertical-collection-options") + placeholder=placeholder + registerAPI=registerAPI + renderInPlace=renderInPlace + search=(action searchAndSuggest) + searchEnabled=searchEnabled + searchField=searchField + searchMessage=searchMessage + searchPlaceholder=searchPlaceholder + selected=selected + selectedItemComponent=selectedItemComponent + tabindex=tabindex + triggerClass=triggerClass + triggerComponent=triggerComponent + triggerId=triggerId + verticalPosition=verticalPosition + as |option term| +}} + {{#if option.__isSuggestion__}} + {{gh-token-input/suggested-option option=option term=term}} + {{else}} + {{get option labelField}} + {{/if}} +{{/gh-token-input/select-multiple}} diff --git a/ghost/admin/app/templates/components/gh-token-input/select-multiple.hbs b/ghost/admin/app/templates/components/gh-token-input/select-multiple.hbs new file mode 100644 index 0000000000..4bbc01573a --- /dev/null +++ b/ghost/admin/app/templates/components/gh-token-input/select-multiple.hbs @@ -0,0 +1,125 @@ +{{!-- + NOTE: changes from ember-power-select: + - `extra` has our custom drag-tracking actions assigned to it + --}} +{{#if (hasBlock "inverse")}} + {{#gh-token-input/select + afterOptionsComponent=afterOptionsComponent + allowClear=allowClear + ariaDescribedBy=ariaDescribedBy + ariaInvalid=ariaInvalid + ariaLabel=ariaLabel + ariaLabelledBy=ariaLabelledBy + beforeOptionsComponent=beforeOptionsComponent + buildSelection=(action "buildSelection") + calculatePosition=calculatePosition + class=class + closeOnSelect=closeOnSelect + defaultHighlighted=defaultHighlighted + destination=destination + dir=dir + disabled=disabled + dropdownClass=dropdownClass + extra=(assign extra (hash + optionMouseDown=(action "optionMouseDown") + optionTouchStart=(action "optionTouchStart") + )) + horizontalPosition=horizontalPosition + initiallyOpened=initiallyOpened + loadingMessage=loadingMessage + matcher=matcher + matchTriggerWidth=matchTriggerWidth + noMatchesMessage=noMatchesMessage + onblur=onblur + onchange=onchange + onclose=onclose + onfocus=(action "handleFocus") + oninput=oninput + onkeydown=(action "handleKeydown") + onopen=(action "handleOpen") + options=options + optionsComponent=optionsComponent + groupComponent=groupComponent + placeholder=placeholder + registerAPI=(readonly registerAPI) + renderInPlace=renderInPlace + required=required + scrollTo=scrollTo + search=search + searchEnabled=searchEnabled + searchField=searchField + searchMessage=searchMessage + searchPlaceholder=searchPlaceholder + selected=selected + selectedItemComponent=selectedItemComponent + tabindex=computedTabIndex + tagName=tagName + triggerClass=concatenatedTriggerClass + triggerComponent=(component triggerComponent tabindex=tabindex) + triggerId=triggerId + verticalPosition=verticalPosition + as |option select|}} + {{yield option select}} + {{else}} + {{yield to="inverse"}} + {{/gh-token-input/select}} +{{else}} + {{#gh-token-input/select + afterOptionsComponent=afterOptionsComponent + allowClear=allowClear + ariaDescribedBy=ariaDescribedBy + ariaInvalid=ariaInvalid + ariaLabel=ariaLabel + ariaLabelledBy=ariaLabelledBy + beforeOptionsComponent=beforeOptionsComponent + buildSelection=(action "buildSelection") + calculatePosition=calculatePosition + class=class + closeOnSelect=closeOnSelect + defaultHighlighted=defaultHighlighted + destination=destination + dir=dir + disabled=disabled + dropdownClass=dropdownClass + extra=(assign extra (hash + optionMouseDown=(action "optionMouseDown") + optionTouchStart=(action "optionTouchStart") + )) + horizontalPosition=horizontalPosition + initiallyOpened=initiallyOpened + loadingMessage=loadingMessage + matcher=matcher + matchTriggerWidth=matchTriggerWidth + noMatchesMessage=noMatchesMessage + onblur=onblur + onchange=onchange + onclose=onclose + onfocus=(action "handleFocus") + oninput=oninput + onkeydown=(action "handleKeydown") + onopen=(action "handleOpen") + options=options + optionsComponent=optionsComponent + groupComponent=groupComponent + placeholder=placeholder + registerAPI=(readonly registerAPI) + renderInPlace=renderInPlace + required=required + scrollTo=scrollTo + search=search + searchEnabled=searchEnabled + searchField=searchField + searchMessage=searchMessage + searchPlaceholder=searchPlaceholder + selected=selected + selectedItemComponent=selectedItemComponent + tabindex=computedTabIndex + tagName=tagName + triggerClass=concatenatedTriggerClass + triggerComponent=(component triggerComponent tabindex=tabindex) + triggerId=triggerId + verticalPosition=verticalPosition + as |option select|}} + {{yield option select}} + {{/gh-token-input/select}} +{{/if}} diff --git a/ghost/admin/app/templates/components/gh-token-input/select.hbs b/ghost/admin/app/templates/components/gh-token-input/select.hbs new file mode 100644 index 0000000000..f67fa52296 --- /dev/null +++ b/ghost/admin/app/templates/components/gh-token-input/select.hbs @@ -0,0 +1,103 @@ +{{!-- + NOTE: the only thing changed here is `eventType="click"` on dropdown.trigger + so it doesn't interfere with the drag-n-drop sorting + + When upgrading ember-power-select ensure the full component template is + copied across here +--}} +{{#basic-dropdown + classNames=(readonly classNames) + horizontalPosition=(readonly horizontalPosition) + calculatePosition=calculatePosition + destination=(readonly destination) + initiallyOpened=(readonly initiallyOpened) + matchTriggerWidth=(readonly matchTriggerWidth) + onClose=(action "onClose") + onOpen=(action "onOpen") + registerAPI=(action "registerAPI") + renderInPlace=(readonly renderInPlace) + verticalPosition=(readonly verticalPosition) + disabled=(readonly disabled) + as |dropdown|}} + + {{#dropdown.trigger + tagName=(readonly _triggerTagName) + ariaDescribedBy=(readonly ariaDescribedBy) + ariaInvalid=(readonly ariaInvalid) + ariaLabel=(readonly ariaLabel) + ariaLabelledBy=(readonly ariaLabelledBy) + ariaRequired=(readonly required) + class=(readonly concatenatedTriggerClasses) + id=(readonly triggerId) + eventType="click" + onKeyDown=(action "onTriggerKeydown") + onFocus=(action "onTriggerFocus") + onBlur=(action "onTriggerBlur") + tabindex=(readonly tabindex)}} + {{#component triggerComponent + allowClear=(readonly allowClear) + buildSelection=(readonly buildSelection) + extra=(readonly extra) + listboxId=(readonly optionsId) + loadingMessage=(readonly loadingMessage) + onFocus=(action "onFocus") + onBlur=(action "onBlur") + onInput=(action "onInput") + placeholder=(readonly placeholder) + placeholderComponent=(readonly placeholderComponent) + onKeydown=(action "onKeydown") + searchEnabled=(readonly searchEnabled) + searchField=(readonly searchField) + select=(readonly publicAPI) + selectedItemComponent=(readonly selectedItemComponent) + as |opt term|}} + {{yield opt term}} + {{/component}} + {{/dropdown.trigger}} + + {{#dropdown.content _contentTagName=_contentTagName class=(readonly concatenatedDropdownClasses)}} + {{component beforeOptionsComponent + extra=(readonly extra) + listboxId=(readonly optionsId) + onInput=(action "onInput") + onKeydown=(action "onKeydown") + searchEnabled=(readonly searchEnabled) + onFocus=(action "onFocus") + onBlur=(action "onBlur") + placeholder=(readonly placeholder) + placeholderComponent=(readonly placeholderComponent) + searchPlaceholder=(readonly searchPlaceholder) + select=(readonly publicAPI)}} + {{#if mustShowSearchMessage}} + {{component searchMessageComponent + searchMessage=(readonly searchMessage) + select=(readonly publicAPI) + }} + {{else if mustShowNoMessages}} + {{#if (hasBlock "inverse")}} + {{yield to="inverse"}} + {{else if noMatchesMessage}} + + {{/if}} + {{else}} + {{#component optionsComponent + class="ember-power-select-options" + extra=(readonly extra) + groupIndex="" + loadingMessage=(readonly loadingMessage) + id=(readonly optionsId) + options=(readonly publicAPI.results) + optionsComponent=(readonly optionsComponent) + groupComponent=(readonly groupComponent) + select=(readonly publicAPI) + as |option term|}} + {{yield option term}} + {{/component}} + {{/if}} + {{component afterOptionsComponent select=(readonly publicAPI) extra=(readonly extra)}} + {{/dropdown.content}} +{{/basic-dropdown}} diff --git a/ghost/admin/app/templates/components/gh-token-input/suggested-option.hbs b/ghost/admin/app/templates/components/gh-token-input/suggested-option.hbs new file mode 100644 index 0000000000..c921bda1ba --- /dev/null +++ b/ghost/admin/app/templates/components/gh-token-input/suggested-option.hbs @@ -0,0 +1 @@ +{{option.text}} diff --git a/ghost/admin/app/templates/components/gh-navigation.hbs b/ghost/admin/app/templates/components/gh-token-input/tag-token.hbs similarity index 100% rename from ghost/admin/app/templates/components/gh-navigation.hbs rename to ghost/admin/app/templates/components/gh-token-input/tag-token.hbs diff --git a/ghost/admin/app/templates/components/gh-token-input/trigger.hbs b/ghost/admin/app/templates/components/gh-token-input/trigger.hbs new file mode 100644 index 0000000000..3b37356305 --- /dev/null +++ b/ghost/admin/app/templates/components/gh-token-input/trigger.hbs @@ -0,0 +1,56 @@ +{{#sortable-objects tagName="ul" + id=(concat "ember-power-select-multiple-options-" select.uniqueId) + class="ember-power-select-multiple-options" + sortableObjectList=select.selected + enableSort=true + useSwap=false + sortEndAction=(action "reorderItems") +}} + {{#each select.selected as |opt idx|}} + {{#component (or extra.tokenComponent draggable-object) + tagName="li" + class="ember-power-select-multiple-option" + select=select + option=(readonly opt) + idx=idx + isSortable=true + mouseDown=(action "handleOptionMouseDown") + touchStart=(action "handleOptionTouchStart") + }} + {{#if selectedItemComponent}} + {{component selectedItemComponent option=(readonly opt) select=(readonly select)}} + {{else}} + {{yield opt select}} + {{/if}} + {{#unless select.disabled}} + + × + + {{/unless}} + {{/component}} + {{else}} + {{#if (and placeholder (not searchEnabled))}} + {{placeholder}} + {{/if}} + {{/each}} + + {{#if searchEnabled}} + + {{/if}} +{{/sortable-objects}} + diff --git a/ghost/admin/app/templates/settings/design.hbs b/ghost/admin/app/templates/settings/design.hbs index 6ed09697e2..912fc8f32c 100644 --- a/ghost/admin/app/templates/settings/design.hbs +++ b/ghost/admin/app/templates/settings/design.hbs @@ -10,11 +10,13 @@
Navigation
- {{#sortable-group onChange=(action 'reorderItems') as |group|}} + {{#sortable-objects sortableObjectList=model.navigation useSwap=false}} {{#each model.navigation as |navItem|}} - {{gh-navitem navItem=navItem baseUrl=blogUrl addItem="addNavItem" deleteItem="deleteNavItem" updateUrl="updateUrl" group=group}} + {{#draggable-object content=navItem dragHandle=".gh-blognav-grab" isSortable=true}} + {{gh-navitem navItem=navItem baseUrl=blogUrl addItem="addNavItem" deleteItem="deleteNavItem" updateUrl="updateUrl"}} + {{/draggable-object}} {{/each}} - {{/sortable-group}} + {{/sortable-objects}} {{gh-navitem navItem=newNavItem baseUrl=blogUrl addItem="addNavItem" updateUrl="updateUrl"}}
diff --git a/ghost/admin/bower.json b/ghost/admin/bower.json index 116df07469..24a8b48e6d 100644 --- a/ghost/admin/bower.json +++ b/ghost/admin/bower.json @@ -4,14 +4,10 @@ "devicejs": "0.2.7", "Faker": "3.1.0", "google-caja": "6005.0.0", - "jquery-ui": "1.11.4", - "jquery.simulate.drag-sortable": "0.1.0", - "jqueryui-touch-punch": "furf/jquery-ui-touch-punch#4bc009145202d9c7483ba85f3a236a8f3470354d", "keymaster": "1.6.3", "normalize.css": "3.0.3", "pretender": "1.1.0", "rangyinputs": "1.2.0", - "selectize": "~0.12.1", "validator-js": "3.39.0" } } diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js index db5a75d23e..fda81c56f9 100644 --- a/ghost/admin/ember-cli-build.js +++ b/ghost/admin/ember-cli-build.js @@ -157,9 +157,6 @@ module.exports = function (defaults) { import: ['simplemde.js', 'simplemde.css'] } }, - 'ember-cli-selectize': { - theme: false - }, svg: { paths: [ 'public/assets/icons' @@ -185,20 +182,7 @@ module.exports = function (defaults) { app.import('bower_components/rangyinputs/rangyinputs-jquery-src.js'); app.import('bower_components/keymaster/keymaster.js'); app.import('bower_components/devicejs/lib/device.js'); - - // jquery-ui partial build - app.import('bower_components/jquery-ui/ui/core.js'); - app.import('bower_components/jquery-ui/ui/widget.js'); - app.import('bower_components/jquery-ui/ui/mouse.js'); - app.import('bower_components/jquery-ui/ui/draggable.js'); - app.import('bower_components/jquery-ui/ui/droppable.js'); - app.import('bower_components/jquery-ui/ui/sortable.js'); app.import('bower_components/google-caja/html-css-sanitizer-bundle.js'); - app.import('bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.js'); - - if (app.env !== 'production') { - app.import(`${app.bowerDirectory}/jquery.simulate.drag-sortable/jquery.simulate.drag-sortable.js`, {type: 'test'}); - } // pull things we rely on via lazy-loading into the test-support.js file so // that tests don't break when running via http://localhost:4200/tests diff --git a/ghost/admin/package.json b/ghost/admin/package.json index b8ace0ba78..a602ae1601 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -42,6 +42,7 @@ "csscomb": "4.2.0", "cssnano": "4.0.0-rc.2", "ember-ajax": "2.5.6", + "ember-assign-helper": "0.1.2", "ember-browserify": "1.2.0", "ember-cli": "2.16.2", "ember-cli-active-link-wrapper": "0.3.2", @@ -60,7 +61,6 @@ "ember-cli-node-assets": "0.2.2", "ember-cli-postcss": "3.5.2", "ember-cli-pretender": "1.0.1", - "ember-cli-selectize": "0.5.12", "ember-cli-shims": "1.1.0", "ember-cli-string-helpers": "1.5.0", "ember-cli-test-loader": "2.2.0", @@ -69,6 +69,7 @@ "ember-concurrency": "0.8.10", "ember-data": "2.16.2", "ember-data-filter": "1.13.0", + "ember-drag-drop": "https://github.com/kevinansfield/ember-drag-drop.git#node-4", "ember-element-resize-detector": "0.1.5", "ember-export-application-global": "2.0.0", "ember-fetch": "3.4.0", @@ -82,16 +83,15 @@ "ember-native-dom-helpers": "0.5.4", "ember-one-way-controls": "2.0.1", "ember-power-datepicker": "0.4.0", - "ember-power-select": "1.9.9", + "ember-power-select": "1.9.11", "ember-resolver": "4.5.0", "ember-responsive": "2.0.5", "ember-route-action-helper": "2.0.6", "ember-simple-auth": "1.4.0", "ember-sinon": "1.0.1", - "ember-sortable": "1.9.1", "ember-source": "2.16.0", "ember-test-selectors": "0.3.7", - "ember-truth-helpers": "1.3.0", + "ember-truth-helpers": "2.0.0", "ember-wormhole": "0.5.2", "emberx-file-input": "1.1.2", "eslint-plugin-ember": "4.5.0", diff --git a/ghost/admin/tests/integration/components/gh-navigation-test.js b/ghost/admin/tests/integration/components/gh-navigation-test.js deleted file mode 100644 index 289904c675..0000000000 --- a/ghost/admin/tests/integration/components/gh-navigation-test.js +++ /dev/null @@ -1,75 +0,0 @@ -/* jshint expr:true */ -import $ from 'jquery'; -import NavItem from 'ghost-admin/models/navigation-item'; -import hbs from 'htmlbars-inline-precompile'; -import wait from 'ember-test-helpers/wait'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {run} from '@ember/runloop'; -import {setupComponentTest} from 'ember-mocha'; - -describe('Integration: Component: gh-navigation', function () { - setupComponentTest('gh-navigation', { - integration: true - }); - - it('renders', function () { - this.render(hbs`{{#gh-navigation}}
{{/gh-navigation}}`); - expect(this.$('section.gh-view')).to.have.length(1); - expect(this.$('.ui-sortable')).to.have.length(1); - }); - - it('triggers reorder action', function () { - let navItems = []; - let expectedOldIndex = -1; - let expectedNewIndex = -1; - - navItems.pushObject(NavItem.create({label: 'First', url: '/first'})); - navItems.pushObject(NavItem.create({label: 'Second', url: '/second'})); - navItems.pushObject(NavItem.create({label: 'Third', url: '/third'})); - navItems.pushObject(NavItem.create({label: '', url: '', last: true})); - this.set('navigationItems', navItems); - this.set('blogUrl', 'http://localhost:2368'); - - this.on('moveItem', (oldIndex, newIndex) => { - expect(oldIndex).to.equal(expectedOldIndex); - expect(newIndex).to.equal(expectedNewIndex); - }); - - run(() => { - this.render(hbs ` - {{#gh-navigation moveItem="moveItem"}} -
- {{#each navigationItems as |navItem|}} - {{gh-navitem navItem=navItem baseUrl=blogUrl addItem="addItem" deleteItem="deleteItem" updateUrl="updateUrl"}} - {{/each}} -
- {{/gh-navigation}}`); - }); - - // check it renders the nav item rows - expect(this.$('.gh-blognav-item')).to.have.length(4); - - // move second item up one - expectedOldIndex = 1; - expectedNewIndex = 0; - run(() => { - $(this.$('.gh-blognav-item')[1]).simulateDragSortable({ - move: -1, - handle: '.gh-blognav-grab' - }); - }); - - wait().then(() => { - // move second item down one - expectedOldIndex = 1; - expectedNewIndex = 2; - run(() => { - $(this.$('.gh-blognav-item')[1]).simulateDragSortable({ - move: 1, - handle: '.gh-blognav-grab' - }); - }); - }); - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-psm-tags-input-test.js b/ghost/admin/tests/integration/components/gh-psm-tags-input-test.js new file mode 100644 index 0000000000..d9091a46e1 --- /dev/null +++ b/ghost/admin/tests/integration/components/gh-psm-tags-input-test.js @@ -0,0 +1,205 @@ +import hbs from 'htmlbars-inline-precompile'; +import mockPosts from '../../../mirage/config/posts'; +import mockTags from '../../../mirage/config/themes'; +import wait from 'ember-test-helpers/wait'; +import {click, findAll} from 'ember-native-dom-helpers'; +import {clickTrigger, selectChoose, typeInSearch} from '../../../tests/helpers/ember-power-select'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {run} from '@ember/runloop'; +import {setupComponentTest} from 'ember-mocha'; +import {startMirage} from 'ghost-admin/initializers/ember-cli-mirage'; + +// NOTE: although Mirage has posts<->tags relationship and can respond +// to :post-id/?include=tags all ordering information is lost so we +// need to build the tags array manually +const assignPostWithTags = function postWithTags(context, ...slugs) { + context.get('store').findRecord('post', 1).then((post) => { + context.get('store').findAll('tag').then((tags) => { + slugs.forEach((slug) => { + post.get('tags').pushObject(tags.findBy('slug', slug)); + }); + + context.set('post', post); + }); + }); +}; + +describe('Integration: Component: gh-psm-tags-input', function() { + setupComponentTest('gh-psm-tags-input', { + integration: true + }); + + let server; + + beforeEach(function () { + server = startMirage(); + server.create('user'); + + mockPosts(server); + mockTags(server); + + server.create('post'); + server.create('tag', {name: 'Tag One', slug: 'one'}); + server.create('tag', {name: 'Tag Two', slug: 'two'}); + server.create('tag', {name: 'Tag Three', slug: 'three'}); + server.create('tag', {name: '#Internal Tag', visibility: 'internal', slug: 'internal'}); + + this.inject.service('store'); + }); + + afterEach(function () { + server.shutdown(); + }); + + it('shows selected tags on render', async function () { + run(() => { + assignPostWithTags(this, 'one', 'three'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + + let selected = findAll('.tag-token'); + expect(selected.length).to.equal(2); + expect(selected[0].textContent).to.have.string('Tag One'); + expect(selected[1].textContent).to.have.string('Tag Three'); + }); + + it('exposes all tags as options', async function () { + run(() => { + this.set('post', this.get('store').findRecord('post', 1)); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await clickTrigger(); + + let options = findAll('.ember-power-select-option'); + expect(options.length).to.equal(4); + expect(options[0].textContent).to.have.string('Tag One'); + expect(options[1].textContent).to.have.string('Tag Two'); + expect(options[2].textContent).to.have.string('Tag Three'); + expect(options[3].textContent).to.have.string('#Internal Tag'); + }); + + it('matches options on lowercase tag names', async function () { + run(() => { + this.set('post', this.get('store').findRecord('post', 1)); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await clickTrigger(); + await typeInSearch('two'); + + let options = findAll('.ember-power-select-option'); + expect(options.length).to.equal(2); + expect(options[0].textContent).to.have.string('Add "two"...'); + expect(options[1].textContent).to.have.string('Tag Two'); + }); + + it('hides create option on exact matches', async function () { + run(() => { + this.set('post', this.get('store').findRecord('post', 1)); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await clickTrigger(); + await typeInSearch('Tag Two'); + + let options = findAll('.ember-power-select-option'); + expect(options.length).to.equal(1); + expect(options[0].textContent).to.have.string('Tag Two'); + }); + + describe('primary tags', function () { + it('adds primary tag class to first tag', async function () { + run(() => { + assignPostWithTags(this, 'one', 'three'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + + let selected = findAll('.tag-token'); + expect(selected.length).to.equal(2); + expect(selected[0].classList.contains('tag-token--primary')).to.be.true; + expect(selected[1].classList.contains('tag-token--primary')).to.be.false; + }); + + it('doesn\'t add primary tag class if first tag is internal', async function () { + run(() => { + assignPostWithTags(this, 'internal', 'two'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + + let selected = findAll('.tag-token'); + expect(selected.length).to.equal(2); + expect(selected[0].classList.contains('tag-token--primary')).to.be.false; + expect(selected[1].classList.contains('tag-token--primary')).to.be.false; + }); + }); + + describe('updateTags', function () { + it('modifies post.tags', async function () { + run(() => { + assignPostWithTags(this, 'internal', 'two'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await selectChoose('.ember-power-select-trigger', 'Tag One'); + + expect( + this.get('post.tags').mapBy('name').join(',') + ).to.equal('#Internal Tag,Tag Two,Tag One'); + }); + + it('destroys new tag records when not selected', async function () { + run(() => { + assignPostWithTags(this, 'internal', 'two'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await clickTrigger(); + await typeInSearch('New'); + await selectChoose('.ember-power-select-trigger', 'Add "New"...'); + + let tags = await this.get('store').peekAll('tag'); + expect(tags.get('length')).to.equal(5); + + let removeBtns = findAll('.ember-power-select-multiple-remove-btn'); + await click(removeBtns[removeBtns.length - 1]); + + tags = await this.get('store').peekAll('tag'); + expect(tags.get('length')).to.equal(4); + }); + }); + + describe('createTag', function () { + it('creates new records', async function () { + run(() => { + assignPostWithTags(this, 'internal', 'two'); + }); + await wait(); + + await this.render(hbs`{{gh-psm-tags-input post=post}}`); + await clickTrigger(); + await typeInSearch('New One'); + await selectChoose('.ember-power-select-trigger', 'Add "New One"...'); + await typeInSearch('New Two'); + await selectChoose('.ember-power-select-trigger', 'Add "New Two"...'); + + let tags = await this.get('store').peekAll('tag'); + expect(tags.get('length')).to.equal(6); + + expect(tags.findBy('name', 'New One').get('isNew')).to.be.true; + expect(tags.findBy('name', 'New Two').get('isNew')).to.be.true; + }); + }); +}); diff --git a/ghost/admin/tests/unit/components/gh-selectize-test.js b/ghost/admin/tests/unit/components/gh-selectize-test.js deleted file mode 100644 index 3a88f1d257..0000000000 --- a/ghost/admin/tests/unit/components/gh-selectize-test.js +++ /dev/null @@ -1,38 +0,0 @@ -/* jshint expr:true */ -import {describe, it} from 'mocha'; -import {A as emberA} from '@ember/array'; -import {expect} from 'chai'; -import {run} from '@ember/runloop'; -import {setupComponentTest} from 'ember-mocha'; - -describe('Unit: Component: gh-selectize', function () { - setupComponentTest('gh-selectize', { - // Specify the other units that are required for this test - // needs: ['component:foo', 'helper:bar'], - unit: true - }); - - it('re-orders selection when selectize order is changed', function () { - let component = this.subject(); - - let item1 = {id: '1', name: 'item 1'}; - let item2 = {id: '2', name: 'item 2'}; - let item3 = {id: '3', name: 'item 3'}; - - run(() => { - component.set('content', emberA([item1, item2, item3])); - component.set('selection', emberA([item2, item3])); - component.set('multiple', true); - component.set('optionValuePath', 'content.id'); - component.set('optionLabelPath', 'content.name'); - }); - - this.render(); - - run(() => { - component._selectize.setValue(['3', '2']); - }); - - expect(component.get('selection').toArray(), 'component selection').to.deep.equal([item3, item2]); - }); -}); diff --git a/ghost/admin/tests/unit/controllers/settings/design-test.js b/ghost/admin/tests/unit/controllers/settings/design-test.js index 04bf9dffd3..c46b5a2b75 100644 --- a/ghost/admin/tests/unit/controllers/settings/design-test.js +++ b/ghost/admin/tests/unit/controllers/settings/design-test.js @@ -154,21 +154,6 @@ describe('Unit: Controller: settings/design', function () { }); }); - it('action - reorderItems: updates navigationItems list', function () { - let ctrl = this.subject(); - let navItems = [ - NavItem.create({label: 'First', url: '/first'}), - NavItem.create({label: 'Second', url: '/second', last: true}) - ]; - - run(() => { - ctrl.set('model', EmberObject.create({navigation: navItems})); - expect(ctrl.get('model.navigation').mapBy('label')).to.deep.equal(['First', 'Second']); - ctrl.send('reorderItems', navItems.reverseObjects()); - expect(ctrl.get('model.navigation').mapBy('label')).to.deep.equal(['Second', 'First']); - }); - }); - it('action - updateUrl: updates URL on navigationItem', function () { let ctrl = this.subject(); let navItems = [ diff --git a/ghost/admin/yarn.lock b/ghost/admin/yarn.lock index 76e98346be..713587705d 100644 --- a/ghost/admin/yarn.lock +++ b/ghost/admin/yarn.lock @@ -3114,7 +3114,7 @@ ember-cli-babel@6.8.2, ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.10, e clone "^2.0.0" ember-cli-version-checker "^2.0.0" -ember-cli-babel@^5.0.0, ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7: +ember-cli-babel@^5.0.0, ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7, ember-cli-babel@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.2.4.tgz#5ce4f46b08ed6f6d21e878619fb689719d6e8e13" dependencies: @@ -3393,15 +3393,6 @@ ember-cli-pretender@1.0.1: pretender "^1.4.2" resolve "^1.2.0" -ember-cli-selectize@0.5.12: - version "0.5.12" - resolved "https://registry.yarnpkg.com/ember-cli-selectize/-/ember-cli-selectize-0.5.12.tgz#38af65b12d01d8ff20ba252235105bed10b7111a" - dependencies: - ember-cli-babel "^5.1.6" - ember-cli-import-polyfill "^0.2.0" - ember-getowner-polyfill "^1.1.1" - ember-new-computed "^1.0.0" - ember-cli-shims@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ember-cli-shims/-/ember-cli-shims-1.1.0.tgz#0e3b8a048be865b4f81cc81d397ff1eeb13f75b6" @@ -3634,6 +3625,12 @@ ember-data@2.16.2: silent-error "^1.0.0" testem "^1.15.0" +"ember-drag-drop@https://github.com/kevinansfield/ember-drag-drop.git#node-4": + version "0.4.6" + resolved "https://github.com/kevinansfield/ember-drag-drop.git#28d063ef5ccf613c5db556e4660db50b4557b68d" + dependencies: + ember-cli-babel "^5.2.4" + ember-element-resize-detector@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/ember-element-resize-detector/-/ember-element-resize-detector-0.1.5.tgz#4b1cdd63c0d42c42c78f0ac0cc8cff3e1e095611" @@ -3758,7 +3755,7 @@ ember-inline-svg@0.1.11: svgo "^0.6.3" walk-sync "^0.3.1" -ember-invoke-action@1.4.0, ember-invoke-action@^1.4.0: +ember-invoke-action@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ember-invoke-action/-/ember-invoke-action-1.4.0.tgz#2899854bd755f9331ca86c902bf6d4dbf8bdfcb3" dependencies: @@ -3848,12 +3845,6 @@ ember-native-dom-helpers@0.5.4, ember-native-dom-helpers@^0.5.3: broccoli-funnel "^1.1.0" ember-cli-babel "^6.6.0" -ember-new-computed@^1.0.0, ember-new-computed@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ember-new-computed/-/ember-new-computed-1.0.3.tgz#592af8a778e0260ce7e812687c3aedface1622bf" - dependencies: - ember-cli-babel "^5.1.5" - ember-one-way-controls@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ember-one-way-controls/-/ember-one-way-controls-2.0.1.tgz#45bd9367554f69e10fa37dfac8f1a6c72360a7f1" @@ -3884,16 +3875,16 @@ ember-power-datepicker@0.4.0: ember-cli-htmlbars "^2.0.1" ember-power-calendar "^0.5.0" -ember-power-select@1.9.9: - version "1.9.9" - resolved "https://registry.yarnpkg.com/ember-power-select/-/ember-power-select-1.9.9.tgz#24b733f5be603a434dffdb57dffe702759448ca5" +ember-power-select@1.9.11: + version "1.9.11" + resolved "https://registry.yarnpkg.com/ember-power-select/-/ember-power-select-1.9.11.tgz#d7aa04e4b6baa93adc9a7b8a8ae989e7a3751eb1" dependencies: ember-basic-dropdown "^0.33.1" - ember-cli-babel "^6.6.0" + ember-cli-babel "^6.8.2" ember-cli-htmlbars "^2.0.1" ember-concurrency "^0.8.1" ember-text-measurer "^0.3.3" - ember-truth-helpers "^1.3.0" + ember-truth-helpers "^2.0.0" ember-resolver@4.5.0: version "4.5.0" @@ -3972,15 +3963,6 @@ ember-sinon@1.0.1: ember-cli-babel "^6.3.0" sinon "^3.2.1" -ember-sortable@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ember-sortable/-/ember-sortable-1.9.1.tgz#e5053866a7547c1ce369700a43cb570f8ac84519" - dependencies: - ember-cli-babel "^5.1.7" - ember-cli-htmlbars "^1.0.10" - ember-invoke-action "^1.4.0" - ember-new-computed "^1.0.2" - ember-source@2.16.0: version "2.16.0" resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-2.16.0.tgz#2becd7966278fe453046b91178ede665c2cf241a" @@ -4031,11 +4013,11 @@ ember-truth-helpers@1.2.0: dependencies: ember-cli-babel "^5.1.5" -ember-truth-helpers@1.3.0, ember-truth-helpers@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-1.3.0.tgz#6ed9f83ce9a49f52bb416d55e227426339a64c60" +ember-truth-helpers@2.0.0, ember-truth-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.0.0.tgz#f3e2eef667859197f1328bb4f83b0b35b661c1ac" dependencies: - ember-cli-babel "^5.1.6" + ember-cli-babel "^6.8.2" ember-try-config@^2.0.1: version "2.1.0"