New tags input, drop selectize & jquery-ui deps (#892)

closes https://github.com/TryGhost/Ghost/issues/6458
- swap `ember-sortable` for `ember-drag-drop` in navigation UI
- extract PSM tag input into new `{{gh-psm-tags-input}}`
- add new `{{gh-token-input}}` that wraps `ember-power-select` and `ember-drag-drop` to replicate the previous selectize based tags input
- enhance `{{gh-psm-tags-input}}` behaviour to highlight selected primary tag and show "primary/internal" in selected tag titles
- 🔥 remove `selectize`
- 🔥 remove `jquery-ui`
- 🔥 remove unused `{{gh-navigation}}` component
This commit is contained in:
Kevin Ansfield 2017-10-31 09:10:49 +00:00 committed by GitHub
parent 9adbcd1fd0
commit 0106a21e3c
38 changed files with 1034 additions and 881 deletions

View File

@ -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');
}
});

View File

@ -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'),

View File

@ -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')();

View File

@ -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();
});
});
}
});

View File

@ -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();
}
});

View File

@ -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 <strong>"${Handlebars.Utils.escapeExpression(term)}"...</strong>`);
},
// 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;
}
});

View File

@ -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;
}
});

View File

@ -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({
});

View File

@ -0,0 +1,5 @@
import Component from '@ember/component';
export default Component.extend({
tagName: ''
});

View File

@ -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;
})
});

View File

@ -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')));
}
}
});

View File

@ -112,10 +112,6 @@ export default Controller.extend({
navItems.removeObject(item);
},
reorderItems(navItems) {
this.set('model.navigation', navItems);
},
updateUrl(url, navItem) {
if (!navItem) {
return;

View File

@ -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');

View File

@ -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, {

View File

@ -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);

View File

@ -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";

View File

@ -151,11 +151,3 @@
.closed > .dropdown-menu {
display: none;
}
/* Selectize
/* ---------------------------------------------------------- */
.selectize-dropdown {
z-index: 200;
}

View File

@ -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%));
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -67,17 +67,7 @@
<div class="form-group">
<label for="tag-input">Tags</label>
{{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"}}
</div>
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="customExcerpt"}}

View File

@ -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
}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}
<ul class="ember-power-select-options" role="listbox">
<li class="ember-power-select-option ember-power-select-option--no-matches-message" role="option">
{{noMatchesMessage}}
</li>
</ul>
{{/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}}

View File

@ -0,0 +1 @@
{{option.text}}

View File

@ -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}}
<span role="button"
aria-label="remove element"
class="ember-power-select-multiple-remove-btn"
data-selected-index={{idx}}>
&times;
</span>
{{/unless}}
{{/component}}
{{else}}
{{#if (and placeholder (not searchEnabled))}}
<span class="ember-power-select-placeholder">{{placeholder}}</span>
{{/if}}
{{/each}}
{{#if searchEnabled}}
<input type="search" class="ember-power-select-trigger-multiple-input"
tabindex="0" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
id="ember-power-select-trigger-multiple-input-{{select.uniqueId}}"
value={{select.searchText}}
aria-controls={{listboxId}}
style={{triggerMultipleInputStyle}}
placeholder={{maybePlaceholder}}
disabled={{select.disabled}}
oninput={{action "onInput"}}
onFocus={{onFocus}}
onBlur={{onBlur}}
tabindex={{tabindex}}
onkeydown={{action "onKeydown"}}>
{{/if}}
{{/sortable-objects}}
<span class="ember-power-select-status-icon"></span>

View File

@ -10,11 +10,13 @@
<div class="gh-setting-header">Navigation</div>
<div class="gh-blognav-container">
<form id="settings-navigation" class="gh-blognav" novalidate="novalidate">
{{#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"}}
</form>
</div>

View File

@ -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"
}
}

View File

@ -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

View File

@ -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",

View File

@ -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}}<div class="js-gh-blognav"><div class="gh-blognav-item"></div></div>{{/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"}}
<form id="settings-navigation" class="gh-blognav js-gh-blognav" novalidate="novalidate">
{{#each navigationItems as |navItem|}}
{{gh-navitem navItem=navItem baseUrl=blogUrl addItem="addItem" deleteItem="deleteItem" updateUrl="updateUrl"}}
{{/each}}
</form>
{{/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'
});
});
});
});
});

View File

@ -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;
});
});
});

View File

@ -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]);
});
});

View File

@ -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 = [

View File

@ -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"