Merge pull request #6492 from acburdine/ember-power-select

Convert gh-search-input to ember-power-select component
This commit is contained in:
Kevin Ansfield 2016-04-19 17:40:27 +01:00
commit 7fb441e640
11 changed files with 292 additions and 95 deletions

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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