Commit 70fb6665 authored by Ujvari Gergely's avatar Ujvari Gergely

Search feature rebased to current develop branch

- No longer part of the viewer
- Has its own controller: Search
- Own page: page_search.html
parent 210d68d6
......@@ -772,6 +772,10 @@ blockquote {
}
}
.search-collapsed {
background-image: url("../images/plus_1.png");
}
//These are all the changes needed to collapse thread objects.
&.collapsed {
display: block !important;
......@@ -1129,3 +1133,384 @@ h3.stream {
word-spacing: normal;
margin-top: 0;
}
//Visual Search////////////////////////////////
//Imported their css here
.collapsed-container {
height: 1em;
visibility: hidden;
}
.small-inner {
margin:0px 0.5em 0px 1em;
}
.search-container {
width: auto;
float: none;
overflow: hidden;
}
.visual_container {
margin-left: 3em;
}
.visual_search {
}
.VS-search .VS-icon {
background-repeat:no-repeat;
background-position:center center;
vertical-align:middle;
width:16px;
height:16px
}
.VS-search .VS-icon-cancel {
width:11px;
height:11px;
background-position:center 0;
background-image:url(../images/cancel_search.png?1311104738);
cursor:pointer
}
.VS-search .VS-icon-cancel:hover {
background-position:center -11px
}
.VS-search .VS-icon-search {
width:12px;
height:12px;
background-image:url(../images/search_glyph.png?1311104738)
}
.VS-search div, .VS-search span, .VS-search a,.VS-search img,
.VS-search ul,.VS-search li,.VS-search form,.VS-search label,.VS-interface ul,
.VS-interface li,.VS-interface {
margin:0;
padding:0;
border:0;
outline:0;
font-weight:inherit;
font-style:inherit;
font-size:100%;
font-family:inherit;
vertical-align:baseline
}
.VS-search :focus {
outline:0
}
.VS-search {
line-height:1;
color:black
}
.VS-search ol,.VS-search ul{
list-style:none
}
.VS-search{
font-family:Arial,sans-serif;
color:#373737;
font-size:12px
}
.VS-search input{
display:block;
border:none;
-moz-box-shadow:none;
-webkit-box-shadow:none;
box-shadow:none;
outline:none;
margin:0;
padding:4px;
background:transparent;
font-size:16px;
line-height:20px;
width:100%
}
.VS-interface,.VS-search .dialog,.VS-search input{
font-family:"Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,sans-serif!important;
line-height:1.1em
}
.VS-search .VS-search-box {
cursor:text;
position:relative;
background:transparent;
border:2px solid #ccc;
border-radius:16px;
-webkit-border-radius:16px;
-moz-border-radius:16px;
background-color:#fafafa;
-webkit-box-shadow:inset 0 0 3px #ccc;
-moz-box-shadow:inset 0 0 3px #ccc;
box-shadow:inset 0 0 3px #ccc;
min-height:28px;
height:1em;
//height:auto;
}
.VS-search .VS-placeholder {
position:absolute;
top:7px;
left:4px;
margin:0 20px 0 22px;
color:#808080;
font-size:14px
}
.VS-search .VS-search-box.VS-focus .VS-placeholder,
.VS-search .VS-search-box .VS-placeholder.VS-hidden {
display:none
}
.VS-search .VS-search-inner{
position:relative;
margin:0 20px 0 22px;
overflow:hidden
}
.VS-search input{
width:100px
}
.VS-search input,.VS-search .VS-input-width-tester {
padding:6px 0;
float:left;
color:#808080;
font:13px/17px Helvetica,Arial
}
.VS-search.VS-focus input{
color:#606060
}
.VS-search .VS-icon-search{
position:absolute;
left:9px;
top:8px
}
.VS-search .VS-icon-cancel{
position:absolute;
right:9px;
top:8px
}
.VS-search .search_facet {
float:left;
margin:0;
padding:0 0 0 14px;
position:relative;
border:1px solid transparent;
height:20px;margin:3px -3px 3px 0
}
.VS-search .search_facet.is_selected {
margin-left:-3px;
-webkit-border-radius:16px;
-moz-border-radius:16px;
border-radius:16px;
background-color:#d2e6fd;
background-image:-moz-linear-gradient(top,#d2e6fd,#b0d1f9);
background-image:-webkit-gradient(linear,left top,left bottom,from(#d2e6fd),to(#b0d1f9));
background-image:linear-gradient(top,#d2e6fd,#b0d1f9);
border:1px solid #6eadf5
}
.VS-search .search_facet .category{
float:left;
text-transform:uppercase;
font-weight:bold;
font-size:10px;
color:#808080;
padding:8px 0 5px;
line-height:13px;
cursor:pointer;
padding:4px 0 0
}
.VS-search .search_facet.is_selected .category{
margin-left:3px
}
.VS-search .search_facet .search_facet_input_container{
float:left
}
.VS-search .search_facet input{
margin:0;
padding:0;
color:#000;
font-size:13px;
line-height:16px;
padding:5px 0 5px 4px;
height:16px;
width:auto;
z-index:100;
position:relative;
padding-top:1px;
padding-bottom:2px;
padding-right:3px
}
.VS-search .search_facet.is_editing input,.VS-search .search_facet.is_selected input{
color:#000
}
.VS-search .search_facet .search_facet_remove{
position:absolute;
left:0;
top:4px
}
.VS-search .search_facet.is_selected .search_facet_remove{
opacity:.4;
left:3px;
filter:alpha(opacity=40);
background-position:center -11px
}
.VS-search .search_facet .search_facet_remove:hover{
opacity:1
}
.VS-search .search_facet.is_editing .category,
.VS-search .search_facet.is_selected .category{
color:#000
}
.VS-search .search_facet.search_facet_maybe_delete .category,
.VS-search .search_facet.search_facet_maybe_delete input{
color:darkred
}
.VS-search .search_input{
height:28px;
float:left;
margin-left:-1px
}
.VS-search .search_input input{
padding:6px 3px 6px 2px;
line-height:10px;
height:22px;
margin-top:-4px;
width:10px;
//width: 15em;
z-index:100;
min-width:4px;
position:relative
}
.VS-search .search_input.is_editing input{
color:#202020
}
.ui-helper-hidden-accessible{
display:none
}
.VS-interface.ui-autocomplete{
position:absolute;
border:1px solid #c0c0c0;
border-top:1px solid #d9d9d9;
background-color:#f6f6f6;
cursor:pointer;
z-index:10000;
padding:0;
margin:0;
width:auto;
min-width:80px;
max-width:220px;
max-height:240px;
overflow-y:auto;
overflow-x:hidden;
font-size:13px;
top:5px;
opacity:.97;
box-shadow:3px 4px 5px -2px rgba(0,0,0,0.5);
-webkit-box-shadow:3px 4px 5px -2px rgba(0,0,0,0.5);
-moz-box-shadow:3px 4px 5px -2px rgba(0,0,0,0.5)
}
.VS-interface.ui-autocomplete .ui-autocomplete-category{
text-transform:capitalize;
font-size:11px;
padding:4px 4px 4px;
border-top:1px solid #a2a2a2;
border-bottom:1px solid #a2a2a2;
background-color:#b7b7b7;
text-shadow:0 -1px 0 #999;
font-weight:bold;
color:white;
cursor:default
}
.VS-interface.ui-autocomplete .ui-menu-item{
float:none
}
.VS-interface.ui-autocomplete .ui-menu-item a{
color:#000;
outline:none;
display:block;
padding:3px 4px 5px;
border-radius:none;
line-height:1;
background-color:#f8f8f8;
background-image:-moz-linear-gradient(top,#f8f8f8,#f3f3f3);
background-image:-webkit-gradient(linear,left top,left bottom,from(#f8f8f8),to(#f3f3f3));
background-image:linear-gradient(top,#f8f8f8,#f3f3f3);
border-top:1px solid #fafafa;
border-bottom:1px solid #f0f0f0
}
.VS-interface.ui-autocomplete .ui-menu-item a:active{
outline:none
}
.VS-interface.ui-autocomplete .ui-menu-item .ui-state-hover,
.VS-interface.ui-autocomplete .ui-menu-item .ui-state-focus{
background-color:#6483f7;
background-image:-moz-linear-gradient(top,#648bf5,#2465f3);
background-image:-webkit-gradient(linear,left top,left bottom,from(#648bf5),to(#2465f3));
background-image:linear-gradient(top,#648bf5,#2465f3);
border-top:1px solid #5b83ec;
border-bottom:1px solid #1459e9;
border-left:none;
border-right:none;
color:white;margin:0
}
.VS-interface.ui-autocomplete .ui-corner-all{
border-radius:0
}
.VS-interface.ui-autocomplete li{
list-style:none;
width:auto
}
////////////datauri.css////////////////////
.VS-search .VS-search-box.VS-focus{
border-color:#acf;
-webkit-box-shadow:inset 0 0 3px #acf;
-moz-box-shadow:inset 0 0 3px #acf;
box-shadow:inset 0 0 3px #acf
}
.VS-search .VS-placeholder{
position:absolute;
top:7px;
left:4px;
margin:0 20px 0 22px;
color:#808080;
font-size:14px
}
......@@ -18,6 +18,10 @@ configure = ($routeProvider) ->
controller: 'ViewerController'
reloadOnSearch: false
templateUrl: 'viewer.html'
$routeProvider.when '/page_search',
controller: 'SearchController'
reloadOnSearch: false
templateUrl: 'page_search.html'
$routeProvider.otherwise
redirectTo: '/viewer'
configure.$inject = ['$routeProvider']
......
......@@ -93,7 +93,7 @@ class App
else
return unless drafts.discard()
dynamicBucket = false
$location.search('id', null)
$location.search({'id' : null })
annotator.showViewer heatmap.buckets[bucket]
$scope.$digest()
......@@ -420,8 +420,41 @@ class Viewer
return new Date()
class Search
this.$inject = ['$location', '$routeParams', '$scope', 'annotator']
constructor: ($location, $routeParams, $scope, annotator) ->
console.log 'SearchController'
refresh = =>
$scope.search_filter = $routeParams.matched
$scope.thread = null
heatmap = annotator.plugins.Heatmap
threads = []
for bucket in heatmap.buckets
for annotation in bucket
thread = annotator.threading.getContainer annotation.id
#Cut out annotation branches which has no search results
children = thread.flattenChildren()
hit_in_children = false
if children?
for child in children
if child.id in $scope.search_filter
hit_in_children = true
break
unless annotation.id in $scope.search_filter or hit_in_children
continue
if $routeParams.whole_document or annotation in $scope.annotations
threads.push thread
$scope.threads = threads
#Replace this with threading call
refresh()
angular.module('h.controllers', ['bootstrap'])
.controller('AppController', App)
.controller('AnnotationController', Annotation)
.controller('EditorController', Editor)
.controller('ViewerController', Viewer)
.controller('SearchController', Search)
......@@ -50,8 +50,8 @@ class Hypothesis extends Annotator
viewer:
addField: (-> )
this.$inject = ['$document', '$location', '$rootScope', '$route', 'drafts']
constructor: ($document, $location, $rootScope, $route, drafts) ->
this.$inject = ['$document', '$location', '$rootScope', '$route', 'drafts', '$filter']
constructor: ($document, $location, $rootScope, $route, drafts, $filter) ->
Gettext.prototype.parse_locale_data annotator_locale_data
super ($document.find 'body')
......@@ -120,6 +120,90 @@ class Hypothesis extends Annotator
# Reload the route after annotations are loaded
this.subscribe 'annotationsLoaded', -> $route.reload()
@user_filter = $filter('userName')
@visualSearch = VS.init
container: $('.visual_search')
query: ''
callbacks:
search: (query, searchCollection) =>
matched = []
whole_document = true
for searchItem in searchCollection.models
if searchItem.attributes.category is 'area' and
searchItem.attributes.value is 'sidebar'
whole_document = false
if whole_document
annotations = @plugins.Store.annotations
else
annotations = $rootScope.annotations
for annotation in annotations
matches = true
for searchItem in searchCollection.models
category = searchItem.attributes.category
value = searchItem.attributes.value
switch category
when 'user'
userName = @user_filter annotation.user
unless userName.toLowerCase() is value.toLowerCase()
matches = false
break
when 'text'
unless annotation.text.toLowerCase().indexOf(value.toLowerCase()) > -1
matches = false
break
when 'time'
delta = Math.round((+new Date - new Date(annotation.updated)) / 1000)
switch value
when '5 minutes'
unless delta <= 60*5
matches = false
when '1 hour'
unless delta <= 60*60
matches = false
when '1 day'
unless delta <= 60*60*24
matches = false
when '1 week'
unless delta <= 60*60*24*7
matches = false
when '1 month'
unless delta <= 60*60*24*31
matches = false
when '1 year'
unless delta <= 60*60*24*366
matches = false
when 'group'
priv_public = 'group:__world__' in (annotation.permissions.read or [])
switch value
when 'Public'
unless priv_public
matches = false
when 'Private'
if priv_public
matches = false
if matches
matched.push annotation.id
#$rootScope.search_filter = matched
# Set the path
search =
whole_document : whole_document
matched : matched
$location.path('/page_search').search(search)
$rootScope.$digest()
facetMatches: (callback) ->
callback ['text','area', 'group', 'tag','time','user'], {preserveOrder: true}
valueMatches: (facet, searchTerm, callback) ->
switch facet
when 'group' then callback ['Public', 'Private']
when 'area' then callback ['sidebar', 'document']
when 'time'
callback ['5 minutes', '1 hour', '1 day', '1 week', '1 month', '1 year']
_setupXDM: ->
$location = @element.injector().get '$location'
$rootScope = @element.injector().get '$rootScope'
......@@ -416,7 +500,68 @@ class DraftProvider
false
angular.module('h.services', ['ngResource'])
class FlashProvider
queues:
'': []
info: []
error: []
success: []
notice: null
timeout: null
this.$inject = ['$httpProvider']
constructor: ($httpProvider) ->
# Configure notification classes
angular.extend Annotator.Notification,
INFO: 'info'
ERROR: 'error'
SUCCESS: 'success'
# Configure the response interceptor
$httpProvider.responseInterceptors.push ['$q', ($q) =>
(promise) =>
promise.then (response) =>
data = response.data
format = response.headers 'content-type'
if format?.match /^application\/json/
if data.flash?
this._flash q, msgs for q, msgs of data.flash
if data.status is 'failure'
this._flash 'error', data.reason
$q.reject(data.reason)
else if data.status is 'okay'
response.data = data.model
response
else
response
]
_process: ->
@timeout = null
for q, msgs of @queues
if msgs.length
msg = msgs.shift()
unless q then [q, msg] = msg
notice = Annotator.showNotification msg, q
@timeout = this._wait =>
# work around Annotator.Notification not removing classes
for _, klass of notice.options.classes
notice.element.removeClass klass
this._process()
break
$get: ['$timeout', 'annotator', ($timeout, annotator) ->
this._wait = (cb) -> $timeout cb, 5000
angular.bind this, this._flash
]
_flash: (queue, messages) ->
if @queues[queue]?
@queues[queue] = @queues[queue]?.concat messages
this._process() unless @timeout?
angular.module('h.services', ['ngResource','h.filters'])
.provider('authentication', AuthenticationProvider)
.provider('drafts', DraftProvider)
.service('annotator', Hypothesis)
// Backbone.js 0.9.10
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){
// Initial Setup
// -------------
// Save a reference to the global object (`window` in the browser, `exports`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to array methods.
var array = [];
var push = array.push;
var slice = array.slice;
var splice = array.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.9.10';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Backbone.Events
// ---------------
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
} else if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
} else {
return true;
}
};
// Optimized internal dispatch function for triggering events. Tries to
// keep the usual cases speedy (most Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length;
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
}
};
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
var Events = Backbone.Events = {
// Bind one or more space separated events, or an events map,
// to a `callback` function. Passing `"all"` will bind the callback to
// all events fired.
on: function(name, callback, context) {
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
this._events || (this._events = {});
var list = this._events[name] || (this._events[name] = []);
list.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind events to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
once: function(name, callback, context) {
if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
this.on(name, once, context);
return this;
},
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
off: function(name, callback, context) {
var list, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) {
this._events = {};
return this;
}
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (list = this._events[name]) {
events = [];
if (callback || context) {
for (j = 0, k = list.length; j < k; j++) {
ev = list[j];
if ((callback && callback !== ev.callback &&
callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
events.push(ev);
}
}
}
this._events[name] = events;
}
}
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(name) {
if (!this._events) return this;
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this;
},
// An inversion-of-control version of `on`. Tell *this* object to listen to
// an event in another object ... keeping track of what it's listening to.
listenTo: function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = obj;
obj.on(name, typeof name === 'object' ? this : callback, this);
return this;
},
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function(obj, name, callback) {
var listeners = this._listeners;
if (!listeners) return;
if (obj) {
obj.off(name, typeof name === 'object' ? this : callback, this);
if (!name && !callback) delete listeners[obj._listenerId];
} else {
if (typeof name === 'object') callback = this;
for (var id in listeners) {
listeners[id].off(name, callback, this);
}
this._listeners = {};
}
return this;
}
};
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);
// Backbone.Model
// --------------
// Create a new model, with defined attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var defaults;
var attrs = attributes || {};
this.cid = _.uniqueId('c');
this.attributes = {};
if (options && options.collection) this.collection = options.collection;
if (options && options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) {
attrs = _.defaults({}, attrs, defaults);
}
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
escape: function(attr) {
return _.escape(this.get(attr));
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.get(attr) != null;
},
// ----------------------------------------------------------------------
// Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it.
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
if (changing) return this;
if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
// Remove an attribute from the model, firing `"change"` unless you choose
// to silence it. `unset` is a noop if the attribute doesn't exist.
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"` unless you choose
// to silence it.
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// ---------------------------------------------------------------------
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overriden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
options.success = function(model, resp, options) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
};
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
var attrs, success, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
options = _.extend({validate: true}, options);
// Do not persist invalid models.
if (!this._validate(attrs, options)) return false;
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
success = options.success;
options.success = function(model, resp, options) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
};
// Finish configuring and sending the Ajax request.
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var destroy = function() {
model.trigger('destroy', model, model.collection, options);
};
options.success = function(model, resp, options) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
};
if (this.isNew()) {
options.success(this, null, options);
return false;
}
var xhr = this.sync('delete', this, options);
if (!options.wait) destroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
url: function() {
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse: function(resp, options) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function() {
return this.id == null;
},
// Check if the model is currently in a valid state.
isValid: function(options) {
return !this.validate || !this.validate(this.attributes, options);
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire a general
// `"error"` event and call the error callback, if specified.
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, options || {});
return false;
}
});
// Backbone.Collection
// -------------------
// Provides a standard collection class for our sets of models, ordered
// or unordered. If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this.models = [];
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options));
};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); });
},
// Proxy `Backbone.sync` by default.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set.
add: function(models, options) {
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr;
add = [];
at = options.at;
sort = this.comparator && (at == null) && options.sort != false;
sortAttr = _.isString(this.comparator) ? this.comparator : null;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = models.length; i < l; i++) {
if (!(model = this._prepareModel(attrs = models[i], options))) {
this.trigger('invalid', this, attrs, options);
continue;
}
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(model)) {
if (options.merge) {
existing.set(attrs === model ? model.attributes : attrs, options);
if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
}
continue;
}
// This is a new model, push it to the `add` list.
add.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
// See if sorting is needed, update `length` and splice in new models.
if (add.length) {
if (sort) doSort = true;
this.length += add.length;
if (at != null) {
splice.apply(this.models, [at, 0].concat(add));
} else {
push.apply(this.models, add);
}
}
// Silently sort the collection if appropriate.
if (doSort) this.sort({silent: true});
if (options.silent) return this;
// Trigger `add` events.
for (i = 0, l = add.length; i < l; i++) {
(model = add[i]).trigger('add', model, this, options);
}
// Trigger `sort` if the collection was sorted.
if (doSort) this.trigger('sort', this, options);
return this;
},
// Remove a model, or a list of models from the set.
remove: function(models, options) {
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return this;
},
// Add a model to the end of the collection.
push: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, _.extend({at: this.length}, options));
return model;
},
// Remove a model from the end of the collection.
pop: function(options) {
var model = this.at(this.length - 1);
this.remove(model, options);
return model;
},
// Add a model to the beginning of the collection.
unshift: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, _.extend({at: 0}, options));
return model;
},
// Remove a model from the beginning of the collection.
shift: function(options) {
var model = this.at(0);
this.remove(model, options);
return model;
},
// Slice out a sub-array of models from the collection.
slice: function(begin, end) {
return this.models.slice(begin, end);
},
// Get a model from the set by id.
get: function(obj) {
if (obj == null) return void 0;
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
},
// Get the model at the given index.
at: function(index) {
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of `filter`.
where: function(attrs) {
if (_.isEmpty(attrs)) return [];
return this.filter(function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
if (!this.comparator) {
throw new Error('Cannot sort a set without a comparator');
}
options || (options = {});
// Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) {
this.models = this.sortBy(this.comparator, this);
} else {
this.models.sort(_.bind(this.comparator, this));
}
if (!options.silent) this.trigger('sort', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
return _.invoke(this.models, 'get', attr);
},
// Smartly update a collection with a change set of models, adding,
// removing, and merging as necessary.
update: function(models, options) {
options = _.extend({add: true, merge: true, remove: true}, options);
if (options.parse) models = this.parse(models, options);
var model, i, l, existing;
var add = [], remove = [], modelMap = {};
// Allow a single model (or no argument) to be passed.
if (!_.isArray(models)) models = models ? [models] : [];
// Proxy to `add` for this case, no need to iterate...
if (options.add && !options.remove) return this.add(models, options);
// Determine which models to add and merge, and which to remove.
for (i = 0, l = models.length; i < l; i++) {
model = models[i];
existing = this.get(model);
if (options.remove && existing) modelMap[existing.cid] = true;
if ((options.add && !existing) || (options.merge && existing)) {
add.push(model);
}
}
if (options.remove) {
for (i = 0, l = this.models.length; i < l; i++) {
model = this.models[i];
if (!modelMap[model.cid]) remove.push(model);
}
}
// Remove models (if applicable) before we add and merge the rest.
if (remove.length) this.remove(remove, options);
if (add.length) this.add(add, options);
return this;
},
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any `add` or `remove` events. Fires `reset` when finished.
reset: function(models, options) {
options || (options = {});
if (options.parse) models = this.parse(models, options);
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
options.previousModels = this.models.slice();
this._reset();
if (models) this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `update: true` is passed, the response
// data will be passed through the `update` method instead of `reset`.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
options.success = function(collection, resp, options) {
var method = options.update ? 'update' : 'reset';
collection[method](resp, options);
if (success) success(collection, resp, options);
};
return this.sync('read', this, options);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false;
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(model, resp, options) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
model.save(null, options);
return model;
},
// **parse** converts a response into a list of models to be added to the
// collection. The default implementation is just to pass it through.
parse: function(resp, options) {
return resp;
},
// Create a new collection with an identical list of models as this one.
clone: function() {
return new this.constructor(this.models);
},
// Reset all internal state. Called when the collection is reset.
_reset: function() {
this.length = 0;
this.models.length = 0;
this._byId = {};
},
// Prepare a model or hash of attributes to be added to this collection.
_prepareModel: function(attrs, options) {
if (attrs instanceof Model) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options || (options = {});
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) return false;
return model;
},
// Internal method to remove a model's ties to a collection.
_removeReference: function(model) {
if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this);
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (model && event === 'change:' + model.idAttribute) {
delete this._byId[model.previous(model.idAttribute)];
if (model.id != null) this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
},
sortedIndex: function (model, value, context) {
value || (value = this.comparator);
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _.sortedIndex(this.models, model, iterator, context);
}
});
// Underscore methods that we want to implement on the Collection.
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
'isEmpty', 'chain'];
// Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
// Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
// Use attributes instead of properties.
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};
});
// Backbone.Router
// ---------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment) {
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
this.trigger('route', name, args);
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
// Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
return this;
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
// order of the routes here to support behavior where the most general
// routes can be defined at the bottom of the route map.
_bindRoutes: function() {
if (!this.routes) return;
var route, routes = _.keys(this.routes);
while ((route = routes.pop()) != null) {
this.route(route, this.routes[route]);
}
},
// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional){
return optional ? match : '([^\/]+)';
})
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted parameters.
_extractParameters: function(route, fragment) {
return route.exec(fragment).slice(1);
}
});
// Backbone.History
// ----------------
// Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
this.location = window.location;
this.history = window.history;
}
};
// Cached regex for stripping a leading hash/slash and trailing space.
var routeStripper = /^[#\/]|\s+$/g;
// Cached regex for stripping leading and trailing slashes.
var rootStripper = /^\/+|\/+$/g;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Cached regex for removing a trailing slash.
var trailingSlash = /\/$/;
// Has the history handling already been started?
History.started = false;
// Set up all inheritable **Backbone.History** properties and methods.
_.extend(History.prototype, Events, {
// The default interval to poll for hash changes, if necessary, is
// twenty times a second.
interval: 50,
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
getHash: function(window) {
var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = this.location.pathname;
var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
} else {
fragment = this.getHash();
}
}
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started");
History.started = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
this.options = _.extend({}, {root: '/'}, this.options, options);
this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
var fragment = this.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
// Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) {
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = this.location;
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
this.location.replace(this.root + this.location.search + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
}
if (!this.options.silent) return this.loadUrl();
},
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval);
History.started = false;
},
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
if (current === this.fragment && this.iframe) {
current = this.getFragment(this.getHash(this.iframe));
}
if (current === this.fragment) return false;
if (this.iframe) this.navigate(current);
this.loadUrl() || this.loadUrl(this.getHash());
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragmentOverride) {
var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
return matched;
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: options};
fragment = this.getFragment(fragment || '');
if (this.fragment === fragment) return;
this.fragment = fragment;
var url = this.root + fragment;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if(!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
if (options.trigger) this.loadUrl(fragment);
},
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment);
} else {
// Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment;
}
}
});
// Create the default Backbone.history.
Backbone.history = new History;
// Backbone.View
// -------------
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save'
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid);
},
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(model, collection, id, className)*, are
// attached directly to the view.
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
this.options = options;
},
// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
});
// Backbone.sync
// -------------
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
var success = options.success;
options.success = function(resp) {
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
var error = options.error;
options.error = function(xhr) {
if (error) error(model, xhr, options);
model.trigger('error', model, xhr, options);
};
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
// Helpers
// -------
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var extend = function(protoProps, staticProps) {
var parent = this;
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
return child;
};
// Set up inheritance for the model, collection, router, view and history.
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
// Throw an error when a URL is needed, and none is supplied.
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
}).call(this);
\ No newline at end of file
// Underscore.js 1.4.3
// http://underscorejs.org
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore may be freely distributed under the MIT license.
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var push = ArrayProto.push,
slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier,
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
// Current version.
_.VERSION = '1.4.3';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
var reduceError = 'Reduce of empty array with no initial value';
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError(reduceError);
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var length = obj.length;
if (length !== +length) {
var keys = _.keys(obj);
length = keys.length;
}
each(obj, function(value, index, list) {
index = keys ? keys[--length] : --length;
if (!initial) {
memo = obj[index];
initial = true;
} else {
memo = iterator.call(context, memo, obj[index], index, list);
}
});
if (!initial) throw new TypeError(reduceError);
return memo;
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
return _.filter(obj, function(value, index, list) {
return !iterator.call(context, value, index, list);
}, context);
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if the array or object contains a given value (using `===`).
// Aliased as `include`.
_.contains = _.include = function(obj, target) {
if (obj == null) return false;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
return any(obj, function(value) {
return value === target;
});
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (_.isFunction(method) ? method : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Convenience version of a common use case of `filter`: selecting only objects
// with specific `key:value` pairs.
_.where = function(obj, attrs) {
if (_.isEmpty(attrs)) return [];
return _.filter(obj, function(value) {
for (var key in attrs) {
if (attrs[key] !== value[key]) return false;
}
return true;
});
};
// Return the maximum element or (element-based computation).
// Can't optimize arrays of integers longer than 65,535 elements.
// See: https://bugs.webkit.org/show_bug.cgi?id=80797
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
return Math.max.apply(Math, obj);
}
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity, value: -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
return Math.min.apply(Math, obj);
}
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity, value: Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Shuffle an array.
_.shuffle = function(obj) {
var rand;
var index = 0;
var shuffled = [];
each(obj, function(value) {
rand = _.random(index++);
shuffled[index - 1] = shuffled[rand];
shuffled[rand] = value;
});
return shuffled;
};
// An internal function to generate lookup iterators.
var lookupIterator = function(value) {
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, value, context) {
var iterator = lookupIterator(value);
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
index : index,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria;
var b = right.criteria;
if (a !== b) {
if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1;
}
return left.index < right.index ? -1 : 1;
}), 'value');
};
// An internal function used for aggregate "group by" operations.
var group = function(obj, value, context, behavior) {
var result = {};
var iterator = lookupIterator(value || _.identity);
each(obj, function(value, index) {
var key = iterator.call(context, value, index, obj);
behavior(result, key, value);
});
return result;
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, value, context) {
return group(obj, value, context, function(result, key, value) {
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
});
};
// Counts instances of an object that group by a certain criterion. Pass
// either a string attribute to count by, or a function that returns the
// criterion.
_.countBy = function(obj, value, context) {
return group(obj, value, context, function(result, key) {
if (!_.has(result, key)) result[key] = 0;
result[key]++;
});
};
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator, context) {
iterator = iterator == null ? _.identity : lookupIterator(iterator);
var value = iterator.call(context, obj);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(obj) {
if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj);
if (obj.length === +obj.length) return _.map(obj, _.identity);
return _.values(obj);
};
// Return the number of elements in an object.
_.size = function(obj) {
if (obj == null) return 0;
return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head` and `take`. The **guard** check
// allows it to work with `_.map`.
_.first = _.head = _.take = function(array, n, guard) {
if (array == null) return void 0;
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the last entry of the array. Especially useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with
// `_.map`.
_.initial = function(array, n, guard) {
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if (array == null) return void 0;
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
// Especially useful on the arguments object. Passing an **n** will return
// the rest N values in the array. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = _.drop = function(array, n, guard) {
return slice.call(array, (n == null) || guard ? 1 : n);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, _.identity);
};
// Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, output) {
each(input, function(value) {
if (_.isArray(value)) {
shallow ? push.apply(output, value) : flatten(value, shallow, output);
} else {
output.push(value);
}
});
return output;
};
// Return a completely flattened version of an array.
_.flatten = function(array, shallow) {
return flatten(array, shallow, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator, context) {
if (_.isFunction(isSorted)) {
context = iterator;
iterator = isSorted;
isSorted = false;
}
var initial = iterator ? _.map(array, iterator, context) : array;
var results = [];
var seen = [];
each(initial, function(value, index) {
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
seen.push(value);
results.push(array[index]);
}
});
return results;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(concat.apply(ArrayProto, arguments));
};
// Produce an array that contains every item shared between all the
// passed-in arrays.
_.intersection = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
return _.filter(array, function(value){ return !_.contains(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) {
results[i] = _.pluck(args, "" + i);
}
return results;
};
// Converts lists into objects. Pass either a single array of `[key, value]`
// pairs, or two parallel arrays of the same length -- one of keys, and one of
// the corresponding values.
_.object = function(list, values) {
if (list == null) return {};
var result = {};
for (var i = 0, l = list.length; i < l; i++) {
if (values) {
result[list[i]] = values[i];
} else {
result[list[i][0]] = list[i][1];
}
}
return result;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i = 0, l = array.length;
if (isSorted) {
if (typeof isSorted == 'number') {
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
} else {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
for (; i < l; i++) if (array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item, from) {
if (array == null) return -1;
var hasIndex = from != null;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
}
var i = (hasIndex ? from : array.length);
while (i--) if (array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function(func, context) {
var args, bound;
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(null, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, result;
var previous = 0;
var later = function() {
previous = new Date;
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(context, args);
return result;
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
memo = func.apply(this, arguments);
func = null;
return memo;
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func];
push.apply(args, arguments);
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) {
return func.apply(this, arguments);
}
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
var values = [];
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
return values;
};
// Convert an object into a list of `[key, value]` pairs.
_.pairs = function(obj) {
var pairs = [];
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
return pairs;
};
// Invert the keys and values of an object. The values must be serializable.
_.invert = function(obj) {
var result = {};
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
return result;
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
if (source) {
for (var prop in source) {
obj[prop] = source[prop];
}
}
});
return obj;
};
// Return a copy of the object only containing the whitelisted properties.
_.pick = function(obj) {
var copy = {};
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
each(keys, function(key) {
if (key in obj) copy[key] = obj[key];
});
return copy;
};
// Return a copy of the object without the blacklisted properties.
_.omit = function(obj) {
var copy = {};
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
for (var key in obj) {
if (!_.contains(keys, key)) copy[key] = obj[key];
}
return copy;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
if (source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Internal recursive comparison function for `isEqual`.
var eq = function(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
// Unwrap any wrapped objects.
if (a instanceof _) a = a._wrapped;
if (b instanceof _) b = b._wrapped;
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = aStack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (aStack[length] == a) return bStack[length] == b;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
var size = 0, result = true;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
if (!(result = eq(a[size], b[size], aStack, bStack))) break;
}
}
} else {
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
return false;
}
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return result;
};
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, [], []);
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (obj == null) return true;
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) == '[object ' + name + ']';
};
});
// Define a fallback version of the method in browsers (ahem, IE), where
// there isn't any inspectable "Arguments" type.
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && _.has(obj, 'callee'));
};
}
// Optimize `isFunction` if appropriate.
if (typeof (/./) !== 'function') {
_.isFunction = function(obj) {
return typeof obj === 'function';
};
}
// Is a given object a finite number?
_.isFinite = function(obj) {
return isFinite(obj) && !isNaN(parseFloat(obj));
};
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
_.isNaN = function(obj) {
return _.isNumber(obj) && obj != +obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype).
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function(n, iterator, context) {
var accum = Array(n);
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
return accum;
};
// Return a random integer between min and max (inclusive).
_.random = function(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + (0 | Math.random() * (max - min + 1));
};
// List of HTML entities for escaping.
var entityMap = {
escape: {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
}
};
entityMap.unescape = _.invert(entityMap.escape);
// Regexes containing the keys and values listed immediately above.
var entityRegexes = {
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
};
// Functions for escaping and unescaping strings to/from HTML interpolation.
_.each(['escape', 'unescape'], function(method) {
_[method] = function(string) {
if (string == null) return '';
return ('' + string).replace(entityRegexes[method], function(match) {
return entityMap[method][match];
});
};
});
// If the value of the named property is a function then invoke it;
// otherwise, return it.
_.result = function(object, property) {
if (object == null) return null;
var value = object[property];
return _.isFunction(value) ? value.call(object) : value;
};
// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result.call(this, func.apply(_, args));
};
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = '' + ++idCounter;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
var render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
// Add a "chain" function, which will delegate to the wrapper.
_.chain = function(obj) {
return _(obj).chain();
};
// OOP
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
// Helper function to continue chaining intermediate results.
var result = function(obj) {
return this._chain ? _(obj).chain() : obj;
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
var obj = this._wrapped;
method.apply(obj, arguments);
if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
return result.call(this, obj);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
return result.call(this, method.apply(this._wrapped, arguments));
};
});
_.extend(_.prototype, {
// Start chaining a wrapped Underscore object.
chain: function() {
this._chain = true;
return this;
},
// Extracts the result from a wrapped and chained object.
value: function() {
return this._wrapped;
}
});
}).call(this);
\ No newline at end of file
// This is the annotated source code for
// [VisualSearch.js](http://documentcloud.github.com/visualsearch/),
// a rich search box for real data.
//
// The annotated source HTML is generated by
// [Docco](http://jashkenas.github.com/docco/).
/** @license VisualSearch.js 0.4.0
* (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc.
* VisualSearch.js may be freely distributed under the MIT license.
* For all details and documentation:
* http://documentcloud.github.com/visualsearch
*/
(function() {
var $ = jQuery; // Handle namespaced jQuery
// Setting up VisualSearch globals. These will eventually be made instance-based.
if (!window.VS) window.VS = {};
if (!VS.app) VS.app = {};
if (!VS.ui) VS.ui = {};
if (!VS.model) VS.model = {};
if (!VS.utils) VS.utils = {};
// Sets the version for VisualSearch to be used programatically elsewhere.
VS.VERSION = '0.4.0';
VS.VisualSearch = function(options) {
var defaults = {
container : '',
query : '',
autosearch : true,
unquotable : [],
remainder : 'text',
showFacets : true,
callbacks : {
search : $.noop,
focus : $.noop,
blur : $.noop,
facetMatches : $.noop,
valueMatches : $.noop
}
};
this.options = _.extend({}, defaults, options);
this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks);
VS.app.hotkeys.initialize();
this.searchQuery = new VS.model.SearchQuery();
this.searchBox = new VS.ui.SearchBox({
app: this,
showFacets: this.options.showFacets
});
if (options.container) {
var searchBox = this.searchBox.render().el;
$(this.options.container).html(searchBox);
}
this.searchBox.value(this.options.query || '');
// Disable page caching for browsers that incorrectly cache the visual search inputs.
// This is forced the browser to re-render the page when it is retrieved in its history.
$(window).bind('unload', function(e) {});
// Gives the user back a reference to the `searchBox` so they
// can use public methods.
return this;
};
// Entry-point used to tie all parts of VisualSearch together. It will either attach
// itself to `options.container`, or pass back the `searchBox` so it can be rendered
// at will.
VS.init = function(options) {
return new VS.VisualSearch(options);
};
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// The search box is responsible for managing the many facet views and input views.
VS.ui.SearchBox = Backbone.View.extend({
id : 'search',
events : {
'click .VS-cancel-search-box' : 'clearSearch',
'mousedown .VS-search-box' : 'maybeFocusSearch',
'dblclick .VS-search-box' : 'highlightSearch',
'click .VS-search-box' : 'maybeTripleClick'
},
// Creating a new SearchBox registers handlers for re-rendering facets when necessary,
// as well as handling typing when a facet is selected.
initialize : function() {
this.app = this.options.app;
this.flags = {
allSelected : false
};
this.facetViews = [];
this.inputViews = [];
_.bindAll(this, 'renderFacets', '_maybeDisableFacets', 'disableFacets',
'deselectAllFacets', 'addedFacet', 'removedFacet', 'changedFacet');
this.app.searchQuery
.bind('reset', this.renderFacets)
.bind('add', this.addedFacet)
.bind('remove', this.removedFacet)
.bind('change', this.changedFacet);
$(document).bind('keydown', this._maybeDisableFacets);
},
// Renders the search box, but requires placement on the page through `this.el`.
render : function() {
$(this.el).append(JST['search_box']({}));
$(document.body).setMode('no', 'search');
return this;
},
// # Querying Facets #
// Either gets a serialized query string or sets the faceted query from a query string.
value : function(query) {
if (query == null) return this.serialize();
return this.setQuery(query);
},
// Uses the VS.app.searchQuery collection to serialize the current query from the various
// facets that are in the search box.
serialize : function() {
var query = [];
var inputViewsCount = this.inputViews.length;
this.app.searchQuery.each(_.bind(function(facet, i) {
query.push(this.inputViews[i].value());
query.push(facet.serialize());
}, this));
if (inputViewsCount) {
query.push(this.inputViews[inputViewsCount-1].value());
}
return _.compact(query).join(' ');
},
// Returns any facet views that are currently selected. Useful for changing the value
// callbacks based on what else is in the search box and which facet is being edited.
selected: function() {
return _.select(this.facetViews, function(view) {
return view.modes.editing == 'is' || view.modes.selected == 'is';
});
},
// Similar to `this.selected`, returns any facet models that are currently selected.
selectedModels: function() {
return _.pluck(this.selected(), 'model');
},
// Takes a query string and uses the SearchParser to parse and render it. Note that
// `VS.app.SearchParser` refreshes the `VS.app.searchQuery` collection, which is bound
// here to call `this.renderFacets`.
setQuery : function(query) {
this.currentQuery = query;
VS.app.SearchParser.parse(this.app, query);
},
// Returns the position of a facet/input view. Useful when moving between facets.
viewPosition : function(view) {
var views = view.type == 'facet' ? this.facetViews : this.inputViews;
var position = _.indexOf(views, view);
if (position == -1) position = 0;
return position;
},
// Used to launch a search. Hitting enter or clicking the search button.
searchEvent : function(e) {
var query = this.value();
this.focusSearch(e);
this.value(query);
this.app.options.callbacks.search(query, this.app.searchQuery);
},
// # Rendering Facets #
// Add a new facet. Facet will be focused and ready to accept a value. Can also
// specify position, in the case of adding facets from an inbetween input.
addFacet : function(category, initialQuery, position) {
category = VS.utils.inflector.trim(category);
initialQuery = VS.utils.inflector.trim(initialQuery || '');
if (!category) return;
var model = new VS.model.SearchFacet({
category : category,
value : initialQuery || '',
app : this.app
});
this.app.searchQuery.add(model, {at: position});
},
// Renders a newly added facet, and selects it.
addedFacet : function (model) {
this.renderFacets();
var facetView = _.detect(this.facetViews, function(view) {
if (view.model == model) return true;
});
_.defer(function() {
facetView.enableEdit();
});
},
// Changing a facet programmatically re-renders it.
changedFacet: function () {
this.renderFacets();
},
// When removing a facet, potentially do something. For now, the adjacent
// remaining facet is selected, but this is handled by the facet's view,
// since its position is unknown by the time the collection triggers this
// remove callback.
removedFacet : function (facet, query, options) {},
// Renders each facet as a searchFacet view.
renderFacets : function() {
this.facetViews = [];
this.inputViews = [];
this.$('.VS-search-inner').empty();
this.app.searchQuery.each(_.bind(this.renderFacet, this));
// Add on an n+1 empty search input on the very end.
this.renderSearchInput();
this.renderPlaceholder();
},
// Render a single facet, using its category and query value.
renderFacet : function(facet, position) {
var view = new VS.ui.SearchFacet({
app : this.app,
model : facet,
order : position
});
// Input first, facet second.
this.renderSearchInput();
this.facetViews.push(view);
this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
view.calculateSize();
_.defer(_.bind(view.calculateSize, view));
return view;
},
// Render a single input, used to create and autocomplete facets
renderSearchInput : function() {
var input = new VS.ui.SearchInput({
position: this.inputViews.length,
app: this.app,
showFacets: this.options.showFacets
});
this.$('.VS-search-inner').append(input.render().el);
this.inputViews.push(input);
},
// Handles showing/hiding the placeholder text
renderPlaceholder : function() {
var $placeholder = this.$('.VS-placeholder');
if (this.app.searchQuery.length) {
$placeholder.addClass("VS-hidden");
} else {
$placeholder.removeClass("VS-hidden")
.text(this.app.options.placeholder);
}
},
// # Modifying Facets #
// Clears out the search box. Command+A + delete can trigger this, as can a cancel button.
//
// If a `clearSearch` callback was provided, the callback is invoked and
// provided with a function performs the actual removal of the data. This
// allows third-party developers to either clear data asynchronously, or
// prior to performing their custom "clear" logic.
clearSearch : function(e) {
var actualClearSearch = _.bind(function() {
this.disableFacets();
this.value('');
this.flags.allSelected = false;
this.searchEvent(e);
this.focusSearch(e);
}, this);
if (this.app.options.callbacks.clearSearch) {
this.app.options.callbacks.clearSearch(actualClearSearch);
} else {
actualClearSearch();
}
},
// Command+A selects all facets.
selectAllFacets : function() {
this.flags.allSelected = true;
$(document).one('click.selectAllFacets', this.deselectAllFacets);
_.each(this.facetViews, function(facetView, i) {
facetView.selectFacet();
});
_.each(this.inputViews, function(inputView, i) {
inputView.selectText();
});
},
// Used by facets and input to see if all facets are currently selected.
allSelected : function(deselect) {
if (deselect) this.flags.allSelected = false;
return this.flags.allSelected;
},
// After `selectAllFacets` is engaged, this method is bound to the entire document.
// This immediate disables and deselects all facets, but it also checks if the user
// has clicked on either a facet or an input, and properly selects the view.
deselectAllFacets : function(e) {
this.disableFacets();
if (this.$(e.target).is('.category,input')) {
var el = $(e.target).closest('.search_facet,.search_input');
var view = _.detect(this.facetViews.concat(this.inputViews), function(v) {
return v.el == el[0];
});
if (view.type == 'facet') {
view.selectFacet();
} else if (view.type == 'input') {
_.defer(function() {
view.enableEdit(true);
});
}
}
},
// Disables all facets except for the passed in view. Used when switching between
// facets, so as not to have to keep state of active facets.
disableFacets : function(keepView) {
_.each(this.inputViews, function(view) {
if (view && view != keepView &&
(view.modes.editing == 'is' || view.modes.selected == 'is')) {
view.disableEdit();
}
});
_.each(this.facetViews, function(view) {
if (view && view != keepView &&
(view.modes.editing == 'is' || view.modes.selected == 'is')) {
view.disableEdit();
view.deselectFacet();
}
});
this.flags.allSelected = false;
this.removeFocus();
$(document).unbind('click.selectAllFacets');
},
// Resize all inputs to account for extra keystrokes which may be changing the facet
// width incorrectly. This is a safety check to ensure inputs are correctly sized.
resizeFacets : function(view) {
_.each(this.facetViews, function(facetView, i) {
if (!view || facetView == view) {
facetView.resize();
}
});
},
// Handles keydown events on the document. Used to complete the Cmd+A deletion, and
// blurring focus.
_maybeDisableFacets : function(e) {
if (this.flags.allSelected && VS.app.hotkeys.key(e) == 'backspace') {
e.preventDefault();
this.clearSearch(e);
return false;
} else if (this.flags.allSelected && VS.app.hotkeys.printable(e)) {
this.clearSearch(e);
}
},
// # Focusing Facets #
// Move focus between facets and inputs. Takes a direction as well as many options
// for skipping over inputs and only to facets, placement of cursor position in facet
// (i.e. at the end), and selecting the text in the input/facet.
focusNextFacet : function(currentView, direction, options) {
options = options || {};
var viewCount = this.facetViews.length;
var viewPosition = options.viewPosition || this.viewPosition(currentView);
if (!options.skipToFacet) {
// Correct for bouncing between matching text and facet arrays.
if (currentView.type == 'text' && direction > 0) direction -= 1;
if (currentView.type == 'facet' && direction < 0) direction += 1;
} else if (options.skipToFacet && currentView.type == 'text' &&
viewCount == viewPosition && direction >= 0) {
// Special case of looping around to a facet from the last search input box.
return false;
}
var view, next = Math.min(viewCount, viewPosition + direction);
if (currentView.type == 'text') {
if (next >= 0 && next < viewCount) {
view = this.facetViews[next];
} else if (next == viewCount) {
view = this.inputViews[this.inputViews.length-1];
}
if (view && options.selectFacet && view.type == 'facet') {
view.selectFacet();
} else if (view) {
view.enableEdit();
view.setCursorAtEnd(direction || options.startAtEnd);
}
} else if (currentView.type == 'facet') {
if (options.skipToFacet) {
if (next >= viewCount || next < 0) {
view = _.last(this.inputViews);
view.enableEdit();
} else {
view = this.facetViews[next];
view.enableEdit();
view.setCursorAtEnd(direction || options.startAtEnd);
}
} else {
view = this.inputViews[next];
view.enableEdit();
}
}
if (options.selectText) view.selectText();
this.resizeFacets();
return true;
},
maybeFocusSearch : function(e) {
if ($(e.target).is('.VS-search-box') ||
$(e.target).is('.VS-search-inner') ||
e.type == 'keydown') {
this.focusSearch(e);
}
},
// Bring focus to last input field.
focusSearch : function(e, selectText) {
var view = this.inputViews[this.inputViews.length-1];
view.enableEdit(selectText);
if (!selectText) view.setCursorAtEnd(-1);
if (e.type == 'keydown') {
view.keydown(e);
view.box.trigger('keydown');
}
_.defer(_.bind(function() {
if (!this.$('input:focus').length) {
view.enableEdit(selectText);
}
}, this));
},
// Double-clicking on the search wrapper should select the existing text in
// the last search input. Also start the triple-click timer.
highlightSearch : function(e) {
if ($(e.target).is('.VS-search-box') ||
$(e.target).is('.VS-search-inner') ||
e.type == 'keydown') {
var lastinput = this.inputViews[this.inputViews.length-1];
lastinput.startTripleClickTimer();
this.focusSearch(e, true);
}
},
maybeTripleClick : function(e) {
var lastinput = this.inputViews[this.inputViews.length-1];
return lastinput.maybeTripleClick(e);
},
// Used to show the user is focused on some input inside the search box.
addFocus : function() {
this.app.options.callbacks.focus();
this.$('.VS-search-box').addClass('VS-focus');
},
// User is no longer focused on anything in the search box.
removeFocus : function() {
this.app.options.callbacks.blur();
var focus = _.any(this.facetViews.concat(this.inputViews), function(view) {
return view.isFocused();
});
if (!focus) this.$('.VS-search-box').removeClass('VS-focus');
},
// Show a menu which adds pre-defined facets to the search box. This is unused for now.
showFacetCategoryMenu : function(e) {
e.preventDefault();
e.stopPropagation();
if (this.facetCategoryMenu && this.facetCategoryMenu.modes.open == 'is') {
return this.facetCategoryMenu.close();
}
var items = [
{title: 'Account', onClick: _.bind(this.addFacet, this, 'account', '')},
{title: 'Project', onClick: _.bind(this.addFacet, this, 'project', '')},
{title: 'Filter', onClick: _.bind(this.addFacet, this, 'filter', '')},
{title: 'Access', onClick: _.bind(this.addFacet, this, 'access', '')}
];
var menu = this.facetCategoryMenu || (this.facetCategoryMenu = new dc.ui.Menu({
items : items,
standalone : true
}));
this.$('.VS-icon-search').after(menu.render().open().content);
return false;
}
});
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// This is the visual search facet that holds the category and its autocompleted
// input field.
VS.ui.SearchFacet = Backbone.View.extend({
type : 'facet',
className : 'search_facet',
events : {
'click .category' : 'selectFacet',
'keydown input' : 'keydown',
'mousedown input' : 'enableEdit',
'mouseover .VS-icon-cancel' : 'showDelete',
'mouseout .VS-icon-cancel' : 'hideDelete',
'click .VS-icon-cancel' : 'remove'
},
initialize : function(options) {
this.flags = {
canClose : false
};
_.bindAll(this, 'set', 'keydown', 'deselectFacet', 'deferDisableEdit');
},
// Rendering the facet sets up autocompletion, events on blur, and populates
// the facet's input with its starting value.
render : function() {
$(this.el).html(JST['search_facet']({
model : this.model
}));
this.setMode('not', 'editing');
this.setMode('not', 'selected');
this.box = this.$('input');
this.box.val(this.model.label());
this.box.bind('blur', this.deferDisableEdit);
// Handle paste events with `propertychange`
this.box.bind('input propertychange', this.keydown);
this.setupAutocomplete();
return this;
},
// This method is used to setup the facet's input to auto-grow.
// This is defered in the searchBox so it can be attached to the
// DOM to get the correct font-size.
calculateSize : function() {
this.box.autoGrowInput();
this.box.unbind('updated.autogrow');
this.box.bind('updated.autogrow', _.bind(this.moveAutocomplete, this));
},
// Forces a recalculation of this facet's input field's value. Called when
// the facet is focused, removed, or otherwise modified.
resize : function(e) {
this.box.trigger('resize.autogrow', e);
},
// Watches the facet's input field to see if it matches the beginnings of
// words in `autocompleteValues`, which is different for every category.
// If the value, when selected from the autocompletion menu, is different
// than what it was, commit the facet and search for it.
setupAutocomplete : function() {
this.box.autocomplete({
source : _.bind(this.autocompleteValues, this),
minLength : 0,
delay : 0,
autoFocus : true,
position : {offset : "0 5"},
create : _.bind(function(e, ui) {
$(this.el).find('.ui-autocomplete-input').css('z-index','auto');
}, this),
select : _.bind(function(e, ui) {
e.preventDefault();
var originalValue = this.model.get('value');
this.set(ui.item.value);
if (originalValue != ui.item.value || this.box.val() != ui.item.value) {
if (this.options.app.options.autosearch) {
this.search(e);
} else {
this.options.app.searchBox.renderFacets();
this.options.app.searchBox.focusNextFacet(this, 1, {viewPosition: this.options.order});
}
}
return false;
}, this),
open : _.bind(function(e, ui) {
var box = this.box;
this.box.autocomplete('widget').find('.ui-menu-item').each(function() {
var $value = $(this),
autoCompleteData = $value.data('item.autocomplete') || $value.data('ui-autocomplete-item');
if (autoCompleteData['value'] == box.val() && box.data('uiAutocomplete').menu.activate) {
box.data('uiAutocomplete').menu.activate(new $.Event("mouseover"), $value);
}
});
}, this)
});
this.box.autocomplete('widget').addClass('VS-interface');
},
// As the facet's input field grows, it may move to the next line in the
// search box. `autoGrowInput` triggers an `updated` event on the input
// field, which is bound to this method to move the autocomplete menu.
moveAutocomplete : function() {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) {
autocomplete.menu.element.position({
my : "left top",
at : "left bottom",
of : this.box.data('uiAutocomplete').element,
collision : "flip",
offset : "0 5"
});
}
},
// When a user enters a facet and it is being edited, immediately show
// the autocomplete menu and size it to match the contents.
searchAutocomplete : function(e) {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) {
var menu = autocomplete.menu.element;
autocomplete.search();
// Resize the menu based on the correctly measured width of what's bigger:
// the menu's original size or the menu items' new size.
menu.outerWidth(Math.max(
menu.width('').outerWidth(),
autocomplete.element.outerWidth()
));
}
},
// Closes the autocomplete menu. Called on disabling, selecting, deselecting,
// and anything else that takes focus out of the facet's input field.
closeAutocomplete : function() {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) autocomplete.close();
},
// Search terms used in the autocomplete menu. These are specific to the facet,
// and only match for the facet's category. The values are then matched on the
// first letter of any word in matches, and finally sorted according to the
// value's own category. You can pass `preserveOrder` as an option in the
// `facetMatches` callback to skip any further ordering done client-side.
autocompleteValues : function(req, resp) {
var category = this.model.get('category');
var value = this.model.get('value');
var searchTerm = req.term;
this.options.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) {
options = options || {};
matches = matches || [];
if (searchTerm && value != searchTerm) {
if (options.preserveMatches) {
resp(matches);
} else {
var re = VS.utils.inflector.escapeRegExp(searchTerm || '');
var matcher = new RegExp('\\b' + re, 'i');
matches = $.grep(matches, function(item) {
return matcher.test(item) ||
matcher.test(item.value) ||
matcher.test(item.label);
});
}
}
if (options.preserveOrder) {
resp(matches);
} else {
resp(_.sortBy(matches, function(match) {
if (match == value || match.value == value) return '';
else return match;
}));
}
});
},
// Sets the facet's model's value.
set : function(value) {
if (!value) return;
this.model.set({'value': value});
},
// Before the searchBox performs a search, we need to close the
// autocomplete menu.
search : function(e, direction) {
if (!direction) direction = 1;
this.closeAutocomplete();
this.options.app.searchBox.searchEvent(e);
_.defer(_.bind(function() {
this.options.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order});
}, this));
},
// Begin editing the facet's input. This is called when the user enters
// the input either from another facet or directly clicking on it.
//
// This method tells all other facets and inputs to disable so it can have
// the sole focus. It also prepares the autocompletion menu.
enableEdit : function() {
if (this.modes.editing != 'is') {
this.setMode('is', 'editing');
this.deselectFacet();
if (this.box.val() == '') {
this.box.val(this.model.get('value'));
}
}
this.flags.canClose = false;
this.options.app.searchBox.disableFacets(this);
this.options.app.searchBox.addFocus();
_.defer(_.bind(function() {
this.options.app.searchBox.addFocus();
}, this));
this.resize();
this.searchAutocomplete();
this.box.focus();
},
// When the user blurs the input, they may either be going to another input
// or off the search box entirely. If they go to another input, this facet
// will be instantly disabled, and the canClose flag will be turned back off.
//
// However, if the user clicks elsewhere on the page, this method starts a timer
// that checks if any of the other inputs are selected or are being edited. If
// not, then it can finally close itself and its autocomplete menu.
deferDisableEdit : function() {
this.flags.canClose = true;
_.delay(_.bind(function() {
if (this.flags.canClose && !this.box.is(':focus') &&
this.modes.editing == 'is' && this.modes.selected != 'is') {
this.disableEdit();
}
}, this), 250);
},
// Called either by other facets receiving focus or by the timer in `deferDisableEdit`,
// this method will turn off the facet, remove any text selection, and close
// the autocomplete menu.
disableEdit : function() {
var newFacetQuery = VS.utils.inflector.trim(this.box.val());
if (newFacetQuery != this.model.get('value')) {
this.set(newFacetQuery);
}
this.flags.canClose = false;
this.box.selectRange(0, 0);
this.box.blur();
this.setMode('not', 'editing');
this.closeAutocomplete();
this.options.app.searchBox.removeFocus();
},
// Selects the facet, which blurs the facet's input and highlights the facet.
// If this is the only facet being selected (and not part of a select all event),
// we attach a mouse/keyboard watcher to check if the next action by the user
// should delete this facet or just deselect it.
selectFacet : function(e) {
if (e) e.preventDefault();
var allSelected = this.options.app.searchBox.allSelected();
if (this.modes.selected == 'is') return;
if (this.box.is(':focus')) {
this.box.setCursorPosition(0);
this.box.blur();
}
this.flags.canClose = false;
this.closeAutocomplete();
this.setMode('is', 'selected');
this.setMode('not', 'editing');
if (!allSelected || e) {
$(document).unbind('keydown.facet', this.keydown);
$(document).unbind('click.facet', this.deselectFacet);
_.defer(_.bind(function() {
$(document).unbind('keydown.facet').bind('keydown.facet', this.keydown);
$(document).unbind('click.facet').one('click.facet', this.deselectFacet);
}, this));
this.options.app.searchBox.disableFacets(this);
this.options.app.searchBox.addFocus();
}
return false;
},
// Turns off highlighting on the facet. Called in a variety of ways, this
// only deselects the facet if it is selected, and then cleans up the
// keyboard/mouse watchers that were created when the facet was first
// selected.
deselectFacet : function(e) {
if (e) e.preventDefault();
if (this.modes.selected == 'is') {
this.setMode('not', 'selected');
this.closeAutocomplete();
this.options.app.searchBox.removeFocus();
}
$(document).unbind('keydown.facet', this.keydown);
$(document).unbind('click.facet', this.deselectFacet);
return false;
},
// Is the user currently focused in this facet's input field?
isFocused : function() {
return this.box.is(':focus');
},
// Hovering over the delete button styles the facet so the user knows that
// the delete button will kill the entire facet.
showDelete : function() {
$(this.el).addClass('search_facet_maybe_delete');
},
// On `mouseout`, the user is no longer hovering on the delete button.
hideDelete : function() {
$(this.el).removeClass('search_facet_maybe_delete');
},
// When switching between facets, depending on the direction the cursor is
// coming from, the cursor in this facet's input field should match the original
// direction.
setCursorAtEnd : function(direction) {
if (direction == -1) {
this.box.setCursorPosition(this.box.val().length);
} else {
this.box.setCursorPosition(0);
}
},
// Deletes the facet and sends the cursor over to the nearest input field.
remove : function(e) {
var committed = this.model.get('value');
this.deselectFacet();
this.disableEdit();
this.options.app.searchQuery.remove(this.model);
if (committed && this.options.app.options.autosearch) {
this.search(e, -1);
} else {
this.options.app.searchBox.renderFacets();
this.options.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order});
}
},
// Selects the text in the facet's input field. When the user tabs between
// facets, convention is to highlight the entire field.
selectText: function() {
this.box.selectRange(0, this.box.val().length);
},
// Handles all keyboard inputs when in the facet's input field. This checks
// for movement between facets and inputs, entering a new value that needs
// to be autocompleted, as well as the removal of this facet.
keydown : function(e) {
var key = VS.app.hotkeys.key(e);
if (key == 'enter' && this.box.val()) {
this.disableEdit();
this.search(e);
} else if (key == 'left') {
if (this.modes.selected == 'is') {
this.deselectFacet();
this.options.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
} else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
this.selectFacet();
}
} else if (key == 'right') {
if (this.modes.selected == 'is') {
e.preventDefault();
this.deselectFacet();
this.setCursorAtEnd(0);
this.enableEdit();
} else if (this.box.getCursorPosition() == this.box.val().length) {
e.preventDefault();
this.disableEdit();
this.options.app.searchBox.focusNextFacet(this, 1);
}
} else if (VS.app.hotkeys.shift && key == 'tab') {
e.preventDefault();
this.options.app.searchBox.focusNextFacet(this, -1, {
startAtEnd : -1,
skipToFacet : true,
selectText : true
});
} else if (key == 'tab') {
e.preventDefault();
this.options.app.searchBox.focusNextFacet(this, 1, {
skipToFacet : true,
selectText : true
});
} else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) {
e.preventDefault();
this.options.app.searchBox.selectAllFacets();
return false;
} else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') {
this.options.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
this.remove(e);
} else if (key == 'backspace') {
if (this.modes.selected == 'is') {
e.preventDefault();
this.remove(e);
} else if (this.box.getCursorPosition() == 0 &&
!this.box.getSelection().length) {
e.preventDefault();
this.selectFacet();
}
}
// Handle paste events
if (e.which == null) {
// this.searchAutocomplete(e);
_.defer(_.bind(this.resize, this, e));
} else {
this.resize(e);
}
}
});
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// This is the visual search input that is responsible for creating new facets.
// There is one input placed in between all facets.
VS.ui.SearchInput = Backbone.View.extend({
type : 'text',
className : 'search_input ui-menu',
events : {
'keypress input' : 'keypress',
'keydown input' : 'keydown',
'click input' : 'maybeTripleClick',
'dblclick input' : 'startTripleClickTimer'
},
initialize : function() {
this.app = this.options.app;
this.flags = {
canClose : false
};
_.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit');
},
// Rendering the input sets up autocomplete, events on focusing and blurring
// the input, and the auto-grow of the input.
render : function() {
$(this.el).html(JST['search_input']({}));
this.setMode('not', 'editing');
this.setMode('not', 'selected');
this.box = this.$('input');
this.box.autoGrowInput();
this.box.bind('updated.autogrow', this.moveAutocomplete);
this.box.bind('blur', this.deferDisableEdit);
this.box.bind('focus', this.addFocus);
this.setupAutocomplete();
return this;
},
// Watches the input and presents an autocompleted menu, taking the
// remainder of the input field and adding a separate facet for it.
//
// See `addTextFacetRemainder` for explanation on how the remainder works.
setupAutocomplete : function() {
this.box.autocomplete({
minLength : this.options.showFacets ? 0 : 1,
delay : 50,
autoFocus : true,
position : {offset : "0 -1"},
source : _.bind(this.autocompleteValues, this),
create : _.bind(function(e, ui) {
$(this.el).find('.ui-autocomplete-input').css('z-index','auto');
}, this),
select : _.bind(function(e, ui) {
e.preventDefault();
// stopPropogation does weird things in jquery-ui 1.9
// e.stopPropagation();
var remainder = this.addTextFacetRemainder(ui.item.value);
var position = this.options.position + (remainder ? 1 : 0);
this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position);
return false;
}, this)
});
// Renders the results grouped by the categories they belong to.
this.box.data('uiAutocomplete')._renderMenu = function(ul, items) {
var category = '';
_.each(items, _.bind(function(item, i) {
if (item.category && item.category != category) {
ul.append('<li class="ui-autocomplete-category">'+item.category+'</li>');
category = item.category;
}
if(this._renderItemData) {
this._renderItemData(ul, item);
} else {
this._renderItem(ul, item);
}
}, this));
};
this.box.autocomplete('widget').addClass('VS-interface');
},
// Search terms used in the autocomplete menu. The values are matched on the
// first letter of any word in matches, and finally sorted according to the
// value's own category. You can pass `preserveOrder` as an option in the
// `facetMatches` callback to skip any further ordering done client-side.
autocompleteValues : function(req, resp) {
var searchTerm = req.term;
var lastWord = searchTerm.match(/\w+\*?$/); // Autocomplete only last word.
var re = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || '');
this.app.options.callbacks.facetMatches(function(prefixes, options) {
options = options || {};
prefixes = prefixes || [];
// Only match from the beginning of the word.
var matcher = new RegExp('^' + re, 'i');
var matches = $.grep(prefixes, function(item) {
return item && matcher.test(item.label || item);
});
if (options.preserveOrder) {
resp(matches);
} else {
resp(_.sortBy(matches, function(match) {
if (match.label) return match.category + '-' + match.label;
else return match;
}));
}
});
},
// Closes the autocomplete menu. Called on disabling, selecting, deselecting,
// and anything else that takes focus out of the facet's input field.
closeAutocomplete : function() {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) autocomplete.close();
},
// As the input field grows, it may move to the next line in the
// search box. `autoGrowInput` triggers an `updated` event on the input
// field, which is bound to this method to move the autocomplete menu.
moveAutocomplete : function() {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) {
autocomplete.menu.element.position({
my : "left top",
at : "left bottom",
of : this.box.data('uiAutocomplete').element,
collision : "none",
offset : '0 -1'
});
}
},
// When a user enters a facet and it is being edited, immediately show
// the autocomplete menu and size it to match the contents.
searchAutocomplete : function(e) {
var autocomplete = this.box.data('uiAutocomplete');
if (autocomplete) {
var menu = autocomplete.menu.element;
autocomplete.search();
// Resize the menu based on the correctly measured width of what's bigger:
// the menu's original size or the menu items' new size.
menu.outerWidth(Math.max(
menu.width('').outerWidth(),
autocomplete.element.outerWidth()
));
}
},
// If a user searches for "word word category", the category would be
// matched and autocompleted, and when selected, the "word word" would
// also be caught as the remainder and then added in its own facet.
addTextFacetRemainder : function(facetValue) {
var boxValue = this.box.val();
var lastWord = boxValue.match(/\b(\w+)$/);
if (!lastWord) {
return '';
}
var matcher = new RegExp(lastWord[0], "i");
if (facetValue.search(matcher) == 0) {
boxValue = boxValue.replace(/\b(\w+)$/, '');
}
boxValue = boxValue.replace('^\s+|\s+$', '');
if (boxValue) {
this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position);
}
return boxValue;
},
// Directly called to focus the input. This is different from `addFocus`
// because this is not called by a focus event. This instead calls a
// focus event causing the input to become focused.
enableEdit : function(selectText) {
this.addFocus();
if (selectText) {
this.selectText();
}
this.box.focus();
},
// Event called on user focus on the input. Tells all other input and facets
// to give up focus, and starts revving the autocomplete.
addFocus : function() {
this.flags.canClose = false;
if (!this.app.searchBox.allSelected()) {
this.app.searchBox.disableFacets(this);
}
this.app.searchBox.addFocus();
this.setMode('is', 'editing');
this.setMode('not', 'selected');
if (!this.app.searchBox.allSelected()) {
this.searchAutocomplete();
}
},
// Directly called to blur the input. This is different from `removeFocus`
// because this is not called by a blur event.
disableEdit : function() {
this.box.blur();
this.removeFocus();
},
// Event called when user blur's the input, either through the keyboard tabbing
// away or the mouse clicking off. Cleans up
removeFocus : function() {
this.flags.canClose = false;
this.app.searchBox.removeFocus();
this.setMode('not', 'editing');
this.setMode('not', 'selected');
this.closeAutocomplete();
},
// When the user blurs the input, they may either be going to another input
// or off the search box entirely. If they go to another input, this facet
// will be instantly disabled, and the canClose flag will be turned back off.
//
// However, if the user clicks elsewhere on the page, this method starts a timer
// that checks if any of the other inputs are selected or are being edited. If
// not, then it can finally close itself and its autocomplete menu.
deferDisableEdit : function() {
this.flags.canClose = true;
_.delay(_.bind(function() {
if (this.flags.canClose &&
!this.box.is(':focus') &&
this.modes.editing == 'is') {
this.disableEdit();
}
}, this), 250);
},
// Starts a timer that will cause a triple-click, which highlights all facets.
startTripleClickTimer : function() {
this.tripleClickTimer = setTimeout(_.bind(function() {
this.tripleClickTimer = null;
}, this), 500);
},
// Event on click that checks if a triple click is in play. The
// `tripleClickTimer` is counting down, ready to be engaged and intercept
// the click event to force a select all instead.
maybeTripleClick : function(e) {
if (!!this.tripleClickTimer) {
e.preventDefault();
this.app.searchBox.selectAllFacets();
return false;
}
},
// Is the user currently focused in the input field?
isFocused : function() {
return this.box.is(':focus');
},
// When serializing the facets, the inputs need to also have their values represented,
// in case they contain text that is not yet faceted (but will be once the search is
// completed).
value : function() {
return this.box.val();
},
// When switching between facets and inputs, depending on the direction the cursor
// is coming from, the cursor in this facet's input field should match the original
// direction.
setCursorAtEnd : function(direction) {
if (direction == -1) {
this.box.setCursorPosition(this.box.val().length);
} else {
this.box.setCursorPosition(0);
}
},
// Selects the entire range of text in the input. Useful when tabbing between inputs
// and facets.
selectText : function() {
this.box.selectRange(0, this.box.val().length);
if (!this.app.searchBox.allSelected()) {
this.box.focus();
} else {
this.setMode('is', 'selected');
}
},
// Before the searchBox performs a search, we need to close the
// autocomplete menu.
search : function(e, direction) {
if (!direction) direction = 0;
this.closeAutocomplete();
this.app.searchBox.searchEvent(e);
_.defer(_.bind(function() {
this.app.searchBox.focusNextFacet(this, direction);
}, this));
},
// Callback fired on key press in the search box. We search when they hit return.
keypress : function(e) {
var key = VS.app.hotkeys.key(e);
if (key == 'enter') {
return this.search(e, 100);
} else if (VS.app.hotkeys.colon(e)) {
this.box.trigger('resize.autogrow', e);
var query = this.box.val();
var prefixes = [];
if (this.app.options.callbacks.facetMatches) {
this.app.options.callbacks.facetMatches(function(p) {
prefixes = p;
});
}
var labels = _.map(prefixes, function(prefix) {
if (prefix.label) return prefix.label;
else return prefix;
});
if (_.contains(labels, query)) {
e.preventDefault();
var remainder = this.addTextFacetRemainder(query);
var position = this.options.position + (remainder?1:0);
this.app.searchBox.addFacet(query, '', position);
return false;
}
} else if (key == 'backspace') {
if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
this.app.searchBox.resizeFacets();
return false;
}
}
},
// Handles all keyboard inputs when in the input field. This checks
// for movement between facets and inputs, entering a new value that needs
// to be autocompleted, as well as stepping between facets with backspace.
keydown : function(e) {
var key = VS.app.hotkeys.key(e);
if (key == 'left') {
if (this.box.getCursorPosition() == 0) {
e.preventDefault();
this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1});
}
} else if (key == 'right') {
if (this.box.getCursorPosition() == this.box.val().length) {
e.preventDefault();
this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true});
}
} else if (VS.app.hotkeys.shift && key == 'tab') {
e.preventDefault();
this.app.searchBox.focusNextFacet(this, -1, {selectText: true});
} else if (key == 'tab') {
var value = this.box.val();
if (value.length) {
e.preventDefault();
var remainder = this.addTextFacetRemainder(value);
var position = this.options.position + (remainder?1:0);
if (value != remainder) {
this.app.searchBox.addFacet(value, '', position);
}
} else {
var foundFacet = this.app.searchBox.focusNextFacet(this, 0, {
skipToFacet: true,
selectText: true
});
if (foundFacet) {
e.preventDefault();
}
}
} else if (VS.app.hotkeys.command &&
String.fromCharCode(e.which).toLowerCase() == 'a') {
e.preventDefault();
this.app.searchBox.selectAllFacets();
return false;
} else if (key == 'backspace' && !this.app.searchBox.allSelected()) {
if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) {
e.preventDefault();
this.app.searchBox.focusNextFacet(this, -1, {backspace: true});
return false;
}
} else if (key == 'end') {
var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1];
view.setCursorAtEnd(-1);
} else if (key == 'home') {
var view = this.app.searchBox.inputViews[0];
view.setCursorAtEnd(-1);
}
this.box.trigger('resize.autogrow', e);
}
});
})();
(function(){
var $ = jQuery; // Handle namespaced jQuery
// Makes the view enter a mode. Modes have both a 'mode' and a 'group',
// and are mutually exclusive with any other modes in the same group.
// Setting will update the view's modes hash, as well as set an HTML class
// of *[mode]_[group]* on the view's element. Convenient way to swap styles
// and behavior.
Backbone.View.prototype.setMode = function(mode, group) {
this.modes || (this.modes = {});
if (this.modes[group] === mode) return;
$(this.el).setMode(mode, group);
this.modes[group] = mode;
};
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
// just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
// on `keydown`.
//
// For the most headache-free way to use this utility, check modifier keys,
// like shift and command, with `VS.app.hotkeys.shift`, and check every other
// key with `VS.app.hotkeys.key(e) == 'key_name'`.
VS.app.hotkeys = {
// Keys that will be mapped to the `hotkeys` namespace.
KEYS: {
'16': 'shift',
'17': 'command',
'91': 'command',
'93': 'command',
'224': 'command',
'13': 'enter',
'37': 'left',
'38': 'upArrow',
'39': 'right',
'40': 'downArrow',
'46': 'delete',
'8': 'backspace',
'35': 'end',
'36': 'home',
'9': 'tab',
'188': 'comma'
},
// Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
initialize : function() {
_.bindAll(this, 'down', 'up', 'blur');
$(document).bind('keydown', this.down);
$(document).bind('keyup', this.up);
$(window).bind('blur', this.blur);
},
// On `keydown`, turn on all keys that match.
down : function(e) {
var key = this.KEYS[e.which];
if (key) this[key] = true;
},
// On `keyup`, turn off all keys that match.
up : function(e) {
var key = this.KEYS[e.which];
if (key) this[key] = false;
},
// If an input is blurred, all keys need to be turned off, since they are no longer
// able to modify the document.
blur : function(e) {
for (var key in this.KEYS) this[this.KEYS[key]] = false;
},
// Check a key from an event and return the common english name.
key : function(e) {
return this.KEYS[e.which];
},
// Colon is special, since the value is different between browsers.
colon : function(e) {
var charCode = e.which;
return charCode && String.fromCharCode(charCode) == ":";
},
// Check a key from an event and match it against any known characters.
// The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
//
// These were determined by looping through every `keyCode` and `charCode` that
// resulted from `keydown` and `keypress` events and counting what was printable.
printable : function(e) {
var code = e.which;
if (e.type == 'keydown') {
if (code == 32 || // space
(code >= 48 && code <= 90) || // 0-1a-z
(code >= 96 && code <= 111) || // 0-9+-/*.
(code >= 186 && code <= 192) || // ;=,-./^
(code >= 219 && code <= 222)) { // (\)'
return true;
}
} else {
// [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
if ((code >= 32 && code <= 126) ||
(code >= 160 && code <= 500) ||
(String.fromCharCode(code) == ":")) {
return true;
}
}
return false;
}
};
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// Naive English transformations on words. Only used for a few transformations
// in VisualSearch.js.
VS.utils.inflector = {
// Delegate to the ECMA5 String.prototype.trim function, if available.
trim : function(s) {
return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
},
// Escape strings that are going to be used in a regex. Escapes punctuation
// that would be incorrect in a regex.
escapeRegExp : function(s) {
return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
}
};
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
$.fn.extend({
// Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
// and are mutually exclusive with any other modes in the same group.
// Setting will update the view's modes hash, as well as set an HTML class
// of *[mode]_[group]* on the view's element. Convenient way to swap styles
// and behavior.
setMode : function(state, group) {
group = group || 'mode';
var re = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
var mode = (state === null) ? "" : state + "_" + group;
this.each(function() {
this.className = (this.className.replace(re, '')+' '+mode)
.replace(/\s\s/g, ' ');
});
return mode;
},
// When attached to an input element, this will cause the width of the input
// to match its contents. This calculates the width of the contents of the input
// by measuring a hidden shadow div that should match the styling of the input.
autoGrowInput: function() {
return this.each(function() {
var $input = $(this);
var $tester = $('<div />').css({
opacity : 0,
top : -9999,
left : -9999,
position : 'absolute',
whiteSpace : 'nowrap'
}).addClass('VS-input-width-tester').addClass('VS-interface');
// Watch for input value changes on all of these events. `resize`
// event is called explicitly when the input has been changed without
// a single keypress.
var events = 'keydown.autogrow keypress.autogrow ' +
'resize.autogrow change.autogrow';
$input.next('.VS-input-width-tester').remove();
$input.after($tester);
$input.unbind(events).bind(events, function(e, realEvent) {
if (realEvent) e = realEvent;
var value = $input.val();
// Watching for the backspace key is tricky because it may not
// actually be deleting the character, but instead the key gets
// redirected to move the cursor from facet to facet.
if (VS.app.hotkeys.key(e) == 'backspace') {
var position = $input.getCursorPosition();
if (position > 0) value = value.slice(0, position-1) +
value.slice(position, value.length);
} else if (VS.app.hotkeys.printable(e) &&
!VS.app.hotkeys.command) {
value += String.fromCharCode(e.which);
}
value = value.replace(/&/g, '&amp;')
.replace(/\s/g,'&nbsp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
$tester.html(value);
$input.width($tester.width() + 3 + parseInt($input.css('min-width')));
$input.trigger('updated.autogrow');
});
// Sets the width of the input on initialization.
$input.trigger('resize.autogrow');
});
},
// Cross-browser method used for calculating where the cursor is in an
// input field.
getCursorPosition: function() {
var position = 0;
var input = this.get(0);
if (document.selection) { // IE
input.focus();
var sel = document.selection.createRange();
var selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
position = sel.text.length - selLen;
} else if (input && $(input).is(':visible') &&
input.selectionStart != null) { // Firefox/Safari
position = input.selectionStart;
}
return position;
},
// A simple proxy for `selectRange` that sets the cursor position in an
// input field.
setCursorPosition: function(position) {
return this.each(function() {
return $(this).selectRange(position, position);
});
},
// Cross-browser way to select text in an input field.
selectRange: function(start, end) {
return this.filter(':visible').each(function() {
if (this.setSelectionRange) { // FF/Webkit
this.focus();
this.setSelectionRange(start, end);
} else if (this.createTextRange) { // IE
var range = this.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
if (end - start >= 0) range.select();
}
});
},
// Returns an object that contains the text selection range values for
// an input field.
getSelection: function() {
var input = this[0];
if (input.selectionStart != null) { // FF/Webkit
var start = input.selectionStart;
var end = input.selectionEnd;
return {
start : start,
end : end,
length : end-start,
text : input.value.substr(start, end-start)
};
} else if (document.selection) { // IE
var range = document.selection.createRange();
if (range) {
var textRange = input.createTextRange();
var copyRange = textRange.duplicate();
textRange.moveToBookmark(range.getBookmark());
copyRange.setEndPoint('EndToStart', textRange);
var start = copyRange.text.length;
var end = start + range.text.length;
return {
start : start,
end : end,
length : end-start,
text : range.text
};
}
}
return {start: 0, end: 0, length: 0};
}
});
// Debugging in Internet Explorer. This allows you to use
// `console.log(['message', var1, var2, ...])`. Just remove the `false` and
// add your console.logs. This will automatically stringify objects using
// `JSON.stringify', so you can read what's going out. Think of this as a
// *Diet Firebug Lite Zero with Lemon*.
if (false) {
window.console = {};
var _$ied;
window.console.log = function(msg) {
if (_.isArray(msg)) {
var message = msg[0];
var vars = _.map(msg.slice(1), function(arg) {
return JSON.stringify(arg);
}).join(' - ');
}
if(!_$ied){
_$ied = $('<div><ol></ol></div>').css({
'position': 'fixed',
'bottom': 10,
'left': 10,
'zIndex': 20000,
'width': $('body').width() - 80,
'border': '1px solid #000',
'padding': '10px',
'backgroundColor': '#fff',
'fontFamily': 'arial,helvetica,sans-serif',
'fontSize': '11px'
});
$('body').append(_$ied);
}
var $message = $('<li>'+message+' - '+vars+'</li>').css({
'borderBottom': '1px solid #999999'
});
_$ied.find('ol').append($message);
_.delay(function() {
$message.fadeOut(500);
}, 5000);
};
}
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// Used to extract keywords and facets from the free text search.
var QUOTES_RE = "('[^']+'|\"[^\"]+\")";
var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
var CATEGORY_RE = FREETEXT_RE + ':\\s*';
VS.app.SearchParser = {
// Matches `category: "free text"`, with and without quotes.
ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),
// Matches a single category without the text. Used to correctly extract facets.
CATEGORY : new RegExp(CATEGORY_RE),
// Called to parse a query into a collection of `SearchFacet` models.
parse : function(instance, query) {
var searchFacets = this._extractAllFacets(instance, query);
instance.searchQuery.reset(searchFacets);
return searchFacets;
},
// Walks the query and extracts facets, categories, and free text.
_extractAllFacets : function(instance, query) {
var facets = [];
var originalQuery = query;
while (query) {
var category, value;
originalQuery = query;
var field = this._extractNextField(query);
if (!field) {
category = instance.options.remainder;
value = this._extractSearchText(query);
query = VS.utils.inflector.trim(query.replace(value, ''));
} else if (field.indexOf(':') != -1) {
category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
value = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
query = VS.utils.inflector.trim(query.replace(field, ''));
} else if (field.indexOf(':') == -1) {
category = instance.options.remainder;
value = field;
query = VS.utils.inflector.trim(query.replace(value, ''));
}
if (category && value) {
var searchFacet = new VS.model.SearchFacet({
category : category,
value : VS.utils.inflector.trim(value),
app : instance
});
facets.push(searchFacet);
}
if (originalQuery == query) break;
}
return facets;
},
// Extracts the first field found, capturing any free text that comes
// before the category.
_extractNextField : function(query) {
var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
var textMatch = query.match(textRe);
if (textMatch && textMatch.length >= 1) {
return textMatch[1];
} else {
return this._extractFirstField(query);
}
},
// If there is no free text before the facet, extract the category and value.
_extractFirstField : function(query) {
var fields = query.match(this.ALL_FIELDS);
return fields && fields.length && fields[0];
},
// If the found match is not a category and facet, extract the trimmed free text.
_extractSearchText : function(query) {
query = query || '';
var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
return text;
}
};
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// The model that holds individual search facets and their categories.
// Held in a collection by `VS.app.searchQuery`.
VS.model.SearchFacet = Backbone.Model.extend({
// Extract the category and value and serialize it in preparation for
// turning the entire searchBox into a search query that can be sent
// to the server for parsing and searching.
serialize : function() {
var category = this.quoteCategory(this.get('category'));
var value = VS.utils.inflector.trim(this.get('value'));
var remainder = this.get("app").options.remainder;
if (!value) return '';
if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
value = this.quoteValue(value);
}
if (category != remainder) {
category = category + ': ';
} else {
category = "";
}
return category + value;
},
// Wrap categories that have spaces or any kind of quote with opposite matching
// quotes to preserve the complex category during serialization.
quoteCategory : function(category) {
var hasDoubleQuote = (/"/).test(category);
var hasSingleQuote = (/'/).test(category);
var hasSpace = (/\s/).test(category);
if (hasDoubleQuote && !hasSingleQuote) {
return "'" + category + "'";
} else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
return '"' + category + '"';
} else {
return category;
}
},
// Wrap values that have quotes in opposite matching quotes. If a value has
// both single and double quotes, just use the double quotes.
quoteValue : function(value) {
var hasDoubleQuote = (/"/).test(value);
var hasSingleQuote = (/'/).test(value);
if (hasDoubleQuote && !hasSingleQuote) {
return "'" + value + "'";
} else {
return '"' + value + '"';
}
},
// If provided, use a custom label instead of the raw value.
label : function() {
return this.get('label') || this.get('value');
}
});
})();
(function() {
var $ = jQuery; // Handle namespaced jQuery
// Collection which holds all of the individual facets (category: value).
// Used for finding and removing specific facets.
VS.model.SearchQuery = Backbone.Collection.extend({
// Model holds the category and value of the facet.
model : VS.model.SearchFacet,
// Turns all of the facets into a single serialized string.
serialize : function() {
return this.map(function(facet){ return facet.serialize(); }).join(' ');
},
facets : function() {
return this.map(function(facet) {
var value = {};
value[facet.get('category')] = facet.get('value');
return value;
});
},
// Find a facet by its category. Multiple facets with the same category
// is fine, but only the first is returned.
find : function(category) {
var facet = this.detect(function(facet) {
return facet.get('category').toLowerCase() == category.toLowerCase();
});
return facet && facet.get('value');
},
// Counts the number of times a specific category is in the search query.
count : function(category) {
return this.select(function(facet) {
return facet.get('category').toLowerCase() == category.toLowerCase();
}).length;
},
// Returns an array of extracted values from each facet in a category.
values : function(category) {
var facets = this.select(function(facet) {
return facet.get('category').toLowerCase() == category.toLowerCase();
});
return _.map(facets, function(facet) { return facet.get('value'); });
},
// Checks all facets for matches of either a category or both category and value.
has : function(category, value) {
return this.any(function(facet) {
var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
if (!value) return categoryMatched;
return categoryMatched && facet.get('value') == value;
});
},
// Used to temporarily hide specific categories and serialize the search query.
withoutCategory : function() {
var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
return this.map(function(facet) {
if (!_.include(categories, facet.get('category').toLowerCase())) {
return facet.serialize();
};
}).join(' ');
}
});
})();
(function(){
window.JST = window.JST || {};
window.JST['search_box'] = _.template('<div class="VS-search">\n <div class="VS-search-box-wrapper VS-search-box">\n <div class="VS-icon VS-icon-search"></div>\n <div class="VS-placeholder"></div>\n <div class="VS-search-inner"></div>\n <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n </div>\n</div>');
window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n <input type="text" class="search_facet_input ui-menu VS-interface" value="" />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
window.JST['search_input'] = _.template('<input type="text" class="ui-menu" />');
})();
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment