Modified: incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/backbone.js URL: http://svn.apache.org/viewvc/incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/backbone.js?rev=1294730&r1=1294729&r2=1294730&view=diff ============================================================================== --- incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/backbone.js (original) +++ incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/backbone.js Tue Feb 28 16:56:02 2012 @@ -1,4 +1,4 @@ -// Backbone.js 0.3.3 +// Backbone.js 0.5.3 // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: @@ -9,26 +9,39 @@ // Initial Setup // ------------- + // Save a reference to the global object. + var root = this; + + // Save the previous value of the `Backbone` variable. + var previousBackbone = root.Backbone; + // 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 = this.Backbone = {}; + Backbone = root.Backbone = {}; } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.3.3'; + Backbone.VERSION = '0.5.3'; // Require Underscore, if we're on the server, and it's not already present. - var _ = this._; - if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; - // For Backbone's purposes, either jQuery or Zepto owns the `$` variable. - var $ = this.jQuery || this.Zepto; + // For Backbone's purposes, jQuery or Zepto owns the `$` variable. + var $ = root.jQuery || root.Zepto; - // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will + // 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; @@ -55,10 +68,10 @@ // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. - bind : function(ev, callback) { + bind : function(ev, callback, context) { var calls = this._callbacks || (this._callbacks = {}); - var list = this._callbacks[ev] || (this._callbacks[ev] = []); - list.push(callback); + var list = calls[ev] || (calls[ev] = []); + list.push([callback, context]); return this; }, @@ -76,8 +89,8 @@ var list = calls[ev]; if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { - if (callback === list[i]) { - list.splice(i, 1); + if (list[i] && callback === list[i][0]) { + list[i] = null; break; } } @@ -89,17 +102,21 @@ // 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(ev) { - var list, calls, i, l; + trigger : function(eventName) { + var list, calls, ev, callback, args; + var both = 2; if (!(calls = this._callbacks)) return this; - if (list = calls[ev]) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - if (list = calls['all']) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, arguments); + while (both--) { + ev = both ? eventName : 'all'; + if (list = calls[ev]) { + for (var i = 0, l = list.length; i < l; i++) { + if (!(callback = list[i])) { + list.splice(i, 1); i--; l--; + } else { + args = both ? Array.prototype.slice.call(arguments, 1) : arguments; + callback[0].apply(callback[1] || this, args); + } + } } } return this; @@ -113,12 +130,17 @@ // 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 (this.defaults) attributes = _.extend({}, this.defaults, attributes); + if (defaults = this.defaults) { + if (_.isFunction(defaults)) defaults = defaults.call(this); + attributes = _.extend({}, defaults, attributes); + } this.attributes = {}; this._escapedAttributes = {}; this.cid = _.uniqueId('c'); this.set(attributes, {silent : true}); + this._changed = false; this._previousAttributes = _.clone(this.attributes); if (options && options.collection) this.collection = options.collection; this.initialize(attributes, options); @@ -134,6 +156,10 @@ // Has the item been changed since the last `"change"` event? _changed : false, + // 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(){}, @@ -153,7 +179,13 @@ var html; if (html = this._escapedAttributes[attr]) return html; var val = this.attributes[attr]; - return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val); + return this._escapedAttributes[attr] = escapeHTML(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 @@ -170,7 +202,11 @@ if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; // Check for changes of `id`. - if ('id' in attrs) this.id = attrs.id; + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // We're about to start triggering change events. + var alreadyChanging = this._changing; + this._changing = true; // Update attributes. for (var attr in attrs) { @@ -178,21 +214,21 @@ if (!_.isEqual(now[attr], val)) { now[attr] = val; delete escaped[attr]; - if (!options.silent) { - this._changed = true; - this.trigger('change:' + attr, this, val, options); - } + this._changed = true; + if (!options.silent) this.trigger('change:' + attr, this, val, options); } } // Fire the `"change"` event, if the model has been changed. - if (!options.silent && this._changed) this.change(options); + if (!alreadyChanging && !options.silent && this._changed) this.change(options); + this._changing = false; return this; }, // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. + // to silence it. `unset` is a noop if the attribute doesn't exist. unset : function(attr, options) { + if (!(attr in this.attributes)) return this; options || (options = {}); var value = this.attributes[attr]; @@ -204,8 +240,9 @@ // Remove the attribute. delete this.attributes[attr]; delete this._escapedAttributes[attr]; + if (attr == this.idAttribute) delete this.id; + this._changed = true; if (!options.silent) { - this._changed = true; this.trigger('change:' + attr, this, void 0, options); this.change(options); } @@ -216,6 +253,7 @@ // to silence it. clear : function(options) { options || (options = {}); + var attr; var old = this.attributes; // Run validation. @@ -225,8 +263,8 @@ this.attributes = {}; this._escapedAttributes = {}; + this._changed = true; if (!options.silent) { - this._changed = true; for (attr in old) { this.trigger('change:' + attr, this, void 0, options); } @@ -241,13 +279,13 @@ fetch : function(options) { options || (options = {}); var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); + 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); }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; + options.error = 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. @@ -257,42 +295,43 @@ options || (options = {}); if (attrs && !this.set(attrs, options)) return false; var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); + 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, xhr); }; - var error = wrapError(options.error, model, options); + options.error = wrapError(options.error, model, options); var method = this.isNew() ? 'create' : 'update'; - (this.sync || Backbone.sync)(method, this, success, error); - return this; + return (this.sync || Backbone.sync).call(this, method, this, options); }, - // Destroy this model on the server. Upon success, the model is removed + // Destroy this model on the server if it was already persisted. Upon success, the model is removed // from its collection, if it has one. destroy : function(options) { options || (options = {}); + if (this.isNew()) return this.trigger('destroy', this, this.collection, options); var model = this; - var success = function(resp) { - if (model.collection) model.collection.remove(model); - if (options.success) options.success(model, resp); + var success = options.success; + options.success = function(resp) { + model.trigger('destroy', model, model.collection, options); + if (success) success(model, resp); }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('delete', this, success, error); - return this; + options.error = wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'delete', this, options); }, // 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 = getUrl(this.collection); + var base = getUrl(this.collection) || this.urlRoot || urlError(); if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; + 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) { + parse : function(resp, xhr) { return resp; }, @@ -301,10 +340,9 @@ return new this.constructor(this); }, - // A model is new if it has never been saved to the server, and has a negative - // ID. + // A model is new if it has never been saved to the server, and lacks an id. isNew : function() { - return !this.id; + return this.id == null; }, // Call this method to manually fire a `change` event for this model. @@ -359,7 +397,7 @@ var error = this.validate(attrs); if (error) { if (options.error) { - options.error(this, error); + options.error(this, error, options); } else { this.trigger('error', this, error, options); } @@ -378,14 +416,11 @@ // 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; - delete options.comparator; - } - this._boundOnModelEvent = _.bind(this._onModelEvent, this); + if (options.comparator) this.comparator = options.comparator; + _.bindAll(this, '_onModelEvent', '_removeReference'); this._reset(); - if (models) this.refresh(models, {silent: true}); - this.initialize(models, options); + if (models) this.reset(models, {silent: true}); + this.initialize.apply(this, arguments); }; // Define the Collection's inheritable methods. @@ -453,7 +488,7 @@ options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, @@ -463,51 +498,53 @@ }, // When you have more items than you want to add or remove individually, - // you can refresh the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `refresh` when finished. - refresh : function(models, options) { + // you can reset the entire set with a new list of models, without firing + // any `added` or `removed` events. Fires `reset` when finished. + reset : function(models, options) { models || (models = []); options || (options = {}); + this.each(this._removeReference); this._reset(); this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, - // Fetch the default set of models for this collection, refreshing the - // collection when they arrive. + // 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 = {}); var collection = this; - var success = function(resp) { - collection.refresh(collection.parse(resp)); - if (options.success) options.success(collection, resp); + 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); }; - var error = wrapError(options.error, collection, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; + options.error = 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. After the model // has been created on the server, it will be added to the collection. + // Returns the model, or 'false' if validation on a new model fails. create : function(model, options) { var coll = this; options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: coll}); - } else { - model.collection = coll; - } - var success = function(nextModel, resp) { - coll.add(nextModel); - if (options.success) options.success(nextModel, resp); + model = this._prepareModel(model, options); + if (!model) return false; + var success = options.success; + options.success = function(nextModel, resp, xhr) { + coll.add(nextModel, options); + if (success) success(nextModel, resp, xhr); }; - return model.save(null, {success : success, error : options.error}); + 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) { + parse : function(resp, xhr) { return resp; }, @@ -518,7 +555,7 @@ return _(this.models).chain(); }, - // Reset all internal state. Called when the collection is refreshed. + // Reset all internal state. Called when the collection is reset. _reset : function(options) { this.length = 0; this.models = []; @@ -526,21 +563,34 @@ this._byCid = {}; }, + // Prepare a model to be added to this collection + _prepareModel: function(model, options) { + if (!(model instanceof Backbone.Model)) { + var attrs = model; + model = new this.model(attrs, {collection: this}); + if (model.validate && !model._performValidation(attrs, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + // Internal implementation of adding a single model to the set, updating // hash indexes for `id` and `cid` lookups. + // Returns the model, or 'false' if validation on a new model fails. _add : function(model, options) { options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: this}); - } + model = this._prepareModel(model, options); + if (!model) return false; var already = this.getByCid(model); if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; - model.collection = this; - var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; + var index = options.at != null ? options.at : + this.comparator ? this.sortedIndex(model, this.comparator) : + this.length; this.models.splice(index, 0, model); - model.bind('all', this._boundOnModelEvent); + model.bind('all', this._onModelEvent); this.length++; if (!options.silent) model.trigger('add', model, this, options); return model; @@ -554,20 +604,32 @@ if (!model) return null; delete this._byId[model.id]; delete this._byCid[model.cid]; - delete model.collection; this.models.splice(this.indexOf(model), 1); this.length--; if (!options.silent) model.trigger('remove', model, this, options); - model.unbind('all', this._boundOnModelEvent); + this._removeReference(model); return model; }, + // Internal method to remove a model's ties to a collection. + _removeReference : function(model) { + if (this == model.collection) { + delete model.collection; + } + model.unbind('all', this._onModelEvent); + }, + // 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. - _onModelEvent : function(ev, model) { - if (ev === 'change:id') { - delete this._byId[model.previous('id')]; + // 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); @@ -578,8 +640,8 @@ // 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', - 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', - 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; + 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', + 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { @@ -588,25 +650,26 @@ }; }); - // Backbone.Controller + // Backbone.Router // ------------------- - // Controllers map faux-URLs to actions, and fire events when routes are + // 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.Controller = function(options) { + Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); - this.initialize(options); + this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. - var namedParam = /:([\w\d]+)/g; - var splatParam = /\*([\w\d]+)/g; + var namedParam = /:([\w\d]+)/g; + var splatParam = /\*([\w\d]+)/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; - // Set up all inheritable **Backbone.Controller** properties and methods. - _.extend(Backbone.Controller.prototype, Backbone.Events, { + // 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. @@ -628,25 +691,31 @@ }, this)); }, - // Simple proxy to `Backbone.history` to save a fragment into the history, - // without triggering routes. - saveLocation : function(fragment) { - Backbone.history.saveLocation(fragment); + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate : function(fragment, triggerRoute) { + Backbone.history.navigate(fragment, triggerRoute); }, - // Bind all defined routes to `Backbone.history`. + // 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) { - var name = this.routes[route]; - this.route(route, name, this[name]); + 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 fragment. + // against the current location hash. _routeToRegExp : function(route) { - route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); + route = route.replace(escapeRegExp, "\\$&") + .replace(namedParam, "([^\/]*)") + .replace(splatParam, "(.*?)"); return new RegExp('^' + route + '$'); }, @@ -661,17 +730,22 @@ // Backbone.History // ---------------- - // Handles cross-browser history management, based on URL hashes. If the + // 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 = []; - this.fragment = this.getFragment(); _.bindAll(this, 'checkUrl'); }; // Cached regex for cleaning hashes. var hashStrip = /^#*/; + // 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, { @@ -679,53 +753,92 @@ // twenty times a second. interval: 50, - // Get the cross-browser normalized URL fragment. - getFragment : function(loc) { - return (loc || window.location).hash.replace(hashStrip, ''); + // 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; + if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); + } else { + fragment = window.location.hash; + } + } + return decodeURIComponent(fragment.replace(hashStrip, '')); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. - start : function() { - var docMode = document.documentMode; - var oldIE = ($.browser.msie && (!docMode || docMode <= 7)); + 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._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 = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; + this.navigate(fragment); } - if ('onhashchange' in window && !oldIE) { + + // 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 ('onhashchange' in window && !oldIE) { $(window).bind('hashchange', this.checkUrl); } else { setInterval(this.checkUrl, this.interval); } - return this.loadUrl(); + + // 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 (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; + } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { + this.fragment = loc.hash.replace(hashStrip, ''); + window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment); + } + + if (!this.options.silent) { + return this.loadUrl(); + } }, - // Add a route to be tested when the hash changes. Routes are matched in the - // order they are added. + // Add a route to be tested when the fragment changes. Routes added later may + // override previous routes. route : function(route, callback) { - this.handlers.push({route : route, callback : 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() { + checkUrl : function(e) { var current = this.getFragment(); - if (current == this.fragment && this.iframe) { - current = this.getFragment(this.iframe.location); - } - if (current == this.fragment || - current == decodeURIComponent(this.fragment)) return false; - if (this.iframe) { - window.location.hash = this.iframe.location.hash = current; - } - this.loadUrl(); + 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() { - var fragment = this.fragment = this.getFragment(); + 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); @@ -738,14 +851,22 @@ // Save a fragment into the hash history. You are responsible for properly // URL-encoding the fragment in advance. This does not trigger // a `hashchange` event. - saveLocation : function(fragment) { - fragment = (fragment || '').replace(hashStrip, ''); - if (this.fragment == fragment) return; - window.location.hash = this.fragment = fragment; - if (this.iframe && (fragment != this.getFragment(this.iframe.location))) { - this.iframe.document.open().close(); - this.iframe.location.hash = fragment; + navigate : function(fragment, triggerRoute) { + var frag = (fragment || '').replace(hashStrip, ''); + if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return; + if (this._hasPushState) { + var loc = window.location; + if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag; + this.fragment = frag; + window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag); + } else { + window.location.hash = this.fragment = frag; + if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) { + this.iframe.document.open().close(); + this.iframe.location.hash = frag; + } } + if (triggerRoute) this.loadUrl(fragment); } }); @@ -756,10 +877,11 @@ // 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.delegateEvents(); - this.initialize(options); + this.initialize.apply(this, arguments); }; // Element lookup, scoped to DOM elements within the current view. @@ -770,7 +892,10 @@ }; // Cached regex to split keys for `delegate`. - var eventSplitter = /^(\w+)\s*(.*)$/; + 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, { @@ -827,12 +952,15 @@ // not `change`, `submit`, and `reset` in Internet Explorer. delegateEvents : function(events) { if (!(events || (events = this.events))) return; - $(this.el).unbind(); + if (_.isFunction(events)) events = events.call(this); + $(this.el).unbind('.delegateEvents' + this.cid); for (var key in events) { - var methodName = events[key]; + var 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]; - var method = _.bind(this[methodName], this); + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; if (selector === '') { $(this.el).bind(eventName, method); } else { @@ -846,22 +974,26 @@ // attached directly to the view. _configure : function(options) { if (this.options) options = _.extend({}, this.options, options); - if (options.model) this.model = options.model; - if (options.collection) this.collection = options.collection; - if (options.el) this.el = options.el; - if (options.id) this.id = options.id; - if (options.className) this.className = options.className; - if (options.tagName) this.tagName = options.tagName; + 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` proeprties. _ensureElement : function() { - if (this.el) return; - var attrs = {}; - if (this.id) attrs.id = this.id; - if (this.className) attrs["class"] = this.className; - this.el = this.make(this.tagName, attrs); + if (!this.el) { + var attrs = this.attributes || {}; + if (this.id) attrs.id = this.id; + if (this.className) attrs['class'] = this.className; + this.el = this.make(this.tagName, attrs); + } else if (_.isString(this.el)) { + this.el = $(this.el).get(0); + } } }); @@ -869,13 +1001,13 @@ // The self-propagating extend function that Backbone classes use. var extend = function (protoProps, classProps) { var child = inherits(this, protoProps, classProps); - child.extend = extend; + child.extend = this.extend; return child; }; // Set up inheritance for the model, collection, and view. Backbone.Model.extend = Backbone.Collection.extend = - Backbone.Controller.extend = Backbone.View.extend = extend; + Backbone.Router.extend = Backbone.View.extend = extend; // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { @@ -903,28 +1035,30 @@ // `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, success, error) { + Backbone.sync = function(method, model, options) { var type = methodMap[method]; - var modelJSON = (method === 'create' || method === 'update') ? - JSON.stringify(model.toJSON()) : null; // Default JSON-request options. - var params = { - url: getUrl(model), + var params = _.extend({ type: type, - contentType: 'application/json', - data: modelJSON, - dataType: 'json', - processData: false, - success: success, - error: error - }; + dataType: 'json' + }, options); + + // Ensure that we have a URL. + if (!params.url) { + params.url = getUrl(model) || urlError(); + } + + // Ensure that we have the appropriate request data. + if (!params.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.processData = true; - params.data = modelJSON ? {model : modelJSON} : {}; + params.data = params.data ? {model : params.data} : {}; } // For older servers, emulate HTTP by mimicking the HTTP method with `_method` @@ -934,13 +1068,18 @@ if (Backbone.emulateJSON) params.data._method = type; params.type = 'POST'; params.beforeSend = function(xhr) { - xhr.setRequestHeader("X-HTTP-Method-Override", type); + 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. - $.ajax(params); + return $.ajax(params); }; // Helpers @@ -964,6 +1103,9 @@ child = function(){ return 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; @@ -976,7 +1118,7 @@ // Add static properties to the constructor function, if supplied. if (staticProps) _.extend(child, staticProps); - // Correctly set child's `prototype.constructor`, for `instanceof`. + // Correctly set child's `prototype.constructor`. child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed later. @@ -988,15 +1130,20 @@ // Helper function to get a URL from a Model or Collection as a property // or as a function. var getUrl = function(object) { - if (!(object && object.url)) throw new Error("A 'url' property or function must be specified"); + if (!(object && object.url)) return null; return _.isFunction(object.url) ? object.url() : object.url; }; + // 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'); + }; + // Wrap an optional error callback with a fallback error event. var wrapError = function(onError, model, options) { return function(resp) { if (onError) { - onError(model, resp); + onError(model, resp, options); } else { model.trigger('error', model, resp, options); } @@ -1005,7 +1152,7 @@ // Helper function to escape a string for HTML rendering. var escapeHTML = function(string) { - return string.replace(/&(?!\w+;)/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); + return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); }; -})(); +}).call(this);
Modified: incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/format.js URL: http://svn.apache.org/viewvc/incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/format.js?rev=1294730&r1=1294729&r2=1294730&view=diff ============================================================================== --- incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/format.js (original) +++ incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/format.js Tue Feb 28 16:56:02 2012 @@ -1,42 +1,64 @@ + +/* Hallo - a rich text editing jQuery UI widget +# (c) 2011 Henri Bergius, IKS Consortium +# Hallo may be freely distributed under the MIT license +*/ + (function() { - var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + (function(jQuery) { return jQuery.widget("IKS.halloformat", { options: { editable: null, toolbar: null, uuid: "", - formattings: ["bold", "italic"] + formattings: { + bold: true, + italic: true, + strikeThrough: true, + underline: true + } }, _create: function() { - var buttonize, buttonset, format, widget, _i, _len, _ref; + var buttonize, buttonset, enabled, format, widget, _ref, + _this = this; widget = this; - buttonset = jQuery("<span></span>"); - buttonize = __bind(function(format) { - var button, id, label; + buttonset = jQuery("<span class=\"" + widget.widgetName + "\"></span>"); + buttonize = function(format) { + var button, element, id, label, queryState; label = format.substr(0, 1).toUpperCase(); - id = "" + this.options.uuid + "-" + format; - buttonset.append(jQuery("<input id=\"" + id + "\" type=\"checkbox\" /><label for=\"" + id + "\">" + label + "</label>").button()); + id = "" + _this.options.uuid + "-" + format; + buttonset.append(jQuery("<input id=\"" + id + "\" type=\"checkbox\" /><label for=\"" + id + "\" class=\"" + format + "_button\">" + label + "</label>").button()); button = jQuery("#" + id, buttonset); button.attr("hallo-command", format); + button.addClass(format); button.bind("change", function(event) { format = jQuery(this).attr("hallo-command"); return widget.options.editable.execute(format); }); - return this.element.bind("keyup paste change mouseup", function(event) { + queryState = function(event) { if (document.queryCommandState(format)) { button.attr("checked", true); + button.next("label").addClass("ui-state-clicked"); return button.button("refresh"); } else { button.attr("checked", false); + button.next("label").removeClass("ui-state-clicked"); return button.button("refresh"); } + }; + element = _this.element; + _this.element.bind("halloenabled", function() { + return element.bind("keyup paste change mouseup", queryState); + }); + return _this.element.bind("hallodisabled", function() { + return element.unbind("keyup paste change mouseup", queryState); }); - }, this); + }; _ref = this.options.formattings; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - format = _ref[_i]; - buttonize(format); + for (format in _ref) { + enabled = _ref[format]; + if (enabled) buttonize(format); } buttonset.buttonset(); return this.options.toolbar.append(buttonset); @@ -44,4 +66,5 @@ _init: function() {} }); })(jQuery); + }).call(this); Modified: incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/hallo.js URL: http://svn.apache.org/viewvc/incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/hallo.js?rev=1294730&r1=1294729&r2=1294730&view=diff ============================================================================== --- incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/hallo.js (original) +++ incubator/stanbol/trunk/demos/webvie/src/main/resources/org/apache/stanbol/commons/web/vie/static/lib/hallo/hallo.js Tue Feb 28 16:56:02 2012 @@ -1,15 +1,34 @@ + +/* Hallo - a rich text editing jQuery UI widget +# (c) 2011 Henri Bergius, IKS Consortium +# Hallo may be freely distributed under the MIT license +*/ + (function() { + (function(jQuery) { return jQuery.widget("IKS.hallo", { toolbar: null, bound: false, originalContent: "", uuid: "", + selection: null, options: { editable: true, plugins: {}, + floating: true, + offset: { + x: 0, + y: 0 + }, + showAlways: false, activated: function() {}, - deactivated: function() {} + deactivated: function() {}, + selected: function() {}, + unselected: function() {}, + enabled: function() {}, + disabled: function() {}, + placeholder: '' }, _create: function() { var options, plugin, _ref, _results; @@ -20,9 +39,7 @@ _results = []; for (plugin in _ref) { options = _ref[plugin]; - if (!jQuery.isPlainObject(options)) { - options = {}; - } + if (!jQuery.isPlainObject(options)) options = {}; options["editable"] = this; options["toolbar"] = this.toolbar; options["uuid"] = this.id; @@ -41,34 +58,100 @@ this.element.attr("contentEditable", false); this.element.unbind("focus", this._activated); this.element.unbind("blur", this._deactivated); - this.element.unbind("keyup paste change", this, this._checkModified); - return this.bound = false; + this.element.unbind("keyup paste change", this._checkModified); + this.element.unbind("keyup", this._keys); + this.element.unbind("keyup mouseup", this._checkSelection); + this.bound = false; + return this._trigger("disabled", null); }, enable: function() { var widget; this.element.attr("contentEditable", true); + if (!this.element.html()) this.element.html(this.options.placeholder); if (!this.bound) { this.element.bind("focus", this, this._activated); - this.element.bind("blur", this, this._deactivated); + if (!this.options.showAlways) { + this.element.bind("blur", this, this._deactivated); + } this.element.bind("keyup paste change", this, this._checkModified); + this.element.bind("keyup", this, this._keys); + this.element.bind("keyup mouseup", this, this._checkSelection); widget = this; - return this.bound = true; + this.bound = true; } + return this._trigger("enabled", null); }, activate: function() { return this.element.focus(); }, + getSelection: function() { + var range, userSelection; + if (jQuery.browser.msie) { + range = document.selection.createRange(); + } else { + if (window.getSelection) { + userSelection = window.getSelection(); + } else if (document.selection) { + userSelection = document.selection.createRange(); + } else { + throw "Your browser does not support selection handling"; + } + if (userSelection.rangeCount > 0) { + range = userSelection.getRangeAt(0); + } else { + range = userSelection; + } + } + return range; + }, + restoreSelection: function(range) { + if (jQuery.browser.msie) { + return range.select(); + } else { + window.getSelection().removeAllRanges(); + return window.getSelection().addRange(range); + } + }, + replaceSelection: function(cb) { + var newTextNode, r, range, sel, t; + if (jQuery.browser.msie) { + t = document.selection.createRange().text; + r = document.selection.createRange(); + return r.pasteHTML(cb(t)); + } else { + sel = window.getSelection(); + range = sel.getRangeAt(0); + newTextNode = document.createTextNode(cb(range.extractContents())); + range.insertNode(newTextNode); + range.setStartAfter(newTextNode); + sel.removeAllRanges(); + return sel.addRange(range); + } + }, + removeAllSelections: function() { + if (jQuery.browser.msie) { + return range.empty(); + } else { + return window.getSelection().removeAllRanges(); + } + }, getContents: function() { return this.element.html(); }, + setContents: function(contents) { + return this.element.html(contents); + }, isModified: function() { return this.originalContent !== this.getContents(); }, setUnmodified: function() { return this.originalContent = this.getContents(); }, - execute: function(command) { - if (document.execCommand(command, false, null)) { + restoreOriginalContent: function() { + return this.element.html(this.originalContent); + }, + execute: function(command, value) { + if (document.execCommand(command, false, value)) { return this.element.trigger("change"); } }, @@ -79,15 +162,80 @@ }; return "" + (S4()) + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + (S4()) + (S4()); }, + _getToolbarPosition: function(event, selection) { + var offset; + if (!event) return; + if (this.options.floating) { + if (event.originalEvent instanceof KeyboardEvent) { + return this._getCaretPosition(selection); + } else if (event.originalEvent instanceof MouseEvent) { + return { + top: event.pageY, + left: event.pageX + }; + } + } else { + offset = parseFloat(this.element.css('outline-width')) + parseFloat(this.element.css('outline-offset')); + return { + top: this.element.offset().top - this.toolbar.outerHeight() - offset, + left: this.element.offset().left - offset + }; + } + }, + _getCaretPosition: function(range) { + var newRange, position, tmpSpan; + tmpSpan = jQuery("<span/>"); + newRange = document.createRange(); + newRange.setStart(range.endContainer, range.endOffset); + newRange.insertNode(tmpSpan.get(0)); + position = { + top: tmpSpan.offset().top, + left: tmpSpan.offset().left + }; + tmpSpan.remove(); + return position; + }, _prepareToolbar: function() { - this.toolbar = jQuery('<div></div>').hide(); + var that; + that = this; + this.toolbar = jQuery('<div class="hallotoolbar"></div>').hide(); this.toolbar.css("position", "absolute"); this.toolbar.css("top", this.element.offset().top - 20); this.toolbar.css("left", this.element.offset().left); jQuery('body').append(this.toolbar); - return this.toolbar.bind("mousedown", function(event) { + this.toolbar.bind("mousedown", function(event) { return event.preventDefault(); }); + if (this.options.showAlways) { + this.options.floating = false; + this.element.bind("halloactivated", function(event, data) { + that._updateToolbarPosition(that._getToolbarPosition(event)); + return that.toolbar.show(); + }); + this.element.bind("hallodeactivated", function(event, data) { + return that.toolbar.hide(); + }); + } else { + this.element.bind("halloselected", function(event, data) { + var position, widget; + widget = data.editable; + position = widget._getToolbarPosition(data.originalEvent, data.selection); + if (position) { + that._updateToolbarPosition(position); + return that.toolbar.show(); + } + }); + this.element.bind("hallounselected", function(event, data) { + return data.editable.toolbar.hide(); + }); + } + return jQuery(window).resize(function(event) { + return that._updateToolbarPosition(that._getToolbarPosition(event)); + }); + }, + _updateToolbarPosition: function(position) { + this.toolbar.css("top", position.top); + return this.toolbar.css("left", position.left); }, _checkModified: function(event) { var widget; @@ -99,21 +247,86 @@ }); } }, - _activated: function(event) { + _keys: function(event) { var widget; widget = event.data; - if (widget.toolbar.html() !== "") { - widget.toolbar.css("top", widget.element.offset().top - widget.toolbar.height()); - widget.toolbar.show(); + if (event.keyCode === 27) { + widget.restoreOriginalContent(); + return widget.turnOff(); } - return widget._trigger("activated", event); }, - _deactivated: function(event) { + _rangesEqual: function(r1, r2) { + return r1.startContainer === r2.startContainer && r1.startOffset === r2.startOffset && r1.endContainer === r2.endContainer && r1.endOffset === r2.endOffset; + }, + _checkSelection: function(event) { var widget; + if (event.keyCode === 27) return; widget = event.data; - widget.toolbar.hide(); - return widget._trigger("deactivated", event); + return setTimeout(function() { + var sel; + sel = widget.getSelection(); + if (widget._isEmptySelection(sel) || widget._isEmptyRange(sel)) { + if (widget.selection) { + widget.selection = null; + widget._trigger("unselected", null, { + editable: widget, + originalEvent: event + }); + } + return; + } + if (!widget.selection || !widget._rangesEqual(sel, widget.selection)) { + widget.selection = sel.cloneRange(); + return widget._trigger("selected", null, { + editable: widget, + selection: widget.selection, + ranges: [widget.selection], + originalEvent: event + }); + } + }, 0); + }, + _isEmptySelection: function(selection) { + if (selection.type === "Caret") return true; + return false; + }, + _isEmptyRange: function(range) { + if (range.collapsed) return true; + if (range.isCollapsed) return range.isCollapsed(); + return false; + }, + turnOn: function() { + var el, widthToAdd; + if (this.getContents() === this.options.placeholder) this.setContents(''); + jQuery(this.element).addClass('inEditMode'); + if (!this.options.floating) { + el = jQuery(this.element); + widthToAdd = parseFloat(el.css('padding-left')); + widthToAdd += parseFloat(el.css('padding-right')); + widthToAdd += parseFloat(el.css('border-left-width')); + widthToAdd += parseFloat(el.css('border-right-width')); + widthToAdd += (parseFloat(el.css('outline-width'))) * 2; + widthToAdd += (parseFloat(el.css('outline-offset'))) * 2; + jQuery(this.toolbar).css("width", el.width() + widthToAdd); + } else { + this.toolbar.css("width", "auto"); + } + return this._trigger("activated", this); + }, + turnOff: function() { + this.toolbar.hide(); + jQuery(this.element).removeClass('inEditMode'); + if (this.options.showAlways) this.element.blur(); + this._trigger("deactivated", this); + if (!this.getContents()) return this.setContents(this.options.placeholder); + }, + _activated: function(event) { + return event.data.turnOn(); + }, + _deactivated: function(event) { + return event.data.turnOff(); } }); })(jQuery); + }).call(this);
