diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/lib | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/lib')
| -rw-r--r-- | addons/point_of_sale/static/lib/backbone/backbone.js | 1581 | ||||
| -rw-r--r-- | addons/point_of_sale/static/lib/html2canvas.js | 2868 | ||||
| -rw-r--r-- | addons/point_of_sale/static/lib/sha1.js | 159 | ||||
| -rw-r--r-- | addons/point_of_sale/static/lib/waitfont.js | 59 |
4 files changed, 4667 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/lib/backbone/backbone.js b/addons/point_of_sale/static/lib/backbone/backbone.js new file mode 100644 index 00000000..f7783c2c --- /dev/null +++ b/addons/point_of_sale/static/lib/backbone/backbone.js @@ -0,0 +1,1581 @@ +// Backbone.js 1.1.0 + +// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + 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 = '1.1.0'; + + // 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, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + + // 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 `"PATCH"`, `"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'); + // + var Events = Backbone.Events = { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + var remove = !name && !callback; + if (!callback && typeof name === 'object') callback = this; + if (obj) (listeningTo = {})[obj._listenId] = obj; + for (var id in listeningTo) { + obj = listeningTo[id]; + obj.off(name, callback, this); + if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + listeningTo[id] = obj; + if (!callback && typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + attrs = _.defaults({}, attrs, _.result(this, 'defaults')); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true}, options); + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !options.wait) { + if (!this.set(attrs, options)) return false; + } else { + if (!this._validate(attrs, options)) return false; + } + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + var singular = !_.isArray(models); + models = singular ? [models] : _.clone(models); + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = models[i] = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return singular ? models[0] : models; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults({}, options, setOptions); + if (options.parse) models = this.parse(models, options); + var singular = !_.isArray(models); + models = singular ? (models ? [models] : []) : _.clone(models); + var i, l, id, model, attrs, existing, sort; + var at = options.at; + var targetModel = this.model; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + var add = options.add, merge = options.merge, remove = options.remove; + var order = !sortable && add && remove ? [] : false; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + attrs = models[i]; + if (attrs instanceof Model) { + id = model = attrs; + } else { + id = attrs[targetModel.prototype.idAttribute]; + } + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(id)) { + if (remove) modelMap[existing.cid] = true; + if (merge) { + attrs = attrs === model ? model.attributes : attrs; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(attrs, options); + if (!model) continue; + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + if (order) order.push(existing || model); + } + + // Remove nonexistent models if appropriate. + if (remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length || (order && order.length)) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + for (i = 0, l = toAdd.length; i < l; i++) { + this.models.splice(at + i, 0, toAdd[i]); + } + } else { + if (order) this.models.length = 0; + var orderedModels = order || toAdd; + for (i = 0, l = orderedModels.length; i < l; i++) { + this.models.push(orderedModels[i]); + } + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort events. + if (!options.silent) { + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + if (sort || (order && order.length)) this.trigger('sort', this, options); + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // 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 granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(model, resp, options) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', + 'lastIndexOf', 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + options || (options = {}); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && noXhrPatch) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, 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. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Cached regex for stripping urls of hash and query. + var pathStripper = /[?#].*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; + this.navigate(fragment); + } + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._hasPushState) { + Backbone.$(window).on('popstate', this.checkUrl); + } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { + Backbone.$(window).on('hashchange', this.checkUrl); + } else if (this._wantsHashChange) { + this._checkUrlInterval = setInterval(this.checkUrl, this.interval); + } + + // Determine if we need to change the base url, for a pushState link + // opened by a non-pushState browser. + this.fragment = fragment; + var loc = this.location; + var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; + + // Transition from hashChange to pushState or vice versa if both are + // requested. + if (this._wantsHashChange && this._wantsPushState) { + + // 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._hasPushState && !atRoot) { + this.fragment = this.getFragment(null, true); + this.location.replace(this.root + this.location.search + '#' + this.fragment); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._hasPushState && atRoot && loc.hash) { + this.fragment = this.getHash().replace(routeStripper, ''); + this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); + } + + } + + if (!this.options.silent) return this.loadUrl(); + }, + + // Disable Backbone.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); + clearInterval(this._checkUrlInterval); + History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + if (current === this.fragment && this.iframe) { + current = this.getFragment(this.getHash(this.iframe)); + } + if (current === this.fragment) return false; + if (this.iframe) this.navigate(current); + this.loadUrl(); + }, + + // 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(fragment) { + fragment = this.fragment = this.getFragment(fragment); + return _.any(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!History.started) return false; + if (!options || options === true) options = {trigger: !!options}; + + var url = this.root + (fragment = this.getFragment(fragment || '')); + + // Strip the fragment of the query and hash for matching. + fragment = fragment.replace(pathStripper, ''); + + if (this.fragment === fragment) return; + this.fragment = fragment; + + // Don't include a trailing slash on the root. + if (fragment === '' && url !== '/') url = url.slice(0, -1); + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._hasPushState) { + this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this._updateHash(this.location, fragment, options.replace); + if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { + // Opening and closing the iframe tricks IE7 and earlier to push a + // history entry on hash-tag change. When replace is true, we don't + // want this. + if(!options.replace) this.iframe.document.open().close(); + this._updateHash(this.iframe.location, fragment, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + return this.location.assign(url); + } + if (options.trigger) return this.loadUrl(fragment); + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + // Some browsers require that `hash` contains a leading #. + location.hash = '#' + fragment; + } + } + + }); + + // Create the default Backbone.history. + Backbone.history = new History; + + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent's constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + var Surrogate = function(){ this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate; + + // Add prototype properties (instance properties) to the subclass, + // if supplied. + if (protoProps) _.extend(child.prototype, protoProps); + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }; + + // Set up inheritance for the model, collection, router, view and history. + Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; + + // Throw an error when a URL is needed, and none is supplied. + var urlError = function() { + throw new Error('A "url" property or function must be specified'); + }; + + // Wrap an optional error callback with a fallback error event. + var wrapError = function(model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error(model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + +}).call(this); diff --git a/addons/point_of_sale/static/lib/html2canvas.js b/addons/point_of_sale/static/lib/html2canvas.js new file mode 100644 index 00000000..4c228889 --- /dev/null +++ b/addons/point_of_sale/static/lib/html2canvas.js @@ -0,0 +1,2868 @@ +/* + html2canvas 0.4.1 <http://html2canvas.hertzen.com> + Copyright (c) 2013 Niklas von Hertzen + + Released under MIT License +*/ + +(function(window, document, undefined){ + + "use strict"; + + var _html2canvas = {}, + previousElement, + computedCSS, + html2canvas; + + _html2canvas.Util = {}; + + _html2canvas.Util.log = function(a) { + if (_html2canvas.logging && window.console && window.console.log) { + window.console.log(a); + } + }; + + _html2canvas.Util.trimText = (function(isNative){ + return function(input) { + return isNative ? isNative.apply(input) : ((input || '') + '').replace( /^\s+|\s+$/g , '' ); + }; + })(String.prototype.trim); + + _html2canvas.Util.asFloat = function(v) { + return parseFloat(v); + }; + + (function() { + // TODO: support all possible length values + var TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){0,})/g; + var TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g; + _html2canvas.Util.parseTextShadows = function (value) { + if (!value || value === 'none') { + return []; + } + + // find multiple shadow declarations + var shadows = value.match(TEXT_SHADOW_PROPERTY), + results = []; + for (var i = 0; shadows && (i < shadows.length); i++) { + var s = shadows[i].match(TEXT_SHADOW_VALUES); + results.push({ + color: s[0], + offsetX: s[1] ? s[1].replace('px', '') : 0, + offsetY: s[2] ? s[2].replace('px', '') : 0, + blur: s[3] ? s[3].replace('px', '') : 0 + }); + } + return results; + }; + })(); + + + _html2canvas.Util.parseBackgroundImage = function (value) { + var whitespace = ' \r\n\t', + method, definition, prefix, prefix_i, block, results = [], + c, mode = 0, numParen = 0, quote, args; + + var appendResult = function(){ + if(method) { + if(definition.substr( 0, 1 ) === '"') { + definition = definition.substr( 1, definition.length - 2 ); + } + if(definition) { + args.push(definition); + } + if(method.substr( 0, 1 ) === '-' && + (prefix_i = method.indexOf( '-', 1 ) + 1) > 0) { + prefix = method.substr( 0, prefix_i); + method = method.substr( prefix_i ); + } + results.push({ + prefix: prefix, + method: method.toLowerCase(), + value: block, + args: args + }); + } + args = []; //for some odd reason, setting .length = 0 didn't work in safari + method = + prefix = + definition = + block = ''; + }; + + appendResult(); + for(var i = 0, ii = value.length; i<ii; i++) { + c = value[i]; + if(mode === 0 && whitespace.indexOf( c ) > -1){ + continue; + } + switch(c) { + case '"': + if(!quote) { + quote = c; + } + else if(quote === c) { + quote = null; + } + break; + + case '(': + if(quote) { break; } + else if(mode === 0) { + mode = 1; + block += c; + continue; + } else { + numParen++; + } + break; + + case ')': + if(quote) { break; } + else if(mode === 1) { + if(numParen === 0) { + mode = 0; + block += c; + appendResult(); + continue; + } else { + numParen--; + } + } + break; + + case ',': + if(quote) { break; } + else if(mode === 0) { + appendResult(); + continue; + } + else if (mode === 1) { + if(numParen === 0 && !method.match(/^url$/i)) { + args.push(definition); + definition = ''; + block += c; + continue; + } + } + break; + } + + block += c; + if(mode === 0) { method += c; } + else { definition += c; } + } + appendResult(); + + return results; + }; + + _html2canvas.Util.Bounds = function (element) { + var clientRect, bounds = {}; + + if (element.getBoundingClientRect){ + clientRect = element.getBoundingClientRect(); + + // TODO add scroll position to bounds, so no scrolling of window necessary + bounds.top = clientRect.top; + bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height); + bounds.left = clientRect.left; + + bounds.width = element.offsetWidth; + bounds.height = element.offsetHeight; + } + + return bounds; + }; + + // TODO ideally, we'd want everything to go through this function instead of Util.Bounds, + // but would require further work to calculate the correct positions for elements with offsetParents + _html2canvas.Util.OffsetBounds = function (element) { + var parent = element.offsetParent ? _html2canvas.Util.OffsetBounds(element.offsetParent) : {top: 0, left: 0}; + + return { + top: element.offsetTop + parent.top, + bottom: element.offsetTop + element.offsetHeight + parent.top, + left: element.offsetLeft + parent.left, + width: element.offsetWidth, + height: element.offsetHeight + }; + }; + + function toPX(element, attribute, value ) { + var rsLeft = element.runtimeStyle && element.runtimeStyle[attribute], + left, + style = element.style; + + // Check if we are not dealing with pixels, (Opera has issues with this) + // Ported from jQuery css.js + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + + if ( !/^-?[0-9]+\.?[0-9]*(?:px)?$/i.test( value ) && /^-?\d/.test(value) ) { + // Remember the original values + left = style.left; + + // Put in the new values to get a computed value out + if (rsLeft) { + element.runtimeStyle.left = element.currentStyle.left; + } + style.left = attribute === "fontSize" ? "1em" : (value || 0); + value = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if (rsLeft) { + element.runtimeStyle.left = rsLeft; + } + } + + if (!/^(thin|medium|thick)$/i.test(value)) { + return Math.round(parseFloat(value)) + "px"; + } + + return value; + } + + function asInt(val) { + return parseInt(val, 10); + } + + function parseBackgroundSizePosition(value, element, attribute, index) { + value = (value || '').split(','); + value = value[index || 0] || value[0] || 'auto'; + value = _html2canvas.Util.trimText(value).split(' '); + + if(attribute === 'backgroundSize' && (!value[0] || value[0].match(/cover|contain|auto/))) { + //these values will be handled in the parent function + } else { + value[0] = (value[0].indexOf( "%" ) === -1) ? toPX(element, attribute + "X", value[0]) : value[0]; + if(value[1] === undefined) { + if(attribute === 'backgroundSize') { + value[1] = 'auto'; + return value; + } else { + // IE 9 doesn't return double digit always + value[1] = value[0]; + } + } + value[1] = (value[1].indexOf("%") === -1) ? toPX(element, attribute + "Y", value[1]) : value[1]; + } + return value; + } + + _html2canvas.Util.getCSS = function (element, attribute, index) { + if (previousElement !== element) { + computedCSS = document.defaultView.getComputedStyle(element, null); + } + + var value = computedCSS[attribute]; + + if (/^background(Size|Position)$/.test(attribute)) { + return parseBackgroundSizePosition(value, element, attribute, index); + } else if (/border(Top|Bottom)(Left|Right)Radius/.test(attribute)) { + var arr = value.split(" "); + if (arr.length <= 1) { + arr[1] = arr[0]; + } + return arr.map(asInt); + } + + return value; + }; + + _html2canvas.Util.resizeBounds = function( current_width, current_height, target_width, target_height, stretch_mode ){ + var target_ratio = target_width / target_height, + current_ratio = current_width / current_height, + output_width, output_height; + + if(!stretch_mode || stretch_mode === 'auto') { + output_width = target_width; + output_height = target_height; + } else if(target_ratio < current_ratio ^ stretch_mode === 'contain') { + output_height = target_height; + output_width = target_height * current_ratio; + } else { + output_width = target_width; + output_height = target_width / current_ratio; + } + + return { + width: output_width, + height: output_height + }; + }; + + function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroundSize ) { + var bgposition = _html2canvas.Util.getCSS( el, prop, imageIndex ) , + topPos, + left, + percentage, + val; + + if (bgposition.length === 1){ + val = bgposition[0]; + + bgposition = []; + + bgposition[0] = val; + bgposition[1] = val; + } + + if (bgposition[0].toString().indexOf("%") !== -1){ + percentage = (parseFloat(bgposition[0])/100); + left = bounds.width * percentage; + if(prop !== 'backgroundSize') { + left -= (backgroundSize || image).width*percentage; + } + } else { + if(prop === 'backgroundSize') { + if(bgposition[0] === 'auto') { + left = image.width; + } else { + if (/contain|cover/.test(bgposition[0])) { + var resized = _html2canvas.Util.resizeBounds(image.width, image.height, bounds.width, bounds.height, bgposition[0]); + left = resized.width; + topPos = resized.height; + } else { + left = parseInt(bgposition[0], 10); + } + } + } else { + left = parseInt( bgposition[0], 10); + } + } + + + if(bgposition[1] === 'auto') { + topPos = left / image.width * image.height; + } else if (bgposition[1].toString().indexOf("%") !== -1){ + percentage = (parseFloat(bgposition[1])/100); + topPos = bounds.height * percentage; + if(prop !== 'backgroundSize') { + topPos -= (backgroundSize || image).height * percentage; + } + + } else { + topPos = parseInt(bgposition[1],10); + } + + return [left, topPos]; + } + + _html2canvas.Util.BackgroundPosition = function( el, bounds, image, imageIndex, backgroundSize ) { + var result = backgroundBoundsFactory( 'backgroundPosition', el, bounds, image, imageIndex, backgroundSize ); + return { left: result[0], top: result[1] }; + }; + + _html2canvas.Util.BackgroundSize = function( el, bounds, image, imageIndex ) { + var result = backgroundBoundsFactory( 'backgroundSize', el, bounds, image, imageIndex ); + return { width: result[0], height: result[1] }; + }; + + _html2canvas.Util.Extend = function (options, defaults) { + for (var key in options) { + if (options.hasOwnProperty(key)) { + defaults[key] = options[key]; + } + } + return defaults; + }; + + + /* + * Derived from jQuery.contents() + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + */ + _html2canvas.Util.Children = function( elem ) { + var children; + try { + children = (elem.nodeName && elem.nodeName.toUpperCase() === "IFRAME") ? elem.contentDocument || elem.contentWindow.document : (function(array) { + var ret = []; + if (array !== null) { + (function(first, second ) { + var i = first.length, + j = 0; + + if (typeof second.length === "number") { + for (var l = second.length; j < l; j++) { + first[i++] = second[j]; + } + } else { + while (second[j] !== undefined) { + first[i++] = second[j++]; + } + } + + first.length = i; + + return first; + })(ret, array); + } + return ret; + })(elem.childNodes); + + } catch (ex) { + _html2canvas.Util.log("html2canvas.Util.Children failed with exception: " + ex.message); + children = []; + } + return children; + }; + + _html2canvas.Util.isTransparent = function(backgroundColor) { + return (backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)" || backgroundColor === undefined); + }; + _html2canvas.Util.Font = (function () { + + var fontData = {}; + + return function(font, fontSize, doc) { + if (fontData[font + "-" + fontSize] !== undefined) { + return fontData[font + "-" + fontSize]; + } + + var container = doc.createElement('div'), + img = doc.createElement('img'), + span = doc.createElement('span'), + sampleText = 'Hidden Text', + baseline, + middle, + metricsObj; + + container.style.visibility = "hidden"; + container.style.fontFamily = font; + container.style.fontSize = fontSize; + container.style.margin = 0; + container.style.padding = 0; + + doc.body.appendChild(container); + + // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever (handtinywhite.gif) + img.src = "data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs="; + img.width = 1; + img.height = 1; + + img.style.margin = 0; + img.style.padding = 0; + img.style.verticalAlign = "baseline"; + + span.style.fontFamily = font; + span.style.fontSize = fontSize; + span.style.margin = 0; + span.style.padding = 0; + + span.appendChild(doc.createTextNode(sampleText)); + container.appendChild(span); + container.appendChild(img); + baseline = (img.offsetTop - span.offsetTop) + 1; + + container.removeChild(span); + container.appendChild(doc.createTextNode(sampleText)); + + container.style.lineHeight = "normal"; + img.style.verticalAlign = "super"; + + middle = (img.offsetTop-container.offsetTop) + 1; + metricsObj = { + baseline: baseline, + lineWidth: 1, + middle: middle + }; + + fontData[font + "-" + fontSize] = metricsObj; + + doc.body.removeChild(container); + + return metricsObj; + }; + })(); + + (function(){ + var Util = _html2canvas.Util, + Generate = {}; + + _html2canvas.Generate = Generate; + + var reGradients = [ + /^(-webkit-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/, + /^(-o-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/, + /^(-webkit-gradient)\((linear|radial),\s((?:\d{1,3}%?)\s(?:\d{1,3}%?),\s(?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)\-]+)\)$/, + /^(-moz-linear-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)]+)\)$/, + /^(-webkit-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/, + /^(-moz-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s?([a-z\-]*)([\w\d\.\s,%\(\)]+)\)$/, + /^(-o-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/ + ]; + + /* + * TODO: Add IE10 vendor prefix (-ms) support + * TODO: Add W3C gradient (linear-gradient) support + * TODO: Add old Webkit -webkit-gradient(radial, ...) support + * TODO: Maybe some RegExp optimizations are possible ;o) + */ + Generate.parseGradient = function(css, bounds) { + var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3, tl,tr,br,bl; + + for(i = 0; i < len; i+=1){ + m1 = css.match(reGradients[i]); + if(m1) { + break; + } + } + + if(m1) { + switch(m1[1]) { + case '-webkit-linear-gradient': + case '-o-linear-gradient': + + gradient = { + type: 'linear', + x0: null, + y0: null, + x1: null, + y1: null, + colorStops: [] + }; + + // get coordinates + m2 = m1[2].match(/\w+/g); + if(m2){ + m2Len = m2.length; + for(i = 0; i < m2Len; i+=1){ + switch(m2[i]) { + case 'top': + gradient.y0 = 0; + gradient.y1 = bounds.height; + break; + + case 'right': + gradient.x0 = bounds.width; + gradient.x1 = 0; + break; + + case 'bottom': + gradient.y0 = bounds.height; + gradient.y1 = 0; + break; + + case 'left': + gradient.x0 = 0; + gradient.x1 = bounds.width; + break; + } + } + } + if(gradient.x0 === null && gradient.x1 === null){ // center + gradient.x0 = gradient.x1 = bounds.width / 2; + } + if(gradient.y0 === null && gradient.y1 === null){ // center + gradient.y0 = gradient.y1 = bounds.height / 2; + } + + // get colors and stops + m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g); + if(m2){ + m2Len = m2.length; + step = 1 / Math.max(m2Len - 1, 1); + for(i = 0; i < m2Len; i+=1){ + m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/); + if(m3[2]){ + stop = parseFloat(m3[2]); + if(m3[3] === '%'){ + stop /= 100; + } else { // px - stupid opera + stop /= bounds.width; + } + } else { + stop = i * step; + } + gradient.colorStops.push({ + color: m3[1], + stop: stop + }); + } + } + break; + + case '-webkit-gradient': + + gradient = { + type: m1[2] === 'radial' ? 'circle' : m1[2], // TODO: Add radial gradient support for older mozilla definitions + x0: 0, + y0: 0, + x1: 0, + y1: 0, + colorStops: [] + }; + + // get coordinates + m2 = m1[3].match(/(\d{1,3})%?\s(\d{1,3})%?,\s(\d{1,3})%?\s(\d{1,3})%?/); + if(m2){ + gradient.x0 = (m2[1] * bounds.width) / 100; + gradient.y0 = (m2[2] * bounds.height) / 100; + gradient.x1 = (m2[3] * bounds.width) / 100; + gradient.y1 = (m2[4] * bounds.height) / 100; + } + + // get colors and stops + m2 = m1[4].match(/((?:from|to|color-stop)\((?:[0-9\.]+,\s)?(?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)\))+/g); + if(m2){ + m2Len = m2.length; + for(i = 0; i < m2Len; i+=1){ + m3 = m2[i].match(/(from|to|color-stop)\(([0-9\.]+)?(?:,\s)?((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\)/); + stop = parseFloat(m3[2]); + if(m3[1] === 'from') { + stop = 0.0; + } + if(m3[1] === 'to') { + stop = 1.0; + } + gradient.colorStops.push({ + color: m3[3], + stop: stop + }); + } + } + break; + + case '-moz-linear-gradient': + + gradient = { + type: 'linear', + x0: 0, + y0: 0, + x1: 0, + y1: 0, + colorStops: [] + }; + + // get coordinates + m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/); + + // m2[1] == 0% -> left + // m2[1] == 50% -> center + // m2[1] == 100% -> right + + // m2[2] == 0% -> top + // m2[2] == 50% -> center + // m2[2] == 100% -> bottom + + if(m2){ + gradient.x0 = (m2[1] * bounds.width) / 100; + gradient.y0 = (m2[2] * bounds.height) / 100; + gradient.x1 = bounds.width - gradient.x0; + gradient.y1 = bounds.height - gradient.y0; + } + + // get colors and stops + m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}%)?)+/g); + if(m2){ + m2Len = m2.length; + step = 1 / Math.max(m2Len - 1, 1); + for(i = 0; i < m2Len; i+=1){ + m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%)?/); + if(m3[2]){ + stop = parseFloat(m3[2]); + if(m3[3]){ // percentage + stop /= 100; + } + } else { + stop = i * step; + } + gradient.colorStops.push({ + color: m3[1], + stop: stop + }); + } + } + break; + + case '-webkit-radial-gradient': + case '-moz-radial-gradient': + case '-o-radial-gradient': + + gradient = { + type: 'circle', + x0: 0, + y0: 0, + x1: bounds.width, + y1: bounds.height, + cx: 0, + cy: 0, + rx: 0, + ry: 0, + colorStops: [] + }; + + // center + m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/); + if(m2){ + gradient.cx = (m2[1] * bounds.width) / 100; + gradient.cy = (m2[2] * bounds.height) / 100; + } + + // size + m2 = m1[3].match(/\w+/); + m3 = m1[4].match(/[a-z\-]*/); + if(m2 && m3){ + switch(m3[0]){ + case 'farthest-corner': + case 'cover': // is equivalent to farthest-corner + case '': // mozilla removes "cover" from definition :( + tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2)); + tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); + br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); + bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2)); + gradient.rx = gradient.ry = Math.max(tl, tr, br, bl); + break; + case 'closest-corner': + tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2)); + tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); + br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); + bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2)); + gradient.rx = gradient.ry = Math.min(tl, tr, br, bl); + break; + case 'farthest-side': + if(m2[0] === 'circle'){ + gradient.rx = gradient.ry = Math.max( + gradient.cx, + gradient.cy, + gradient.x1 - gradient.cx, + gradient.y1 - gradient.cy + ); + } else { // ellipse + + gradient.type = m2[0]; + + gradient.rx = Math.max( + gradient.cx, + gradient.x1 - gradient.cx + ); + gradient.ry = Math.max( + gradient.cy, + gradient.y1 - gradient.cy + ); + } + break; + case 'closest-side': + case 'contain': // is equivalent to closest-side + if(m2[0] === 'circle'){ + gradient.rx = gradient.ry = Math.min( + gradient.cx, + gradient.cy, + gradient.x1 - gradient.cx, + gradient.y1 - gradient.cy + ); + } else { // ellipse + + gradient.type = m2[0]; + + gradient.rx = Math.min( + gradient.cx, + gradient.x1 - gradient.cx + ); + gradient.ry = Math.min( + gradient.cy, + gradient.y1 - gradient.cy + ); + } + break; + + // TODO: add support for "30px 40px" sizes (webkit only) + } + } + + // color stops + m2 = m1[5].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g); + if(m2){ + m2Len = m2.length; + step = 1 / Math.max(m2Len - 1, 1); + for(i = 0; i < m2Len; i+=1){ + m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/); + if(m3[2]){ + stop = parseFloat(m3[2]); + if(m3[3] === '%'){ + stop /= 100; + } else { // px - stupid opera + stop /= bounds.width; + } + } else { + stop = i * step; + } + gradient.colorStops.push({ + color: m3[1], + stop: stop + }); + } + } + break; + } + } + + return gradient; + }; + + function addScrollStops(grad) { + return function(colorStop) { + try { + grad.addColorStop(colorStop.stop, colorStop.color); + } + catch(e) { + Util.log(['failed to add color stop: ', e, '; tried to add: ', colorStop]); + } + }; + } + + Generate.Gradient = function(src, bounds) { + if(bounds.width === 0 || bounds.height === 0) { + return; + } + + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + gradient, grad; + + canvas.width = bounds.width; + canvas.height = bounds.height; + + // TODO: add support for multi defined background gradients + gradient = _html2canvas.Generate.parseGradient(src, bounds); + + if(gradient) { + switch(gradient.type) { + case 'linear': + grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1); + gradient.colorStops.forEach(addScrollStops(grad)); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, bounds.width, bounds.height); + break; + + case 'circle': + grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx); + gradient.colorStops.forEach(addScrollStops(grad)); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, bounds.width, bounds.height); + break; + + case 'ellipse': + var canvasRadial = document.createElement('canvas'), + ctxRadial = canvasRadial.getContext('2d'), + ri = Math.max(gradient.rx, gradient.ry), + di = ri * 2; + + canvasRadial.width = canvasRadial.height = di; + + grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri); + gradient.colorStops.forEach(addScrollStops(grad)); + + ctxRadial.fillStyle = grad; + ctxRadial.fillRect(0, 0, di, di); + + ctx.fillStyle = gradient.colorStops[gradient.colorStops.length - 1].color; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry); + break; + } + } + + return canvas; + }; + + Generate.ListAlpha = function(number) { + var tmp = "", + modulus; + + do { + modulus = number % 26; + tmp = String.fromCharCode((modulus) + 64) + tmp; + number = number / 26; + }while((number*26) > 26); + + return tmp; + }; + + Generate.ListRoman = function(number) { + var romanArray = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"], + decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], + roman = "", + v, + len = romanArray.length; + + if (number <= 0 || number >= 4000) { + return number; + } + + for (v=0; v < len; v+=1) { + while (number >= decimal[v]) { + number -= decimal[v]; + roman += romanArray[v]; + } + } + + return roman; + }; + })(); + function h2cRenderContext(width, height) { + var storage = []; + return { + storage: storage, + width: width, + height: height, + clip: function() { + storage.push({ + type: "function", + name: "clip", + 'arguments': arguments + }); + }, + translate: function() { + storage.push({ + type: "function", + name: "translate", + 'arguments': arguments + }); + }, + fill: function() { + storage.push({ + type: "function", + name: "fill", + 'arguments': arguments + }); + }, + save: function() { + storage.push({ + type: "function", + name: "save", + 'arguments': arguments + }); + }, + restore: function() { + storage.push({ + type: "function", + name: "restore", + 'arguments': arguments + }); + }, + fillRect: function () { + storage.push({ + type: "function", + name: "fillRect", + 'arguments': arguments + }); + }, + createPattern: function() { + storage.push({ + type: "function", + name: "createPattern", + 'arguments': arguments + }); + }, + drawShape: function() { + + var shape = []; + + storage.push({ + type: "function", + name: "drawShape", + 'arguments': shape + }); + + return { + moveTo: function() { + shape.push({ + name: "moveTo", + 'arguments': arguments + }); + }, + lineTo: function() { + shape.push({ + name: "lineTo", + 'arguments': arguments + }); + }, + arcTo: function() { + shape.push({ + name: "arcTo", + 'arguments': arguments + }); + }, + bezierCurveTo: function() { + shape.push({ + name: "bezierCurveTo", + 'arguments': arguments + }); + }, + quadraticCurveTo: function() { + shape.push({ + name: "quadraticCurveTo", + 'arguments': arguments + }); + } + }; + + }, + drawImage: function () { + storage.push({ + type: "function", + name: "drawImage", + 'arguments': arguments + }); + }, + fillText: function () { + storage.push({ + type: "function", + name: "fillText", + 'arguments': arguments + }); + }, + setVariable: function (variable, value) { + storage.push({ + type: "variable", + name: variable, + 'arguments': value + }); + return value; + } + }; + } + _html2canvas.Parse = function (images, options) { + window.scroll(0,0); + + var element = (( options.elements === undefined ) ? document.body : options.elements[0]), // select body by default + numDraws = 0, + doc = element.ownerDocument, + Util = _html2canvas.Util, + support = Util.Support(options, doc), + ignoreElementsRegExp = new RegExp("(" + options.ignoreElements + ")"), + body = doc.body, + getCSS = Util.getCSS, + pseudoHide = "___html2canvas___pseudoelement", + hidePseudoElements = doc.createElement('style'); + + hidePseudoElements.innerHTML = '.' + pseudoHide + '-before:before { content: "" !important; display: none !important; }' + + '.' + pseudoHide + '-after:after { content: "" !important; display: none !important; }'; + + body.appendChild(hidePseudoElements); + + images = images || {}; + + function documentWidth () { + return Math.max( + Math.max(doc.body.scrollWidth, doc.documentElement.scrollWidth), + Math.max(doc.body.offsetWidth, doc.documentElement.offsetWidth), + Math.max(doc.body.clientWidth, doc.documentElement.clientWidth) + ); + } + + function documentHeight () { + return Math.max( + Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight), + Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight), + Math.max(doc.body.clientHeight, doc.documentElement.clientHeight) + ); + } + + function getCSSInt(element, attribute) { + var val = parseInt(getCSS(element, attribute), 10); + return (isNaN(val)) ? 0 : val; // borders in old IE are throwing 'medium' for demo.html + } + + function renderRect (ctx, x, y, w, h, bgcolor) { + if (bgcolor !== "transparent"){ + ctx.setVariable("fillStyle", bgcolor); + ctx.fillRect(x, y, w, h); + numDraws+=1; + } + } + + function capitalize(m, p1, p2) { + if (m.length > 0) { + return p1 + p2.toUpperCase(); + } + } + + function textTransform (text, transform) { + switch(transform){ + case "lowercase": + return text.toLowerCase(); + case "capitalize": + return text.replace( /(^|\s|:|-|\(|\))([a-z])/g, capitalize); + case "uppercase": + return text.toUpperCase(); + default: + return text; + } + } + + function noLetterSpacing(letter_spacing) { + return (/^(normal|none|0px)$/.test(letter_spacing)); + } + + function drawText(currentText, x, y, ctx){ + if (currentText !== null && Util.trimText(currentText).length > 0) { + ctx.fillText(currentText, x, y); + numDraws+=1; + } + } + + function setTextVariables(ctx, el, text_decoration, color) { + var align = false, + bold = getCSS(el, "fontWeight"), + family = getCSS(el, "fontFamily"), + size = getCSS(el, "fontSize"), + shadows = Util.parseTextShadows(getCSS(el, "textShadow")); + + switch(parseInt(bold, 10)){ + case 401: + bold = "bold"; + break; + case 400: + bold = "normal"; + break; + } + + ctx.setVariable("fillStyle", color); + ctx.setVariable("font", [getCSS(el, "fontStyle"), getCSS(el, "fontVariant"), bold, size, family].join(" ")); + ctx.setVariable("textAlign", (align) ? "right" : "left"); + + if (shadows.length) { + // TODO: support multiple text shadows + // apply the first text shadow + ctx.setVariable("shadowColor", shadows[0].color); + ctx.setVariable("shadowOffsetX", shadows[0].offsetX); + ctx.setVariable("shadowOffsetY", shadows[0].offsetY); + ctx.setVariable("shadowBlur", shadows[0].blur); + } + + if (text_decoration !== "none"){ + return Util.Font(family, size, doc); + } + } + + function renderTextDecoration(ctx, text_decoration, bounds, metrics, color) { + switch(text_decoration) { + case "underline": + // Draws a line at the baseline of the font + // TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size + renderRect(ctx, bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, color); + break; + case "overline": + renderRect(ctx, bounds.left, Math.round(bounds.top), bounds.width, 1, color); + break; + case "line-through": + // TODO try and find exact position for line-through + renderRect(ctx, bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, color); + break; + } + } + + function getTextBounds(state, text, textDecoration, isLast, transform) { + var bounds; + if (support.rangeBounds && !transform) { + if (textDecoration !== "none" || Util.trimText(text).length !== 0) { + bounds = textRangeBounds(text, state.node, state.textOffset); + } + state.textOffset += text.length; + } else if (state.node && typeof state.node.nodeValue === "string" ){ + var newTextNode = (isLast) ? state.node.splitText(text.length) : null; + bounds = textWrapperBounds(state.node, transform); + state.node = newTextNode; + } + return bounds; + } + + function textRangeBounds(text, textNode, textOffset) { + var range = doc.createRange(); + range.setStart(textNode, textOffset); + range.setEnd(textNode, textOffset + text.length); + return range.getBoundingClientRect(); + } + + function textWrapperBounds(oldTextNode, transform) { + var parent = oldTextNode.parentNode, + wrapElement = doc.createElement('wrapper'), + backupText = oldTextNode.cloneNode(true); + + wrapElement.appendChild(oldTextNode.cloneNode(true)); + parent.replaceChild(wrapElement, oldTextNode); + + var bounds = transform ? Util.OffsetBounds(wrapElement) : Util.Bounds(wrapElement); + parent.replaceChild(backupText, wrapElement); + return bounds; + } + + function renderText(el, textNode, stack) { + var ctx = stack.ctx, + color = getCSS(el, "color"), + textDecoration = getCSS(el, "textDecoration"), + textAlign = getCSS(el, "textAlign"), + metrics, + textList, + state = { + node: textNode, + textOffset: 0 + }; + + if (Util.trimText(textNode.nodeValue).length > 0) { + textNode.nodeValue = textTransform(textNode.nodeValue, getCSS(el, "textTransform")); + textAlign = textAlign.replace(["-webkit-auto"],["auto"]); + + textList = (!options.letterRendering && /^(left|right|justify|auto)$/.test(textAlign) && noLetterSpacing(getCSS(el, "letterSpacing"))) ? + textNode.nodeValue.split(/(\b| )/) + : textNode.nodeValue.split(""); + + metrics = setTextVariables(ctx, el, textDecoration, color); + + if (options.chinese) { + textList.forEach(function(word, index) { + if (/.*[\u4E00-\u9FA5].*$/.test(word)) { + word = word.split(""); + word.unshift(index, 1); + textList.splice.apply(textList, word); + } + }); + } + + textList.forEach(function(text, index) { + var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1), stack.transform.matrix); + if (bounds) { + drawText(text, bounds.left, bounds.bottom, ctx); + renderTextDecoration(ctx, textDecoration, bounds, metrics, color); + } + }); + } + } + + function listPosition (element, val) { + var boundElement = doc.createElement( "boundelement" ), + originalType, + bounds; + + boundElement.style.display = "inline"; + + originalType = element.style.listStyleType; + element.style.listStyleType = "none"; + + boundElement.appendChild(doc.createTextNode(val)); + + element.insertBefore(boundElement, element.firstChild); + + bounds = Util.Bounds(boundElement); + element.removeChild(boundElement); + element.style.listStyleType = originalType; + return bounds; + } + + function elementIndex(el) { + var i = -1, + count = 1, + childs = el.parentNode.childNodes; + + if (el.parentNode) { + while(childs[++i] !== el) { + if (childs[i].nodeType === 1) { + count++; + } + } + return count; + } else { + return -1; + } + } + + function listItemText(element, type) { + var currentIndex = elementIndex(element), text; + switch(type){ + case "decimal": + text = currentIndex; + break; + case "decimal-leading-zero": + text = (currentIndex.toString().length === 1) ? currentIndex = "0" + currentIndex.toString() : currentIndex.toString(); + break; + case "upper-roman": + text = _html2canvas.Generate.ListRoman( currentIndex ); + break; + case "lower-roman": + text = _html2canvas.Generate.ListRoman( currentIndex ).toLowerCase(); + break; + case "lower-alpha": + text = _html2canvas.Generate.ListAlpha( currentIndex ).toLowerCase(); + break; + case "upper-alpha": + text = _html2canvas.Generate.ListAlpha( currentIndex ); + break; + } + + return text + ". "; + } + + function renderListItem(element, stack, elBounds) { + var x, + text, + ctx = stack.ctx, + type = getCSS(element, "listStyleType"), + listBounds; + + if (/^(decimal|decimal-leading-zero|upper-alpha|upper-latin|upper-roman|lower-alpha|lower-greek|lower-latin|lower-roman)$/i.test(type)) { + text = listItemText(element, type); + listBounds = listPosition(element, text); + setTextVariables(ctx, element, "none", getCSS(element, "color")); + + if (getCSS(element, "listStylePosition") === "inside") { + ctx.setVariable("textAlign", "left"); + x = elBounds.left; + } else { + return; + } + + drawText(text, x, listBounds.bottom, ctx); + } + } + + function loadImage (src){ + var img = images[src]; + return (img && img.succeeded === true) ? img.img : false; + } + + function clipBounds(src, dst){ + var x = Math.max(src.left, dst.left), + y = Math.max(src.top, dst.top), + x2 = Math.min((src.left + src.width), (dst.left + dst.width)), + y2 = Math.min((src.top + src.height), (dst.top + dst.height)); + + return { + left:x, + top:y, + width:x2-x, + height:y2-y + }; + } + + function setZ(element, stack, parentStack){ + var newContext, + isPositioned = stack.cssPosition !== 'static', + zIndex = isPositioned ? getCSS(element, 'zIndex') : 'auto', + opacity = getCSS(element, 'opacity'), + isFloated = getCSS(element, 'cssFloat') !== 'none'; + + // https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context + // When a new stacking context should be created: + // the root element (HTML), + // positioned (absolutely or relatively) with a z-index value other than "auto", + // elements with an opacity value less than 1. (See the specification for opacity), + // on mobile WebKit and Chrome 22+, position: fixed always creates a new stacking context, even when z-index is "auto" (See this post) + + stack.zIndex = newContext = h2czContext(zIndex); + newContext.isPositioned = isPositioned; + newContext.isFloated = isFloated; + newContext.opacity = opacity; + newContext.ownStacking = (zIndex !== 'auto' || opacity < 1); + + if (parentStack) { + parentStack.zIndex.children.push(stack); + } + } + + function renderImage(ctx, element, image, bounds, borders) { + + var paddingLeft = getCSSInt(element, 'paddingLeft'), + paddingTop = getCSSInt(element, 'paddingTop'), + paddingRight = getCSSInt(element, 'paddingRight'), + paddingBottom = getCSSInt(element, 'paddingBottom'); + + drawImage( + ctx, + image, + 0, //sx + 0, //sy + image.width, //sw + image.height, //sh + bounds.left + paddingLeft + borders[3].width, //dx + bounds.top + paddingTop + borders[0].width, // dy + bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight), //dw + bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom) //dh + ); + } + + function getBorderData(element) { + return ["Top", "Right", "Bottom", "Left"].map(function(side) { + return { + width: getCSSInt(element, 'border' + side + 'Width'), + color: getCSS(element, 'border' + side + 'Color') + }; + }); + } + + function getBorderRadiusData(element) { + return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) { + return getCSS(element, 'border' + side + 'Radius'); + }); + } + + var getCurvePoints = (function(kappa) { + + return function(x, y, r1, r2) { + var ox = (r1) * kappa, // control point offset horizontal + oy = (r2) * kappa, // control point offset vertical + xm = x + r1, // x-middle + ym = y + r2; // y-middle + return { + topLeft: bezierCurve({ + x:x, + y:ym + }, { + x:x, + y:ym - oy + }, { + x:xm - ox, + y:y + }, { + x:xm, + y:y + }), + topRight: bezierCurve({ + x:x, + y:y + }, { + x:x + ox, + y:y + }, { + x:xm, + y:ym - oy + }, { + x:xm, + y:ym + }), + bottomRight: bezierCurve({ + x:xm, + y:y + }, { + x:xm, + y:y + oy + }, { + x:x + ox, + y:ym + }, { + x:x, + y:ym + }), + bottomLeft: bezierCurve({ + x:xm, + y:ym + }, { + x:xm - ox, + y:ym + }, { + x:x, + y:y + oy + }, { + x:x, + y:y + }) + }; + }; + })(4 * ((Math.sqrt(2) - 1) / 3)); + + function bezierCurve(start, startControl, endControl, end) { + + var lerp = function (a, b, t) { + return { + x:a.x + (b.x - a.x) * t, + y:a.y + (b.y - a.y) * t + }; + }; + + return { + start: start, + startControl: startControl, + endControl: endControl, + end: end, + subdivide: function(t) { + var ab = lerp(start, startControl, t), + bc = lerp(startControl, endControl, t), + cd = lerp(endControl, end, t), + abbc = lerp(ab, bc, t), + bccd = lerp(bc, cd, t), + dest = lerp(abbc, bccd, t); + return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)]; + }, + curveTo: function(borderArgs) { + borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]); + }, + curveToReversed: function(borderArgs) { + borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]); + } + }; + } + + function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) { + if (radius1[0] > 0 || radius1[1] > 0) { + borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]); + corner1[0].curveTo(borderArgs); + corner1[1].curveTo(borderArgs); + } else { + borderArgs.push(["line", x, y]); + } + + if (radius2[0] > 0 || radius2[1] > 0) { + borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]); + } + } + + function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) { + var borderArgs = []; + + if (radius1[0] > 0 || radius1[1] > 0) { + borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]); + outer1[1].curveTo(borderArgs); + } else { + borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]); + } + + if (radius2[0] > 0 || radius2[1] > 0) { + borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]); + outer2[0].curveTo(borderArgs); + borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]); + inner2[0].curveToReversed(borderArgs); + } else { + borderArgs.push([ "line", borderData.c2[0], borderData.c2[1]]); + borderArgs.push([ "line", borderData.c3[0], borderData.c3[1]]); + } + + if (radius1[0] > 0 || radius1[1] > 0) { + borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]); + inner1[1].curveToReversed(borderArgs); + } else { + borderArgs.push([ "line", borderData.c4[0], borderData.c4[1]]); + } + + return borderArgs; + } + + function calculateCurvePoints(bounds, borderRadius, borders) { + + var x = bounds.left, + y = bounds.top, + width = bounds.width, + height = bounds.height, + + tlh = borderRadius[0][0], + tlv = borderRadius[0][1], + trh = borderRadius[1][0], + trv = borderRadius[1][1], + brh = borderRadius[2][0], + brv = borderRadius[2][1], + blh = borderRadius[3][0], + blv = borderRadius[3][1], + + topWidth = width - trh, + rightHeight = height - brv, + bottomWidth = width - brh, + leftHeight = height - blv; + + return { + topLeftOuter: getCurvePoints( + x, + y, + tlh, + tlv + ).topLeft.subdivide(0.5), + + topLeftInner: getCurvePoints( + x + borders[3].width, + y + borders[0].width, + Math.max(0, tlh - borders[3].width), + Math.max(0, tlv - borders[0].width) + ).topLeft.subdivide(0.5), + + topRightOuter: getCurvePoints( + x + topWidth, + y, + trh, + trv + ).topRight.subdivide(0.5), + + topRightInner: getCurvePoints( + x + Math.min(topWidth, width + borders[3].width), + y + borders[0].width, + (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, + trv - borders[0].width + ).topRight.subdivide(0.5), + + bottomRightOuter: getCurvePoints( + x + bottomWidth, + y + rightHeight, + brh, + brv + ).bottomRight.subdivide(0.5), + + bottomRightInner: getCurvePoints( + x + Math.min(bottomWidth, width + borders[3].width), + y + Math.min(rightHeight, height + borders[0].width), + Math.max(0, brh - borders[1].width), + Math.max(0, brv - borders[2].width) + ).bottomRight.subdivide(0.5), + + bottomLeftOuter: getCurvePoints( + x, + y + leftHeight, + blh, + blv + ).bottomLeft.subdivide(0.5), + + bottomLeftInner: getCurvePoints( + x + borders[3].width, + y + leftHeight, + Math.max(0, blh - borders[3].width), + Math.max(0, blv - borders[2].width) + ).bottomLeft.subdivide(0.5) + }; + } + + function getBorderClip(element, borderPoints, borders, radius, bounds) { + var backgroundClip = getCSS(element, 'backgroundClip'), + borderArgs = []; + + switch(backgroundClip) { + case "content-box": + case "padding-box": + parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width); + parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width); + parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width); + parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width); + break; + + default: + parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top); + parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top); + parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height); + parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height); + break; + } + + return borderArgs; + } + + function parseBorders(element, bounds, borders){ + var x = bounds.left, + y = bounds.top, + width = bounds.width, + height = bounds.height, + borderSide, + bx, + by, + bw, + bh, + borderArgs, + // http://www.w3.org/TR/css3-background/#the-border-radius + borderRadius = getBorderRadiusData(element), + borderPoints = calculateCurvePoints(bounds, borderRadius, borders), + borderData = { + clip: getBorderClip(element, borderPoints, borders, borderRadius, bounds), + borders: [] + }; + + for (borderSide = 0; borderSide < 4; borderSide++) { + + if (borders[borderSide].width > 0) { + bx = x; + by = y; + bw = width; + bh = height - (borders[2].width); + + switch(borderSide) { + case 0: + // top border + bh = borders[0].width; + + borderArgs = drawSide({ + c1: [bx, by], + c2: [bx + bw, by], + c3: [bx + bw - borders[1].width, by + bh], + c4: [bx + borders[3].width, by + bh] + }, borderRadius[0], borderRadius[1], + borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner); + break; + case 1: + // right border + bx = x + width - (borders[1].width); + bw = borders[1].width; + + borderArgs = drawSide({ + c1: [bx + bw, by], + c2: [bx + bw, by + bh + borders[2].width], + c3: [bx, by + bh], + c4: [bx, by + borders[0].width] + }, borderRadius[1], borderRadius[2], + borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner); + break; + case 2: + // bottom border + by = (by + height) - (borders[2].width); + bh = borders[2].width; + + borderArgs = drawSide({ + c1: [bx + bw, by + bh], + c2: [bx, by + bh], + c3: [bx + borders[3].width, by], + c4: [bx + bw - borders[3].width, by] + }, borderRadius[2], borderRadius[3], + borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner); + break; + case 3: + // left border + bw = borders[3].width; + + borderArgs = drawSide({ + c1: [bx, by + bh + borders[2].width], + c2: [bx, by], + c3: [bx + bw, by + borders[0].width], + c4: [bx + bw, by + bh] + }, borderRadius[3], borderRadius[0], + borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner); + break; + } + + borderData.borders.push({ + args: borderArgs, + color: borders[borderSide].color + }); + + } + } + + return borderData; + } + + function createShape(ctx, args) { + var shape = ctx.drawShape(); + args.forEach(function(border, index) { + shape[(index === 0) ? "moveTo" : border[0] + "To" ].apply(null, border.slice(1)); + }); + return shape; + } + + function renderBorders(ctx, borderArgs, color) { + if (color !== "transparent") { + ctx.setVariable( "fillStyle", color); + createShape(ctx, borderArgs); + ctx.fill(); + numDraws+=1; + } + } + + function renderFormValue (el, bounds, stack){ + + var valueWrap = doc.createElement('valuewrap'), + cssPropertyArray = ['lineHeight','textAlign','fontFamily','color','fontSize','paddingLeft','paddingTop','width','height','border','borderLeftWidth','borderTopWidth'], + textValue, + textNode; + + cssPropertyArray.forEach(function(property) { + try { + valueWrap.style[property] = getCSS(el, property); + } catch(e) { + // Older IE has issues with "border" + Util.log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message); + } + }); + + valueWrap.style.borderColor = "black"; + valueWrap.style.borderStyle = "solid"; + valueWrap.style.display = "block"; + valueWrap.style.position = "absolute"; + + if (/^(submit|reset|button|text|password)$/.test(el.type) || el.nodeName === "SELECT"){ + valueWrap.style.lineHeight = getCSS(el, "height"); + } + + valueWrap.style.top = bounds.top + "px"; + valueWrap.style.left = bounds.left + "px"; + + textValue = (el.nodeName === "SELECT") ? (el.options[el.selectedIndex] || 0).text : el.value; + if(!textValue) { + textValue = el.placeholder; + } + + textNode = doc.createTextNode(textValue); + + valueWrap.appendChild(textNode); + body.appendChild(valueWrap); + + renderText(el, textNode, stack); + body.removeChild(valueWrap); + } + + function drawImage (ctx) { + ctx.drawImage.apply(ctx, Array.prototype.slice.call(arguments, 1)); + numDraws+=1; + } + + function getPseudoElement(el, which) { + var elStyle = window.getComputedStyle(el, which); + if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content" || elStyle.display === "none") { + return; + } + var content = elStyle.content + '', + first = content.substr( 0, 1 ); + //strips quotes + if(first === content.substr( content.length - 1 ) && first.match(/'|"/)) { + content = content.substr( 1, content.length - 2 ); + } + + var isImage = content.substr( 0, 3 ) === 'url', + elps = document.createElement( isImage ? 'img' : 'span' ); + + elps.className = pseudoHide + "-before " + pseudoHide + "-after"; + + Object.keys(elStyle).filter(indexedProperty).forEach(function(prop) { + // Prevent assigning of read only CSS Rules, ex. length, parentRule + try { + elps.style[prop] = elStyle[prop]; + } catch (e) { + Util.log(['Tried to assign readonly property ', prop, 'Error:', e]); + } + }); + + if(isImage) { + elps.src = Util.parseBackgroundImage(content)[0].args[0]; + } else { + elps.innerHTML = content; + } + return elps; + } + + function indexedProperty(property) { + return (isNaN(window.parseInt(property, 10))); + } + + function injectPseudoElements(el, stack) { + var before = getPseudoElement(el, ':before'), + after = getPseudoElement(el, ':after'); + if(!before && !after) { + return; + } + + if(before) { + el.className += " " + pseudoHide + "-before"; + el.parentNode.insertBefore(before, el); + parseElement(before, stack, true); + el.parentNode.removeChild(before); + el.className = el.className.replace(pseudoHide + "-before", "").trim(); + } + + if (after) { + el.className += " " + pseudoHide + "-after"; + el.appendChild(after); + parseElement(after, stack, true); + el.removeChild(after); + el.className = el.className.replace(pseudoHide + "-after", "").trim(); + } + + } + + function renderBackgroundRepeat(ctx, image, backgroundPosition, bounds) { + var offsetX = Math.round(bounds.left + backgroundPosition.left), + offsetY = Math.round(bounds.top + backgroundPosition.top); + + ctx.createPattern(image); + ctx.translate(offsetX, offsetY); + ctx.fill(); + ctx.translate(-offsetX, -offsetY); + } + + function backgroundRepeatShape(ctx, image, backgroundPosition, bounds, left, top, width, height) { + var args = []; + args.push(["line", Math.round(left), Math.round(top)]); + args.push(["line", Math.round(left + width), Math.round(top)]); + args.push(["line", Math.round(left + width), Math.round(height + top)]); + args.push(["line", Math.round(left), Math.round(height + top)]); + createShape(ctx, args); + ctx.save(); + ctx.clip(); + renderBackgroundRepeat(ctx, image, backgroundPosition, bounds); + ctx.restore(); + } + + function renderBackgroundColor(ctx, backgroundBounds, bgcolor) { + renderRect( + ctx, + backgroundBounds.left, + backgroundBounds.top, + backgroundBounds.width, + backgroundBounds.height, + bgcolor + ); + } + + function renderBackgroundRepeating(el, bounds, ctx, image, imageIndex) { + var backgroundSize = Util.BackgroundSize(el, bounds, image, imageIndex), + backgroundPosition = Util.BackgroundPosition(el, bounds, image, imageIndex, backgroundSize), + backgroundRepeat = getCSS(el, "backgroundRepeat").split(",").map(Util.trimText); + + image = resizeImage(image, backgroundSize); + + backgroundRepeat = backgroundRepeat[imageIndex] || backgroundRepeat[0]; + + switch (backgroundRepeat) { + case "repeat-x": + backgroundRepeatShape(ctx, image, backgroundPosition, bounds, + bounds.left, bounds.top + backgroundPosition.top, 99999, image.height); + break; + + case "repeat-y": + backgroundRepeatShape(ctx, image, backgroundPosition, bounds, + bounds.left + backgroundPosition.left, bounds.top, image.width, 99999); + break; + + case "no-repeat": + backgroundRepeatShape(ctx, image, backgroundPosition, bounds, + bounds.left + backgroundPosition.left, bounds.top + backgroundPosition.top, image.width, image.height); + break; + + default: + renderBackgroundRepeat(ctx, image, backgroundPosition, { + top: bounds.top, + left: bounds.left, + width: image.width, + height: image.height + }); + break; + } + } + + function renderBackgroundImage(element, bounds, ctx) { + var backgroundImage = getCSS(element, "backgroundImage"), + backgroundImages = Util.parseBackgroundImage(backgroundImage), + image, + imageIndex = backgroundImages.length; + + while(imageIndex--) { + backgroundImage = backgroundImages[imageIndex]; + + if (!backgroundImage.args || backgroundImage.args.length === 0) { + continue; + } + + var key = backgroundImage.method === 'url' ? + backgroundImage.args[0] : + backgroundImage.value; + + image = loadImage(key); + + // TODO add support for background-origin + if (image) { + renderBackgroundRepeating(element, bounds, ctx, image, imageIndex); + } else { + Util.log("html2canvas: Error loading background:", backgroundImage); + } + } + } + + function resizeImage(image, bounds) { + if(image.width === bounds.width && image.height === bounds.height) { + return image; + } + + var ctx, canvas = doc.createElement('canvas'); + canvas.width = bounds.width; + canvas.height = bounds.height; + ctx = canvas.getContext("2d"); + drawImage(ctx, image, 0, 0, image.width, image.height, 0, 0, bounds.width, bounds.height ); + return canvas; + } + + function setOpacity(ctx, element, parentStack) { + return ctx.setVariable("globalAlpha", getCSS(element, "opacity") * ((parentStack) ? parentStack.opacity : 1)); + } + + function removePx(str) { + return str.replace("px", ""); + } + + var transformRegExp = /(matrix)\((.+)\)/; + + function getTransform(element, parentStack) { + var transform = getCSS(element, "transform") || getCSS(element, "-webkit-transform") || getCSS(element, "-moz-transform") || getCSS(element, "-ms-transform") || getCSS(element, "-o-transform"); + var transformOrigin = getCSS(element, "transform-origin") || getCSS(element, "-webkit-transform-origin") || getCSS(element, "-moz-transform-origin") || getCSS(element, "-ms-transform-origin") || getCSS(element, "-o-transform-origin") || "0px 0px"; + + transformOrigin = transformOrigin.split(" ").map(removePx).map(Util.asFloat); + + var matrix; + if (transform && transform !== "none") { + var match = transform.match(transformRegExp); + if (match) { + switch(match[1]) { + case "matrix": + matrix = match[2].split(",").map(Util.trimText).map(Util.asFloat); + break; + } + } + } + + return { + origin: transformOrigin, + matrix: matrix + }; + } + + function createStack(element, parentStack, bounds, transform) { + var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height), + stack = { + ctx: ctx, + opacity: setOpacity(ctx, element, parentStack), + cssPosition: getCSS(element, "position"), + borders: getBorderData(element), + transform: transform, + clip: (parentStack && parentStack.clip) ? Util.Extend( {}, parentStack.clip ) : null + }; + + setZ(element, stack, parentStack); + + // TODO correct overflow for absolute content residing under a static position + if (options.useOverflow === true && /(hidden|scroll|auto)/.test(getCSS(element, "overflow")) === true && /(BODY)/i.test(element.nodeName) === false){ + stack.clip = (stack.clip) ? clipBounds(stack.clip, bounds) : bounds; + } + + return stack; + } + + function getBackgroundBounds(borders, bounds, clip) { + var backgroundBounds = { + left: bounds.left + borders[3].width, + top: bounds.top + borders[0].width, + width: bounds.width - (borders[1].width + borders[3].width), + height: bounds.height - (borders[0].width + borders[2].width) + }; + + if (clip) { + backgroundBounds = clipBounds(backgroundBounds, clip); + } + + return backgroundBounds; + } + + function getBounds(element, transform) { + var bounds = (transform.matrix) ? Util.OffsetBounds(element) : Util.Bounds(element); + transform.origin[0] += bounds.left; + transform.origin[1] += bounds.top; + return bounds; + } + + function renderElement(element, parentStack, pseudoElement, ignoreBackground) { + var transform = getTransform(element, parentStack), + bounds = getBounds(element, transform), + image, + stack = createStack(element, parentStack, bounds, transform), + borders = stack.borders, + ctx = stack.ctx, + backgroundBounds = getBackgroundBounds(borders, bounds, stack.clip), + borderData = parseBorders(element, bounds, borders), + backgroundColor = (ignoreElementsRegExp.test(element.nodeName)) ? "#efefef" : getCSS(element, "backgroundColor"); + + + createShape(ctx, borderData.clip); + + ctx.save(); + ctx.clip(); + + if (backgroundBounds.height > 0 && backgroundBounds.width > 0 && !ignoreBackground) { + renderBackgroundColor(ctx, bounds, backgroundColor); + renderBackgroundImage(element, backgroundBounds, ctx); + } else if (ignoreBackground) { + stack.backgroundColor = backgroundColor; + } + + ctx.restore(); + + borderData.borders.forEach(function(border) { + renderBorders(ctx, border.args, border.color); + }); + + if (!pseudoElement) { + injectPseudoElements(element, stack); + } + + switch(element.nodeName){ + case "IMG": + if ((image = loadImage(element.getAttribute('src')))) { + renderImage(ctx, element, image, bounds, borders); + } else { + Util.log("html2canvas: Error loading <img>:" + element.getAttribute('src')); + } + break; + case "INPUT": + // TODO add all relevant type's, i.e. HTML5 new stuff + // todo add support for placeholder attribute for browsers which support it + if (/^(text|url|email|submit|button|reset)$/.test(element.type) && (element.value || element.placeholder || "").length > 0){ + renderFormValue(element, bounds, stack); + } + break; + case "TEXTAREA": + if ((element.value || element.placeholder || "").length > 0){ + renderFormValue(element, bounds, stack); + } + break; + case "SELECT": + if ((element.options||element.placeholder || "").length > 0){ + renderFormValue(element, bounds, stack); + } + break; + case "LI": + renderListItem(element, stack, backgroundBounds); + break; + case "CANVAS": + renderImage(ctx, element, element, bounds, borders); + break; + } + + return stack; + } + + function isElementVisible(element) { + return (getCSS(element, 'display') !== "none" && getCSS(element, 'visibility') !== "hidden" && !element.hasAttribute("data-html2canvas-ignore")); + } + + function parseElement (element, stack, pseudoElement) { + if (isElementVisible(element)) { + stack = renderElement(element, stack, pseudoElement, false) || stack; + if (!ignoreElementsRegExp.test(element.nodeName)) { + parseChildren(element, stack, pseudoElement); + } + } + } + + function parseChildren(element, stack, pseudoElement) { + Util.Children(element).forEach(function(node) { + if (node.nodeType === node.ELEMENT_NODE) { + parseElement(node, stack, pseudoElement); + } else if (node.nodeType === node.TEXT_NODE) { + renderText(element, node, stack); + } + }); + } + + function init() { + var background = getCSS(document.documentElement, "backgroundColor"), + transparentBackground = (Util.isTransparent(background) && element === document.body), + stack = renderElement(element, null, false, transparentBackground); + parseChildren(element, stack); + + if (transparentBackground) { + background = stack.backgroundColor; + } + + body.removeChild(hidePseudoElements); + return { + backgroundColor: background, + stack: stack + }; + } + + return init(); + }; + + function h2czContext(zindex) { + return { + zindex: zindex, + children: [] + }; + } + + _html2canvas.Preload = function( options ) { + + var images = { + numLoaded: 0, // also failed are counted here + numFailed: 0, + numTotal: 0, + cleanupDone: false + }, + pageOrigin, + Util = _html2canvas.Util, + methods, + i, + count = 0, + element = options.elements[0] || document.body, + doc = element.ownerDocument, + domImages = element.getElementsByTagName('img'), // Fetch images of the present element only + imgLen = domImages.length, + link = doc.createElement("a"), + supportCORS = (function( img ){ + return (img.crossOrigin !== undefined); + })(new Image()), + timeoutTimer; + + link.href = window.location.href; + pageOrigin = link.protocol + link.host; + + function isSameOrigin(url){ + link.href = url; + link.href = link.href; // YES, BELIEVE IT OR NOT, that is required for IE9 - http://jsfiddle.net/niklasvh/2e48b/ + var origin = link.protocol + link.host; + return (origin === pageOrigin); + } + + function start(){ + Util.log("html2canvas: start: images: " + images.numLoaded + " / " + images.numTotal + " (failed: " + images.numFailed + ")"); + if (!images.firstRun && images.numLoaded >= images.numTotal){ + Util.log("Finished loading images: # " + images.numTotal + " (failed: " + images.numFailed + ")"); + + if (typeof options.complete === "function"){ + options.complete(images); + } + + } + } + + // TODO modify proxy to serve images with CORS enabled, where available + function proxyGetImage(url, img, imageObj){ + var callback_name, + scriptUrl = options.proxy, + script; + + link.href = url; + url = link.href; // work around for pages with base href="" set - WARNING: this may change the url + + callback_name = 'html2canvas_' + (count++); + imageObj.callbackname = callback_name; + + if (scriptUrl.indexOf("?") > -1) { + scriptUrl += "&"; + } else { + scriptUrl += "?"; + } + scriptUrl += 'url=' + encodeURIComponent(url) + '&callback=' + callback_name; + script = doc.createElement("script"); + + window[callback_name] = function(a){ + if (a.substring(0,6) === "error:"){ + imageObj.succeeded = false; + images.numLoaded++; + images.numFailed++; + start(); + } else { + setImageLoadHandlers(img, imageObj); + img.src = a; + } + window[callback_name] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9) + try { + delete window[callback_name]; // for all browser that support this + } catch(ex) {} + script.parentNode.removeChild(script); + script = null; + delete imageObj.script; + delete imageObj.callbackname; + }; + + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", scriptUrl); + imageObj.script = script; + window.document.body.appendChild(script); + + } + + function loadPseudoElement(element, type) { + var style = window.getComputedStyle(element, type), + content = style.content; + if (content.substr(0, 3) === 'url') { + methods.loadImage(_html2canvas.Util.parseBackgroundImage(content)[0].args[0]); + } + loadBackgroundImages(style.backgroundImage, element); + } + + function loadPseudoElementImages(element) { + loadPseudoElement(element, ":before"); + loadPseudoElement(element, ":after"); + } + + function loadGradientImage(backgroundImage, bounds) { + var img = _html2canvas.Generate.Gradient(backgroundImage, bounds); + + if (img !== undefined){ + images[backgroundImage] = { + img: img, + succeeded: true + }; + images.numTotal++; + images.numLoaded++; + start(); + } + } + + function invalidBackgrounds(background_image) { + return (background_image && background_image.method && background_image.args && background_image.args.length > 0 ); + } + + function loadBackgroundImages(background_image, el) { + var bounds; + + _html2canvas.Util.parseBackgroundImage(background_image).filter(invalidBackgrounds).forEach(function(background_image) { + if (background_image.method === 'url') { + methods.loadImage(background_image.args[0]); + } else if(background_image.method.match(/\-?gradient$/)) { + if(bounds === undefined) { + bounds = _html2canvas.Util.Bounds(el); + } + loadGradientImage(background_image.value, bounds); + } + }); + } + + function getImages (el) { + var elNodeType = false; + + // Firefox fails with permission denied on pages with iframes + try { + Util.Children(el).forEach(getImages); + } + catch( e ) {} + + try { + elNodeType = el.nodeType; + } catch (ex) { + elNodeType = false; + Util.log("html2canvas: failed to access some element's nodeType - Exception: " + ex.message); + } + + if (elNodeType === 1 || elNodeType === undefined) { + loadPseudoElementImages(el); + try { + loadBackgroundImages(Util.getCSS(el, 'backgroundImage'), el); + } catch(e) { + Util.log("html2canvas: failed to get background-image - Exception: " + e.message); + } + loadBackgroundImages(el); + } + } + + function setImageLoadHandlers(img, imageObj) { + img.onload = function() { + if ( imageObj.timer !== undefined ) { + // CORS succeeded + window.clearTimeout( imageObj.timer ); + } + + images.numLoaded++; + imageObj.succeeded = true; + img.onerror = img.onload = null; + start(); + }; + img.onerror = function() { + if (img.crossOrigin === "anonymous") { + // CORS failed + window.clearTimeout( imageObj.timer ); + + // let's try with proxy instead + if ( options.proxy ) { + var src = img.src; + img = new Image(); + imageObj.img = img; + img.src = src; + + proxyGetImage( img.src, img, imageObj ); + return; + } + } + + images.numLoaded++; + images.numFailed++; + imageObj.succeeded = false; + img.onerror = img.onload = null; + start(); + }; + } + + methods = { + loadImage: function( src ) { + var img, imageObj; + if ( src && images[src] === undefined ) { + img = new Image(); + if ( src.match(/data:image\/.*;base64,/i) ) { + img.src = src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, ''); + imageObj = images[src] = { + img: img + }; + images.numTotal++; + setImageLoadHandlers(img, imageObj); + } else if ( isSameOrigin( src ) || options.allowTaint === true ) { + imageObj = images[src] = { + img: img + }; + images.numTotal++; + setImageLoadHandlers(img, imageObj); + img.src = src; + } else if ( supportCORS && !options.allowTaint && options.useCORS ) { + // attempt to load with CORS + + img.crossOrigin = "anonymous"; + imageObj = images[src] = { + img: img + }; + images.numTotal++; + setImageLoadHandlers(img, imageObj); + img.src = src; + } else if ( options.proxy ) { + imageObj = images[src] = { + img: img + }; + images.numTotal++; + proxyGetImage( src, img, imageObj ); + } + } + + }, + cleanupDOM: function(cause) { + var img, src; + if (!images.cleanupDone) { + if (cause && typeof cause === "string") { + Util.log("html2canvas: Cleanup because: " + cause); + } else { + Util.log("html2canvas: Cleanup after timeout: " + options.timeout + " ms."); + } + + for (src in images) { + if (images.hasOwnProperty(src)) { + img = images[src]; + if (typeof img === "object" && img.callbackname && img.succeeded === undefined) { + // cancel proxy image request + window[img.callbackname] = undefined; // to work with IE<9 // NOTE: that the undefined callback property-name still exists on the window object (for IE<9) + try { + delete window[img.callbackname]; // for all browser that support this + } catch(ex) {} + if (img.script && img.script.parentNode) { + img.script.setAttribute("src", "about:blank"); // try to cancel running request + img.script.parentNode.removeChild(img.script); + } + images.numLoaded++; + images.numFailed++; + Util.log("html2canvas: Cleaned up failed img: '" + src + "' Steps: " + images.numLoaded + " / " + images.numTotal); + } + } + } + + // cancel any pending requests + if(window.stop !== undefined) { + window.stop(); + } else if(document.execCommand !== undefined) { + document.execCommand("Stop", false); + } + if (document.close !== undefined) { + document.close(); + } + images.cleanupDone = true; + if (!(cause && typeof cause === "string")) { + start(); + } + } + }, + + renderingDone: function() { + if (timeoutTimer) { + window.clearTimeout(timeoutTimer); + } + } + }; + + if (options.timeout > 0) { + timeoutTimer = window.setTimeout(methods.cleanupDOM, options.timeout); + } + + Util.log('html2canvas: Preload starts: finding background-images'); + images.firstRun = true; + + getImages(element); + + Util.log('html2canvas: Preload: Finding images'); + // load <img> images + for (i = 0; i < imgLen; i+=1){ + methods.loadImage( domImages[i].getAttribute( "src" ) ); + } + + images.firstRun = false; + Util.log('html2canvas: Preload: Done.'); + if (images.numTotal === images.numLoaded) { + start(); + } + + return methods; + }; + + _html2canvas.Renderer = function(parseQueue, options){ + + // http://www.w3.org/TR/CSS21/zindex.html + function createRenderQueue(parseQueue) { + var queue = [], + rootContext; + + rootContext = (function buildStackingContext(rootNode) { + var rootContext = {}; + function insert(context, node, specialParent) { + var zi = (node.zIndex.zindex === 'auto') ? 0 : Number(node.zIndex.zindex), + contextForChildren = context, // the stacking context for children + isPositioned = node.zIndex.isPositioned, + isFloated = node.zIndex.isFloated, + stub = {node: node}, + childrenDest = specialParent; // where children without z-index should be pushed into + + if (node.zIndex.ownStacking) { + // '!' comes before numbers in sorted array + contextForChildren = stub.context = { '!': [{node:node, children: []}]}; + childrenDest = undefined; + } else if (isPositioned || isFloated) { + childrenDest = stub.children = []; + } + + if (zi === 0 && specialParent) { + specialParent.push(stub); + } else { + if (!context[zi]) { context[zi] = []; } + context[zi].push(stub); + } + + node.zIndex.children.forEach(function(childNode) { + insert(contextForChildren, childNode, childrenDest); + }); + } + insert(rootContext, rootNode); + return rootContext; + })(parseQueue); + + function sortZ(context) { + Object.keys(context).sort().forEach(function(zi) { + var nonPositioned = [], + floated = [], + positioned = [], + list = []; + + // positioned after static + context[zi].forEach(function(v) { + if (v.node.zIndex.isPositioned || v.node.zIndex.opacity < 1) { + // http://www.w3.org/TR/css3-color/#transparency + // non-positioned element with opactiy < 1 should be stacked as if it were a positioned element with ‘z-index: 0’ and ‘opacity: 1’. + positioned.push(v); + } else if (v.node.zIndex.isFloated) { + floated.push(v); + } else { + nonPositioned.push(v); + } + }); + + (function walk(arr) { + arr.forEach(function(v) { + list.push(v); + if (v.children) { walk(v.children); } + }); + })(nonPositioned.concat(floated, positioned)); + + list.forEach(function(v) { + if (v.context) { + sortZ(v.context); + } else { + queue.push(v.node); + } + }); + }); + } + + sortZ(rootContext); + + return queue; + } + + function getRenderer(rendererName) { + var renderer; + + if (typeof options.renderer === "string" && _html2canvas.Renderer[rendererName] !== undefined) { + renderer = _html2canvas.Renderer[rendererName](options); + } else if (typeof rendererName === "function") { + renderer = rendererName(options); + } else { + throw new Error("Unknown renderer"); + } + + if ( typeof renderer !== "function" ) { + throw new Error("Invalid renderer defined"); + } + return renderer; + } + + return getRenderer(options.renderer)(parseQueue, options, document, createRenderQueue(parseQueue.stack), _html2canvas); + }; + + _html2canvas.Util.Support = function (options, doc) { + + function supportSVGRendering() { + var img = new Image(), + canvas = doc.createElement("canvas"), + ctx = (canvas.getContext === undefined) ? false : canvas.getContext("2d"); + if (ctx === false) { + return false; + } + canvas.width = canvas.height = 10; + img.src = [ + "data:image/svg+xml,", + "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>", + "<foreignObject width='10' height='10'>", + "<div xmlns='http://www.w3.org/1999/xhtml' style='width:10;height:10;'>", + "sup", + "</div>", + "</foreignObject>", + "</svg>" + ].join(""); + try { + ctx.drawImage(img, 0, 0); + canvas.toDataURL(); + } catch(e) { + return false; + } + _html2canvas.Util.log('html2canvas: Parse: SVG powered rendering available'); + return true; + } + + // Test whether we can use ranges to measure bounding boxes + // Opera doesn't provide valid bounds.height/bottom even though it supports the method. + + function supportRangeBounds() { + var r, testElement, rangeBounds, rangeHeight, support = false; + + if (doc.createRange) { + r = doc.createRange(); + if (r.getBoundingClientRect) { + testElement = doc.createElement('boundtest'); + testElement.style.height = "123px"; + testElement.style.display = "block"; + doc.body.appendChild(testElement); + + r.selectNode(testElement); + rangeBounds = r.getBoundingClientRect(); + rangeHeight = rangeBounds.height; + + if (rangeHeight === 123) { + support = true; + } + doc.body.removeChild(testElement); + } + } + + return support; + } + + return { + rangeBounds: supportRangeBounds(), + svgRendering: options.svgRendering && supportSVGRendering() + }; + }; + window.html2canvas = function(elements, opts) { + elements = (elements.length) ? elements : [elements]; + var queue, + canvas, + options = { + // general + logging: false, + elements: elements, + background: "#fff", + + // preload options + proxy: null, + timeout: 0, // no timeout + useCORS: false, // try to load images as CORS (where available), before falling back to proxy + allowTaint: false, // whether to allow images to taint the canvas, won't need proxy if set to true + + // parse options + svgRendering: false, // use svg powered rendering where available (FF11+) + ignoreElements: "IFRAME|OBJECT|PARAM", + useOverflow: true, + letterRendering: false, + chinese: false, + + // render options + + width: null, + height: null, + taintTest: true, // do a taint test with all images before applying to canvas + renderer: "Canvas" + }; + + options = _html2canvas.Util.Extend(opts, options); + + _html2canvas.logging = options.logging; + options.complete = function( images ) { + + if (typeof options.onpreloaded === "function") { + if ( options.onpreloaded( images ) === false ) { + return; + } + } + queue = _html2canvas.Parse( images, options ); + + if (typeof options.onparsed === "function") { + if ( options.onparsed( queue ) === false ) { + return; + } + } + + canvas = _html2canvas.Renderer( queue, options ); + + if (typeof options.onrendered === "function") { + options.onrendered( canvas ); + } + + + }; + + // for pages without images, we still want this to be async, i.e. return methods before executing + window.setTimeout( function(){ + _html2canvas.Preload( options ); + }, 0 ); + + return { + render: function( queue, opts ) { + return _html2canvas.Renderer( queue, _html2canvas.Util.Extend(opts, options) ); + }, + parse: function( images, opts ) { + return _html2canvas.Parse( images, _html2canvas.Util.Extend(opts, options) ); + }, + preload: function( opts ) { + return _html2canvas.Preload( _html2canvas.Util.Extend(opts, options) ); + }, + log: _html2canvas.Util.log + }; + }; + + window.html2canvas.log = _html2canvas.Util.log; // for renderers + window.html2canvas.Renderer = { + Canvas: undefined // We are assuming this will be used + }; + _html2canvas.Renderer.Canvas = function(options) { + options = options || {}; + + var doc = document, + safeImages = [], + testCanvas = document.createElement("canvas"), + testctx = testCanvas.getContext("2d"), + Util = _html2canvas.Util, + canvas = options.canvas || doc.createElement('canvas'); + + function createShape(ctx, args) { + ctx.beginPath(); + args.forEach(function(arg) { + ctx[arg.name].apply(ctx, arg['arguments']); + }); + ctx.closePath(); + } + + function safeImage(item) { + if (safeImages.indexOf(item['arguments'][0].src ) === -1) { + testctx.drawImage(item['arguments'][0], 0, 0); + try { + testctx.getImageData(0, 0, 1, 1); + } catch(e) { + testCanvas = doc.createElement("canvas"); + testctx = testCanvas.getContext("2d"); + return false; + } + safeImages.push(item['arguments'][0].src); + } + return true; + } + + function renderItem(ctx, item) { + switch(item.type){ + case "variable": + ctx[item.name] = item['arguments']; + break; + case "function": + switch(item.name) { + case "createPattern": + if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) { + try { + ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat"); + } + catch(e) { + Util.log("html2canvas: Renderer: Error creating pattern", e.message); + } + } + break; + case "drawShape": + createShape(ctx, item['arguments']); + break; + case "drawImage": + if (item['arguments'][8] > 0 && item['arguments'][7] > 0) { + if (!options.taintTest || (options.taintTest && safeImage(item))) { + ctx.drawImage.apply( ctx, item['arguments'] ); + } + } + break; + default: + ctx[item.name].apply(ctx, item['arguments']); + } + break; + } + } + + return function(parsedData, options, document, queue, _html2canvas) { + var ctx = canvas.getContext("2d"), + newCanvas, + bounds, + fstyle, + zStack = parsedData.stack; + + canvas.width = canvas.style.width = options.width || zStack.ctx.width; + canvas.height = canvas.style.height = options.height || zStack.ctx.height; + + fstyle = ctx.fillStyle; + ctx.fillStyle = (Util.isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = fstyle; + + queue.forEach(function(storageContext) { + // set common settings for canvas + ctx.textBaseline = "bottom"; + ctx.save(); + + if (storageContext.transform.matrix) { + ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]); + ctx.transform.apply(ctx, storageContext.transform.matrix); + ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]); + } + + if (storageContext.clip){ + ctx.beginPath(); + ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height); + ctx.clip(); + } + + if (storageContext.ctx.storage) { + storageContext.ctx.storage.forEach(function(item) { + renderItem(ctx, item); + }); + } + + ctx.restore(); + }); + + Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj"); + + if (options.elements.length === 1) { + if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") { + // crop image to the bounds of selected (single) element + bounds = _html2canvas.Util.Bounds(options.elements[0]); + newCanvas = document.createElement('canvas'); + newCanvas.width = Math.ceil(bounds.width); + newCanvas.height = Math.ceil(bounds.height); + ctx = newCanvas.getContext("2d"); + + ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height); + canvas = null; + return newCanvas; + } + } + + return canvas; + }; + }; + })(window,document);
\ No newline at end of file diff --git a/addons/point_of_sale/static/lib/sha1.js b/addons/point_of_sale/static/lib/sha1.js new file mode 100644 index 00000000..0fc324cf --- /dev/null +++ b/addons/point_of_sale/static/lib/sha1.js @@ -0,0 +1,159 @@ +/* from http://www.movable-type.co.uk/scripts/sha1.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* SHA-1 implementation in JavaScript (c) Chris Veness 2002-2014 / MIT Licence */ +/* */ +/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */ +/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +/* jshint node:true *//* global define, escape, unescape */ +'use strict'; + + +/** + * SHA-1 hash function reference implementation. + * + * @namespace + */ +var Sha1 = {}; + + +/** + * Generates SHA-1 hash of string. + * + * @param {string} msg - (Unicode) string to be hashed. + * @returns {string} Hash of msg as hex character string. + */ +Sha1.hash = function(msg) { + // convert string to UTF-8, as SHA only deals with byte-streams + msg = msg.utf8Encode(); + + // constants [§4.2.1] + var K = [ 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 ]; + + // PREPROCESSING + + msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1] + + // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] + var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length + var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints + var M = new Array(N); + + for (var i=0; i<N; i++) { + M[i] = new Array(16); + for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding + M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) | + (msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3)); + } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0 + } + // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1] + // note: most significant word would be (len-1)*8 >>> 32, but since JS converts + // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators + M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]); + M[N-1][15] = ((msg.length-1)*8) & 0xffffffff; + + // set initial hash value [§5.3.1] + var H0 = 0x67452301; + var H1 = 0xefcdab89; + var H2 = 0x98badcfe; + var H3 = 0x10325476; + var H4 = 0xc3d2e1f0; + + // HASH COMPUTATION [§6.1.2] + + var W = new Array(80); var a, b, c, d, e; + for (var i=0; i<N; i++) { + + // 1 - prepare message schedule 'W' + for (var t=0; t<16; t++) W[t] = M[i][t]; + for (var t=16; t<80; t++) W[t] = Sha1.ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1); + + // 2 - initialise five working variables a, b, c, d, e with previous hash value + a = H0; b = H1; c = H2; d = H3; e = H4; + + // 3 - main loop + for (var t=0; t<80; t++) { + var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants + var T = (Sha1.ROTL(a,5) + Sha1.f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff; + e = d; + d = c; + c = Sha1.ROTL(b, 30); + b = a; + a = T; + } + + // 4 - compute the new intermediate hash value (note 'addition modulo 2^32') + H0 = (H0+a) & 0xffffffff; + H1 = (H1+b) & 0xffffffff; + H2 = (H2+c) & 0xffffffff; + H3 = (H3+d) & 0xffffffff; + H4 = (H4+e) & 0xffffffff; + } + + return Sha1.toHexStr(H0) + Sha1.toHexStr(H1) + Sha1.toHexStr(H2) + + Sha1.toHexStr(H3) + Sha1.toHexStr(H4); +}; + + +/** + * Function 'f' [§4.1.1]. + * @private + */ +Sha1.f = function(s, x, y, z) { + switch (s) { + case 0: return (x & y) ^ (~x & z); // Ch() + case 1: return x ^ y ^ z; // Parity() + case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj() + case 3: return x ^ y ^ z; // Parity() + } +}; + +/** + * Rotates left (circular left shift) value x by n positions [§3.2.5]. + * @private + */ +Sha1.ROTL = function(x, n) { + return (x<<n) | (x>>>(32-n)); +}; + + +/** + * Hexadecimal representation of a number. + * @private + */ +Sha1.toHexStr = function(n) { + // note can't use toString(16) as it is implementation-dependant, + // and in IE returns signed numbers when used on full words + var s="", v; + for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); } + return s; +}; + + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + + +/** Extend String object with method to encode multi-byte string to utf8 + * - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */ +if (typeof String.prototype.utf8Encode == 'undefined') { + String.prototype.utf8Encode = function() { + return unescape( encodeURIComponent( this ) ); + }; +} + +/** Extend String object with method to decode utf8 string to multi-byte */ +if (typeof String.prototype.utf8Decode == 'undefined') { + String.prototype.utf8Decode = function() { + try { + return decodeURIComponent( escape( this ) ); + } catch (e) { + return this; // invalid UTF-8? return as-is + } + }; +} + + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export +if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD diff --git a/addons/point_of_sale/static/lib/waitfont.js b/addons/point_of_sale/static/lib/waitfont.js new file mode 100644 index 00000000..e9359896 --- /dev/null +++ b/addons/point_of_sale/static/lib/waitfont.js @@ -0,0 +1,59 @@ +// http://stackoverflow.com/questions/4383226/using-jquery-to-know-when-font-face-fonts-are-loaded +(function(){ + function waitForWebfonts(fonts, callback) { + var loadedFonts = 0; + for(var i = 0, l = fonts.length; i < l; ++i) { + (function(font) { + var node = document.createElement('span'); + // Characters that vary significantly among different fonts + node.innerHTML = 'giItT1WQy@!-/#'; + // Visible - so we can measure it - but not on the screen + node.style.position = 'absolute'; + node.style.left = '-10000px'; + node.style.top = '-10000px'; + // Large font size makes even subtle changes obvious + node.style.fontSize = '300px'; + // Reset any font properties + node.style.fontFamily = 'sans-serif'; + node.style.fontVariant = 'normal'; + node.style.fontStyle = 'normal'; + node.style.fontWeight = 'normal'; + node.style.letterSpacing = '0'; + document.body.appendChild(node); + + // Remember width with no applied web font + var width = node.offsetWidth; + + node.style.fontFamily = font; + + var interval; + function checkFont() { + // Compare current width with original width + if(node && node.offsetWidth != width) { + ++loadedFonts; + node.parentNode.removeChild(node); + node = null; + } + + // If all fonts have been loaded + if(loadedFonts >= fonts.length) { + if(interval) { + clearInterval(interval); + } + if(loadedFonts == fonts.length) { + callback(); + return true; + } + } + }; + + if(!checkFont()) { + interval = setInterval(checkFont, 50); + } + })(fonts[i]); + } + } + window.waitForWebfonts = waitForWebfonts; +})(); + + |
