diff --git a/core/client/app/components/gh-search-input.js b/core/client/app/components/gh-search-input.js index 7720cfe6c7..93372cd32f 100644 --- a/core/client/app/components/gh-search-input.js +++ b/core/client/app/components/gh-search-input.js @@ -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 = '
'; - - 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); + }); } } diff --git a/core/client/app/components/gh-search-input/trigger.js b/core/client/app/components/gh-search-input/trigger.js new file mode 100644 index 0000000000..e004203b97 --- /dev/null +++ b/core/client/app/components/gh-search-input/trigger.js @@ -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(); + } + } + } +}); diff --git a/core/client/app/helpers/highlighted-text.js b/core/client/app/helpers/highlighted-text.js new file mode 100644 index 0000000000..d3562c7356 --- /dev/null +++ b/core/client/app/helpers/highlighted-text.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +export function highlightedText([text, termToHighlight]) { + return Ember.String.htmlSafe(text.replace(new RegExp(termToHighlight, 'ig'), '$&')); +} + +export default Ember.Helper.helper(highlightedText); diff --git a/core/client/app/styles/app.css b/core/client/app/styles/app.css index 68c251c4b0..0bde0c40bf 100644 --- a/core/client/app/styles/app.css +++ b/core/client/app/styles/app.css @@ -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 diff --git a/core/client/app/styles/components/power-select.css b/core/client/app/styles/components/power-select.css new file mode 100644 index 0000000000..1724c13fde --- /dev/null +++ b/core/client/app/styles/components/power-select.css @@ -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); +} diff --git a/core/client/app/templates/components/gh-search-input.hbs b/core/client/app/templates/components/gh-search-input.hbs index 07eb356eaa..04a0fef6cb 100644 --- a/core/client/app/templates/components/gh-search-input.hbs +++ b/core/client/app/templates/components/gh-search-input.hbs @@ -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"}} - + 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}} diff --git a/core/client/app/templates/components/gh-search-input/trigger.hbs b/core/client/app/templates/components/gh-search-input/trigger.hbs new file mode 100644 index 0000000000..f9f613ab1c --- /dev/null +++ b/core/client/app/templates/components/gh-search-input/trigger.hbs @@ -0,0 +1,12 @@ +