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';
|
||||
|
||||
const {
|
||||
$,
|
||||
Component,
|
||||
RSVP,
|
||||
computed,
|
||||
run,
|
||||
inject: {service},
|
||||
observer
|
||||
isBlank,
|
||||
isEmpty
|
||||
} = 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({
|
||||
|
||||
@ -19,20 +34,17 @@ export default Component.extend({
|
||||
isLoading: false,
|
||||
contentExpiry: 10 * 1000,
|
||||
contentExpiresAt: false,
|
||||
currentSearch: '',
|
||||
|
||||
posts: filterBy('content', 'category', 'Posts'),
|
||||
pages: filterBy('content', 'category', 'Pages'),
|
||||
users: filterBy('content', 'category', 'Users'),
|
||||
tags: filterBy('content', 'category', 'Tags'),
|
||||
posts: computedGroup('Posts'),
|
||||
pages: computedGroup('Pages'),
|
||||
users: computedGroup('Users'),
|
||||
tags: computedGroup('Tags'),
|
||||
|
||||
_store: service('store'),
|
||||
_routing: service('-routing'),
|
||||
ajax: service(),
|
||||
|
||||
_selectize: computed(function () {
|
||||
return this.$('select')[0].selectize;
|
||||
}),
|
||||
|
||||
refreshContent() {
|
||||
let promises = [];
|
||||
let now = new Date();
|
||||
@ -40,7 +52,7 @@ export default Component.extend({
|
||||
let contentExpiresAt = this.get('contentExpiresAt');
|
||||
|
||||
if (this.get('isLoading') || contentExpiresAt > now) {
|
||||
return;
|
||||
return RSVP.resolve();
|
||||
}
|
||||
|
||||
this.set('isLoading', true);
|
||||
@ -49,12 +61,34 @@ export default Component.extend({
|
||||
promises.pushObject(this._loadUsers());
|
||||
promises.pushObject(this._loadTags());
|
||||
|
||||
RSVP.all(promises).then(() => { }).finally(() => {
|
||||
return RSVP.all(promises).then(() => { }).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
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() {
|
||||
let store = this.get('_store');
|
||||
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
|
||||
@ -62,6 +96,7 @@ export default Component.extend({
|
||||
let content = this.get('content');
|
||||
|
||||
return this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
|
||||
|
||||
content.pushObjects(posts.posts.map((post) => {
|
||||
return {
|
||||
id: `post.${post.id}`,
|
||||
@ -106,11 +141,17 @@ export default Component.extend({
|
||||
});
|
||||
},
|
||||
|
||||
_keepSelectionClear: observer('selection', function () {
|
||||
if (this.get('selection') !== null) {
|
||||
this.set('selection', null);
|
||||
_performSearch(term, resolve, reject) {
|
||||
if (isBlank(term)) {
|
||||
return resolve([]);
|
||||
}
|
||||
}),
|
||||
|
||||
this.refreshContent().then(() => {
|
||||
this.set('currentSearch', term);
|
||||
|
||||
return resolve(this.get('groupedContent'));
|
||||
}).catch(reject);
|
||||
},
|
||||
|
||||
_setKeymasterScope() {
|
||||
key.setScope('search-input');
|
||||
@ -127,68 +168,38 @@ export default Component.extend({
|
||||
|
||||
actions: {
|
||||
openSelected(selected) {
|
||||
let transition = null;
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.category === 'Posts' || selected.category === 'Pages') {
|
||||
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') {
|
||||
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') {
|
||||
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() {
|
||||
this._setKeymasterScope();
|
||||
this.refreshContent();
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
let selectize = this.get('_selectize');
|
||||
|
||||
this._resetKeymasterScope();
|
||||
selectize.$empty_results_container.hide();
|
||||
},
|
||||
|
||||
onType() {
|
||||
let selectize = this.get('_selectize');
|
||||
|
||||
if (!selectize.hasOptions) {
|
||||
selectize.open();
|
||||
selectize.$empty_results_container.show();
|
||||
} else {
|
||||
selectize.$empty_results_container.hide();
|
||||
}
|
||||
search(term) {
|
||||
return new RSVP.Promise((resolve, reject) => {
|
||||
run.debounce(this, this._performSearch, term, resolve, reject, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
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/settings-menu.css";
|
||||
@import "components/selectize.css";
|
||||
@import "components/power-select.css";
|
||||
|
||||
|
||||
/* 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"
|
||||
selection=selection
|
||||
content=content
|
||||
loading=isLoading
|
||||
optionValuePath="content.id"
|
||||
optionLabelPath="content.title"
|
||||
optionGroupPath="content.category"
|
||||
openOnFocus=false
|
||||
maxItems="1"
|
||||
on-init="onInit"
|
||||
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>
|
||||
onopen=(action "onFocus")
|
||||
onclose=(action "onBlur")
|
||||
searchEnabled=false
|
||||
triggerComponent="gh-search-input/trigger"
|
||||
renderInPlace=true
|
||||
loadingMessage="Loading"
|
||||
as |name term|}}
|
||||
{{highlighted-text name.title term}}
|
||||
{{/power-select}}
|
||||
|
@ -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-myth": "0.1.1",
|
||||
"ember-one-way-controls": "0.6.2",
|
||||
"ember-power-select": "0.9.2",
|
||||
"ember-resolver": "2.0.3",
|
||||
"ember-route-action-helper": "0.3.0",
|
||||
"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