Skip to content

Instantly share code, notes, and snippets.

@runspired
Created February 1, 2014 00:37
Show Gist options
  • Save runspired/8746216 to your computer and use it in GitHub Desktop.
Save runspired/8746216 to your computer and use it in GitHub Desktop.
/*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