diff --git a/addon/components/paper-autocomplete-highlight.js b/addon/components/paper-autocomplete-highlight.js new file mode 100644 index 000000000..eb595e490 --- /dev/null +++ b/addon/components/paper-autocomplete-highlight.js @@ -0,0 +1,35 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'span', + flags: '', + + highlight: Ember.computed('searchText', 'label', 'flags', function () { + var unsafeText = Ember.Handlebars.Utils.escapeExpression(this.get('label')), + text = unsafeText, + flags = this.get('flags'), + regex = this.getRegExp(this.get('searchText'), flags), + html = text.replace(regex, '$&'); + return new Ember.Handlebars.SafeString(html); + }), + + sanitize (term) { + if (!term) { + return term; + } + return term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&'); + }, + + getRegExp (text, flags) { + var str = ''; + if (flags.indexOf('^') >= 1) { + str += '^'; + } + str += text; + if (flags.indexOf('$') >= 1) { + str += '$'; + } + return new RegExp(this.sanitize(str), flags.replace(/[\$\^]/g, '')); + } + +}); diff --git a/addon/components/paper-autocomplete-item.js b/addon/components/paper-autocomplete-item.js new file mode 100644 index 000000000..d698d9cc6 --- /dev/null +++ b/addon/components/paper-autocomplete-item.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'li', + attributeBindings: ['tabindex', 'role'], + classNameBindings: ['isSelected:selected'], + tabindex: 0, + role: 'option', + + + label: Ember.computed('item',function () { + return this.lookupLabelOfItem(this.get('item')); + }), + + isSelected: Ember.computed('selectedIndex', function () { + return this.get('selectedIndex') === this.get('index'); + }), + + lookupLabelOfItem (model) { + var value; + if (this.get('lookupKey')) { + value = model[this.get('lookupKey')]; + } else { + value = model; + } + return value; + }, + + + + click () { + this.sendAction('pick', this.get('item')); + } +}); diff --git a/addon/components/paper-autocomplete-list.js b/addon/components/paper-autocomplete-list.js new file mode 100644 index 000000000..db43318e2 --- /dev/null +++ b/addon/components/paper-autocomplete-list.js @@ -0,0 +1,124 @@ +import Ember from 'ember'; + +var ITEM_HEIGHT = 41, + MAX_HEIGHT = 5.5 * ITEM_HEIGHT, + MENU_PADDING = 8; + +export default Ember.Component.extend({ + util: Ember.inject.service(), + + tagName: 'ul', + classNames: ['md-default-theme', 'md-autocomplete-suggestions', 'md-whiteframe-z1'], + attributeNameBindings: ['role'], + role: 'presentation', + stickToElement: null, + + hidden: true, + + + isVisible: Ember.computed.not('hidden'), + + mouseEnter () { + this.sendAction('mouse-enter'); + }, + mouseLeave () { + this.sendAction('mouse-leave'); + }, + mouseUp () { + this.sendAction('mouse-up'); + }, + + + hideSuggestionObserver: Ember.observer('hidden', function () { + if (this.get('hidden') === true) { + this.get('util').enableScrolling(); + } else { + this.get('util').disableScrollAround(this.$()); + this.positionDropdown(); + } + }), + + + positionDropdown () { + var hrect = Ember.$('#' + this.get('wrapToElementId'))[0].getBoundingClientRect(), + vrect = hrect, + root = document.body.getBoundingClientRect(), + top = vrect.bottom - root.top, + bot = root.bottom - vrect.top, + left = hrect.left - root.left, + width = hrect.width, + styles = { + left: left + 'px', + minWidth: width + 'px', + maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px' + }, + ul = this.$(); + + if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) { + styles.top = 'auto'; + styles.bottom = bot + 'px'; + styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px'; + } else { + styles.top = top + 'px'; + styles.bottom = 'auto'; + styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom - hrect.bottom - MENU_PADDING) + 'px'; + } + ul.css(styles); + correctHorizontalAlignment(); + + /** + * Makes sure that the menu doesn't go off of the screen on either side. + */ + function correctHorizontalAlignment () { + var dropdown = ul[0].getBoundingClientRect(), + styles = {}; + if (dropdown.right > root.right - MENU_PADDING) { + styles.left = (hrect.right - dropdown.width) + 'px'; + } + ul.css(styles); + } + }, + + + observeIndex: Ember.observer('selectedIndex', function () { + var suggestions = this.get('suggestions'); + if (!suggestions[this.get('selectedIndex')]) { + return; + } + + var ul = this.$(), + li = ul.find('li:eq('+this.get('selectedIndex')+')')[0], + top = li.offsetTop, + bot = top + li.offsetHeight, + hgt = ul[0].clientHeight; + if (top < ul[0].scrollTop) { + ul[0].scrollTop = top; + } else if (bot > ul[0].scrollTop + hgt) { + ul[0].scrollTop = bot - hgt; + } + }), + + resizeWindowEvent ()  { + this.positionDropdown(); + }, + + + didInsertElement () { + var _self = this; + var ul = this.$().detach(); + Ember.$('body').append(ul); + + this.set('___resizeFunction', function () { + _self.positionDropdown(); + }); + Ember.$(window).on('resize', this.get('___resizeFunction')); + }, + willDestroyElement () { + Ember.$(window).off('resize',this.get('___resizeFunction')); + this.get('util').enableScrolling(); + } + + + + +}); diff --git a/addon/components/paper-autocomplete.js b/addon/components/paper-autocomplete.js new file mode 100644 index 000000000..837562d16 --- /dev/null +++ b/addon/components/paper-autocomplete.js @@ -0,0 +1,313 @@ +import Ember from 'ember'; +import HasBlockMixin from '../mixins/hasblock-mixin'; + +function isString (item) { + return typeof item === 'string' || item instanceof String; +} + +/** + * @name paper-autocomplete + * + * @description + * Provides material design autocomplete. + * + * + * ## Dependencies + * - paper-autocomplete-item + * - paper-autocomplete-list + * - paper-input + * - paper-button + * - input + */ +export default Ember.Component.extend(HasBlockMixin, { + util: Ember.inject.service(), + constants: Ember.inject.service(), + + tagName: 'md-autocomplete', + classNameBindings: ['notFloating:md-default-theme'], + attributeBindings: ['floating:md-floating-label', 'showDisabled:disabled'], + + + // Internal + suggestions: Ember.A([]), + loading: false, + hidden: true, + selectedIndex: null, + messages: [], + noBlur: false, + hasFocus: false, + + // Public + disabled: null, + required: null, + lookupKey: null, + placeholder: '', + delay: 0, + minLength: 1, + allowNonExisting: false, + noCache: false, + notFoundMessage: 'No matches found for "%@".', + + init:function(){ + this._super(...arguments); + this.set('itemCache', {}); + if (this.get('model')) { + this.set('searchText', this.lookupLabelOfItem(this.get('model'))); + } else if (typeof this.get('searchText') === 'undefined') { + this.set('searchText', ''); + } + }, + + + notFloating: Ember.computed.not('floating'), + notHidden: Ember.computed.not('hidden'), + + + + notFoundMsg: Ember.computed('searchText', 'notFoundMessage', function () { + return Ember.String.fmt(this.get('notFoundMessage'), [this.get('searchText')]); + }), + + /** + * Needed because of false = disabled="false". + */ + showDisabled: Ember.computed('disabled', function () { + if (this.get('disabled')) { + return true; + } + }), + + showLoadingBar: Ember.computed('loading', 'allowNonExisting', 'debouncingState', function () { + return !this.get('loading') && !this.get('allowNonExisting') && !this.get('debouncingState'); + }), + + enableClearButton: Ember.computed('searchText', 'disabled', function () { + return this.get('searchText') && !this.get('disabled'); + }), + + wrapperClasses: Ember.computed('notFloating', 'notHidden', function () { + var classes = ''; + if (this.get('notFloating')) { + classes += ' md-whiteframe-z1'; + } + if (this.get('notHidden')) { + classes += ' md-menu-showing'; + } + return classes; + }), + + + observeSearchText: Ember.observer('searchText', function () { + if (this.get('searchText') === this.get('previousSearchText')) { + return; + } + if (!this.get('allowNonExisting')) { + this.set('model', null); + } else { + this.set('model', this.get('searchText')); + } + + var wait = parseInt(this.get('delay'), 10) || 0; + this.set('debouncingState', true); + Ember.run.debounce(this, this.handleSearchText, wait); + this.set('previousSearchText', this.get('searchText')); + }), + + + + shouldHide () { + if (!this.isMinLengthMet()) { + return true; + } + return false; + }, + + isMinLengthMet () { + return this.get('searchText').length >= this.get('minLength'); + }, + + + + handleSearchText () { + this.set('selectedIndex', this.get('defaultIndex')); + this.set('debouncingState', false); + if (!this.isMinLengthMet()) { + this.send('clear'); + } else { + this.handleQuery(); + } + }, + + handleQuery () { + var suggestions, + _self = this, + source = this.get('source'), + lookupKey = this.get('lookupKey'), + text = this.get('searchText').toLowerCase(), + cached = this.itemsFromCache(text); + if (cached) { + suggestions = cached; + this.set('selectedIndex', _self.get('defaultIndex')); + this.set('suggestions', suggestions); + this.set('hidden', this.shouldHide()); + } else if (typeof source !== 'function') { + if (text) { + suggestions = source.filter(function (item) { + var search; + if (isString(item)) { + search = item; + } else { + if (lookupKey === null) { + console.error("You have not defined 'lookupKey' on paper-autocomplete, when source contained " + + "items that are not of type String. To fix this error provide a " + + "lookupKey='key to lookup from source item'."); + } + search = item[lookupKey]; + } + search = search.toLowerCase(); + return search.indexOf(text) === 0; + }); + } else { + suggestions = source; + } + this.set('selectedIndex', _self.get('defaultIndex')); + this.set('suggestions', suggestions); + this.set('hidden', this.shouldHide()); + } else { + this.set('loading', true); + + var promise = source.call(this, text); + promise.then(function (items) { + _self.get('itemCache')[text] = items; + if (_self.get('lastPromise') === promise) { + suggestions = items; + _self.set('suggestions', suggestions); + _self.set('hidden', _self.shouldHide()); + _self.set('selectedIndex', _self.get('defaultIndex')); // Reset index of list position. + _self.set('loading', false); + } + }); + this.set('lastPromise', promise); + } + }, + + itemsFromCache (text) { + if (this.get('noCache') === true) { + return; + } + if (this.get('itemCache')[text]) { + return this.get('itemCache')[text]; + } + return null; + }, + + + + lookupLabelOfItem (model) { + var value; + if (this.get('lookupKey')) { + value = model[this.get('lookupKey')]; + } else { + value = model; + } + return value; + }, + + autocompleteWrapperId: Ember.computed('elementId', function () { + return 'autocomplete-wrapper-' + this.get('elementId'); + }), + + actions: { + clear: function () { + this.set('searchText', ''); + this.set('selectedIndex', -1); + this.set('loading', false); + this.set('model', null); + this.set('hidden', true); + }, + + pickModel: function (model) { + this.set('model', model); + var value = this.lookupLabelOfItem(model); + // First set previousSearchText then searchText ( do not trigger observer only update value! ). + this.set('previousSearchText', value); + this.set('searchText', value); + this.set('hidden', true); + }, + + inputFocusOut () { + this.set('hasFocus', false); + if (this.get('noBlur') === false) { + this.set('hidden', true); + } + }, + + inputFocusIn () { + this.set('hasFocus', true); + this.set('hidden', this.shouldHide()); + if (!this.get('hidden')) { + this.handleSearchText(); + } + }, + + inputKeyDown (value, event) { + switch (event.keyCode) { + case this.get('constants').KEYCODE.DOWN_ARROW: + if (this.get('loading')) { + return; + } + event.stopPropagation(); + event.preventDefault(); + this.set('selectedIndex', Math.min(this.get('selectedIndex') + 1, this.get('suggestions').length - 1)); + break; + case this.get('constants').KEYCODE.UP_ARROW: + if (this.get('loading')) { + return; + } + event.stopPropagation(); + event.preventDefault(); + this.set('selectedIndex', this.get('selectedIndex') < 0 ? this.get('suggestions').length - 1 : Math.max(0, this.get('selectedIndex') - 1)); + break; + case this.get('constants').KEYCODE.TAB: + case this.get('constants').KEYCODE.ENTER: + if (this.get('hidden') || this.get('loading') || this.get('selectedIndex') < 0 || this.get('suggestions').length < 1) { + return; + } + event.stopPropagation(); + event.preventDefault(); + this.send('pickModel', this.get('suggestions')[this.get('selectedIndex')]); + break; + case this.get('constants').KEYCODE.ESCAPE: + event.stopPropagation(); + event.preventDefault(); + this.set('suggestions', Ember.A([])); + this.set('hidden', true); + this.set('selectedIndex', this.get('defaultIndex')); + break; + default: + break; + } + }, + listMouseEnter () { + this.set('noBlur', true); + }, + listMouseLeave () { + this.set('noBlur', false); + if (this.get('hasFocus') === false) { + this.set('hidden', true); + } + }, + listMouseUp () { + this.$().find('input').focus(); + } + }, + + /** + * Returns the default index based on whether or not autoselect is enabled. + * @returns {number} + */ + defaultIndex: Ember.computed('autoselect', function () { + return this.get('autoselect') ? 0 : -1; + }) + +}); diff --git a/addon/components/paper-button.js b/addon/components/paper-button.js index 92e558b40..b07b8fadd 100644 --- a/addon/components/paper-button.js +++ b/addon/components/paper-button.js @@ -7,8 +7,8 @@ import ColorMixin from 'ember-paper/mixins/color-mixin'; export default BaseFocusable.extend(RippleMixin, ProxiableMixin, ColorMixin, { attributeBindings: ['target', 'action'], tagName: 'button', - classNames: ['md-button','md-default-theme'], - classNameBindings: ['raised:md-raised', 'icon-button:md-icon-button'], + themed: true, + classNameBindings: ['raised:md-raised', 'icon-button:md-icon-button', 'themed:md-default-theme', 'themed:md-button'], /* RippleMixin overrides */ isIconButton: Ember.computed(function() { diff --git a/addon/components/paper-input.js b/addon/components/paper-input.js index 8ab3280f5..3c56e10c4 100644 --- a/addon/components/paper-input.js +++ b/addon/components/paper-input.js @@ -1,8 +1,9 @@ import Ember from 'ember'; import BaseFocusable from './base-focusable'; import ColorMixin from 'ember-paper/mixins/color-mixin'; +import FlexMixin from 'ember-paper/mixins/flex-mixin'; -export default BaseFocusable.extend(ColorMixin, { +export default BaseFocusable.extend(ColorMixin, FlexMixin, { tagName: 'md-input-container', classNames: ['md-default-theme'], classNameBindings: ['hasValue:md-input-has-value', 'focus:md-input-focused', 'isInvalid:md-input-invalid'], @@ -63,11 +64,18 @@ export default BaseFocusable.extend(ColorMixin, { }, actions: { - focusIn() { + focusIn(value) { + // We resend action so other components can take use of the actions also ( if they want ). + // Actions must be sent before focusing. + this.sendAction('focus-in', value); this.set('focus',true); }, - focusOut() { + focusOut(value) { + this.sendAction('focus-out', value); this.set('focus',false); + }, + keyDown(value, event) { + this.sendAction('key-down', value, event); } } }); diff --git a/app/components/paper-autocomplete-highlight.js b/app/components/paper-autocomplete-highlight.js new file mode 100644 index 000000000..0de8d8183 --- /dev/null +++ b/app/components/paper-autocomplete-highlight.js @@ -0,0 +1 @@ +export { default } from 'ember-paper/components/paper-autocomplete-highlight'; \ No newline at end of file diff --git a/app/components/paper-autocomplete-item.js b/app/components/paper-autocomplete-item.js new file mode 100644 index 000000000..7641a4be4 --- /dev/null +++ b/app/components/paper-autocomplete-item.js @@ -0,0 +1 @@ +export { default } from 'ember-paper/components/paper-autocomplete-item'; diff --git a/app/components/paper-autocomplete-list.js b/app/components/paper-autocomplete-list.js new file mode 100644 index 000000000..5ff0cfea6 --- /dev/null +++ b/app/components/paper-autocomplete-list.js @@ -0,0 +1 @@ +export { default } from 'ember-paper/components/paper-autocomplete-list'; \ No newline at end of file diff --git a/app/components/paper-autocomplete.js b/app/components/paper-autocomplete.js new file mode 100644 index 000000000..b88975292 --- /dev/null +++ b/app/components/paper-autocomplete.js @@ -0,0 +1 @@ +export { default } from 'ember-paper/components/paper-autocomplete'; \ No newline at end of file diff --git a/app/services/util.js b/app/services/util.js new file mode 100644 index 000000000..4206687fb --- /dev/null +++ b/app/services/util.js @@ -0,0 +1,108 @@ +import Ember from 'ember'; + +/* global jQuery */ + +var Util = Ember.Service.extend({ + + // Disables scroll around the passed element. + disableScrollAround: function (element) { + var $mdUtil = this, + $document = jQuery(window.document); + + $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0; + ++$mdUtil.disableScrollAround._count; + if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling; + var body = $document[0].body, + restoreBody = disableBodyScroll(), + restoreElement = disableElementScroll(); + + return $mdUtil.disableScrollAround._enableScrolling = function () { + if (!--$mdUtil.disableScrollAround._count) { + restoreBody(); + restoreElement(); + delete $mdUtil.disableScrollAround._enableScrolling; + } + }; + + // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events + function disableElementScroll() { + var zIndex = 50; + var scrollMask = jQuery( + '
' + + '
' + + '
'); + body.appendChild(scrollMask[0]); + + scrollMask.on('wheel', preventDefault); + scrollMask.on('touchmove', preventDefault); + $document.on('keydown', disableKeyNav); + + return function restoreScroll() { + scrollMask.off('wheel'); + scrollMask.off('touchmove'); + scrollMask[0].parentNode.removeChild(scrollMask[0]); + $document.off('keydown', disableKeyNav); + delete $mdUtil.disableScrollAround._enableScrolling; + }; + + // Prevent keypresses from elements inside the body + // used to stop the keypresses that could cause the page to scroll + // (arrow keys, spacebar, tab, etc). + function disableKeyNav(e) { + //-- temporarily removed this logic, will possibly re-add at a later date + return; + if (!element[0].contains(e.target)) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + } + + function preventDefault(e) { + e.preventDefault(); + } + } + + // Converts the body to a position fixed block and translate it to the proper scroll + // position + function disableBodyScroll() { + var htmlNode = body.parentNode; + var restoreHtmlStyle = htmlNode.getAttribute('style') || ''; + var restoreBodyStyle = body.getAttribute('style') || ''; + var scrollOffset = body.scrollTop + body.parentElement.scrollTop; + var clientWidth = body.clientWidth; + + if (body.scrollHeight > body.clientHeight) { + applyStyles(body, { + position: 'fixed', + width: '100%', + top: -scrollOffset + 'px' + }); + + applyStyles(htmlNode, { + overflowY: 'scroll' + }); + } + + + if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); + + return function restoreScroll() { + body.setAttribute('style', restoreBodyStyle); + htmlNode.setAttribute('style', restoreHtmlStyle); + body.scrollTop = scrollOffset; + }; + } + + function applyStyles(el, styles) { + for (var key in styles) { + el.style[key] = styles[key]; + } + } + }, + enableScrolling: function () { + var method = this.disableScrollAround._enableScrolling; + method && method(); + } +}); + +export default Util; diff --git a/app/styles/ember-paper.scss b/app/styles/ember-paper.scss index 6ee9f607c..3e7d97c18 100644 --- a/app/styles/ember-paper.scss +++ b/app/styles/ember-paper.scss @@ -75,6 +75,7 @@ @import 'paper-icon'; @import 'paper-slider'; @import 'paper-subheader'; +@import 'paper-autocomplete'; @import 'paper-progress-linear'; @import 'paper-progress-circular'; diff --git a/app/styles/paper-autocomplete.scss b/app/styles/paper-autocomplete.scss new file mode 100644 index 000000000..334590c8b --- /dev/null +++ b/app/styles/paper-autocomplete.scss @@ -0,0 +1,253 @@ +$autocomplete-option-height: 48px; +$input-container-padding: 2px !default; +$input-error-height: 24px !default; + +@keyframes md-autocomplete-list-out { + 0% { + animation-timing-function: linear; + } + 50% { + opacity: 0; + height: 40px; + animation-timing-function: ease-in; + } + 100% { + height: 0; + opacity: 0; + } +} +@keyframes md-autocomplete-list-in { + 0% { + opacity: 0; + height: 0; + animation-timing-function: ease-out; + } + 50% { + opacity: 0; + height: 40px; + } + 100% { + opacity: 1; + height: 40px; + } +} +md-autocomplete { + border-radius: 2px; + display: block; + height: 40px; + position: relative; + overflow: visible; + min-width: 190px; + &[disabled] { + input { + cursor: not-allowed; + } + } + &[md-floating-label] { + padding-bottom: $input-container-padding + $input-error-height; + border-radius: 0; + background: transparent; + height: auto; + md-input-container { + padding-bottom: 0; + } + md-autocomplete-wrap { + height: auto; + } + button { + position: absolute; + top: auto; + bottom: 0; + right: 0; + width: 30px; + height: 30px; + } + } + md-autocomplete-wrap { + display: block; + position: relative; + overflow: visible; + height: 40px; + &.md-menu-showing { + z-index: $z-index-backdrop + 1; + } + md-progress-linear[md-mode=indeterminate] { + position: absolute; + bottom: 0; left: 0; width: 100%; + height: 3px; + transition: none; + + .md-container { + transition: none; + top: auto; + height: 3px; + } + &.ng-enter { + transition: opacity 0.15s linear; + &.ng-enter-active { + opacity: 1; + } + } + &.ng-leave { + transition: opacity 0.15s linear; + &.ng-leave-active { + opacity: 0; + } + } + } + } + input:not(.md-input) { + width: 100%; + box-sizing: border-box; + border: none; + box-shadow: none; + padding: 0 15px; + font-size: 14px; + line-height: 40px; + height: 40px; + outline: none; + background: transparent; + &::-ms-clear { + display: none; + } + } + button { + position: relative; + line-height: 20px; + text-align: center; + width: 30px; + height: 30px; + cursor: pointer; + border: none; + border-radius: 50%; + padding: 0; + font-size: 12px; + background: transparent; + margin: auto 5px; + &:after { + content: ''; + position: absolute; + top: -6px; + right: -6px; + bottom: -6px; + left: -6px; + border-radius: 50%; + transform: scale(0); + opacity: 0; + transition: $swift-ease-out; + } + &:focus { + outline: none; + + &:after { + transform: scale(1); + opacity: 1; + } + } + md-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0) scale(0.9); + path { + stroke-width: 0; + } + } + &.ng-enter { + transform: scale(0); + transition: transform 0.15s ease-out; + &.ng-enter-active { + transform: scale(1); + } + } + &.ng-leave { + transition: transform 0.15s ease-out; + &.ng-leave-active { + transform: scale(0); + } + } + } + @media screen and (-ms-high-contrast: active) { + $border-color: #fff; + + input { + border: 1px solid $border-color; + } + li:focus { + color: #fff; + } + } +} +.md-autocomplete-suggestions { + position: absolute; + margin: 0; + list-style: none; + padding: 0; + overflow: auto; + max-height: 41px * 5.5; + z-index: $z-index-tooltip; + li { + cursor: pointer; + font-size: 14px; + overflow: hidden; + padding: 0 15px; + line-height: $autocomplete-option-height; + height: $autocomplete-option-height; + transition: background 0.15s linear; + margin: 0; + white-space: nowrap; + text-overflow: ellipsis; + &.ng-enter, + &.ng-hide-remove { + transition: none; + animation: md-autocomplete-list-in 0.2s; + } + &.ng-leave, + &.ng-hide-add { + transition: none; + animation: md-autocomplete-list-out 0.2s; + } + + &:focus { + outline: none; + } + + } +} +@media screen and (-ms-high-contrast: active) { + md-autocomplete, + .md-autocomplete-suggestions { + border: 1px solid #fff; + } +} + +//THEME +md-autocomplete.md-#{$theme-name}-theme { + background: color($background, '50'); + &[disabled] { + background: color($background, '100'); + } + button { + md-icon { + path { + fill: color($background, '600'); + } + } + &:after { + background: color($background, '600'); + } + } +} +.md-autocomplete-suggestions.md-#{$theme-name}-theme { + background: color($background, '50'); + li { + color: color($background, '900'); + .highlight { + color: color($background, '600'); + } + &:hover, + &.selected { + background: color($background, '200'); + } + } +} diff --git a/app/templates/components/paper-autocomplete-highlight.hbs b/app/templates/components/paper-autocomplete-highlight.hbs new file mode 100644 index 000000000..da402e7ac --- /dev/null +++ b/app/templates/components/paper-autocomplete-highlight.hbs @@ -0,0 +1 @@ +{{highlight}} \ No newline at end of file diff --git a/app/templates/components/paper-autocomplete-item.hbs b/app/templates/components/paper-autocomplete-item.hbs new file mode 100644 index 000000000..cb11eb126 --- /dev/null +++ b/app/templates/components/paper-autocomplete-item.hbs @@ -0,0 +1 @@ +{{yield label}} \ No newline at end of file diff --git a/app/templates/components/paper-autocomplete.hbs b/app/templates/components/paper-autocomplete.hbs new file mode 100644 index 000000000..ebec52cc1 --- /dev/null +++ b/app/templates/components/paper-autocomplete.hbs @@ -0,0 +1,70 @@ + + {{#if floating}} + {{paper-input + type="search" + label=placeholder + focus-in="inputFocusIn" + focus-out="inputFocusOut" + key-down="inputKeyDown" + value=searchText + disabled=disabled + required=required + flex=true + }} + {{else}} + {{input type="search" + flex=true + placeholder=placeholder + value=searchText + focus-in="inputFocusIn" + focus-out="inputFocusOut" + key-down="inputKeyDown" + autocomplete="off" + disabled=disabled + required=required + aria-haspopup=true + aria-autocomplete="list" + aria-activedescendant="" + aria-expanded=notHidden + }} + + + {{#if enableClearButton}} + {{#paper-button icon-button=true themed=false action="clear"}}{{paper-icon icon="close"}}{{/paper-button}} + {{/if}} + + {{/if}} + + {{#if loading}} + {{paper-progress-linear}} + {{/if}} + + + {{#paper-autocomplete-list suggestions=suggestions hidden=hidden selectedIndex=selectedIndex wrapToElementId=autocompleteWrapperId mouse-up="listMouseUp" mouse-leave="listMouseLeave" mouse-enter="listMouseEnter"}} + {{#each suggestions as |item index|}} + {{#paper-autocomplete-item lookupKey=lookupKey item=item selectedIndex=selectedIndex index=index pick="pickModel" as |label|}} + {{#if hasBlock}} + {{yield item index}} + {{else}} + {{paper-autocomplete-highlight searchText=searchText label=label}} + {{/if}} + {{/paper-autocomplete-item}} + {{else}} + {{#if showLoadingBar}} + {{#if hasBlock}} +
  • {{yield to="inverse"}}
  • + {{else}} +
  • {{notFoundMsg}}
  • + {{/if}} + {{/if}} + {{/each}} + {{/paper-autocomplete-list}} +
    + + + {{#each messages as |message index|}} + {{#if message}} +

    {{message}}

    + {{/if}} + {{/each}} +
    \ No newline at end of file diff --git a/app/templates/components/paper-input.hbs b/app/templates/components/paper-input.hbs index 26dea537c..7e928a59b 100644 --- a/app/templates/components/paper-input.hbs +++ b/app/templates/components/paper-input.hbs @@ -1,9 +1,9 @@ {{#if textarea}} - {{textarea class="md-input" id=inputElementId value=value focus-in="focusIn" focus-out="focusOut" disabled=disabled required=required}} + {{textarea class="md-input" id=inputElementId value=value focus-in="focusIn" key-down="keyDown" focus-out="focusOut" disabled=disabled required=required}} {{else}} - {{input class="md-input" id=inputElementId type=type value=value focus-in="focusIn" focus-out="focusOut" disabled=disabled required=required}} + {{input class="md-input" id=inputElementId type=type value=value focus-in="focusIn" key-down="keyDown" focus-out="focusOut" disabled=disabled required=required}} {{/if}}
    diff --git a/tests/dummy/app/controllers/autocomplete.js b/tests/dummy/app/controllers/autocomplete.js new file mode 100644 index 000000000..9b7ef31aa --- /dev/null +++ b/tests/dummy/app/controllers/autocomplete.js @@ -0,0 +1,294 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + + + myModel: {name: 'United States', code: 'US'}, + + /** + * This is a sample of data loaded dynamically. + * Here we use a fake promise, but this can come directly from the ember-data filter API or e.g. jQuery $.getJSON + * + * @param searchText Search text from the autocomplete API. Lower cased version. + * @returns {Promise} + */ + dataFromPromise: function (searchText) { + var _self = this; + + var SOME_DATA_FROM_API = Ember.A([ + {name: 'Computer', id: 1}, + {name: 'Ham', id: 2}, + {name: 'Unfair', id: 3}, + {name: 'Ram', id: 4}, + {name: 'Test', id: 5}, + ]); + + // Can also come from e.g. this.store('countries').filter({text: searchText}).then( ... ); + return new Ember.RSVP.Promise(function(resolve) { + // Just wait for 800ms to 2 seconds for a fake progress, so it feels like a query. + var waitMS = Math.floor(Math.random() * 1000) + 800; + + Ember.run.later(_self, function() { + var result = SOME_DATA_FROM_API.filter(function (item) { + return item.name.toLowerCase().indexOf(searchText) === 0; + }); + resolve(result); + }, waitMS); + + }); + }, + + + arrayOfItems: ['Ember', 'Paper', 'One', 'Two', 'Three','Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve'], + + /** + * Array of static Objects. + * When having objects, use lookupKey="name" on the paper-autocomplete component so it knows to use "name" to search in. + */ + items: Ember.A([ + {name: 'Afghanistan', code: 'AF'}, + {name: 'Åland Islands', code: 'AX'}, + {name: 'Albania', code: 'AL'}, + {name: 'Algeria', code: 'DZ'}, + {name: 'American Samoa', code: 'AS'}, + {name: 'AndorrA', code: 'AD'}, + {name: 'Angola', code: 'AO'}, + {name: 'Anguilla', code: 'AI'}, + {name: 'Antarctica', code: 'AQ'}, + {name: 'Antigua and Barbuda', code: 'AG'}, + {name: 'Argentina', code: 'AR'}, + {name: 'Armenia', code: 'AM'}, + {name: 'Aruba', code: 'AW'}, + {name: 'Australia', code: 'AU'}, + {name: 'Austria', code: 'AT'}, + {name: 'Azerbaijan', code: 'AZ'}, + {name: 'Bahamas', code: 'BS'}, + {name: 'Bahrain', code: 'BH'}, + {name: 'Bangladesh', code: 'BD'}, + {name: 'Barbados', code: 'BB'}, + {name: 'Belarus', code: 'BY'}, + {name: 'Belgium', code: 'BE'}, + {name: 'Belize', code: 'BZ'}, + {name: 'Benin', code: 'BJ'}, + {name: 'Bermuda', code: 'BM'}, + {name: 'Bhutan', code: 'BT'}, + {name: 'Bolivia', code: 'BO'}, + {name: 'Bosnia and Herzegovina', code: 'BA'}, + {name: 'Botswana', code: 'BW'}, + {name: 'Bouvet Island', code: 'BV'}, + {name: 'Brazil', code: 'BR'}, + {name: 'British Indian Ocean Territory', code: 'IO'}, + {name: 'Brunei Darussalam', code: 'BN'}, + {name: 'Bulgaria', code: 'BG'}, + {name: 'Burkina Faso', code: 'BF'}, + {name: 'Burundi', code: 'BI'}, + {name: 'Cambodia', code: 'KH'}, + {name: 'Cameroon', code: 'CM'}, + {name: 'Canada', code: 'CA'}, + {name: 'Cape Verde', code: 'CV'}, + {name: 'Cayman Islands', code: 'KY'}, + {name: 'Central African Republic', code: 'CF'}, + {name: 'Chad', code: 'TD'}, + {name: 'Chile', code: 'CL'}, + {name: 'China', code: 'CN'}, + {name: 'Christmas Island', code: 'CX'}, + {name: 'Cocos (Keeling) Islands', code: 'CC'}, + {name: 'Colombia', code: 'CO'}, + {name: 'Comoros', code: 'KM'}, + {name: 'Congo', code: 'CG'}, + {name: 'Congo, The Democratic Republic of the', code: 'CD'}, + {name: 'Cook Islands', code: 'CK'}, + {name: 'Costa Rica', code: 'CR'}, + {name: 'Cote D\'Ivoire', code: 'CI'}, + {name: 'Croatia', code: 'HR'}, + {name: 'Cuba', code: 'CU'}, + {name: 'Cyprus', code: 'CY'}, + {name: 'Czech Republic', code: 'CZ'}, + {name: 'Denmark', code: 'DK'}, + {name: 'Djibouti', code: 'DJ'}, + {name: 'Dominica', code: 'DM'}, + {name: 'Dominican Republic', code: 'DO'}, + {name: 'Ecuador', code: 'EC'}, + {name: 'Egypt', code: 'EG'}, + {name: 'El Salvador', code: 'SV'}, + {name: 'Equatorial Guinea', code: 'GQ'}, + {name: 'Eritrea', code: 'ER'}, + {name: 'Estonia', code: 'EE'}, + {name: 'Ethiopia', code: 'ET'}, + {name: 'Falkland Islands (Malvinas)', code: 'FK'}, + {name: 'Faroe Islands', code: 'FO'}, + {name: 'Fiji', code: 'FJ'}, + {name: 'Finland', code: 'FI'}, + {name: 'France', code: 'FR'}, + {name: 'French Guiana', code: 'GF'}, + {name: 'French Polynesia', code: 'PF'}, + {name: 'French Southern Territories', code: 'TF'}, + {name: 'Gabon', code: 'GA'}, + {name: 'Gambia', code: 'GM'}, + {name: 'Georgia', code: 'GE'}, + {name: 'Germany', code: 'DE'}, + {name: 'Ghana', code: 'GH'}, + {name: 'Gibraltar', code: 'GI'}, + {name: 'Greece', code: 'GR'}, + {name: 'Greenland', code: 'GL'}, + {name: 'Grenada', code: 'GD'}, + {name: 'Guadeloupe', code: 'GP'}, + {name: 'Guam', code: 'GU'}, + {name: 'Guatemala', code: 'GT'}, + {name: 'Guernsey', code: 'GG'}, + {name: 'Guinea', code: 'GN'}, + {name: 'Guinea-Bissau', code: 'GW'}, + {name: 'Guyana', code: 'GY'}, + {name: 'Haiti', code: 'HT'}, + {name: 'Heard Island and Mcdonald Islands', code: 'HM'}, + {name: 'Holy See (Vatican City State)', code: 'VA'}, + {name: 'Honduras', code: 'HN'}, + {name: 'Hong Kong', code: 'HK'}, + {name: 'Hungary', code: 'HU'}, + {name: 'Iceland', code: 'IS'}, + {name: 'India', code: 'IN'}, + {name: 'Indonesia', code: 'ID'}, + {name: 'Iran, Islamic Republic Of', code: 'IR'}, + {name: 'Iraq', code: 'IQ'}, + {name: 'Ireland', code: 'IE'}, + {name: 'Isle of Man', code: 'IM'}, + {name: 'Israel', code: 'IL'}, + {name: 'Italy', code: 'IT'}, + {name: 'Jamaica', code: 'JM'}, + {name: 'Japan', code: 'JP'}, + {name: 'Jersey', code: 'JE'}, + {name: 'Jordan', code: 'JO'}, + {name: 'Kazakhstan', code: 'KZ'}, + {name: 'Kenya', code: 'KE'}, + {name: 'Kiribati', code: 'KI'}, + {name: 'Korea, Democratic People\'S Republic of', code: 'KP'}, + {name: 'Korea, Republic of', code: 'KR'}, + {name: 'Kuwait', code: 'KW'}, + {name: 'Kyrgyzstan', code: 'KG'}, + {name: 'Lao People\'S Democratic Republic', code: 'LA'}, + {name: 'Latvia', code: 'LV'}, + {name: 'Lebanon', code: 'LB'}, + {name: 'Lesotho', code: 'LS'}, + {name: 'Liberia', code: 'LR'}, + {name: 'Libyan Arab Jamahiriya', code: 'LY'}, + {name: 'Liechtenstein', code: 'LI'}, + {name: 'Lithuania', code: 'LT'}, + {name: 'Luxembourg', code: 'LU'}, + {name: 'Macao', code: 'MO'}, + {name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK'}, + {name: 'Madagascar', code: 'MG'}, + {name: 'Malawi', code: 'MW'}, + {name: 'Malaysia', code: 'MY'}, + {name: 'Maldives', code: 'MV'}, + {name: 'Mali', code: 'ML'}, + {name: 'Malta', code: 'MT'}, + {name: 'Marshall Islands', code: 'MH'}, + {name: 'Martinique', code: 'MQ'}, + {name: 'Mauritania', code: 'MR'}, + {name: 'Mauritius', code: 'MU'}, + {name: 'Mayotte', code: 'YT'}, + {name: 'Mexico', code: 'MX'}, + {name: 'Micronesia, Federated States of', code: 'FM'}, + {name: 'Moldova, Republic of', code: 'MD'}, + {name: 'Monaco', code: 'MC'}, + {name: 'Mongolia', code: 'MN'}, + {name: 'Montserrat', code: 'MS'}, + {name: 'Morocco', code: 'MA'}, + {name: 'Mozambique', code: 'MZ'}, + {name: 'Myanmar', code: 'MM'}, + {name: 'Namibia', code: 'NA'}, + {name: 'Nauru', code: 'NR'}, + {name: 'Nepal', code: 'NP'}, + {name: 'Netherlands', code: 'NL'}, + {name: 'Netherlands Antilles', code: 'AN'}, + {name: 'New Caledonia', code: 'NC'}, + {name: 'New Zealand', code: 'NZ'}, + {name: 'Nicaragua', code: 'NI'}, + {name: 'Niger', code: 'NE'}, + {name: 'Nigeria', code: 'NG'}, + {name: 'Niue', code: 'NU'}, + {name: 'Norfolk Island', code: 'NF'}, + {name: 'Northern Mariana Islands', code: 'MP'}, + {name: 'Norway', code: 'NO'}, + {name: 'Oman', code: 'OM'}, + {name: 'Pakistan', code: 'PK'}, + {name: 'Palau', code: 'PW'}, + {name: 'Palestinian Territory, Occupied', code: 'PS'}, + {name: 'Panama', code: 'PA'}, + {name: 'Papua New Guinea', code: 'PG'}, + {name: 'Paraguay', code: 'PY'}, + {name: 'Peru', code: 'PE'}, + {name: 'Philippines', code: 'PH'}, + {name: 'Pitcairn', code: 'PN'}, + {name: 'Poland', code: 'PL'}, + {name: 'Portugal', code: 'PT'}, + {name: 'Puerto Rico', code: 'PR'}, + {name: 'Qatar', code: 'QA'}, + {name: 'Reunion', code: 'RE'}, + {name: 'Romania', code: 'RO'}, + {name: 'Russian Federation', code: 'RU'}, + {name: 'RWANDA', code: 'RW'}, + {name: 'Saint Helena', code: 'SH'}, + {name: 'Saint Kitts and Nevis', code: 'KN'}, + {name: 'Saint Lucia', code: 'LC'}, + {name: 'Saint Pierre and Miquelon', code: 'PM'}, + {name: 'Saint Vincent and the Grenadines', code: 'VC'}, + {name: 'Samoa', code: 'WS'}, + {name: 'San Marino', code: 'SM'}, + {name: 'Sao Tome and Principe', code: 'ST'}, + {name: 'Saudi Arabia', code: 'SA'}, + {name: 'Senegal', code: 'SN'}, + {name: 'Serbia and Montenegro', code: 'CS'}, + {name: 'Seychelles', code: 'SC'}, + {name: 'Sierra Leone', code: 'SL'}, + {name: 'Singapore', code: 'SG'}, + {name: 'Slovakia', code: 'SK'}, + {name: 'Slovenia', code: 'SI'}, + {name: 'Solomon Islands', code: 'SB'}, + {name: 'Somalia', code: 'SO'}, + {name: 'South Africa', code: 'ZA'}, + {name: 'South Georgia and the South Sandwich Islands', code: 'GS'}, + {name: 'Spain', code: 'ES'}, + {name: 'Sri Lanka', code: 'LK'}, + {name: 'Sudan', code: 'SD'}, + {name: 'Suriname', code: 'SR'}, + {name: 'Svalbard and Jan Mayen', code: 'SJ'}, + {name: 'Swaziland', code: 'SZ'}, + {name: 'Sweden', code: 'SE'}, + {name: 'Switzerland', code: 'CH'}, + {name: 'Syrian Arab Republic', code: 'SY'}, + {name: 'Taiwan, Province of China', code: 'TW'}, + {name: 'Tajikistan', code: 'TJ'}, + {name: 'Tanzania, United Republic of', code: 'TZ'}, + {name: 'Thailand', code: 'TH'}, + {name: 'Timor-Leste', code: 'TL'}, + {name: 'Togo', code: 'TG'}, + {name: 'Tokelau', code: 'TK'}, + {name: 'Tonga', code: 'TO'}, + {name: 'Trinidad and Tobago', code: 'TT'}, + {name: 'Tunisia', code: 'TN'}, + {name: 'Turkey', code: 'TR'}, + {name: 'Turkmenistan', code: 'TM'}, + {name: 'Turks and Caicos Islands', code: 'TC'}, + {name: 'Tuvalu', code: 'TV'}, + {name: 'Uganda', code: 'UG'}, + {name: 'Ukraine', code: 'UA'}, + {name: 'United Arab Emirates', code: 'AE'}, + {name: 'United Kingdom', code: 'GB'}, + {name: 'United States', code: 'US'}, + {name: 'United States Minor Outlying Islands', code: 'UM'}, + {name: 'Uruguay', code: 'UY'}, + {name: 'Uzbekistan', code: 'UZ'}, + {name: 'Vanuatu', code: 'VU'}, + {name: 'Venezuela', code: 'VE'}, + {name: 'Viet Nam', code: 'VN'}, + {name: 'Virgin Islands, British', code: 'VG'}, + {name: 'Virgin Islands, U.S.', code: 'VI'}, + {name: 'Wallis and Futuna', code: 'WF'}, + {name: 'Western Sahara', code: 'EH'}, + {name: 'Yemen', code: 'YE'}, + {name: 'Zambia', code: 'ZM'}, + {name: 'Zimbabwe', code: 'ZW'} + ]), + +}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 0e0ad962c..085996b84 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -7,6 +7,7 @@ var Router = Ember.Router.extend({ Router.map(function() { this.route('introduction'); + this.route('autocomplete'); this.route('button'); this.route('card'); this.route('checkbox'); diff --git a/tests/dummy/app/styles/app.scss b/tests/dummy/app/styles/app.scss index 0a57ac5ae..693730a85 100644 --- a/tests/dummy/app/styles/app.scss +++ b/tests/dummy/app/styles/app.scss @@ -124,7 +124,7 @@ ul li:first-child { body { - &> .ember-view { + &> div.ember-view { width: 100%; height: 100%; } diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 3f217ecc8..2db43285b 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -14,6 +14,7 @@ {{#paper-list}} {{#paper-item action="transitionTo" param="index"}}Introduction{{/paper-item}} + {{#paper-item action="transitionTo" param="autocomplete"}}Autocomplete{{/paper-item}} {{#paper-item action="transitionTo" param="sidenav"}}Sidenav{{/paper-item}} {{#paper-item action="transitionTo" param="typography"}}Typography{{/paper-item}} {{#paper-item action="transitionTo" param="list"}}List{{/paper-item}} diff --git a/tests/dummy/app/templates/autocomplete.hbs b/tests/dummy/app/templates/autocomplete.hbs new file mode 100644 index 000000000..882e1ce24 --- /dev/null +++ b/tests/dummy/app/templates/autocomplete.hbs @@ -0,0 +1,280 @@ +{{#paper-toolbar}} +

    + {{#paper-sidenav-toggle class="menu-sidenav-toggle"}} + {{paper-icon icon="menu"}} + {{/paper-sidenav-toggle}} + Autocomplete +

    +{{/paper-toolbar}} + +{{#paper-content classNames="md-padding"}} +
    + +{{#paper-card}} +{{#paper-card-content}} +

    Basic Usage

    +
    Use \{{paper-autocomplete}} to search for matches from local or remote data sources.
    + + {{paper-autocomplete + disabled=firstDisabled + placeholder="Select a Country ..." + notFoundMessage='Oops country: "%@" doesn\'t exist here.' + source=items lookupKey="name" + model=myModel}} +

    Selected country is + {{#if myModel}} + {{myModel.name}} + ({{myModel.code}}) + {{else}} + Nothing selected... + {{/if}} +

    + {{#paper-checkbox checked=firstDisabled}}Disable input{{/paper-checkbox}} + + + +

    Template

    +{{#code-block language='handlebars'}} +\{{paper-autocomplete + disabled=firstDisabled + placeholder="Select a Country ..." + notFoundMessage='Oops country: "%@" doesn\'t exist here.' + source=items lookupKey="name" + model=myModel}}{{/code-block}} + +

    Allow non existing items

    +
    Use attribute allowNonExisting=true for the autocomplete to allow setting model to non existing items in the autocomplete. This is useful for search boxes.
    + + {{paper-autocomplete minLength=0 allowNonExisting=true placeholder="Type e.g. ember, paper, one, two etc." source=arrayOfItems model=sixthModel}} +

    Selected thing was: + {{#if sixthModel}} + {{sixthModel}} + {{else}} + Nothing ... + {{/if}} +

    + +{{/paper-card-content}} +{{/paper-card}} + + +{{#paper-card}} +{{#paper-card-content}} +

    Promise of data / AJAX

    +
    You may pass a callback to the source attribute, this callback takes the searchText as an argument and must + return a promise. This means that it works with e.g. jQuery's "$.getJSON" or Ember Data. When dealing with AJAX the delay attribute + is recommended to use to set a delay for the search to start. +
    + + {{paper-autocomplete minLength=0 delay=300 placeholder="Type e.g. Ram, Test, etc." source=dataFromPromise lookupKey="name" model=otherModel}} +

    You have selected: + {{#if otherModel}} + {{otherModel.name}} + ({{otherModel.id}}) + {{else}} + No Item Selected. + {{/if}} +

    + +

    Template

    +{{#code-block language='handlebars'}} +\{{paper-autocomplete minLength=0 delay=300 placeholder="Type e.g. Ram, Test, etc." source=dataFromPromise lookupKey="name" model=otherModel}}{{/code-block}} + + + +

    Javascript (Ember-data)

    +

    + In the above template we use the variable dataFromPromise, in your controller you would want to + define a function that returns a promise. Here is a sample configuration that would work if you have a + country model in the store with a name attribute: +

    +{{#code-block language='javascript'}} + ... + // searchText is received from the autocomplete component, this is what the user typed in the input field. + dataFromPromise: function (searchText) { + // First param is the name of the model + // Second param is sent to the server, so server needs to support the "search" param + // Third param is local lookup of cached results. + return this.store.filter('country', { search: searchText + '%' }, function(countryObject) { + return countryObject.get('name').toLowerCase().indexOf(searchText) === 0; + }); + }, + ...{{/code-block}} + + + +{{/paper-card-content}} +{{/paper-card}} + + + +{{#paper-card}} +{{#paper-card-content}} +

    Custom template

    +
    Use \{{paper-autocomplete}} with custom templates to show styled autocomplete results. In this example we also use minLength=0 which allow to see all results if input is empty.
    + + {{#paper-autocomplete searchText=mySearchText minLength=0 placeholder="Type e.g. ember, paper, one, two etc." source=arrayOfItems model=fourthModel as |item index|}} + + {{paper-icon icon="star"}} + {{paper-autocomplete-highlight searchText=mySearchText label=item}} (index {{index}} ) + + {{else}} + Whoops! Could not find "{{mySearchText}}". + {{/paper-autocomplete}} + +

    Selected thing was: + {{#if fourthModel}} + {{fourthModel}} + {{else}} + Nothing selected... + {{/if}} +

    + + + +

    Template

    +{{#code-block language='handlebars'}} +\{{#paper-autocomplete searchText=mySearchText minLength=0 placeholder="Type e.g. ember, paper, one, two etc." source=arrayOfItems model=fourthModel as |item index|}} + <span class="item-title"> + \{{paper-icon icon="star"}} + <span>\{{paper-autocomplete-highlight searchText=mySearchText label=item}} (index \{{index}} )</span> + </span> +\{{else}} + Whoops! Could not find "\{{mySearchText}}". +\{{/paper-autocomplete}}{{/code-block}} + +

    The custom template receives 2 block parameters (item, index). +

    + + + + + + +{{/paper-card-content}} +{{/paper-card}} + + + + +{{#paper-card}} +{{#paper-card-content}} +

    Floating Label

    +
    The following example demonstrates floating labels being used as a normal form element.
    + {{paper-autocomplete floating=true placeholder="Select a Country ..." source=items lookupKey="name" model=thirdModel}} +

    Selected country is + {{#if thirdModel}} + {{thirdModel.name}} + ({{thirdModel.code}}) + {{else}} + Nothing selected... + {{/if}} +

    + + +

    Template

    +{{#code-block language='handlebars'}} +\{{paper-autocomplete floating=true placeholder="Select a Country ..." source=items lookupKey="name" model=thirdModel}}{{/code-block}} + +{{/paper-card-content}} +{{/paper-card}} + + +

    Attributes for paper-autocomplete

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeTypeDescription
    sourcemixedThe source attribute is used to look up possible suggestions for the autocomplete. + The source attribute accepts: +
      +
    • Array of strings: Simple array of strings. Example: ['One', 'Two']
    • +
    • Function: A function that returns a promise. This is useful if you want to fetch suggestions from the server-side with e.g. Ember-data or jQuery's $.ajax method. The search string is passed as the first parameter to this function.
    • +
    • Array of objects: If you pass array of objects, paper-autocomplete needs to know what key in each object to search in. Use in combination with lookupKey. Example if you have provided [{id: 1, name="Car"}] as source, you will also need to configure the lookupKey to lookupKey="name".
    • +
    +
    modelmixedWhen a user selects item from the suggestions, model will be set and updated. Provide a model so you can do something about the value when user clicks the item.
    placeholderstringSets a placeholder for the autocomplete input field.
    minLengthintegerSets how many characters the user must type before the autocomplete gives suggestions. Default is 1.
    delayintegerThe delay attribute lets you configure how many milliseconds to wait before we trigger a search, this is + useful to avoid mass sending HTTP requests to your backend if you are using Function based source with + AJAX calls. Somewhere around 300 ms is good.
    noCachebooleanOnly effective if you use promise as source. This disables the cache of promise loaded suggestions. By default they are cached when loaded the first time.
    floatingbooleanMakes the autocomplete field a normal input field with floating labels.
    autoselectbooleanWhen suggestions is being displayed, by default when autoselect is true it will select the first element as selected. Default is false.
    disabledbooleanDisables the autocomplete.
    requiredbooleanMakes the autocomplete a required field.
    allowNonExistingbooleanallowNonExisting is useful for search boxes. It allows to use items that are not in the autocomplete selection. If you type e.g. "Chees" the model will also be set to "Chees".
    notFoundMessagestringThe message to display if no items was found. Default is: No matches found for "%@".. The %@ part will be replaced by the users input.
    + + +
    +{{/paper-content}} diff --git a/tests/unit/components/paper-autocomplete-highlight-test.js b/tests/unit/components/paper-autocomplete-highlight-test.js new file mode 100644 index 000000000..d6365eee9 --- /dev/null +++ b/tests/unit/components/paper-autocomplete-highlight-test.js @@ -0,0 +1,41 @@ +import { moduleForComponent, test } from 'ember-qunit'; + +moduleForComponent('paper-autocomplete-highlight', 'Unit | Component | paper autocomplete highlight', { + // Specify the other units that are required for this test + // needs: ['component:foo', 'helper:bar'], + unit: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Creates the component instance + var component = this.subject(); + assert.equal(component._state, 'preRender'); + + // Renders the component to the page + this.render(); + assert.equal(component._state, 'inDOM'); +}); + + + +test('should highlight correct text', function(assert) { + assert.expect(2); + + var component = this.subject({ + searchText: 'ed S', + label: 'United States' + }); + + this.render(); + + var el = this.$(); + + + assert.equal(el.html(), 'United States', 'Sets correct html with highlight based on searchText'); + + assert.equal(component.get('highlight'), 'United States', 'Sets highlight based on searchText'); + + +}); diff --git a/tests/unit/components/paper-autocomplete-item-test.js b/tests/unit/components/paper-autocomplete-item-test.js new file mode 100644 index 000000000..1409a9190 --- /dev/null +++ b/tests/unit/components/paper-autocomplete-item-test.js @@ -0,0 +1,81 @@ +import { moduleForComponent, test } from 'ember-qunit'; + +moduleForComponent('paper-autocomplete-item', 'Unit | Component | paper autocomplete item', { + // Specify the other units that are required for this test + // needs: ['component:foo', 'helper:bar'], + unit: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Creates the component instance + var component = this.subject(); + assert.equal(component._state, 'preRender'); + + // Renders the component to the page + this.render(); + assert.equal(component._state, 'inDOM'); +}); + +test('it sets correct label when lookupKey is defined', function(assert) { + assert.expect(1); + + // Creates the component instance + var component = this.subject({ + lookupKey: 'name', + item: {id: 2, name: 'Blah Test'}, + }); + + assert.equal(component.get('label'), 'Blah Test'); + +}); + +test('it sets correct label when lookupKey is NOT defined', function(assert) { + assert.expect(1); + + // Creates the component instance + var component = this.subject({ + item: 'Blah Test' + }); + + assert.equal(component.get('label'), 'Blah Test'); + +}); + +test('it sets isSelected when index is equal to selectedIndex', function(assert) { + assert.expect(1); + + // Creates the component instance + var component = this.subject({ + index: 23, + selectedIndex: 23 + }); + + assert.equal(component.get('isSelected'), true); + +}); + + +test('trigger external action when item is clicked', function(assert) { + assert.expect(1); + + var item = {name: "test"}; + + var component = this.subject({ + item: item, + lookupKey: 'name' + }); + this.$(); + + var targetObject = { + externalAction: function(item2) { + // we have the assertion here which will be + // called when the action is triggered + assert.equal(item, item2, 'external Action was called and item received was correct.'); + } + }; + component.set('pick', 'externalAction'); + component.set('targetObject', targetObject); + this.$().click(); +}); diff --git a/tests/unit/components/paper-autocomplete-list-test.js b/tests/unit/components/paper-autocomplete-list-test.js new file mode 100644 index 000000000..bfd32e47b --- /dev/null +++ b/tests/unit/components/paper-autocomplete-list-test.js @@ -0,0 +1,56 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; + +moduleForComponent('paper-autocomplete-list', 'Unit | Component | paper autocomplete list', { + // Specify the other units that are required for this test + needs: ['service:util'], + unit: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Creates the component instance + var component = this.subject(); + assert.equal(component._state, 'preRender'); + + // Renders the component to the page + this.render(); + assert.equal(component._state, 'inDOM'); +}); + +test('it renders correctly into body element.', function(assert) { + assert.expect(2); + + // Creates the component instance + this.subject(); + + this.$(); + + assert.equal(this.$().parent().prop('tagName'), 'BODY', 'It has BODY as parent DOM element'); + + assert.equal(this.$().is(':visible'), false, 'Its hidden by default'); + + +}); + + + +test('it sets positional styles on component when toggling hidden attribute', function(assert) { + + // Creates the component instance + var component = this.subject({ + wrapToElementId: 'elId' + }); + Ember.$('#qunit-fixture').append('
    '); + + this.$(); + Ember.run(function() { + component.set('hidden', false); + }); + + assert.ok(component.$().attr('style'), 'Has styles set by hideSuggestionObserver'); + +}); + + diff --git a/tests/unit/components/paper-autocomplete-test.js b/tests/unit/components/paper-autocomplete-test.js new file mode 100644 index 000000000..1875f692e --- /dev/null +++ b/tests/unit/components/paper-autocomplete-test.js @@ -0,0 +1,44 @@ +import { moduleForComponent, test } from 'ember-qunit'; + +moduleForComponent('paper-autocomplete', 'Unit | Component | paper autocomplete', { + // Specify the other units that are required for this test + needs: [ + 'service:util', + 'service:sniffer', + 'service:constants', + 'component:paper-autocomplete-list', + 'component:paper-autocomplete-item', + 'component:paper-autocomplete-highlight', + 'component:paper-progress-linear', + 'component:paper-button', + 'component:paper-icon' + ], + unit: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Creates the component instance + var component = this.subject(); + assert.equal(component._state, 'preRender'); + + // Renders the component to the page + this.render(); + assert.equal(component._state, 'inDOM'); + +}); + +test('it propagates placeholder to input box', function(assert) { + assert.expect(1); + + // Creates the component instance + var component = this.subject({ + placeholder: "Testing" + }); + + this.render(); + assert.equal(component.$().find('input').attr('placeholder'), 'Testing', 'Sets correct placeholder on input box.'); +}); + +