mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-15 03:12:54 +03:00
Merge pull request #6492 from acburdine/ember-power-select
Convert gh-search-input to ember-power-select component
This commit is contained in:
commit
7fb441e640
@ -3,14 +3,29 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
$,
|
|
||||||
Component,
|
Component,
|
||||||
RSVP,
|
RSVP,
|
||||||
computed,
|
computed,
|
||||||
|
run,
|
||||||
inject: {service},
|
inject: {service},
|
||||||
observer
|
isBlank,
|
||||||
|
isEmpty
|
||||||
} = Ember;
|
} = Ember;
|
||||||
const {filterBy} = computed;
|
|
||||||
|
export function computedGroup(category) {
|
||||||
|
return computed('content', 'currentSearch', function () {
|
||||||
|
if (!this.get('currentSearch') || !this.get('content')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.get('content').filter((item) => {
|
||||||
|
let search = new RegExp(this.get('currentSearch'), 'ig');
|
||||||
|
|
||||||
|
return (item.category === category) &&
|
||||||
|
item.title.match(search);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
@ -19,20 +34,17 @@ export default Component.extend({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
contentExpiry: 10 * 1000,
|
contentExpiry: 10 * 1000,
|
||||||
contentExpiresAt: false,
|
contentExpiresAt: false,
|
||||||
|
currentSearch: '',
|
||||||
|
|
||||||
posts: filterBy('content', 'category', 'Posts'),
|
posts: computedGroup('Posts'),
|
||||||
pages: filterBy('content', 'category', 'Pages'),
|
pages: computedGroup('Pages'),
|
||||||
users: filterBy('content', 'category', 'Users'),
|
users: computedGroup('Users'),
|
||||||
tags: filterBy('content', 'category', 'Tags'),
|
tags: computedGroup('Tags'),
|
||||||
|
|
||||||
_store: service('store'),
|
_store: service('store'),
|
||||||
_routing: service('-routing'),
|
_routing: service('-routing'),
|
||||||
ajax: service(),
|
ajax: service(),
|
||||||
|
|
||||||
_selectize: computed(function () {
|
|
||||||
return this.$('select')[0].selectize;
|
|
||||||
}),
|
|
||||||
|
|
||||||
refreshContent() {
|
refreshContent() {
|
||||||
let promises = [];
|
let promises = [];
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
@ -40,7 +52,7 @@ export default Component.extend({
|
|||||||
let contentExpiresAt = this.get('contentExpiresAt');
|
let contentExpiresAt = this.get('contentExpiresAt');
|
||||||
|
|
||||||
if (this.get('isLoading') || contentExpiresAt > now) {
|
if (this.get('isLoading') || contentExpiresAt > now) {
|
||||||
return;
|
return RSVP.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('isLoading', true);
|
this.set('isLoading', true);
|
||||||
@ -49,12 +61,34 @@ export default Component.extend({
|
|||||||
promises.pushObject(this._loadUsers());
|
promises.pushObject(this._loadUsers());
|
||||||
promises.pushObject(this._loadTags());
|
promises.pushObject(this._loadTags());
|
||||||
|
|
||||||
RSVP.all(promises).then(() => { }).finally(() => {
|
return RSVP.all(promises).then(() => { }).finally(() => {
|
||||||
this.set('isLoading', false);
|
this.set('isLoading', false);
|
||||||
this.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
|
this.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
groupedContent: computed('posts', 'pages', 'users', 'tags', function () {
|
||||||
|
let groups = [];
|
||||||
|
|
||||||
|
if (!isEmpty(this.get('posts'))) {
|
||||||
|
groups.pushObject({groupName: 'Posts', options: this.get('posts')});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(this.get('pages'))) {
|
||||||
|
groups.pushObject({groupName: 'Pages', options: this.get('pages')});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(this.get('users'))) {
|
||||||
|
groups.pushObject({groupName: 'Users', options: this.get('users')});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(this.get('tags'))) {
|
||||||
|
groups.pushObject({groupName: 'Tags', options: this.get('tags')});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}),
|
||||||
|
|
||||||
_loadPosts() {
|
_loadPosts() {
|
||||||
let store = this.get('_store');
|
let store = this.get('_store');
|
||||||
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
|
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
|
||||||
@ -62,6 +96,7 @@ export default Component.extend({
|
|||||||
let content = this.get('content');
|
let content = this.get('content');
|
||||||
|
|
||||||
return this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
|
return this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
|
||||||
|
|
||||||
content.pushObjects(posts.posts.map((post) => {
|
content.pushObjects(posts.posts.map((post) => {
|
||||||
return {
|
return {
|
||||||
id: `post.${post.id}`,
|
id: `post.${post.id}`,
|
||||||
@ -106,11 +141,17 @@ export default Component.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_keepSelectionClear: observer('selection', function () {
|
_performSearch(term, resolve, reject) {
|
||||||
if (this.get('selection') !== null) {
|
if (isBlank(term)) {
|
||||||
this.set('selection', null);
|
return resolve([]);
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
|
this.refreshContent().then(() => {
|
||||||
|
this.set('currentSearch', term);
|
||||||
|
|
||||||
|
return resolve(this.get('groupedContent'));
|
||||||
|
}).catch(reject);
|
||||||
|
},
|
||||||
|
|
||||||
_setKeymasterScope() {
|
_setKeymasterScope() {
|
||||||
key.setScope('search-input');
|
key.setScope('search-input');
|
||||||
@ -127,68 +168,38 @@ export default Component.extend({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
openSelected(selected) {
|
openSelected(selected) {
|
||||||
let transition = null;
|
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.category === 'Posts' || selected.category === 'Pages') {
|
if (selected.category === 'Posts' || selected.category === 'Pages') {
|
||||||
let id = selected.id.replace('post.', '');
|
let id = selected.id.replace('post.', '');
|
||||||
transition = this.get('_routing.router').transitionTo('editor.edit', id);
|
this.get('_routing.router').transitionTo('editor.edit', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.category === 'Users') {
|
if (selected.category === 'Users') {
|
||||||
let id = selected.id.replace('user.', '');
|
let id = selected.id.replace('user.', '');
|
||||||
transition = this.get('_routing.router').transitionTo('team.user', id);
|
this.get('_routing.router').transitionTo('team.user', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.category === 'Tags') {
|
if (selected.category === 'Tags') {
|
||||||
let id = selected.id.replace('tag.', '');
|
let id = selected.id.replace('tag.', '');
|
||||||
transition = this.get('_routing.router').transitionTo('settings.tags.tag', id);
|
this.get('_routing.router').transitionTo('settings.tags.tag', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.then(() => {
|
|
||||||
if (this.get('_selectize').$control_input.is(':focus')) {
|
|
||||||
this._setKeymasterScope();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
focusInput() {
|
|
||||||
this.get('_selectize').focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
onInit() {
|
|
||||||
let selectize = this.get('_selectize');
|
|
||||||
let html = '<div class="dropdown-empty-message">Nothing found…</div>';
|
|
||||||
|
|
||||||
selectize.$empty_results_container = $(html);
|
|
||||||
selectize.$empty_results_container.hide();
|
|
||||||
selectize.$dropdown.append(selectize.$empty_results_container);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus() {
|
onFocus() {
|
||||||
this._setKeymasterScope();
|
this._setKeymasterScope();
|
||||||
this.refreshContent();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlur() {
|
onBlur() {
|
||||||
let selectize = this.get('_selectize');
|
|
||||||
|
|
||||||
this._resetKeymasterScope();
|
this._resetKeymasterScope();
|
||||||
selectize.$empty_results_container.hide();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onType() {
|
search(term) {
|
||||||
let selectize = this.get('_selectize');
|
return new RSVP.Promise((resolve, reject) => {
|
||||||
|
run.debounce(this, this._performSearch, term, resolve, reject, 200);
|
||||||
if (!selectize.hasOptions) {
|
});
|
||||||
selectize.open();
|
|
||||||
selectize.$empty_results_container.show();
|
|
||||||
} else {
|
|
||||||
selectize.$empty_results_container.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
core/client/app/components/gh-search-input/trigger.js
Normal file
42
core/client/app/components/gh-search-input/trigger.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {run, isBlank, Component} = Ember;
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
open() {
|
||||||
|
this.get('select.actions').open();
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.get('select.actions').close();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
captureMouseDown(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
search(term) {
|
||||||
|
if (isBlank(term) === this.get('select.isOpen')) {
|
||||||
|
run.scheduleOnce('afterRender', this, isBlank(term) ? this.close : this.open);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get('select.actions.search')(term);
|
||||||
|
},
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.$('input')[0].focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
resetInput() {
|
||||||
|
this.$('input').val('');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleKeydown(e) {
|
||||||
|
let select = this.get('select');
|
||||||
|
if (!select.isOpen) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
7
core/client/app/helpers/highlighted-text.js
Normal file
7
core/client/app/helpers/highlighted-text.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
export function highlightedText([text, termToHighlight]) {
|
||||||
|
return Ember.String.htmlSafe(text.replace(new RegExp(termToHighlight, 'ig'), '<span class="highlight">$&</span>'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ember.Helper.helper(highlightedText);
|
@ -27,6 +27,7 @@
|
|||||||
@import "components/popovers.css";
|
@import "components/popovers.css";
|
||||||
@import "components/settings-menu.css";
|
@import "components/settings-menu.css";
|
||||||
@import "components/selectize.css";
|
@import "components/selectize.css";
|
||||||
|
@import "components/power-select.css";
|
||||||
|
|
||||||
|
|
||||||
/* Layouts: Groups of Components
|
/* Layouts: Groups of Components
|
||||||
|
108
core/client/app/styles/components/power-select.css
Normal file
108
core/client/app/styles/components/power-select.css
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
.ember-power-select-trigger {
|
||||||
|
border: 1px solid #dfe1e3;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-basic-dropdown--opened > .ember-power-select-trigger,
|
||||||
|
.ember-power-select-trigger[aria-expanded="true"],
|
||||||
|
.ember-power-select-search input {
|
||||||
|
outline: 0;
|
||||||
|
border-color: #b1b1b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-options:not([role="group"]) {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-search 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;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group {
|
||||||
|
float: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 0 none;
|
||||||
|
border-right: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group .ember-power-select-group-name {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 7px 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--midgrey);
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group .ember-power-select-group-name: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) {
|
||||||
|
.ember-power-select-group .ember-power-select-group-name:after {
|
||||||
|
width: calc(224px - 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.ember-power-select-group .ember-power-select-group-name:after {
|
||||||
|
width: calc(80vw - 45px - 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group:first-of-type .ember-power-select-group-name {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group .ember-power-select-option {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 7px 8px;
|
||||||
|
line-height: 1.35em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group .ember-power-select-option .highlight {
|
||||||
|
background: #fff3b8;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-power-select-group .ember-power-select-option[aria-current="true"] {
|
||||||
|
background: color(var(--blue) alpha(-85%));
|
||||||
|
color: var(--darkgrey);
|
||||||
|
}
|
@ -1,16 +1,13 @@
|
|||||||
{{gh-selectize
|
{{#power-select
|
||||||
|
search=(action "search")
|
||||||
|
onchange=(action "openSelected")
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
selection=selection
|
onopen=(action "onFocus")
|
||||||
content=content
|
onclose=(action "onBlur")
|
||||||
loading=isLoading
|
searchEnabled=false
|
||||||
optionValuePath="content.id"
|
triggerComponent="gh-search-input/trigger"
|
||||||
optionLabelPath="content.title"
|
renderInPlace=true
|
||||||
optionGroupPath="content.category"
|
loadingMessage="Loading"
|
||||||
openOnFocus=false
|
as |name term|}}
|
||||||
maxItems="1"
|
{{highlighted-text name.title term}}
|
||||||
on-init="onInit"
|
{{/power-select}}
|
||||||
on-focus="onFocus"
|
|
||||||
on-blur="onBlur"
|
|
||||||
update-filter="onType"
|
|
||||||
select-item="openSelected"}}
|
|
||||||
<button class="gh-nav-search-button" {{action "focusInput"}}><i class="icon-search"></i><span class="sr-only">Search</span></button>
|
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<div class="ember-power-select-search" onmousedown={{action "captureMouseDown"}}>
|
||||||
|
<input type="search" autocomplete="off"
|
||||||
|
autocorrect="off" autocapitalize="off"
|
||||||
|
value={{if extra.labelPath (get selected extra.labelPath) selected}}
|
||||||
|
spellcheck="false" role="combobox"
|
||||||
|
placeholder={{placeholder}}
|
||||||
|
oninput={{action 'search' value="target.value"}}
|
||||||
|
onmousedown={{action "captureMouseDown"}}
|
||||||
|
onkeydown={{action "handleKeydown"}}
|
||||||
|
onblur={{action "resetInput"}}>
|
||||||
|
<button class="gh-nav-search-button" {{action "focusInput"}}><i class="icon-search"></i><span class="sr-only">Search</span></button>
|
||||||
|
</div>
|
@ -44,6 +44,7 @@
|
|||||||
"ember-load-initializers": "0.5.1",
|
"ember-load-initializers": "0.5.1",
|
||||||
"ember-myth": "0.1.1",
|
"ember-myth": "0.1.1",
|
||||||
"ember-one-way-controls": "0.6.2",
|
"ember-one-way-controls": "0.6.2",
|
||||||
|
"ember-power-select": "0.9.2",
|
||||||
"ember-resolver": "2.0.3",
|
"ember-resolver": "2.0.3",
|
||||||
"ember-route-action-helper": "0.3.0",
|
"ember-route-action-helper": "0.3.0",
|
||||||
"ember-simple-auth": "1.0.1",
|
"ember-simple-auth": "1.0.1",
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {run} = Ember;
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'gh-search-input',
|
||||||
|
'Integration: Component: gh-search-input',
|
||||||
|
{
|
||||||
|
integration: true
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
it('renders', function () {
|
||||||
|
// renders the component on the page
|
||||||
|
this.render(hbs`{{gh-search-input}}`);
|
||||||
|
|
||||||
|
expect(this.$('.ember-power-select-search input')).to.have.length(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -1,26 +0,0 @@
|
|||||||
/* jshint expr:true */
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import {
|
|
||||||
describeComponent,
|
|
||||||
it
|
|
||||||
} from 'ember-mocha';
|
|
||||||
|
|
||||||
describeComponent(
|
|
||||||
'gh-search-input',
|
|
||||||
'Unit: Component: gh-search-input',
|
|
||||||
{
|
|
||||||
unit: true,
|
|
||||||
needs: ['component:gh-selectize']
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
it('renders', function () {
|
|
||||||
// creates the component instance
|
|
||||||
let component = this.subject();
|
|
||||||
expect(component._state).to.equal('preRender');
|
|
||||||
|
|
||||||
// renders the component on the page
|
|
||||||
this.render();
|
|
||||||
expect(component._state).to.equal('inDOM');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
18
core/client/tests/unit/helpers/highlighted-text-test.js
Normal file
18
core/client/tests/unit/helpers/highlighted-text-test.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it
|
||||||
|
} from 'mocha';
|
||||||
|
import {
|
||||||
|
highlightedText
|
||||||
|
} from 'ghost/helpers/highlighted-text';
|
||||||
|
|
||||||
|
describe('Unit: Helper: highlighted-text', function() {
|
||||||
|
|
||||||
|
it('works', function() {
|
||||||
|
let result = highlightedText(['Test', 'e']);
|
||||||
|
expect(result).to.be.an('object');
|
||||||
|
expect(result.string).to.equal('T<span class="highlight">e</span>st');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user