Created
February 1, 2014 00:37
-
-
Save runspired/8746216 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*jshint eqeqeq:false */ | |
/** | |
* Provides A base view for building select like objects. | |
*/ | |
/*global Nexus, Ember, Utils*/ | |
(function () { | |
"use strict"; | |
var App = this, | |
set = Ember.set, | |
get = Ember.get, | |
indexOf = Ember.EnumerableUtils.indexOf, | |
indexesOf = Ember.EnumerableUtils.indexesOf, | |
forEach = Ember.EnumerableUtils.forEach, | |
replace = Ember.EnumerableUtils.replace, | |
isArray = Ember.isArray, | |
precompileTemplate = Ember.Handlebars.compile, | |
inArray = Utils.array.inArray; | |
Ember.SelectableOption = Ember.View.extend(Ember.ViewTargetActionSupport, { | |
classNames : ['selectableItem'], | |
classNameBindings : ['selected', 'status'], | |
selected : function () { | |
//Ember.Logger.debug('SelectableOption view: selection changed.'); | |
var value = get(this, 'content'), | |
selection = get(this, 'parentView.controlledSelection'); | |
//Ember.Logger.debug('selection', selection); | |
//Ember.Logger.debug('value', value); | |
if (get(this, 'parentView.multiple')) { | |
//Ember.Logger.debug('selection is multiple checking index:', indexOf(selection, value)); | |
return selection && indexOf(selection, value) > -1; | |
} | |
return value === selection.objectAt(0); | |
}.property('value', 'parentView.controlledSelection.@each'), | |
template : precompileTemplate('{{view.label}}'), | |
label : function () { | |
var labelPath = get(this, 'parentView.optionLabelPath'); | |
if (!labelPath) { return ''; } | |
return get(this, labelPath); | |
}.property('parentView.optionLabelPath', 'content'), | |
status : function () { | |
var statusPath = get(this, 'parentView.optionStatusClassPath'); | |
if (!statusPath) { return ''; } | |
return get(this, statusPath); | |
}.property('parentView.optionStatusClassPath', 'content'), | |
mouseDown : function () { | |
if (!get(this, 'parentView.disabled')) { | |
this.triggerAction({ | |
action : 'select', | |
actionContext : get(this, 'content'), | |
target : this.get('parentView'), | |
bubbles : false | |
}); | |
} | |
} | |
}); | |
Ember.SelectableGroup = Ember.CollectionView.extend(Ember.ViewTargetActionSupport, { | |
classNames : ['selectableGroup'], | |
classNameBindings : ['label'], | |
controlledSelectionBinding : 'parentView.controlledSelection', | |
disabledBinding : 'parentView.disabled', | |
multipleBinding : 'parentView.multiple', | |
optionLabelPathBinding : 'parentView.optionLabelPath', | |
optionStatusClassPathBinding : 'parentView.optionStatusClassPath', | |
itemViewClassBinding: 'parentView.optionView', | |
targetBinding : 'parentView' | |
}); | |
/** | |
@class Selectable | |
@namespace Ember | |
@extends Ember.View | |
*/ | |
Ember.Selectable = Ember.View.extend({ | |
//structure | |
classNames : ['selectable'], | |
classNameBindings : ['disabled'], | |
defaultTemplate : precompileTemplate('{{#if view.optionGroupPath}}{{#each view.groupedContent}}<div class="selectableGroupLabel">{{label}}</div>{{view view.groupView content=content label=label}}{{/each}}{{else}}{{#each view.filteredContent}}{{view view.optionView content=this}}{{/each}}{{/if}}'), | |
/** | |
Indicates whether multiple options can be selected. | |
@property multiple | |
@type Boolean | |
@default false | |
*/ | |
multiple : true, | |
/** | |
The `disabled` attribute of the input element. Indicates whether | |
the element is disabled from interactions. | |
@property disabled | |
@type Boolean | |
@default false | |
*/ | |
disabled : false, | |
/** | |
The list of options. | |
If `optionLabelPath` and `optionValuePath` are not overridden, this should | |
be a list of strings, which will serve simultaneously as labels and values. | |
Otherwise, this should be a list of objects. | |
@property content | |
@type Array | |
@default null | |
*/ | |
content : null, | |
/** | |
When `multiple` is `false`, the element of `content` that is currently | |
selected, if any. | |
When `multiple` is `true`, an array of such elements. | |
Set `selection` not `value to set the initial value. | |
@property selection | |
@type Object or Array | |
@default null | |
*/ | |
selection : null, | |
controlledSelection : null, | |
values : function () { | |
var objects = get(this, 'controlledSelection'), | |
valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''), | |
multiple = get(this, 'multiple'), | |
selection = get(this, 'selection'), | |
values = []; | |
if (!objects || Ember.typeOf(objects) !== 'array') { | |
return null; | |
} | |
objects.forEach(function (object) { | |
values.push(get(object, valuePath)); | |
}); | |
//update selection | |
if (multiple) { | |
//TODO this may cause an unforseen upgrade from an array to an ember array | |
set(this, 'selection', values); | |
} else { | |
set(this, 'selection', values[0]); | |
} | |
return values; | |
}.property().volatile(), | |
labels : function () { | |
var objects = get(this, 'controlledSelection'), | |
labelPath = get(this, 'optionLabelPath').replace(/^content\.?/, ''), | |
labels = Ember.A(); | |
if (!objects || Ember.typeOf(objects) !== 'array') { | |
return null; | |
} | |
objects.forEach(function (object) { | |
labels.addObject(get(object, labelPath)); | |
}); | |
return labels; | |
}.property().volatile(), | |
/** | |
@private | |
In single selection mode (when `multiple` is `false`), value can be used to | |
get the current selection's value or set the selection by it's value. | |
In multiple selection mode, value can be used to get an array of the values | |
represented by the `selection` array, or to set the selection by presence | |
(see notes on presence). | |
`Set by presence` During multiple selection mode, setting value operates | |
mechanically identically to single selection, but magic happens underneath | |
to support pushing the value to and splicing the value from arrays. To do | |
this you pass the object in both to set it and to remove it. | |
Example: | |
Given | |
content = [{id:1},{id:2},{id:3}] | |
selection = [] | |
optionValuePath = 'content.id' | |
Then | |
this.set('value' , {id:1}) => selection = [{id:1}] | |
this.get('value') => '1' | |
this.set('value' , {id:2}) => selection = [{id:1},{id:2}] | |
this.get('value') => '1; 2' | |
this.set('value' , {id:1}) => selection = [{id:2}] | |
this.get('value') => '2' | |
@property value | |
@type String | |
@default null | |
*/ | |
value : function (key, obj) { | |
Ember.Logger.debug('value set called:', obj); | |
var selection = get(this, 'controlledSelection'), | |
index = indexOf(selection, obj);; | |
//handle as array | |
if (get(this, 'multiple')) { | |
if (arguments.length === 2) { | |
if (index === -1) { | |
selection.addObject(obj); | |
//set(this, 'controlledSelection.[]', selection); | |
} else { | |
selection.removeObject(obj); | |
//set(this, 'controlledSelection.[]', selection); | |
} | |
} | |
return selection; | |
} | |
//handle as string | |
if (arguments.length === 2) { | |
if (index === -1) { | |
set(this, 'controlledSelection.[]', [obj]); | |
} else { | |
set(this, 'controlledSelection.[]', []); | |
} | |
} | |
//trigger change | |
get(this, 'values'); | |
get(this, 'labels'); | |
return selection; | |
}.property('controlledSelection'), | |
/** | |
The path of the option labels. | |
@property optionLabelPath | |
@type String | |
@default 'content' | |
*/ | |
optionLabelPath : 'content', | |
/** | |
The path of the option labels. | |
@property optionLabelPath | |
@type String | |
@default 'content' | |
*/ | |
optionValuePath : 'content', | |
/** | |
Path on the option to that should be bound as a class | |
@property optionStatusClassPath | |
@type String | |
@default null | |
*/ | |
optionStatusClassPath : null, | |
/** | |
The path of the option group. | |
`content` should be pre-sorted by `optionGroupPath`, set | |
`sortContentByGroupPath` to `true` to have ember.Selectable sort it. | |
@property optionGroupPath | |
@type String | |
@default null | |
*/ | |
optionGroupPath : null, | |
/** | |
Determines whether to sort content by `optionGroupPath` or to rely on | |
initial user order. WARNING this WILL replace content with a new content array | |
@property sortContentByGroupPath | |
@type Boolean | |
@default false | |
*/ | |
sortContent : false, | |
/** | |
The view class for optgroup. | |
@property groupView | |
@type Ember.View | |
@default As defined below. | |
*/ | |
groupView : Ember.SelectableGroup, | |
optionView : Ember.SelectableOption, | |
controlledContent : null, | |
filteredContent : function () { | |
return get(this, 'controlledContent'); | |
}.property('controlledContent', 'controlledContent.@each'), | |
groupedContent : function () { | |
var groupPath = get(this, 'optionGroupPath'), | |
groupedContent = Ember.A(), | |
content = get(this, 'filteredContent') || []; | |
forEach(content, function (item) { | |
var label = get(item, groupPath); | |
if (get(groupedContent, 'lastObject.label') !== label) { | |
groupedContent.pushObject({ | |
label: label, | |
content: Ember.A() | |
}); | |
} | |
get(groupedContent, 'lastObject.content').push(item); | |
}); | |
return groupedContent; | |
}.property('optionGroupPath', 'filteredContent', 'filteredContent.@each'), | |
getObjectByValue : function (objects, value, valuePath) { | |
var object = null; | |
if (!objects || Ember.typeOf(objects) !== 'array') { | |
return null; | |
} | |
objects.forEach(function (obj) { | |
if (get(obj, valuePath) === value) { | |
object = obj; | |
} | |
}); | |
return object; | |
}, | |
init : function () { | |
this._super(); | |
var selection = Ember.copy(get(this, 'selection')), | |
self = this, | |
controlledSelection = Ember.A(), | |
multiple = get(this, 'multiple'), | |
content = get(this, 'content'), | |
valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''), | |
labelPath = get(this, 'optionLabelPath').replace(/^content\.?/, ''), | |
sortOptions = []; | |
if (multiple) { | |
if (!isArray(selection)) { | |
controlledSelection.addObject(this.getObjectByValue(content, selection, valuePath)); | |
} else { | |
controlledSelection.addObjects(selection.map(function (object) { | |
return self.getObjectByValue(object); | |
})); | |
} | |
} else if (isArray(selection)) { | |
Ember.Logger.error('You specified `multiple=false` but provided an Array for selection.'); | |
} else { | |
controlledSelection.addObject(this.getObjectByValue(content, selection, valuePath)); | |
} | |
set(this, 'controlledSelection', controlledSelection); | |
if (get(this, 'sortContent')) { | |
if (get(this, 'optionGroupPath')) { | |
sortOptions.push(get(this, 'optionGroupPath')); | |
} | |
sortOptions.push(labelPath); | |
} | |
set(this, 'controlledContent', Ember.ArrayController.create({ | |
content : content, | |
sortProperties : sortOptions, | |
sortAscending : true | |
})); | |
}, | |
actions : { | |
select : function (object) { | |
set(this, 'value', object); | |
return false; | |
} | |
} | |
}); | |
/** | |
Author: James Thoburn | |
Ember Autocomplete is a complete autocomplete solution with support for option groups and label paths. | |
*/ | |
/** | |
@class Autocomplete | |
@namespace Ember | |
@extends Ember.View | |
*/ | |
Ember.Autocomplete = Ember.Selectable.extend({ | |
//structure | |
classNames : ['ember-autocomplete', 'autocompleteBox'], | |
classNameBindings : ['isFocused:focused:'], | |
tagName : 'ul', | |
placeholder : 'Search...', | |
autofocus : false, | |
defaultTemplate : precompileTemplate('{{#if view.multiple}}' + | |
'{{#each view.controlledSelection}}' + | |
'{{view view.Tag content=this}}' + | |
'{{/each}}' + | |
'{{/if}}' + | |
'<li class="autocompleteTag selectableTag currentCompletionBox">' + | |
'{{view view.textInput autofocus=view.autofocus ' + | |
'value=view.searchString placeholder=view.placeholder ' + | |
'class="autocomplete invisiBox"}}' + | |
'<div class="autocompleteOptions selectable">' + | |
'{{#if view.optionGroupPath}}' + | |
'{{#each view.groupedContent}}' + | |
'<div class="selectableGroupLabel">{{label}}</div>' + | |
'{{view view.groupView content=content label=label}}' + | |
'{{/each}}' + | |
'{{else}}' + | |
'{{#each view.filteredContent}}' + | |
'{{view view.optionView content=this}}' + | |
'{{/each}}' + | |
'{{/if}}' + | |
'</div>' + | |
'</li>' | |
), | |
Tag : Ember.View.extend(Ember.ViewTargetActionSupport, { | |
tagName : 'li', | |
classNames : ['autocompleteTag', 'selectableTag'], | |
classNameBindings : ['status'], | |
defaultTemplate : precompileTemplate('{{view.label}}<span {{action "removeTag" view.content target="view"}}>X</span>'), | |
label : function () { | |
var labelPath = get(this, 'parentView.optionLabelPath'); | |
if (!labelPath) { return ''; } | |
Ember.Logger.debug('getting label', labelPath, get(this, labelPath)); | |
return get(this, labelPath); | |
}.property('parentView.optionLabelPath', 'content'), | |
status : function () { | |
var statusPath = get(this, 'parentView.optionStatusClassPath'); | |
if (!statusPath) { return ''; } | |
Ember.Logger.debug('getting status', statusPath); | |
return get(this, statusPath); | |
}.property('parentView.optionStatusClassPath', 'content'), | |
actions : { | |
removeTag : function () { | |
Ember.Logger.debug('removing tag'); | |
this.triggerAction({ | |
action : 'select', | |
actionContext : get(this, 'content'), | |
target : this.get('parentView'), | |
bubbles : false | |
}); | |
return false; | |
} | |
}, | |
init : function () { | |
Ember.Logger.debug('content init', get(this, 'content')); | |
Ember.Logger.debug('label init', get(this, 'label')); | |
} | |
}), | |
/** | |
Allow us to edit the text field without automatically updating | |
the value | |
*/ | |
searchString : '', | |
multiple : false, | |
disallowMultiple : true, | |
disabled : false, | |
content : null, | |
selection : null, | |
enforceOne : false, | |
/** | |
* | |
*/ | |
/* | |
contentChangeObserver : function () { | |
var searchString = this.get('searchString'), | |
selection = this.get('controlledSelection.[]'), | |
options = this.get('filteredContent'), | |
enforce = this.get('enforceOne'), | |
label = this.get('optionLabelPath').replace(/^content\.?/, ''); | |
if (enforce && !selection.length && options.length) { | |
selection.addObject(options.objectAt(0)); | |
} | |
}.observes('content'), | |
*/ | |
/** | |
*/ | |
labelsChangeObserver : function () { | |
var labels = get(this, 'labels.[]'), | |
multiple = get(this, 'multiple'); | |
if (!multiple) { | |
Ember.Logger.debug('setting search string to first label', labels); | |
set(this, 'searchString', labels.objectAt(0)); | |
} else { | |
Ember.Logger.debug('setting search string to empty'); | |
set(this, 'searchString', ''); | |
} | |
}.observes('labels', 'controlledSelection.@each'), | |
/** | |
@private | |
True when the textInput has focus. | |
To focus the textInput on initialization set `autofocus` to `true` | |
@property isFocused | |
@type Boolean | |
@default false | |
*/ | |
isFocused : false, | |
isHovered : false, | |
/** | |
@private | |
The option to which a 'pre-selection' hovered state is given | |
when the user utilizes up or down arrow keys to pick an option. | |
@property hoveredOption | |
@type Object | |
@default null | |
*/ | |
hoveredOption : null, | |
textInputElement : null, | |
/** | |
The view class for textfield | |
@property textInput | |
@type Ember.TextField | |
@default As defined below. | |
*/ | |
textInput : Ember.TextField.extend(Ember.TargetActionSupport, { | |
keyDown : function (e) { | |
var options, last, index, newIndex, | |
selected, | |
currentString = get(this, 'value'); | |
if (e.keyCode === 8 && get(this, 'parentView.multiple') && currentString === '') { | |
selected = get(this, 'parentView.controlledSelection'); | |
selected.removeObject(selected.objectAt(selected.length - 1)); | |
} else if (e.keyCode === 13) { //return | |
this.triggerAction({ | |
action : 'select', | |
actionContext : get(this, 'parentView.hoveredOption'), | |
target : this.get('parentView'), | |
bubbles : false | |
}); | |
//focus forward | |
if (!get(this, 'parentView.multiple')) { | |
Ember.$(":input:eq(" + Ember.$(":input").index(this.$()) + 1 + ")").focus(); | |
} else { | |
;// tag it | |
} | |
return false; | |
} else if (get(this, 'parentView.multiple') && inArray([188, 13], e.keyCode)) { | |
//32 space shouldn't work since many items will have spaces | |
//return, space or comma: tab key should still be passed through to allow context switching | |
set(this, 'parentView.value', get(this, 'parentView.hoveredOption')); | |
return false; | |
} else if (e.keyCode === 40) { //arrow down | |
options = get(this, 'parentView.filteredContent'); | |
last = options.length - 1; | |
index = indexOf( | |
options, | |
get(this, 'parentView.hoveredOption') | |
); | |
newIndex = (index === last) ? last : index + 1; | |
Ember.Logger.debug(options, last, index, newIndex); | |
set( | |
this, | |
'parentView.hoveredOption', | |
options.objectAt(newIndex) | |
); | |
Ember.Logger.debug('hovered option is:', get(this, 'parentView.hoveredOption')); | |
return false; | |
} else if (e.keyCode === 38) { //arrow up | |
options = this.get('parentView.filteredContent'); | |
index = indexOf( | |
options, | |
this.get('parentView.hoveredOption') | |
); | |
newIndex = (index === 0) ? 0 : index - 1; | |
set( | |
this, | |
'parentView.hoveredOption', | |
options.objectAt(newIndex) | |
); | |
Ember.Logger.debug('hovered option is:', get(this, 'parentView.hoveredOption')); | |
return false; | |
} else { //any other key | |
this.set( | |
'parentView.hoveredOption', | |
this.get('parentView.filteredContent').objectAt(0) | |
); | |
} | |
}, | |
focusOut : function (e) { | |
set(this, 'parentView.isFocused', false); | |
if (get(this, 'parentView.enforceOne')) { | |
if (!get(this, 'parentView.controlledSelection.length')) { | |
set(this, 'parentView.value', get(this, 'parentView.hoveredOption')); | |
} | |
} else { | |
/*this.triggerAction({ | |
action: 'change', | |
context : { | |
value : get(this, 'value'), | |
view : get(this, 'parentView') | |
}, | |
target : 'parentView' | |
});*/ | |
} | |
}, | |
focusIn : function () { | |
set(this, 'parentView.isFocused', true); | |
}, | |
autofocus : false, | |
didInsertElement : function () { | |
if (!!this.autofocus) { | |
this.$().focus(); | |
} | |
set(this, 'parentView.textInputElement', this.$()); | |
} | |
}), | |
filteredContent : function () { | |
var searchString = get(this, 'searchString'), | |
content = get(this, 'controlledContent.[]'), | |
selection = get(this, 'controlledSelection.[]'), | |
multiple = get(this, 'multiple'), | |
disallowMultiple = get(this, 'disallowMultiple'), | |
label = get(this, 'optionLabelPath').replace(/^content\.?/, ''), | |
regex = new RegExp(searchString, 'i'), | |
opts; | |
if (!content) { | |
return []; | |
} | |
if (!searchString && !selection.length) { | |
return content; | |
} | |
opts = content.filter( | |
function (option) { | |
if (multiple && disallowMultiple && indexOf(selection, option) > -1) { | |
return false; | |
} | |
return get(option, label) ? get(option, label).match(regex) : false; | |
} | |
); | |
set(this, 'hoveredOption', opts[0]); | |
return opts; | |
}.property('searchString', 'controlledSelection.@each', 'controlledContent.@each'), | |
groupView : Ember.SelectableGroup.extend({ | |
hoveredOptionBinding : 'parentView.hoveredOption', | |
textInputElementBinding : 'parentView.textInputElement', | |
classNames : ['autocompleteGroup'] | |
}), | |
/** | |
The view class for option. | |
@property optionView | |
@type Ember.View | |
@default As defined below. | |
*/ | |
optionView : Ember.SelectableOption.extend({ | |
classNames : ['autocompleteOption'], | |
classNameBindings : ['hovered'], | |
hovered : function () { | |
var content = get(this, 'content'), | |
hovered = get(this, 'parentView.hoveredOption'); | |
return content === hovered; | |
}.property('content', 'parentView.hoveredOption').readOnly(), | |
mouseDown : function () { | |
if (!get(this, 'parentView.disabled')) { | |
this.triggerAction({ | |
action : 'select', | |
actionContext : get(this, 'content'), | |
target : this.get('parentView') | |
}); | |
//focus forward | |
if (!get(this, 'parentView.multiple')) { | |
var element = get(this, 'parentView.textInputElement'), | |
index = Ember.$(":input").index(element) + 1; | |
Ember.$(":input:eq(" + index + ")").focus(); | |
} else { | |
get(this, 'parentView.textInputElement').focus(); | |
} | |
} | |
} | |
}), | |
didInsertElement : function () { | |
this.labelsChangeObserver(); | |
} | |
}); | |
}).call(Nexus); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment