/*** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Svidget.js v0.3.5 * Release Date: 2018-03-02 * * A framework for creating complex widgets using SVG. * * http://www.svidget.com/ * * Copyright 2018, Joe Agster * Licensed under the MIT license. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ ; !function(global, factory) { "function" == typeof define && define.amd ? define([ "svidget" ], factory) : "object" == typeof module && module.exports ? module.exports = factory : // don't defer, instantiate svidget immediately // global === window global.svidget = factory(global); }(this, function(global, createOptions) { var VERSION = "0.3.5", Svidget = {}, window = global, document = window.document || {}; //var root = null; Svidget.root = null; // set in Svidget.Root, this is the singleton instance to the global "svidget" object. Svidget.global = global; // In server side environments will === global, in browser === window //Svidget.window = null; // In server side environments will be mock window or jsdom-like window //Svidget.document = null; // In server side environments will be window.document or jsdom-like document Svidget.version = VERSION; Svidget.declaredHandlerName = "_declared"; Svidget.emptyArray = []; Svidget.defaultType = "object"; /** @function Svidget.array Builds an array from any collection, be it another array, HTMLCollection, etc. */ Svidget.array = function(anyCollection) { // oops, we need a collection with a length property if (!anyCollection || !anyCollection.length) return null; try { // this may blow up for IE8 and below and other less than modern browsers return Svidget.emptyArray.slice.call(anyCollection, 0); } catch (e) { for (var res = [], i = 0; i < anyCollection.length; i++) res.push(anyCollection[i]); return res; } }; /** @function Svidget.isArray Determines if passed in object is actually an array. This is a more relaxed check and handles the case when array is a Svidget.Collection and/or across frame boundaries */ Svidget.isArray = function(array) { return null != array && (Array.isArray(array) || Array.prototype.isPrototypeOf(array) || array.length && array.push); }; Svidget.isFunction = function(func) { return "function" == typeof func; }; Svidget.isString = function(str) { return "string" == typeof str || str.length && str.trim && str.charAt; }; Svidget.isColor = function(color) { // todo return false; }; Svidget.convert = function(val, type, subtype, typedata) { return Svidget.Conversion.to(val, type, subtype, typedata); }; Svidget.getType = function(val) { if (null == val) return Svidget.defaultType; // object is the default type (as of 0.3.0, formerly "string") if (Svidget.isArray(val)) return "array"; var type = typeof val; // if (type === "boolean") return "bool"; // as of 0.3.0 using "boolean" not "bool" // if (type === "boolean") return "bool"; // as of 0.3.0 using "boolean" not "bool" return "string" === type || "number" === type || "boolean" === type ? type : "object"; }; // Given a type, determines if it is valid and if not returns the default type. Svidget.resolveType = function(type) { type = Svidget.Conversion.toString(type); return void 0 === Svidget.ParamTypes[type] ? Svidget.defaultType : "bool" === type ? "boolean" : type; }; // Given a subtype, determines if it is valid and if not returns null. Svidget.resolveSubtype = function(type, subtype) { type = Svidget.resolveType(type); //subtype is not valid, so return null return void 0 === Svidget.ParamSubTypes[subtype] ? null : "string" != type || "color" != subtype && "regex" != subtype && "color" != subtype ? "number" == type && "integer" == subtype ? subtype : null : subtype; }; Svidget.extend = function(objtype, prototype, overwrite) { for (var methodName in prototype) // do we check for hasOwnProperty here? (overwrite || void 0 === objtype.prototype[methodName]) && (objtype.prototype[methodName] = prototype[methodName]); }; Svidget.wrap = function(func, context) { // todo: use function.bind() if available // ensure func is function, return undefined if not if (null != func && "function" == typeof func) { return function() { return func.apply(context, arguments); }; } }; // Find the function by name in the specified scope, or just return it if is already a function // By default scope == global scope Svidget.findFunction = function(funcNameOrInstance, scope) { if ("function" == typeof funcNameOrInstance) return funcNameOrInstance; null == scope && (scope = window); // global scope if (null != funcNameOrInstance) { var bind = funcNameOrInstance + "", func = scope[bind]; null == func && scope !== global && (func = global[bind]); // bind is an expression, so just wrap it in a function return null == func ? null : "function" == typeof func ? func : "return " != bind.substr(0, 7) ? new Function("return " + bind) : new Function(bind); } return null; }; Svidget.log = function(msg) { Svidget.Settings.enableLogging && console.log(msg); }; Svidget.readOnlyProperty = function(value) { return { enumerable: true, configurable: false, writable: false, value: value }; }; Svidget.fixedProperty = function(value) { return { enumerable: true, configurable: false, writable: true, value: value }; }; Svidget.getPrivateAccessor = function(privates) { return function(p) { return privates[p]; }; }; Svidget.setPrivateAccessor = function(privates) { return function(p, val) { if (!privates.writable.contains(p)) return false; privates[p] = val; return true; }; }; Svidget.returnFalse = function() { return false; }; Svidget.returnTrue = function() { return true; }; Svidget.Settings = {}; Svidget.Settings.showPrivates = true; //show private members of objects, useful for debugging Svidget.Settings.enableLogging = false; // whether console.logging is enabled, turn on for troubleshooting Array.prototype.contains || (Array.prototype.contains = function(obj) { for (var i = this.length; i--; ) if (this[i] === obj) return true; return false; }); // a note about "bla instanceof Array" checks, they don't work across frame boundaries // so we use isArray Array.isArray || (Array.isArray = function(arg) { return "[object Array]" === Object.prototype.toString.call(arg); }); /** * Encapsulates common functionality for all object types in the framework. * @class * @static * @memberof Svidget */ Svidget.ObjectPrototype = { // protected setup: function(privates) { this.getPrivate = Svidget.getPrivateAccessor(privates); this.setPrivate = Svidget.setPrivateAccessor(privates); Svidget.Settings.showPrivates && (this.privates = privates); }, // protected // as of 0.3.0: type argument converts val to type getset: function(prop, val, type, validator) { // ** get mode var curProp = this.getPrivate(prop); if (void 0 === val) return curProp; // ** set mode // first, convert if needed null != type && (val = Svidget.convert(val, type)); // if value is current value then do nothing // if value is current value then do nothing // then, validate return val !== curProp && (!(validator && !validator.call(this, val)) && this.setPrivate(prop, val)); }, // protected // should always return a collection select: function(col, selector) { if ("number" == typeof selector) { selector = parseInt(selector); // coerce to integer return col.wrap(col.getByIndex(selector)); } return "function" == typeof selector ? col.where(selector) : void 0 !== selector ? col.wrap(col.getByName(selector + "")) : col; }, // protected // should always return a single item selectFirst: function(col, selector) { if ("number" == typeof selector) { selector = parseInt(selector); // coerce to integer return col.getByIndex(selector); } return "function" == typeof selector ? col.first(selector) : void 0 !== selector ? col.getByName(selector + "") : col.first(); }, // protected wireCollectionAddRemoveHandlers: function(col, addFunc, removeFunc) { if (null != col) { col.onAdded(Svidget.wrap(addFunc, this)); col.onRemoved(Svidget.wrap(removeFunc, this)); } } }; /** * Encapsulates a set of prototype methods for managing events. * @class * @memberof Svidget * @param {array} typelist - A list of event types. */ Svidget.EventPrototype = function(typelist) { // todo: validate typelist // these are the event types that the base object supports, like "invoke" for action. this.eventTypes = new Svidget.Collection(typelist); }; Svidget.EventPrototype.prototype = { on: function(type, data, name, handler) { // resolve handler from whether name, data passed var argsCount = Svidget.isFunction(handler) ? 4 : Svidget.isFunction(name) ? 3 : Svidget.isFunction(data) ? 2 : 1; //handler = handler || (Svidget.isFunction(name) ? name : (Svidget.isFunction(data) ? data : null)); handler = 4 == argsCount ? handler : 3 == argsCount ? name : 2 == argsCount ? data : null; data = argsCount > 2 ? data : null; name = argsCount > 3 ? name : null; return !!handler && this.addHandler(type, handler, name, data); }, off: function(type, handlerOrName) { // separate handlerOrName into handler, name var handler = Svidget.isFunction(handlerOrName) ? handlerOrName : null, name = null != handler ? null : handlerOrName; return this.removeHandler(type, handler, name); }, trigger: function(type, value, originalTarget) { if (null != type) { // nothing to do // get event object from handlers var e = this.triggerHandlers(type, value, originalTarget); Svidget.log("trigger: " + type); // if not stopPropagation call bubble e.isPropagationStopped() || this.bubble(type, e); } }, triggerHandlers: function(type, value, originalTarget) { // generate event object var e = new Svidget.Event(null, type, null, this.getTarget(), originalTarget, value); if (null == type || null == this.handlers || null == this.handlers[type]) return e; // loop through each handler (make sure it is a function) // h == { handler: handler, name: name, data: data } this.handlers[type].each(function(h) { if (e.isImmediatePropagationStopped()) return false; // if stopImmediatePropagation, then exit loop by returning false if (null != h && null != h.handler && "function" == typeof h.handler) { // handler is not a function // set name/data e.name = h.name; e.data = h.data; // invoke handler h.handler.call(null, e); } }); return e; }, bubble: function(type, sourceEvent) { // invoked from child this.ensureBubbleParents(); sourceEvent.name = null; sourceEvent.data = null; this.bubbleParents[type] && this.bubbleParents[type](type, sourceEvent, this.getTarget()); }, addHandler: function(type, handler, name, data) { this.ensureHandlersByType(type); // todo: get handler function name, we will use to off() handlers by name //if (this.handlers[type].contains(handler)) return false; if (this.handlerExists(type, handler, name)) return false; var obj = this.toHandlerObject(handler, name, data); this.handlers[type].push(obj); return true; }, removeHandler: function(type, handler, name) { this.ensureHandlers(); if (!this.handlers[type]) return false; //return this.handlers[type].removeAll(handler); var that = this; return this.handlers[type].removeWhere(function(item) { return that.handlerMatch(item, handler, name); }); }, handlerExists: function(type, handler, name) { var that = this; return this.handlers[type].any(function(item) { return that.handlerMatch(item, handler, name); }); }, handlerMatch: function(handlerObj, handler, name) { return null != name && handlerObj.name === name || handler === handlerObj.handler; }, setBubbleParent: function(type, callback) { this.ensureBubbleParents(); this.bubbleParents[type] = callback; }, // private, called by the object to register a single callback for all its event types // bubbleTarget: usually a parent object registerBubbleCallback: function(types, bubbleTarget, callback) { if (bubbleTarget && callback) for (var i = 0; i < types.length; i++) this.setBubbleParent(types[i], Svidget.wrap(callback, bubbleTarget)); }, toHandlerObject: function(handler, name, data) { return { handler: handler, name: name, data: data }; }, bubbleFuncs: function(objectType) {}, ensureHandlers: function() { this.handlers || (this.handlers = {}); }, ensureHandlersByType: function(type) { this.ensureHandlers(); this.handlers[type] || (this.handlers[type] = new Svidget.Collection()); }, ensureBubbleParents: function() { this.bubbleParents || (this.bubbleParents = {}); }, // internal // returns the target object to use for the event object // override in eventContainer getTarget: function() { return this; } }; /** * Encapsulates common functionality for a Param and ActionParam. * @class * @abstract * @memberof Svidget */ Svidget.ParamPrototype = { /** * Gets the param name. * @method * @returns {string} */ name: function(val) { return void 0 === val && this.getPrivate("name"); }, /** * Gets or sets the description. * @method * @param {string} [val] - Sets the value when specified. * @returns {string} - The value for a get, or true/false if succeeded or failed for a set. */ description: function(val) { var res = this.getset("description", val, "string"); // if undefined its a get so return value, if res is false then set failed if (void 0 === val || !!!res) return res; // fire "changed" event val = this.getPrivate("description"); // get converted value this.trigger && this.trigger("change", { property: "description", value: val }); // set was successful return true; }, /** * Gets or sets the param type. This can be "string", "number", etc. See the Svidget.ParamTypes enum. * @method * @param {string} [val] - Sets the value when specified. * @returns {string} - The value for a get, or true/false if succeeded or failed for a set. */ type: function(val) { null === val && (val = Svidget.resolveType(null)); // rturns default type "bool" == val && (val = "boolean"); var res = this.getset("type", val, "string", this.validateType); // if undefined its a get so return value, if res is false then set failed if (void 0 === val || !!!res) return res; // fire "changed" event val = this.getPrivate("type"); // get converted value this.trigger && this.trigger("change", { property: "type", value: val }); // set was successful return true; }, /** * Gets or sets the param subtype. This can be on of the values from the Svidget.ParamSubTypes enum. * @method * @param {string} [val] - Sets the value when specified. * @returns {string} - The value for a get, or true/false if succeeded or failed for a set. */ subtype: function(val) { var res = this.getset("subtype", val, "string", this.validateSubtype); // if undefined its a get so return value, if res is false then set failed if (void 0 === val || !!!res) return res; // fire "changed" event val = this.getPrivate("subtype"); // get converted value this.trigger && this.trigger("change", { property: "subtype", value: val }); // set was successful return true; }, /** * Gets or sets the type data associated with the subtype. This can be a regex pattern or choice string, among other things. * @method * @param {string} [val] - Sets the typedata when specified. * @returns {string} - The typedata when nothing is passed, or true/false if succeeded or failed when setting. */ typedata: function(val) { var res = this.getset("typedata", val, "string"); // if undefined its a get so return value, if res is false then set failed if (void 0 === val || !!!res) return res; // fire "changed" event val = this.getPrivate("typedata"); // get converted value this.trigger && this.trigger("change", { property: "typedata", value: val }); // set was successful return true; }, /** * Gets or sets the param defvalue. This is the default value if nothing (or undefined) is set. * @method * @param {string} [val] - Sets the value when specified. * @returns {string} - The value for a get, or true/false if succeeded or failed for a set. */ defvalue: function(val) { var res = this.getset("defvalue", val); // if undefined its a get so return value, if res is false then set failed if (void 0 === val || !!!res) return res; // fire "change" event this.trigger && this.trigger("change", { property: "defvalue", value: val }); return true; }, validateType: function(t) { return "string" !== !typeof t && void 0 !== Svidget.ParamTypes[t]; }, validateSubtype: function(t) { return true; }, _resolveType: function(type, value, defvalue) { // infer the type from the value or defvalue value = null != value ? value : defvalue; type = null == type ? Svidget.getType(value) : Svidget.resolveType(type); // normalize type to a valid type return type; } }; /** * Represents a structured collection. Extends Array by providing additional methods to select, tranverse, and modify an array. * @constructor * @augments Array * @param {Array} array - The initial elements of the collection. */ Svidget.Collection = function(array) { this.__type = "Svidget.Collection"; this.source = array; //this.baseType = Svidget.Collection; // this.items = []; // append items from source to this instance array && (Svidget.isArray(array) || Array.prototype.isPrototypeOf(array)) && this.push.apply(this, array); }; Svidget.Collection.prototype = new Array(); Svidget.extend(Svidget.Collection, { /** * Returns true if any of the items satisfies the specified predicate function. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {boolean} */ any: function(predicate) { if (null == predicate) return this.length > 0; for (var i = 0; i < this.length; i++) if (predicate(this[i])) return true; return false; }, /** * Returns true if all of the items satisfies the specified predicate function. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {boolean} */ all: function(predicate) { if (null == predicate) return false; for (var i = 0; i < this.length; i++) if (!predicate(this[i])) return false; return true; }, /** * Returns true if the item is contained in the collection. * @method * @param {object} obj - The object to look for. * @returns {boolean} */ contains: function(obj) { return this.indexOf(obj) >= 0; }, /** * Iterates on each item in the collection and performs the operation. * @method * @param {Function} operation - A function that accepts an item as input. */ each: function(operation) { for (var i = 0; i < this.length; i++) { if (false === operation(this[i])) break; } }, /** * Returns the first item in the collection that satisfies the condition in the specified predicate function. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {object} - The item in the collection. */ first: function(predicate) { if (0 == this.length) return null; if (null == predicate || "function" === !typeof predicate) return this[0]; for (var i = 0; i < this.length; i++) if (predicate(this[i])) return this[i]; return null; }, /** * Returns the last item in the collection that satisfies the condition in the specified predicate function. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {object} - The item in the collection. */ last: function(predicate) { if (0 == this.length) return null; if (null == predicate || "function" === !typeof predicate) return this[0]; for (var i = this.length - 1; i >= 0; i--) if (predicate(this[i])) return this[i]; return null; }, /** * Iterates on the collection calling the specified selector function and returns a new collection. Chainable. * @method * @param {Function} selector - A function that accepts an item as input and returns a value based on it. * @returns {Svidget.Collection} - The result collection. */ select: function(selector) { for (var result = [], i = 0; i < this.length; i++) result.push(selector(this[i])); return new Svidget.Collection(result); }, /** * Iterates on the collection calling the specified predicate filter function and returns a new collection. Chainable. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {Svidget.Collection} - The result collection. */ where: function(predicate) { for (var result = [], i = 0; i < this.length; i++) predicate(this[i]) && result.push(this[i]); return new Svidget.Collection(result); }, // modifiers /** * Adds an item to the collection. * @method * @param {object} obj - The item to add. * @returns {boolean} - True if add succeeds. */ add: function(obj) { if (this.indexOf(obj) >= 0) return false; this.push(obj); return true; }, /** * Adds an array of items to the collection. * @method * @param {Array} array - The items to add. * @returns {boolean} - True if add succeeds. */ addRange: function(array) { if (!Svidget.isArray(array)) return false; this.push.apply(this, array); return true; }, /** * Inserts an item to the collection at the specified index. * @method * @param {object} obj - The item to add. * @param {number} index - The items to add. * @returns {boolean} - True if add succeeds. */ insert: function(obj, index) { index = parseInt(index); if (!isNaN(index) && (index < 0 || index > this.length)) return false; this.splice(index, 0, obj); return true; }, /** * Removes an item from the collection. Only removes the first instance of the item found. * @method * @param {object} obj - The item to remove. * @returns {boolean} - True if remove succeeds. */ remove: function(obj) { var pos = this.indexOf(obj); if (pos < 0) return false; this.splice(pos, 1); return true; }, /** * Removes an item from the collection. Removes all instances of the item. * @method * @param {object} obj - The item to remove. * @returns {boolean} - True if remove succeeds. */ removeAll: function(obj) { for (var removed = false; this.remove(obj); ) removed = true; return removed; }, /** * Clears all items in the collection. * @method * @returns {boolean} - True after collection cleared. */ clear: function() { this.splice(0, this.length); return true; }, /** * Removes an item from the collection based on the specified predicate function. * @method * @param {Function} predicate - A function that accepts an item as input and returns true/false. * @returns {boolean} - True if remove succeeds. */ removeWhere: function(predicate) { for (var result = [], removed = false, i = 0; i < this.length; i++) predicate(this[i]) && result.push(this[i]); for (var i = 0; i < result.length; i++) removed = this.remove(result[i]) || removed; return removed; }, // misc /** * Returns a new array based on items in the collection. * @method * @returns {Array} */ toArray: function() { for (var arr = [], i = 0; i < this.length; i++) arr.push(this[i]); //var check = arr instanceof Array; //Svidget.log('is it array: ' + check); return arr; } }); /** * Represents a specialized collection for framework objects. * @constructor * @mixes Svidget.ObjectPrototype * @augments Svidget.Collection * @param {Array} array - The initial elements of the collection. * @param {Function} type - The type of objects that the collection should hold. */ Svidget.ObjectCollection = function(array, type) { // todo: filter input array by type specified Svidget.Collection.apply(this, [ array ]); this.__type = "Svidget.ObjectCollection"; this._ctor = Svidget.ObjectCollection; // private fields var privates = new function() { this.writable = [ "addedFunc", "removedFunc" ]; this.type = type; this.addedFunc = null; this.removedFunc = null; }(); // private accessors this.setup(privates); }; var base = new Svidget.Collection(); Svidget.ObjectCollection.prototype = base; Svidget.ObjectCollection.prototype.base_add = base.add; Svidget.ObjectCollection.prototype.base_remove = base.remove; Svidget.extend(Svidget.ObjectCollection, { /** * Gets an item in the collection by its index (if number passed) or id/name (if string passed). * @method * @param {(string|number)} selector - The selector. * @returns {object} - The object in the collection. */ get: function(selector) { return "number" == typeof selector ? col.getByIndex(selector) : col.getByName(selector); }, /** * Gets an item in the collection by its index. * @method * @param {number} selector - The index. * @returns {object} - The object in the collection. */ getByIndex: function(index) { if (null == index || isNaN(index)) return null; index = parseInt(index); var res = this[index]; return null == res ? null : res; }, /** * Gets an item in the collection by its name. * @method * @param {string} selector - The name. * @returns {object} - The object in the collection. */ getByName: function(name) { return this.first(function(p) { return p.name() == name; }); }, /** * Gets the object type that this collection holds. * @method * @returns {Function} - The type constructor that represents the type. */ type: function() { return this.getset("type"); }, /** * Adds an item to the collection. * @method * @param {object} obj - The item to add. * @returns {boolean} - The item that was added. */ add: function() { if (0 == arguments.length) return null; // call add overload var item, arg0, success; arguments.length >= 1 && (arg0 = arguments[0]); item = "string" == typeof arg0 ? this.create.apply(this, arguments) : arg0; if (null == item) return null; success = this.addObject(item); if (!success) return null; // if succeeded, notify widget this.triggerAdded(item); return item; }, addObject: function(obj) { // ensure obj is a valid Param if (null == obj || !obj instanceof this.type()) return false; // ensure no other parameter exists in the collection by that name if (void 0 !== obj.name && null != this.getByName(obj.name())) return false; // add to collection this.push(obj); return obj; }, /** * Creates an returns the item. * @abstract */ create: function() { // override me return null; }, /** * Removes an item from the collection. Only removes the first instance of the item found. * @method * @param {object} obj - The item to remove. * @returns {boolean} - True if remove succeeds. */ remove: function(name) { var item = this.getByName(name); if (null == item) return false; if (!this.base_remove(item)) return false; // if succeeded, notify widget this.triggerRemoved(item); return true; }, /** * Wraps the specified item in a new ObjectCollection. * @method * @param {object} item - The item to make a collection from. * @returns {Svidget.ObjectCollection} - The collection. */ wrap: function(item) { var items = [ item ]; (null == item || !item instanceof this.type()) && (items = []); //, this.parent); return new Svidget.Collection(items); }, // internal // wires up an internal handler when an item is added to the collection onAdded: function(func) { this.getset("addedFunc", func); }, // internal // wires up an internal handler when an item is removed from the collection onRemoved: function(func) { this.getset("removedFunc", func); }, // private triggerAdded: function(item) { var func = this.getset("addedFunc"); func && func(item); }, // private triggerRemoved: function(item) { var func = this.getset("removedFunc"); func && func(item); } }, true); // overwrite base methods, copied above Svidget.extend(Svidget.Collection, Svidget.ObjectPrototype); /** * Provides communications functionality between a page and a widget. Internal class only. * @class */ Svidget.Communicator = function() { this.__type = "Svidget.Communicator"; this.sameDomain = null; this._init(); }; Svidget.Communicator.prototype = { _init: function() { this.addMessageEvent(); }, // REGION: Events addMessageEvent: function() { window.addEventListener && window.addEventListener("message", Svidget.wrap(this.receiveXSM, this), false); }, // REGION: Receive receiveFromParent: function(name, payload) { svidget.receiveFromParent(name, payload); }, receiveFromWidget: function(name, payload, widgetID) { svidget.receiveFromWidget(name, payload, widgetID); }, receiveXSM: function(message) { if (null != message) { var msgData = message.data; null != msgData && (// it's possible in future we'll have a nested widget scenario, so a widget can have both a parent and widgets. void 0 !== msgData.widget ? this.receiveFromWidget(msgData.name, msgData.payload, msgData.widget) : this.receiveFromParent(msgData.name, msgData.payload)); } }, // REGION: Signal Parent signalParent: function(name, payload, widgetID) { // todo: cache result of isParentSameDomain this.isParentSameDomain() ? this.signalParentDirect(name, payload, widgetID) : this.signalParentXSM(name, payload, widgetID); }, signalParentDirect: function(name, payload, widgetID) { //var check = window === window.parent; // note: if window.parent.svidget is null it means the widget DOM was ready before the page DOM, although unlikely it is possible // so we need to handle somehow if (null != window.parent && window !== window.parent && null != window.parent.svidget && window.parent.svidget) { var root = window.parent.svidget; setTimeout(function() { root.routeFromWidget(name, payload, widgetID); }, 0); } }, signalParentXSM: function(name, payload, widgetID) { if (null != window.parent && null != window.parent.postMessage) { //alert('postMessage'); var msg = this.buildSignalParentMessage(name, payload, widgetID); window.parent.postMessage(msg, "*"); } }, buildSignalParentMessage: function(name, payload, widgetID) { return { name: name, payload: payload, widget: widgetID }; }, // todo: move to widget // note: this returns true when widget is forced cross domain isParentSameDomain: function() { null == this.sameParentDomain && (this.sameParentDomain = this.checkParentSameDomain()); return this.sameParentDomain; }, // todo: move to Svidget.DOM checkParentSameDomain: function() { if (null == window.parent) return false; try { window.parent.document; return true; } catch (ex) { return false; } }, // REGION: Widgets signalWidget: function(widgetRef, name, payload) { Svidget.log("communicator: signalWidget {name: " + name + "}"); widgetRef.isCrossDomain() ? this.signalWidgetXSM(widgetRef, name, payload) : //this.isWidgetSameDomain(widgetProxy)) this.signalWidgetDirect(widgetRef, name, payload); }, signalWidgetDirect: function(widgetRef, name, payload) { if (null != widgetRef) { var root = widgetRef.root(); null != root && setTimeout(function() { root.receiveFromParent(name, payload); }, 0); } }, signalWidgetXSM: function(widgetRef, name, payload) { if (null != widgetRef && null != widgetRef.window()) { var msg = this.buildSignalWidgetMessage(name, payload); //widgetRef.window().postMessage(msg, '*'); setTimeout(function() { Svidget.log("communicator: postMessage"); widgetRef.window().postMessage(msg, "*"); }, 0); } }, buildSignalWidgetMessage: function(name, payload) { return { name: name, payload: payload }; } }; /** * Handles conversion to/from different types. * @static * @memberof Svidget */ Svidget.Conversion = {}; // type == string type i.e. "string", "number" Svidget.Conversion.to = function(val, type, subtype, typedata) { var t = Svidget.ParamTypes[type] || Svidget.ParamTypes.object, st = Svidget.ParamSubTypes[subtype] || Svidget.ParamSubTypes.none; switch (t) { case Svidget.ParamTypes.string: return Svidget.Conversion.toString(val, st, typedata); // todo: coerce when subtype choice, default to first one case Svidget.ParamTypes.number: return Svidget.Conversion.toNumber(val, st == Svidget.ParamSubTypes.integer); case Svidget.ParamTypes.bool: return Svidget.Conversion.toBool(val); case Svidget.ParamTypes.array: return Svidget.Conversion.toArray(val); default: return Svidget.Conversion.toObject(val); } }; /** * Converts any value to a string. * @static */ Svidget.Conversion.toString = function(val, subtype, typedata) { return null == val ? null : subtype == Svidget.ParamSubTypes.choice ? Svidget.Conversion.toChoiceString(val, typedata) : Svidget.isArray(val) || "object" == typeof val ? JSON.stringify(val) : val + ""; }; /** * Converts any value to a string among a choice list. * @static */ Svidget.Conversion.toChoiceString = function(val, typedata) { val += ""; if (null == typedata) return val; var choices = typedata.split("|"); return null == choices || 0 == choices.length ? null : choices.indexOf(val) >= 0 ? val : choices[0]; }; /** * Converts any value to a number. * @static */ Svidget.Conversion.toNumber = function(val, isInt) { if (!val) return 0; // null, undefined, false all convert to 0 if (true === val) return 1; // true converts to 1 if (isInt) return parseInt(val + ""); val = parseFloat(val); isNaN(val) && (val = 0); return val; }; /** * Converts any value to a bool. * @static */ Svidget.Conversion.toBool = function(val) { return "false" != (val + "").toLowerCase() && 0 != +val && !!val; }; /** * Converts any value to a array. * @static */ Svidget.Conversion.toArray = function(val) { if (null == val) return val; // if val is array just return it if (Svidget.isArray(val)) return val; // if val is stringified array, JSON parse and return array if (Svidget.Conversion.isArrayString(val)) { var a = Svidget.Conversion.parseArray(val); if (null != a) return a; } // else wrap as an array return [ val ]; }; /** * Converts any value to a object. * @static */ Svidget.Conversion.toObject = function(val) { if (null == val) return val; // if val is valid JSON string, JSON parse and return array if (Svidget.Conversion.isJSONString(val)) { var newval = Svidget.Conversion.jsonifyString(val); // convert single to double chars or else parser chokes try { return JSON.parse(newval); } catch (ex) {} } // else return original val return val; }; Svidget.Conversion.isJSONString = function(val) { // note: this doesn't check if its strictly JSON, just does a sanity check before attempting to convert if (!val) return false; val = (val + "").trim(); return val.length > 0 && Svidget.isString(val) && "{" == val.charAt(0) && "}" == val.charAt(val.length - 1); }; Svidget.Conversion.isArrayString = function(val) { if (null == val) return false; val = (val + "").trim(); return val.length > 0 && "[" == val.charAt(0) && "]" == val.charAt(val.length - 1); }; Svidget.Conversion.isQuotedString = function(val) { if (!val) return false; val = (val + "").trim(); return val.length > 0 && Svidget.isString(val) && ("'" == val.charAt(0) && "'" == val.charAt(val.length - 1) || '"' == val.charAt(0) && '"' == val.charAt(val.length - 1)); }; Svidget.Conversion.parseArray = function(val) { val = Svidget.Conversion.jsonifyString(val); // convert single to double chars or else parser chokes var wrap = '{"d":' + val + "}"; // note, this fails when the array uses single quotes try { var result = JSON.parse(wrap); return result && result.d ? result.d : null; } catch (ex) { // not an array after all return null; } }; // ['hello', 'world'] // ['hello', 'world"it"'] // ['hello', ['world"it"']] // ['he\'ll\'o', ['world"it"']] // try this if below doesn't work: https://gist.github.com/thejsj/aad9c0392a59a7d87d9c Svidget.Conversion.jsonifyString = function(val) { if (null == val || val.indexOf("'") < 0) return val; val = (val + "").trim(); for (var SQ = "'", DQ = '"', BS = "\\", result = "", inQuotes = false, quoteChar = null, escaped = false, i = 0; i < val.length; i++) { var char = val[i], newchar = char; // 'he\'ll\'o' => "he'll'o" // 'world"it"' => "world\"it\"" if (char == SQ || char == DQ) { if (escaped) { quoteChar == SQ && char == SQ ? result = result.substr(0, result.length - 1) : quoteChar == SQ && char == DQ && (// add a backslash because one was there before for double quote i.e. 'hello\"there' => "hello\\\"there" newchar = BS + BS + DQ); escaped = false; } else if (inQuotes && char == quoteChar) { inQuotes = false; quoteChar = null; newchar = DQ; } else if (inQuotes && char == DQ) // handle case of 'world"it"' => "world\"it\"" need to add backslashes newchar = BS + DQ; else if (!inQuotes) { quoteChar = char; inQuotes = true; newchar = DQ; } } else char == BS && (escaped = true); result += newchar; } return result; }; /** * A collection of methods for working with DOM elements. * @static * @memberof Svidget */ Svidget.DOM = { // REMARKS // Wrapper for getElementById get: function(sel) { return document.getElementById ? document.getElementById(sel) : null; }, getByName: function(tagName, asCollection) { return this.getChildrenByName(document, tagName, asCollection); }, getByNameNS: function(namespace, tagName, asCollection) { if (!document.getElementsByTagNameNS) return null; var tags = document.getElementsByTagNameNS(namespace, tagName); return asCollection ? new Svidget.Collection(Svidget.array(tags)) : tags; }, // Gets elements by tag name belonging to the svidget namespace getByNameSvidget: function(tagName, asCollection) { return this.getByNameNS(Svidget.Namespaces.svidget, tagName, asCollection); }, getChildrenByName: function(source, tagName, asCollection) { var tags = source.getElementsByTagName(tagName); return asCollection ? new Svidget.Collection(Svidget.array(tags)) : tags; }, // if sel is a string calls get() getElement: function(sel) { return "string" == typeof sel ? this.get(sel) : sel; }, // gets an element by ID and returns a DomItem getItem: function(sel) { return this.wrap(this.get(sel)); }, // returns a DomQuery object select: function(sel) { // given a selector string free from any commas, looks up the elements/attributes for it // and returns a collection. function selectSingle(s) { if (null == s) return null; var attrName, attrRXEnd = /@([^=#\s]+)$/g, sel = s, match = attrRXEnd.exec(s); if (match) { attrName = match[1]; // match the subgroup sel = sel.replace(match[0], ""); } // query var eles; try { eles = document.querySelectorAll(sel); } catch (ex) { // seelctor was invalid, so just return null return null; } for (var col = [], i = 0; i < eles.length; i++) { var item, ele = eles[i]; if (attrName) { var attr = Svidget.DOM.attr(ele, attrName); null != attr && (item = new Svidget.DOMItem(attr)); } else item = new Svidget.DOMItem(ele); item && col.push(item); } return new Svidget.Collection(col); } // todo: support jQuery selector if querySelectorAll fails, it might be loaded on the page so we might as well use it if we can if (!document.querySelectorAll) return null; if (null == sel) return null; // new Svidget.DomQuery(); // use querySelectorAll var res, attrRX = /@[^=#\s]+/g, hasAttr = attrRX.test(sel), col = new Svidget.Collection(); // if selector has an attribute in it, split it first by comma if (hasAttr) for (var parts = sel.split(","), i = 0; i < parts.length; i++) { res = selectSingle(parts[i]); res && col.addRange(res); } else { res = selectSingle(sel); // its possible selector was bad, in which case we get null, so check res && col.addRange(res); } return new Svidget.DOMQuery(col, sel); }, // Returns the first DOM element in a query selectElement: function(sel) { if ("string" == typeof sel) { var q = this.select(sel); return null == q || 0 == q.length ? null : q.item(0).source(); } return this.isElement(sel) ? sel : null; }, // returns a DomItem if ele is a element or attribute object // else returns the original item wrap: function(ele) { return new Svidget.DOMItem(ele); }, // converts an HTML or SVG DOM node into a pure object literal used to transport transportize: function(ele) { return null == ele ? null : { name: ele.localName, namespace: ele.namespaceURI, value: ele.value, type: 1 == ele.nodeType ? Svidget.NodeType.element : 2 == ele.nodeType ? Svidget.NodeType.attribute : null }; }, // Elements root: function() { return document.documentElement; }, rootItem: function() { // should we cache? return this.wrap(this.root()); }, attr: function(ele, attrName) { return ele.attributes[attrName]; }, attrValue: function(ele, attrName) { var a = this.attr(ele, attrName); return a ? a.value : null; }, // determines if attribute is specified but empty/no value (i.e. isAttrEmpty: function(ele, attrName) { var av = this.attrValue(ele, attrName); return null != av && 0 == av.length; }, clone: function(item) {}, cloneDetached: function(item) {}, isDOMNode: function(node) { return null == node ? null : !(null == node.namespaceURI || null == node.localName || null == node.nodeType || null == node.value && null == node.textContent || 1 != node.nodeType && 2 != node.nodeType); }, fromNodeType: function(type) { return 1 == type ? "element" : 2 == type ? "attribute" : 3 == type ? "text" : null; }, // TEXT text: function(sel, text) { var obj = this.select(sel); if (void 0 === text) return this.getText(obj); this.setText(obj, text); }, getText: function(obj) { return obj.textContent ? obj.textContent : obj.innerHTML ? obj.innerHTML : null; }, setText: function(obj, text) { obj.textContent ? obj.textContent = text + "" : obj.innerHTML && (obj.innerHTML = text + ""); }, // returns null - it means document hasn't loaded yet // returns undefined - it means document is loaded but not accessible due to security (cross domain) constraints getDocument: function(objOrWinEle) { try { var doc = objOrWinEle.contentDocument; null != objOrWinEle.contentWindow && objOrWinEle.contentWindow.document; // certain browsers (Chrome) returns a blank document instead of null // certain browsers (Chrome) returns a blank document instead of null return null != doc && "about:blank" == doc.URL ? null : doc; } catch (ex) { return; } }, // determines if document for or