Skip to content

Instantly share code, notes, and snippets.

@heygrady
Last active December 21, 2015 22:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heygrady/fc6a34e236bcce2a2e09 to your computer and use it in GitHub Desktop.
Save heygrady/fc6a34e236bcce2a2e09 to your computer and use it in GitHub Desktop.
import Ember from 'ember';
import SearchableSelect from 'ember-searchable-select/components/searchable-select';
import layout from './template';
const menuSelector = '.Searchable-select__options-list-scroll-wrapper';
export default SearchableSelect.extend({
layout,
classNames: ['Searchable-select-infinite'],
teardown: Ember.on('willDestroyElement', function() {
// ensure that scroll event is removed on destroy
this.$(menuSelector).off(`scroll.${this.get('elementId')}`);
}),
_bindMenuScroll() {
if (this.get('autoloadMore')) {
const component = this;
component.$(menuSelector).on(`scroll.${component.elementId}`, function(event) {
Ember.run.debounce(component, '_debouncedMenuScroll', event.target, 50);
});
}
},
_unbindMenuScroll() {
const component = this;
component.$(menuSelector).off(`scroll.${component.elementId}`);
},
_debouncedMenuScroll(target) {
const isLoading = this.get('isLoading');
const isLoadingMore = this.get('isLoadingMore');
const isShowingMenu = this.get('_isShowingMenu');
if (isLoading || isLoadingMore || !isShowingMenu) {
return;
}
const menu = this.$(target);
const more = menu.find('.Searchable-select__more-label');
const height = this.get('_menuHeight');
if (!more.length || !menu.length || !height) {
return;
}
const scrollPosition = more.offset().top - menu.offset().top - height;
const hasMore = this.get('_hasMore');
const shouldSearchMore = hasMore && scrollPosition < 0;
if (shouldSearchMore) {
this.send('searchMore');
}
},
// cache menu height, no need to measure it on every scroll
_menuHeight: Ember.computed('_filteredContent.length', '_isShowingMenu', function() {
if (!this.get('_isShowingMenu')) {
return 0;
}
const menu = this.$(menuSelector);
if (!menu.length) {
return 0;
}
return menu.height();
}),
resultCountMessage: 'result',
loadingMoreMessage: 'Searching more results...',
moreMessage: 'More results available',
noMoreMessage: 'No more results',
autoloadMore: true,
isLoadingMore: false,
_isLoading: Ember.computed('isLoading', 'isLoadingMore', function() {
const isLoading = this.get('isLoading');
const isLoadingMore = this.get('isLoadingMore');
return isLoading && !isLoadingMore;
}),
showResultCount: true,
_isShowingResultCount: Ember.computed.and('showResultCount', 'meta.total', '_searchText'),
showSearchClear: true,
_isShowingSearchClear: Ember.computed.and('showSearchClear', '_searchText'),
meta: null,
_resultCount: Ember.computed('meta.total', function() {
return parseInt(this.get('meta.total'), 10);
}),
_currentPage: Ember.computed('meta.page', function() {
return parseInt(this.get('meta.page'), 10);
}),
_totalPages: Ember.computed('meta.pages', function() {
return parseInt(this.get('meta.pages'), 10);
}),
_hasMeta: Ember.computed.bool('meta'),
_hasMore: Ember.computed('_currentPage', '_totalPages', function() {
const currentPage = this.get('_currentPage') || 0;
const totalPages = this.get('_totalPages') || 0;
return currentPage < totalPages;
}),
_hasNoMore: Ember.computed.not('_hasMore'),
'on-search-more': Ember.K,
actions: {
// override for scrolling wrapper to top
updateSearch(text) {
this.set('_searchText', text);
this['on-search'].call(this, text);
Ember.run.scheduleOnce('afterRender', this, function() {
this.$(menuSelector).scrollTop(0);
});
},
clearSearch() {
this.set('_searchText', '');
this.$('.Searchable-select__input').val('').focus().keyup();
},
searchMore() {
const searchText = this.get('_searchText');
const nextPage = this.get('_currentPage') + 1;
Ember.run(this, 'on-search-more', searchText, nextPage);
},
// override for binding scroll
showMenu() {
this.set('_isShowingMenu', true);
Ember.run.scheduleOnce('afterRender', this, function() {
this.$('.Searchable-select__input').focus();
});
Ember.run.next(this, function() {
this._bindOutsideClicks();
this._bindMenuScroll();
});
},
// override for unbinding scroll
hideMenu() {
this._unbindMenuScroll();
this._unbindOutsideClicks();
this.set('_isShowingMenu', false);
this.set('_searchText', '');
this.$('.Searchable-select__label').focus();
},
clickResultCountLabel() {
this.$('.Searchable-select__input').focus();
},
}
});
.Searchable-select-infinite {
.Searchable-select__options-list-scroll-wrapper {
max-height: 300px;
overflow: auto;
}
.Searchable-select__options-list {
overflow: visible;
max-height: auto;
}
.Searchable-select__options-list__has-more-item {
.Searchable-select__loader {
float:left;
margin-left: -20px;
}
}
.Searchable-select__more-label {
cursor: pointer;
}
.Searchable-select__clear-search {
border: none;
background: none;
color: #ccc;
position: absolute;
right: 20px;
top: 17px;
}
.Searchable-select__result-count-label {
color: #ccc;
position: absolute;
right: 20px;
top: 20px;
&.Searchable-select__result-count-label__with-search-clear {
right: 50px;
}
}
}
<a href="#"
class="Searchable-select__label
{{if _isShowingMenu 'Searchable-select__label--menu-open'}}
{{if multiple 'Searchable-select__label--multiple'}}
{{if _hasMultipleSelection 'Searchable-select__label--multiple-selected'}}"
{{action "toggleMenu"}}>
{{#if _hasSingleSelection}}
{{searchable-select-get _selected optionLabelKey}}
{{/if}}
{{#if _hasMultipleSelection}}
{{#each _selected as |selectedOption|}}
<span class="Searchable-select__selected-pill">
{{searchable-select-get selectedOption optionLabelKey}}
<span
class="Searchable-select__selected-pill-clear"
{{action "removeOption" selectedOption bubbles=false}}>
&times;
</span>
</span>
{{/each}}
{{/if}}
{{#unless _hasSelection}}
{{prompt}}
{{/unless}}
</a>
{{#if _isShowingMenu}}
<div class="Searchable-select__menu">
<div class="Searchable-select__search" {{action "noop" bubbles=false}}>
{{#if _isShowingResultCount}}
<span class="Searchable-select__result-count-label {{if _isShowingSearchClear "Searchable-select__result-count-label__with-search-clear"}}" {{action "clickResultCountLabel"}}>{{pluralize _resultCount resultCountMessage}}</span>
{{/if}}
{{#if _isShowingSearchClear}}
<button class="Searchable-select__clear-search" {{action "clearSearch" bubbles=false}}>&times;</button>
{{/if}}
{{input
class="Searchable-select__input"
type="text"
placeholder=searchPrompt
key-up="updateSearch"
tabindex=-1
bubbles=false}}
</div>
{{#if _isShowingClear}}
<div class="Searchable-select__clear"
tabindex=-1
data-enter-key-action="clear"
{{action "clear" bubbles=false}}>
<span class="Searchable-select__icon Searchable-select__icon--clear">
&times;
</span>
{{clearLabel}}
</div>
{{/if}}
{{#if _isShowingAddNew}}
<div class="Searchable-select__add-new"
tabindex=-1
data-enter-key-action="addNew"
{{action "addNew" bubbles=false}}>
{{addLabel}} <em>{{_searchText}}</em>
</div>
{{/if}}
{{#if _isShowingNoResultsMessage}}
<div class="Searchable-select__info">
{{noResultsMessage}}
</div>
{{/if}}
{{#if _isLoading}}
<div class="Searchable-select__info">
<!-- Loading SVG by Sam Herbert (@sherb), More @ http://goo.gl/7AJzbL -->
<svg
width="12" height="12"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg"
stroke="#9a9a9a"
class="Searchable-select__loader">
<g fill="none" fill-rule="evenod d">
<g transform="translate(2 2)" stroke-width="4">
<circle
stroke-opacity=".6"
cx="18" cy="18" r="18"
class="Searchable-select__loader-circle"/>
<path
d="M36 18c0-9.94-8.06-18-18-18"
class="Searchable-select__loader-spinner">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.8s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>
<span class="Searchable-select__loader-text">
{{loadingMessage}}
</span>
</div>
{{/if}}
<div class="Searchable-select__options-list-scroll-wrapper">
{{#if _hasResults}}
<ul class="Searchable-select__options-list">
{{#each _filteredContent as |option|}}
{{searchable-select-option
option=option
selected=_selected
searchText=_searchText
optionLabelKey=optionLabelKey
optionDisabledKey=optionDisabledKey
select-item=(action "selectItem")}}
{{/each}}
{{#if _hasMeta}}
{{#if _hasMore}}
<li class="Searchable-select__options-list__has-more-item">
{{#if isLoadingMore}}
<svg
width="12" height="12"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg"
stroke="#9a9a9a"
class="Searchable-select__loader">
<g fill="none" fill-rule="evenod d">
<g transform="translate(2 2)" stroke-width="4">
<circle
stroke-opacity=".6"
cx="18" cy="18" r="18"
class="Searchable-select__loader-circle"/>
<path
d="M36 18c0-9.94-8.06-18-18-18"
class="Searchable-select__loader-spinner">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.8s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>
<span class="Searchable-select__loading-more-label text-muted">{{loadingMoreMessage}}</span>
{{else}}
<span class="Searchable-select__more-label text-muted" {{action "searchMore" bubbles=false}}>{{moreMessage}}</span>
{{/if}}
</li>
{{/if}}
{{#if _hasNoMore}}
<li><span class="Searchable-select__no-more-label text-muted">{{noMoreMessage}}</span></li>
{{/if}}
{{/if}}
</ul>
{{/if}}
</div>
</div>
{{/if}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment