diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index e995be8bc57..c6d5e8f85f1 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -35,6 +35,8 @@ "static/lib/qweb/qweb2.js", "static/lib/underscore/underscore.js", "static/lib/underscore/underscore.string.js", + "static/lib/backbone/backbone.js", + "static/lib/visualsearch/build/visualsearch.js", "static/lib/labjs/LAB.src.js", "static/lib/py.js/lib/py.js", "static/lib/novajs/src/nova.js", @@ -61,6 +63,7 @@ "static/lib/jquery.ui.timepicker/css/jquery-ui-timepicker-addon.css", "static/lib/jquery.ui.notify/css/ui.notify.css", "static/lib/jquery.tipsy/tipsy.css", + "static/lib/visualsearch/build/visualsearch-datauri.css", "static/src/css/base_old.css", "static/src/css/base.css", "static/src/css/data_export.css", diff --git a/addons/web/static/lib/backbone/backbone.js b/addons/web/static/lib/backbone/backbone.js new file mode 100644 index 00000000000..7c56f42363c --- /dev/null +++ b/addons/web/static/lib/backbone/backbone.js @@ -0,0 +1,1290 @@ +// Backbone.js 0.9.1 + +// (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, `global` + // 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 slice/splice. + var slice = Array.prototype.slice; + var splice = Array.prototype.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.1'; + + // 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. + var $ = root.jQuery || root.Zepto || root.ender; + + // Set the JavaScript library that will be used for DOM manipulation and + // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, + // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an + // alternate JavaScript library (or a mock library for testing your views + // outside of a browser). + Backbone.setDomLibrary = function(lib) { + $ = lib; + }; + + // 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 + // ----------------- + + // 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'); + // + Backbone.Events = { + + // Bind an event, specified by a string name, `ev`, to a `callback` + // function. Passing `"all"` will bind the callback to all events fired. + on: function(events, callback, context) { + var ev; + events = events.split(/\s+/); + var calls = this._callbacks || (this._callbacks = {}); + while (ev = events.shift()) { + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + var list = calls[ev] || (calls[ev] = {}); + var tail = list.tail || (list.tail = list.next = {}); + tail.callback = callback; + tail.context = context; + list.tail = tail.next = {}; + } + 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 `ev` is null, removes all bound callbacks for all events. + off: function(events, callback, context) { + var ev, calls, node; + if (!events) { + delete this._callbacks; + } else if (calls = this._callbacks) { + events = events.split(/\s+/); + while (ev = events.shift()) { + node = calls[ev]; + delete calls[ev]; + if (!callback || !node) continue; + // Create a new list, omitting the indicated event/context pairs. + while ((node = node.next) && node.next) { + if (node.callback === callback && + (!context || node.context === context)) continue; + this.on(ev, node.callback, node.context); + } + } + } + return this; + }, + + // Trigger an event, firing all bound callbacks. Callbacks are passed the + // same arguments as `trigger` is, apart from the event name. + // Listening for `"all"` passes the true event name as the first argument. + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) return this; + all = calls['all']; + (events = events.split(/\s+/)).push(null); + // Save references to the current heads & tails. + while (event = events.shift()) { + if (all) events.push({next: all.next, tail: all.tail, event: event}); + if (!(node = calls[event])) continue; + events.push({next: node.next, tail: node.tail}); + } + // Traverse each list, stopping when the saved tail is reached. + rest = slice.call(arguments, 1); + while (node = events.pop()) { + tail = node.tail; + args = node.event ? [node.event].concat(rest) : rest; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + return this; + } + + }; + + // Aliases for backwards compatibility. + Backbone.Events.bind = Backbone.Events.on; + Backbone.Events.unbind = Backbone.Events.off; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (options && options.parse) attributes = this.parse(attributes); + if (defaults = getValue(this, 'defaults')) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) this.collection = options.collection; + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + if (!this.set(attributes, {silent: true})) { + throw new Error("Can't create an invalid model"); + } + delete this._changed; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Backbone.Model.prototype, Backbone.Events, { + + // 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() { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.attributes[attr]; + return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.attributes[attr] != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, value, options) { + var attrs, attr, val; + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs instanceof Backbone.Model) attrs = attrs.attributes; + if (options.unset) for (attr in attrs) attrs[attr] = void 0; + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + var now = this.attributes; + var escaped = this._escapedAttributes; + var prev = this._previousAttributes || {}; + var alreadySetting = this._setting; + this._changed || (this._changed = {}); + this._setting = true; + + // Update attributes. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(now[attr], val)) delete escaped[attr]; + options.unset ? delete now[attr] : now[attr] = val; + if (this._changing && !_.isEqual(this._changed[attr], val)) { + this.trigger('change:' + attr, this, val, options); + this._moreChanges = true; + } + delete this._changed[attr]; + if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { + this._changed[attr] = val; + } + } + + // Fire the `"change"` events, if the model has been changed. + if (!alreadySetting) { + if (!options.silent && this.hasChanged()) this.change(options); + this._setting = 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) { + (options || (options = {})).unset = true; + return this.set(attr, null, options); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + (options || (options = {})).unset = true; + return this.set(_.clone(this.attributes), options); + }, + + // 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) : {}; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = Backbone.wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, '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, value, options) { + var attrs, current; + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + options = options ? _.clone(options) : {}; + if (options.wait) current = _.clone(this.attributes); + var silentOptions = _.extend({}, options, {silent: true}); + if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { + return false; + } + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + var serverAttrs = model.parse(resp, xhr); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (!model.set(serverAttrs, options)) return false; + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + options.error = Backbone.wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + var xhr = (this.sync || Backbone.sync).call(this, method, this, options); + if (options.wait) this.set(current, silentOptions); + 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 triggerDestroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + if (this.isNew()) return triggerDestroy(); + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + 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 = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || 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, xhr) { + 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; + }, + + // Call this method to manually fire a `"change"` event for this model and + // a `"change:attribute"` event for each changed attribute. + // Calling this will cause all objects observing the model to update. + change: function(options) { + if (this._changing || !this.hasChanged()) return this; + this._changing = true; + this._moreChanges = true; + for (var attr in this._changed) { + this.trigger('change:' + attr, this, this._changed[attr], options); + } + while (this._moreChanges) { + this._moreChanges = false; + this.trigger('change', this, options); + } + this._previousAttributes = _.clone(this.attributes); + delete this._changed; + this._changing = false; + return this; + }, + + // 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 (!arguments.length) return !_.isEmpty(this._changed); + return this._changed && _.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, old = this._previousAttributes; + 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 (!arguments.length || !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); + }, + + // Check if the model is currently in a valid state. It's only possible to + // get into an *invalid* state if you're using silent changes. + isValid: function() { + return !this.validate(this.attributes); + }, + + // Run validation against a set of incoming attributes, returning `true` + // if all is well. If a specific `error` callback has been passed, + // call that instead of firing the general `"error"` event. + _validate: function(attrs, options) { + if (options.silent || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validate(attrs, options); + if (!error) return true; + if (options && options.error) { + options.error(this, error, options); + } else { + this.trigger('error', 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. + Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.comparator) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, {silent: true, parse: options.parse}); + }; + + // Define the Collection's inheritable methods. + _.extend(Backbone.Collection.prototype, Backbone.Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Backbone.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() { + return this.map(function(model){ return model.toJSON(); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `add` event for every new model. + add: function(models, options) { + var i, index, length, model, cid, id, cids = {}, ids = {}; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + + // Begin by turning bare objects into model references, and preventing + // invalid models or duplicate models from being added. + for (i = 0, length = models.length; i < length; i++) { + if (!(model = models[i] = this._prepareModel(models[i], options))) { + throw new Error("Can't add an invalid model to a collection"); + } + if (cids[cid = model.cid] || this._byCid[cid] || + (((id = model.id) != null) && (ids[id] || this._byId[id]))) { + throw new Error("Can't add the same model to a collection twice"); + } + cids[cid] = ids[id] = model; + } + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + for (i = 0; i < length; i++) { + (model = models[i]).on('all', this._onModelEvent, this); + this._byCid[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // Insert models into the collection, re-sorting if needed, and triggering + // `add` events unless silenced. + this.length += length; + index = options.at != null ? options.at : this.models.length; + splice.apply(this.models, [index, 0].concat(models)); + if (this.comparator) this.sort({silent: true}); + if (options.silent) return this; + for (i = 0, length = this.models.length; i < length; i++) { + if (!cids[(model = this.models[i]).cid]) continue; + options.index = i; + model.trigger('add', model, this, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `remove` event for every model removed. + remove: function(models, options) { + var i, l, index, model; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + for (i = 0, l = models.length; i < l; i++) { + model = this.getByCid(models[i]) || this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byCid[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; + }, + + // Get a model from the set by id. + get: function(id) { + if (id == null) return null; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid: function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // 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) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var boundComparator = _.bind(this.comparator, this); + if (this.comparator.length == 1) { + this.models = this.sortBy(boundComparator); + } else { + this.models.sort(boundComparator); + } + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // 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) { + models || (models = []); + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + this._reset(); + this.add(models, {silent: true, parse: options.parse}); + 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 `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === undefined) options.parse = true; + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = Backbone.wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, '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) { + var coll = this; + options = options ? _.clone(options) : {}; + model = this._prepareModel(model, options); + if (!model) return false; + if (!options.wait) coll.add(model, options); + var success = options.success; + options.success = function(nextModel, resp, xhr) { + if (options.wait) coll.add(nextModel, options); + if (success) { + success(nextModel, resp); + } else { + nextModel.trigger('sync', 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, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(model, options) { + if (!(model instanceof Backbone.Model)) { + var attrs = model; + options.collection = this; + model = new this.model(attrs, options); + if (!model._validate(model.attributes, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + 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(ev, model, collection, options) { + if ((ev == 'add' || ev == 'remove') && collection != this) return; + if (ev == 'destroy') { + this.remove(model, options); + } + if (model && ev === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', + 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', + 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', + 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', + 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Backbone.Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // 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. + 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 namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Backbone.Router.prototype, Backbone.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) { + Backbone.history || (Backbone.history = new Backbone.History); + 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)); + 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); + }, + + // 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 routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .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. + Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + var historyStarted = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(Backbone.History.prototype, Backbone.Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // 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 || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + } else { + fragment = window.location.hash; + } + } + fragment = decodeURIComponent(fragment); + if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); + 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) { + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + if (historyStarted) throw new Error("Backbone.history has already been started"); + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + if (oldIE) { + this.iframe = $('').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) { + $(window).bind('popstate', this.checkUrl); + } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { + $(window).bind('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; + historyStarted = true; + var loc = window.location; + var atRoot = loc.pathname == this.options.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); + window.location.replace(this.options.root + '#' + 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 = loc.hash.replace(routeStripper, ''); + window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment); + } + + 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() { + $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl); + clearInterval(this._checkUrlInterval); + historyStarted = 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.iframe.location.hash); + if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false; + if (this.iframe) this.navigate(current); + this.loadUrl() || this.loadUrl(window.location.hash); + }, + + // 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 which to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!historyStarted) return false; + if (!options || options === true) options = {trigger: options}; + var frag = (fragment || '').replace(routeStripper, ''); + if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return; + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._hasPushState) { + if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag; + this.fragment = frag; + window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this.fragment = frag; + this._updateHash(window.location, frag, options.replace); + if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) { + // 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, frag, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + window.location.assign(this.options.root + fragment); + } + 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) { + location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment); + } else { + location.hash = fragment; + } + } + }); + + // Backbone.View + // ------------- + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + 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 eventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(Backbone.View.prototype, Backbone.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 from the DOM. Note that the view isn't present in the + // DOM by default, so calling this method may be a no-op. + remove: function() { + this.$el.remove(); + return this; + }, + + // For small amounts of DOM Elements, where a full-blown template isn't + // needed, use **make** to manufacture elements, one at a time. + // + // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); + // + make: function(tagName, attributes, content) { + var el = document.createElement(tagName); + if (attributes) $(el).attr(attributes); + if (content) $(el).html(content); + return el; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + this.$el = $(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 = getValue(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('Event "' + events[key] + '" does not exist'); + var match = key.match(eventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.bind(eventName, method); + } else { + this.$el.delegate(selector, eventName, 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.unbind('.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({}, this.options, options); + for (var i = 0, l = viewOptions.length; i < l; i++) { + var attr = viewOptions[i]; + if (options[attr]) this[attr] = options[attr]; + } + 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 = getValue(this, 'attributes') || {}; + if (this.id) attrs.id = this.id; + if (this.className) attrs['class'] = this.className; + this.setElement(this.make(this.tagName, attrs), false); + } else { + this.setElement(this.el, false); + } + } + + }); + + // The self-propagating extend function that Backbone classes use. + var extend = function (protoProps, classProps) { + var child = inherits(this, protoProps, classProps); + child.extend = this.extend; + return child; + }; + + // Set up inheritance for the model, collection, and view. + Backbone.Model.extend = Backbone.Collection.extend = + Backbone.Router.extend = Backbone.View.extend = extend; + + // Backbone.sync + // ------------- + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + '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 JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = getValue(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (!options.data && model && (method == 'create' || method == 'update')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(model.toJSON()); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (Backbone.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 (Backbone.emulateHTTP) { + if (type === 'PUT' || type === 'DELETE') { + if (Backbone.emulateJSON) params.data._method = type; + params.type = 'POST'; + params.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + }; + } + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !Backbone.emulateJSON) { + params.processData = false; + } + + // Make the request, allowing the user to override any Ajax options. + return $.ajax(_.extend(params, options)); + }; + + // Wrap an optional error callback with a fallback error event. + Backbone.wrapError = function(onError, originalModel, options) { + return function(model, resp) { + resp = model === originalModel ? resp : model; + if (onError) { + onError(originalModel, resp, options); + } else { + originalModel.trigger('error', originalModel, resp, options); + } + }; + }; + + // Helpers + // ------- + + // Shared empty constructor function to aid in prototype-chain creation. + var ctor = function(){}; + + // 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 inherits = function(parent, protoProps, staticProps) { + 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 && protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function(){ parent.apply(this, arguments); }; + } + + // Inherit class (static) properties from parent. + _.extend(child, parent); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if (protoProps) _.extend(child.prototype, protoProps); + + // Add static properties to the constructor function, if supplied. + if (staticProps) _.extend(child, staticProps); + + // Correctly set child's `prototype.constructor`. + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed later. + child.__super__ = parent.prototype; + + return child; + }; + + // Helper function to get a value from a Backbone object as a property + // or as a function. + var getValue = function(object, prop) { + if (!(object && object[prop])) return null; + return _.isFunction(object[prop]) ? object[prop]() : object[prop]; + }; + + // 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); diff --git a/addons/web/static/lib/visualsearch/LICENSE b/addons/web/static/lib/visualsearch/LICENSE new file mode 100755 index 00000000000..fe0ea1154c4 --- /dev/null +++ b/addons/web/static/lib/visualsearch/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2011 Samuel Clay, @samuelclay, DocumentCloud + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/addons/web/static/lib/visualsearch/README b/addons/web/static/lib/visualsearch/README new file mode 100755 index 00000000000..c059a9824d6 --- /dev/null +++ b/addons/web/static/lib/visualsearch/README @@ -0,0 +1,16 @@ + __ ___ _ _____ _ _ + \ \ / (_) | |/ ____| | | (_) + \ \ / / _ ___ _ _ __ _| | (___ ___ __ _ _ __ ___| |__ _ ___ + \ \/ / | / __| | | |/ _` | |\___ \ / _ \/ _` | '__/ __| '_ \ | / __| + \ / | \__ \ |_| | (_| | |____) | __/ (_| | | | (__| | | |_| \__ \ + \/ |_|___/\__,_|\__,_|_|_____/ \___|\__,_|_| \___|_| |_(_) |___/ + _/ | + |__/ + +VisualSearch.js enhances ordinary search boxes with the ability to autocomplete +faceted search queries. Specify the facets for completion, along with the +completable values for any facet. You can retrieve the search query as a +structured object, so you don't have to parse the query string yourself. + +For documentation, pre-packed downloads, demos, and tests, see: +http://documentcloud.github.com/visualsearch \ No newline at end of file diff --git a/addons/web/static/lib/visualsearch/Rakefile b/addons/web/static/lib/visualsearch/Rakefile new file mode 100755 index 00000000000..192a50b944e --- /dev/null +++ b/addons/web/static/lib/visualsearch/Rakefile @@ -0,0 +1,37 @@ +require 'rubygems' +require 'jammit' +require 'fileutils' + +desc "Use Jammit to compile the multiple versions of Visual Search" +task :build do + $VS_MIN = false + Jammit.package!({ + :config_path => "assets.yml", + :output_folder => "build" + }) + + $VS_MIN = true + Jammit.package!({ + :config_path => "assets.yml", + :output_folder => "build-min" + }) + + # Move the JSTs back to lib to accomodate the demo page. + FileUtils.mv("build/visualsearch_templates.js", "lib/js/templates/templates.js") + + # Fix image url paths. + ['build', 'build-min'].each do |build| + File.open("#{build}/visualsearch.css", 'r+') do |file| + css = file.read + css.gsub!(/url\((.*?)images\/embed\/icons/, 'url(../images/embed/icons') + file.rewind + file.write(css) + file.truncate(css.length) + end + end +end + +desc "Build the docco documentation" +task :docs do + sh "docco lib/js/*.js lib/js/**/*.js" +end diff --git a/addons/web/static/lib/visualsearch/assets.yml b/addons/web/static/lib/visualsearch/assets.yml new file mode 100755 index 00000000000..357c37c74aa --- /dev/null +++ b/addons/web/static/lib/visualsearch/assets.yml @@ -0,0 +1,28 @@ +embed_assets: datauri +javascript_compressor: closure +template_function: _.template +gzip_assets: <% if $VS_MIN %>on<% else %>off<% end %> +compress_assets: <% if $VS_MIN %>on<% else %>off<% end %> + +javascripts: + dependencies: + - vendor/jquery-*.js + - vendor/jquery.ui.core.js + - vendor/jquery.ui.widget.js + - vendor/jquery.ui.position.js + - vendor/jquery.ui.*.js + - vendor/underscore-*.js + - vendor/backbone-*.js + visualsearch: + - lib/js/visualsearch.js + - lib/js/views/*.js + - lib/js/utils/*.js + - lib/js/models/*.js + - lib/js/templates/*.jst + <% unless $VS_MIN %>visualsearch_templates: + - lib/js/templates/*.jst + <% end %> + +stylesheets: + visualsearch: + - lib/css/*.css \ No newline at end of file diff --git a/addons/web/static/lib/visualsearch/build/visualsearch-datauri.css b/addons/web/static/lib/visualsearch/build/visualsearch-datauri.css new file mode 100755 index 00000000000..1ae6ed28215 --- /dev/null +++ b/addons/web/static/lib/visualsearch/build/visualsearch-datauri.css @@ -0,0 +1,297 @@ +.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("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAWCAYAAAAW5GZjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAb9JREFUeNqUUr1qAkEQ3j0khQp6kihaeGgEEa18gTQR0iRY+BaBSMDGwidIEUKqFL6BopgqBAJ5AMFGjUU0d4WHEvwJarvZ77gRIzGYgb1hZr+Z75vZ40IIzqTNZrPj8Xicn0wmmcViEXS73aaqqq+BQODG6/W+A8MBNk3zfDAY3C6Xy0O2ZS6X6zMSiVwHg8FHLjtq7Xb7RQKj7BeTzVCgJ5PJU2U0GhUk7REuMpkMi8fjFggeMeecrVYrFRId0CgTAgDDMFg4HLbA8IjJgHNgGEr0er0fQIphUmZAwdSUADUB4RFDsz3oSMF6CLzZkQqgGebz+Z75dDqNdTqdp13bgDmdTj2VSp0oWHg0Gr2UNH2Z/9o+yMv7K4/HY/C/XhDUfr//jl7QQVT9fp/V63VWqVRYt9tliUSCZbPZg1wux9Lp9PqFeK1Wu9A0DdXz7YM87i0FrVZLs4Fi1wmFQh/NZjOmVKvVgq7rR/QflMtlixGedjwcDlUpMQ9tbzalkAAB2/R297mNW+sT2wUbUnA//V/nYrH4QOBNABUQuFQq3TNMuc82sDVrz41G42yvPeODAwZQ0QzwiJEnzLcAAwBJ6WXlwoBgZAAAAABJRU5ErkJggg=="); + cursor: pointer; + } + .VS-search .VS-icon-cancel:hover { + background-position: center -11px; + } + .VS-search .VS-icon-search { + width: 12px; height: 12px; + background-image: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUZJREFUeNpUUM2qgmAQzS8NiUgLzTIXLZQW1QuI9AY9QPSW9gQ9QiriwpJQEBVrVWT2d7p2L9xZzDdzZs7M+YYqy/J8Ptu2vd/v4zgeDAaqqk4mE47jar9GnU6nzWbjOA5FUa/Xq0Jns9l8Pud5vkpp58cwAOzhcBhFkeu6GNztdg3D+Db5vo9nOp2iiWGYTqdDCMFe4LquI0aVpGmKR9M0lmUbjQY8YiBJklTb4YkoilBzOBzq9TogeMQIJEmqmlAlo9EIyXa7tSyrKAp4xEBkWUb5q2k8Hh+PR8/zwjCEgufz+aESstvtoKnVan2GgY31kBkEAfT1ej1FUZDiNIIgrFYr9H1ug3teLpfH43G/3/FBUJGu1+s8z8FZLpc0mmiabrfbf5fEumazuVgsTNO8Xq+3242qRNT+G0CMz7IMzH6//xZgAA60tj6rqzxpAAAAAElFTkSuQmCC"); + } + +/*------------------------------ RESET + DEFAULT STYLES ---------------------------------*/ + +/* +Eric Meyer's final reset.css +Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ +*/ +.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; +} + +/* ===================== */ +/* = General and Reset = */ +/* ===================== */ + +.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; +} + +/* ========== */ +/* = Layout = */ +/* ========== */ + +.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 0px 0px 3px #ccc; + -moz-box-shadow: inset 0px 0px 3px #ccc; + box-shadow: inset 0px 0px 3px #ccc; + min-height: 28px; + height: auto; +} + .VS-search .VS-search-box.VS-focus { + border-color: #acf; + -webkit-box-shadow: inset 0px 0px 3px #acf; + -moz-box-shadow: inset 0px 0px 3px #acf; + box-shadow: inset 0px 0px 3px #acf; + } + .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; + } + +/* ================ */ +/* = Search Facet = */ +/* ================ */ + +.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); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#d2e6fd), to(#b0d1f9)); /* Saf4+, Chrome */ + 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: 0.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; + } + +/* ================ */ +/* = Search Input = */ +/* ================ */ + +.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; + z-index: 100; + min-width: 4px; + position: relative; + } + .VS-search .search_input.is_editing input { + color: #202020; + } + +/* ================ */ +/* = Autocomplete = */ +/* ================ */ + +.VS-interface.ui-autocomplete { + position: absolute; + border: 1px solid #C0C0C0; + border-top: 1px solid #D9D9D9; + background-color: #F6F6F6; + cursor: pointer; + z-index: 10000; + width: auto; + min-width: 80px; + max-width: 220px; + max-height: 240px; + overflow-y: auto; + overflow-x: hidden; + font-size: 13px; + top: 5px; + opacity: 0.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 a { + color: #000; + outline: none; + display: block; + padding: 3px 4px 5px; + background-color: #F8F8F8; + background-image: -moz-linear-gradient(top, #F8F8F8, #F3F3F3); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#F8F8F8), to(#F3F3F3)); /* Saf4+, Chrome */ + 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 { + background-color: #6483F7; + background-image: -moz-linear-gradient(top, #648bF5, #2465f3); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#648bF5), to(#2465f3)); /* Saf4+, Chrome */ + background-image: linear-gradient(top, #648bF5, #2465f3); + border-top: 1px solid #5b83ec; + border-bottom: 1px solid #1459e9; + color: white; + } + .VS-interface.ui-autocomplete li { + list-style: none; + width: auto; + } diff --git a/addons/web/static/lib/visualsearch/build/visualsearch.css b/addons/web/static/lib/visualsearch/build/visualsearch.css new file mode 100755 index 00000000000..f4f404388c3 --- /dev/null +++ b/addons/web/static/lib/visualsearch/build/visualsearch.css @@ -0,0 +1,297 @@ +.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/embed/icons/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/embed/icons/search_glyph.png?1311104738); + } + +/*------------------------------ RESET + DEFAULT STYLES ---------------------------------*/ + +/* +Eric Meyer's final reset.css +Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ +*/ +.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; +} + +/* ===================== */ +/* = General and Reset = */ +/* ===================== */ + +.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; +} + +/* ========== */ +/* = Layout = */ +/* ========== */ + +.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 0px 0px 3px #ccc; + -moz-box-shadow: inset 0px 0px 3px #ccc; + box-shadow: inset 0px 0px 3px #ccc; + min-height: 28px; + height: auto; +} + .VS-search .VS-search-box.VS-focus { + border-color: #acf; + -webkit-box-shadow: inset 0px 0px 3px #acf; + -moz-box-shadow: inset 0px 0px 3px #acf; + box-shadow: inset 0px 0px 3px #acf; + } + .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; + } + +/* ================ */ +/* = Search Facet = */ +/* ================ */ + +.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); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#d2e6fd), to(#b0d1f9)); /* Saf4+, Chrome */ + 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: 0.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; + } + +/* ================ */ +/* = Search Input = */ +/* ================ */ + +.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; + z-index: 100; + min-width: 4px; + position: relative; + } + .VS-search .search_input.is_editing input { + color: #202020; + } + +/* ================ */ +/* = Autocomplete = */ +/* ================ */ + +.VS-interface.ui-autocomplete { + position: absolute; + border: 1px solid #C0C0C0; + border-top: 1px solid #D9D9D9; + background-color: #F6F6F6; + cursor: pointer; + z-index: 10000; + width: auto; + min-width: 80px; + max-width: 220px; + max-height: 240px; + overflow-y: auto; + overflow-x: hidden; + font-size: 13px; + top: 5px; + opacity: 0.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 a { + color: #000; + outline: none; + display: block; + padding: 3px 4px 5px; + background-color: #F8F8F8; + background-image: -moz-linear-gradient(top, #F8F8F8, #F3F3F3); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#F8F8F8), to(#F3F3F3)); /* Saf4+, Chrome */ + 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 { + background-color: #6483F7; + background-image: -moz-linear-gradient(top, #648bF5, #2465f3); /* FF3.6 */ + background-image: -webkit-gradient(linear, left top, left bottom, from(#648bF5), to(#2465f3)); /* Saf4+, Chrome */ + background-image: linear-gradient(top, #648bF5, #2465f3); + border-top: 1px solid #5b83ec; + border-bottom: 1px solid #1459e9; + color: white; + } + .VS-interface.ui-autocomplete li { + list-style: none; + width: auto; + } diff --git a/addons/web/static/lib/visualsearch/build/visualsearch.js b/addons/web/static/lib/visualsearch/build/visualsearch.js new file mode 100755 index 00000000000..0fc6eeb2c75 --- /dev/null +++ b/addons/web/static/lib/visualsearch/build/visualsearch.js @@ -0,0 +1,1818 @@ +// 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.2.1 + * (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.2.1'; + + VS.VisualSearch = function(options) { + var defaults = { + container : '', + query : '', + unquotable : [], + 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}); + + 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'); + this.app.searchQuery.bind('reset', this.renderFacets); + $(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(' '); + }, + + // 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}); + this.renderFacets(); + var facetView = _.detect(this.facetViews, function(view) { + if (view.model == model) return true; + }); + + _.defer(function() { + facetView.enableEdit(); + }); + }, + + // Renders each facet as a searchFacet view. + renderFacets : function() { + this.facetViews = []; + this.inputViews = []; + + this.$('.VS-search-inner').empty(); + + this.app.searchQuery.each(_.bind(function(facet, i) { + this.renderFacet(facet, i); + }, this)); + + // Add on an n+1 empty search input on the very end. + this.renderSearchInput(); + }, + + // 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}); + this.$('.VS-search-inner').append(input.render().el); + this.inputViews.push(input); + }, + + // # 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. + viewPosition = 0; + direction = 0; + } + 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(); + }, + + 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.get('value')); + 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) { + this.search(e); + } + 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); + if ($value.data('item.autocomplete')['value'] == box.val()) { + box.data('autocomplete').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('autocomplete'); + if (autocomplete) { + autocomplete.menu.element.position({ + my : "left top", + at : "left bottom", + of : this.box.data('autocomplete').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('autocomplete'); + 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('autocomplete'); + 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) { + 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.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(); + } + } + + this.resize(e); + + // Handle paste events + if (e.which == null) { + this.searchAutocomplete(e); + _.defer(_.bind(this.resize, this, 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', + + 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 : 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(); + e.stopPropagation(); + var remainder = this.addTextFacetRemainder(ui.item.value); + var position = this.options.position + (remainder ? 1 : 0); + this.app.searchBox.addFacet(ui.item.value, '', position); + return false; + }, this) + }); + + // Renders the results grouped by the categories they belong to. + this.box.data('autocomplete')._renderMenu = function(ul, items) { + var category = ''; + _.each(items, _.bind(function(item, i) { + if (item.category && item.category != category) { + ul.append('
+ VisualSearch.js + enhances ordinary search boxes with the ability to autocomplete + faceted search queries. Specify the facets for completion, along with the + completable values for any facet. You can retrieve the search query as a + structured object, so you don't have to parse the query string yourself. +
+ ++ The complete annotated source code + is also available. +
+ ++ The project is + hosted on GitHub. + You can report bugs and discuss features on the + issues page, + on Freenode in the #documentcloud channel, + or send tweets to @documentcloud. +
+ ++ VisualSearch.js is an open-source component of DocumentCloud. +
++ + Created by Samuel Clay, + @samuelclay. + +
+ ++ VisualSearch.js + enhances ordinary search boxes with the ability to autocomplete + faceted search queries. Specify the facets for completion, along with the + completable values for any facet. You can retrieve the search query as a + structured object, so you don't have to parse the query string yourself. +
+ ++ Here's an example of a search on DocumentCloud.org that uses facets. +
+ ++ The project is + hosted on GitHub. + You can report bugs and discuss features on the + issues page, + on Freenode in the #documentcloud channel, + or send tweets to @documentcloud. +
+ +
+ VisualSearch.js is an open-source component of DocumentCloud.
+
+ The complete annotated source code
+ is also available.
+
| 0. Everything (visualsearch.zip) | +|
|---|---|
| Download everything | +|
| 1. VisualSearch JavaScript (visualsearch.js) | +|
| Production Version (0.2.1) | +8kb, Minified and Gzipped | +
| Development Version (0.2.1) | +45kb, Uncompressed with Comments | +
| 2. VisualSearch Stylesheets (visualsearch.css) | +|
| You should include both the datauri and image urls versions. See how to include both | +|
| Production Version - datauri | +4kb, Minified and Gzipped | +
| Production Version - image urls | +4kb, Minified and Gzipped | +
| Development Version | +8kb, Uncompressed with Comments | +
| 3. VisualSearch Images | +|
| 4kb, embedded in visualsearch-datauri.css | +|
| 4kb, embedded in visualsearch-datauri.css | +|
| 4. VisualSearch Dependencies (jQuery 1.4+, jQuery UI, Underscore.js, Backbone.js) | +|
| You should only include the dependencies you don't already have. | +|
| Production Version - All | +49kb, Minified and Gzipped | +
| Development Version - All | +340kb, Uncompressed with Comments | +
| jQuery 1.6.1 | +238kb, Uncompressed with Comments | +
| jQuery UI 1.8.13: Core Position Widget Autocomplete |
+ 48kb, Uncompressed with Comments | +
| Underscore 1.1.5 | +29kb, Uncompressed with Comments | +
| Backbone 0.5.0 | +41kb, Uncompressed with Comments | +
To use VisualSearch.js on your site, follow these instructions on installation, configuration, and customization.
+ +<script src="visualsearch.js" type="text/javascript"></script>
+<!--[if (!IE)|(gte IE 8)]><!-->
+ <link href="visualsearch-datauri.css" media="screen" rel="stylesheet" type="text/css" />
+<!--<![endif]-->
+<!--[if lte IE 7]><!-->
+ <link href="visualsearch.css" media="screen" rel="stylesheet" type="text/css" />
+<!--<![endif]-->
+ <div class="visual_search"></div>
+
+<script type="text/javascript" charset="utf-8">
+ $(document).ready(function() {
+ var visualSearch = VS.init({
+ container : $('.visual_search'),
+ query : '',
+ callbacks : {
+ search : function(query, searchCollection) {},
+ facetMatches : function(callback) {},
+ valueMatches : function(facet, searchTerm, callback) {}
+ }
+ });
+ });
+</script>
+ callbacks : {
+ ...
+ // These are the facets that will be autocompleted in an empty input.
+ facetMatches : function(callback) {
+ callback([
+ 'account', 'filter', 'access', 'title',
+ { label: 'city', category: 'location' },
+ { label: 'address', category: 'location' },
+ { label: 'country', category: 'location' },
+ { label: 'state', category: 'location' },
+ ]);
+ }
+ ...
+ // These are the values that match specific categories, autocompleted
+ // in a category's input field. searchTerm can be used to filter the
+ // list on the server-side, prior to providing a list to the widget.
+ valueMatches : function(facet, searchTerm, callback) {
+ switch (facet) {
+ case 'account':
+ callback([
+ { value: '1-amanda', label: 'Amanda' },
+ { value: '2-aron', label: 'Aron' },
+ { value: '3-eric', label: 'Eric' },
+ { value: '4-jeremy', label: 'Jeremy' },
+ { value: '5-samuel', label: 'Samuel' },
+ { value: '6-scott', label: 'Scott' }
+ ]);
+ break;
+ case 'filter':
+ callback(['published', 'unpublished', 'draft']);
+ break;
+ case 'access':
+ callback(['public', 'private', 'protected']);
+ break;
+ case 'title':
+ callback([
+ 'Pentagon Papers',
+ 'CoffeeScript Manual',
+ 'Laboratory for Object Oriented Thinking',
+ 'A Repository Grows in Brooklyn'
+ ]);
+ break;
+ }
+ }
+...
+}
+ // Returns the unstructured search query
+visualSearch.searchBox.value()
+// "country: "South Africa" account: 5-samuel title: "Pentagon Papers""
+
+// Returns an array of Facet model instances
+visualSearch.searchQuery.facets()
+// [FacetModel<country:"South Africa">,
+// FacetModel<account:5-samuel>,
+// FacetModel<title:"Pentagon Papers">]
+
+
+ 0.2.1 November 14th, 2011
+ The autocompleted facets and values that are provided by your callbacks facetMatches
+ and valueMatches can now preserve the order of items you give them. Simply pass an
+ options hash with preserveOrder: true as the second argument to the callback. See
+ the demo page for an example.
+
+ 0.2.0 August 10th, 2011
+ Multiple instances of VisualSearch on a single page. VS.init now returns
+ a reference to the instance. The search callback now contains both the
+ serialized search query and a reference to the search query collection (as a
+ Backbone.Collection),
+ which can be used to manipulate each facet directly. See
+ the source code for search_query.js for available
+ methods on the collection.
+
+ 0.1.0 June 23rd, 2011
+ Initial release of VisualSearch.js.
+
+
+
+
+