UNPKG

wigjs

Version:

Minimalistic, scalable, extensible, dependency-less Front-end factory for HTML5 applications

1,418 lines (1,178 loc) 38.5 kB
/** * wig - 0.2.1 */ // Uses Node, AMD or browser globals to create a module. This example creates // a global even when AMD is used. This is useful if you have some scripts // that are loaded by an AMD loader, but they still want access to globals. // If you do not need to export a global for the AMD case, // see returnExports.js. // If you want something that will work in other stricter CommonJS environments, // or if you need to create a circular dependency, see commonJsStrictGlobal.js // Defines a module "returnExportsGlobal" that depends another module called // "b". Note that the name of the module is implied by the file name. It is // best if the file name and the exported global have matching names. // If the 'b' module also uses this type of boilerplate, then // in the browser, it will create a global .b that is used below. (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(function () { return (root.wig = factory({})); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like enviroments that support module.exports, // like Node. module.exports = factory({}); } else { // Browser globals root.wig = factory({}); } }(this, function (wig) { // wig internal environment var env = wig.env = {}; var Class = wig.Class = function () {}; /** * @static * @param props * @param statik * @returns {*} */ Class.extend = function (props, statik) { var Super = this, prototype = Object.create(Super.prototype), Constructor; // create constructor if not defined if (props && props.hasOwnProperty('constructor')) { Constructor = props.constructor; } else { Constructor = function () { Super.apply(this, arguments); }; } // prototype properties extend(prototype, props); // Constructor (static) properties extend(Constructor, statik); Constructor.extend = Super.extend; // prototype inheritance Constructor.prototype = prototype; Constructor.prototype.constructor = Constructor; return Constructor; }; /* * @namespace * user overrides to introduce backwards compatibility or custom templating */ extend(env, { /** * Method compiles a template with a context object. * If selector is empty or not defined it will return the original node * Introduce custom DOM query by override. * @param {Element} element * @param {string} selector * @returns {Element} */ getElement: function (element, selector) { return (selector ? element.querySelector(selector) : element); }, /** * Method returns the currently active Element in the DOM. * Override method for older browser support. * @returns {Element} */ getFocusedElement: function () { return document.activeElement; }, /** * Method compiles a template with a context object. * Introduce custom template compilation by override. * @param {string} template * @param {object} context * @returns {String} */ compile: function (template, context) { return env.compiler.compile(template, context); } }); var module = wig.module = {}; /** * ID * @static * @private * @type {number} */ var Id = 0; /** * noop * @static * @function */ var NoOp = function () {}; /** * Data attribute wig attaches the View#_ID to. * @static * @constant * @type {string} */ var DATA_ATTRIBUTE = 'wig_view_id'; var VIEW_DATA_ATTRIBUTE = 'data-' + DATA_ATTRIBUTE; var arrayIndexOf = Array.prototype.indexOf; env.DATA_ATTRIBUTE = DATA_ATTRIBUTE; /** * @class * @classdesc Compiles templates and caches them. */ var Compiler = module.Compiler = Class.extend({ start: '{{', end: '}}', constructor: function () { this.templateCache = new Registry(); /** * RegExp to find the placeholders inside the templates. * @type {RegExp} */ this.regExp = new RegExp( this.start + '\\s*[\\w\\d\\.]+\\s*' + this.end, 'gim' ); }, /** * Discovers the nested/attribute to fetch from attribute passed. * @param {string} sanitized - bound * @param {Object} map - attributes * @returns {String} */ replacer: function (sanitized, map) { var result = map[sanitized[0]], length = sanitized.length, ns = map, i = 0; // digging down the namespace if (length > 1) { while (ns && i < length) { ns = ns[sanitized[i++]]; } result = ns; } return result; }, /** * Memoizes a method for a placeholder to access attributes. * @param {String} placeholder - eg: "{{ myPlaceholder }}" * @type {Function} * @returns {Function} */ compilerMethodFactory: function (placeholder) { var sanitized = placeholder.substring( this.start.length, placeholder.length - this.end.length ); sanitized = sanitized.trim().split("."); return this.replacer.bind(this, sanitized); }, /** * Generates a compiled, cache-able template to reuse. * @param {String} text - eg: "hakuna {{ timon }} matata" * @type {Function} * @returns {Array} */ generateCompiledResult: function (text) { // placeholders to replace var placeholders = text.match(this.regExp), i = 0, splitText, compiledResults; if (placeholders) { // actual template content splitText = text.split(this.regExp); // precompiled array of content compiledResults = []; while (placeholders.length > 0) { compiledResults.push( splitText[i], this.compilerMethodFactory(placeholders.shift()) ); i += 1; } compiledResults.push(splitText[i]); // cache template this.templateCache.set(text, compiledResults); } return (this.templateCache.get(text) || text); }, /** * Pre-compiles and caches the given template. * If attributes is defined, it will compile the template into a String. * @param {String} text - eg: "hakuna {{ timon }} matata" * @param {Object} [context] - context * @returns {String} */ compile: function (text, context) { var compiledTemplate = this.templateCache.get(text), markup = "", item, i, l; if (!compiledTemplate) { compiledTemplate = this.generateCompiledResult(text); } // if a map of key-value pairs is provided, compile too if (context && typeof context === 'object') { for (i = 0, l = compiledTemplate.length; i < l; i++) { item = compiledTemplate[i]; markup += (typeof item === 'function' ? item(context) : item); } } return markup; }, /** * Returns the specified comiled markups. * @param {String} template - eg: "hakuna {{ timon }} matata" * @returns {String} */ getCompiled: function (template) { return this.templateCache.get(template); }, /** * Disposes all previously compiled and cached markups. */ disposeMarkups: function () { this.templateCache.empty(); } }); var DOM = module.DOM = Class.extend({ initNode: function (element, classSet, attributes, dataSet) { var classes = classSet, cl; extend(element, attributes); extend(element.dataset, dataSet); if (Array.isArray(classSet)) { classes = classSet.join(' '); } else if (classSet && typeof classSet === 'object') { classes = []; for (cl in classSet) { if (classSet.hasOwnProperty(cl) && classSet[cl]) { classes.push(cl); } } classes = classes.join(' '); } if (classes) { element.className = classes; } return element; }, findClosestViewNode: function (element, attribute) { var attributeValue; do { attributeValue = element.getAttribute(attribute); if (attributeValue != null) { return attributeValue; } element = element.parentNode; } while (element !== document); }, attachNodeToParent: function (childNode, parentNode, index) { if (typeof index === 'number') { parentNode.insertBefore(childNode, parentNode.children[index]); } else { parentNode.appendChild(childNode); } } }); var Insurer = module.Insurer = wig.Class.extend({ is: { notDefined: function (arg, message) { if (typeof arg === 'undefined' || arg === null) { throw new TypeError(message || 'Argument is not defined!'); } }, defined: function (arg, message) { if (typeof arg !== 'undefined' || arg === null) { throw new TypeError(message || 'Argument is defined (not null and not undefined)!'); } } }, exists: { object: function (arg, message) { if (arg == null || typeof arg !== 'object') { throw new TypeError(message || 'Argument must be a function!'); } } } }); /** * @classdesc Provides a convenient API for a key-value pair store. * @class */ var Registry = module.Registry = Class.extend({ constructor: function () { this.root = {}; }, /** * Returns the stored value for the specified key. * Returns {undefined} if key doesn't exist. * @param {string} key * @returns {*} */ get: function (key) { return this.root[key]; }, /** * Registers a value for the specified key. * @param {string} key * @param {*} value */ set: function (key, value) { this.root[key] = value; }, /** * Removes the value specified by the key. * @param {string} key */ unset: function (key) { delete this.root[key]; }, /** * Iterates over each item in the registry and executes the provided callback for each value and key. * @param {function} callback * @param {object|undefined} thisArg * @throws {TypeError} */ each: function (callback, thisArg) { var key, value; if (typeof callback === 'function') { for (key in this.root) { value = this.get(key); callback.call(thisArg || this, key, value); } } }, /** * This is an internal method, don't use it! * Empties the registry. */ empty: function () { Object.keys(this.root).forEach(this.unset, this); } }); var ViewRegistry = module.ViewRegistry = Registry.extend({ /** * Registers a (child) View instance in the ViewRegistry. * If parentView is specified, parent View's ID will be mapped against the child View's ID. * @param {View} childView * @param {View} [parentView] */ registerView: function (childView, parentView) { var viewID = childView.getID(), viewItem = new ViewRegistryItem(childView, parentView); this.set(viewID, viewItem); }, removeView: function (view) { if (typeof view !== 'string') { view = view.getID(); } this.get(view).contextRegistry.empty(); this.unset(view); }, getCustomEventsForView: function (viewID) { return this.get(viewID).getCustomEvents(); }, getContextRegistryForView: function (viewID) { return this.get(viewID).getContextRegistry(); }, setContextForChildView: function (viewID, childViewID, serializedChild) { this.getContextRegistryForView(viewID) .set(childViewID, serializedChild); }, emptyViewContextRegistry: function (viewID) { this.getContextRegistryForView(viewID) .empty(); } }); var ViewRegistryItem = module.ViewRegistryItem = Class.extend({ constructor: function (view, parentView) { this.view = view; this.parent = (parentView && parentView.getID()); this.children = []; this.customEvents = {}; this.contextRegistry = new Registry(); }, getCustomEvents: function () { return this.customEvents; }, getContextRegistry: function () { return this.contextRegistry; } }); var Selection = module.Selection = Class.extend({ constructor: function (DOM) { this.DOM = DOM; this.id = undefined; this.path = undefined; this.start = 0; this.end = 0; }, preserveSelection: function () { var node = wig.env.getFocusedElement(); this.start = node.selectionStart; this.end = node.selectionEnd; }, getIndexOfNode: function (node, viewNode) { var path = []; do { path.push(node.classList[0] || arrayIndexOf.call(node.parentNode.children, node)); node = node.parentNode; } while (node !== viewNode); return path; }, preserveSelectionInView: function (updatingView) { var node = wig.env.getFocusedElement(), focusedViewID = this.DOM.findClosestViewNode(node, VIEW_DATA_ATTRIBUTE), updatingViewID = updatingView.getID(), viewNode; if (focusedViewID && focusedViewID === updatingViewID) { try { this.preserveSelection(); } catch (e) {} viewNode = updatingView.getNode(); this.id = updatingViewID; if (node !== viewNode) { this.path = (node.id || this.getIndexOfNode(node, viewNode)); } } }, restoreSelection: function (node) { if (typeof node.setSelectionRange !== 'function') { return; } node.setSelectionRange(this.start, this.end); }, findNodeByIndex: function (index, node) { if (typeof index === 'number') { node = node.children[index]; } else { node = node.children[0]; if (node.classList[0] !== index) { do { node = node.nextSibling; } while (node.classList[0] !== index); } } return node; }, restoreSelectionInView: function (view) { // place focus in the node var node = view.getNode(), path = this.path, index; if (this.id && this.id === view.getID()) { if (path && path.length > 0) { do { // dig down to find focused node index = path.pop(); node = this.findNodeByIndex(index, node); } while (path.length !== 0); } // restore selection if node is an editable element try { this.restoreSelection(node); } catch (e) {} node.focus(); this.id = undefined; this.path = undefined; } } }); var UIEventProxy = module.UIEventProxy = Class.extend({ listeners: [], constructor: function (DOM, ViewManager) { this.DOM = DOM; this.ViewManager = ViewManager; this.listener = this.listener.bind(this); }, findFirstViewAndFireEvent: function (event, view) { do { // find the first view that is listening to the same type of event if (env.viewHelper.hasEvent(view, event)) { env.viewHelper.fireDOMEvent(view, event); return; } view = this.ViewManager.getParentView(view); } while (view); }, addListener: function (node, type) { node.addEventListener(type, this.listener); }, removeListener: function (node, type) { node.removeEventListener(type, this.listener); }, listener: function (event) { var viewID = this.DOM.findClosestViewNode(event.target, VIEW_DATA_ATTRIBUTE), view = this.ViewManager.getView(viewID); if (view) { return this.findFirstViewAndFireEvent(event, view); } }, startListenTo: function (type) { if (!this.isListeningTo(type)) { this.listeners.push(type); this.addListener(document, type); } }, stopListenTo: function (type) { var index = this.listeners.indexOf(type); if (index > -1) { this.removeListener(document, type); this.listeners.splice(index, 1); } }, isListeningTo: function (type) { return (this.listeners.indexOf(type) > -1); } }); // helper module to provide privacy on the public View interface var ViewHelper = module.ViewHelper = Class.extend({ constructor: function (viewManager, uiEventProxy, dom, insurer) { Class.apply(this, arguments); this.DOM = dom; this.Insurer = insurer; this.ViewManager = viewManager; this.UIEventProxy = uiEventProxy; }, /** * @param {View} view * @param {Function} ViewClass * @param {object} options */ createChildView: function (view, ViewClass, options) { var childView = new ViewClass(options); this.ViewManager.registerChildForView(view, childView); return childView; }, /** * @param {View} view */ initializeChildren: function (view) { var children = this.ViewManager.getChildViews(view.getID()), length = children.length, i = 0, childView; while (i < length) { childView = this.ViewManager.getView(children[i]); childView.initialize(); i += 1; } }, paint: function (view) { var node = view.getNode(), html = this.ViewManager.compileTemplate(view); node.innerHTML = (html || ''); this._emptyAndPreserveChildContext(view); view.render(); this.updateCSSClasses(view); this.paintChildren(view); }, /** * @param {View} view */ paintChildren: function (view) { var children = this.ViewManager.getChildViews(view.getID()), length = children.length, i = 0, childView; while (i < length) { childView = this.ViewManager.getView(children[i]); this.ViewManager.updateView(childView); i += 1; } }, /** * @param {View} view * @param {string} childViewID */ paintChildView: function (view, childViewID) { var childView = view.getView(childViewID); if (childView) { this.ViewManager.updateView(childView); } }, updateCSSClasses: function (view) { var classes = [view.className], customCSS = view.getCSS(); if (view.css) { classes.push(view.css); } if(customCSS) { classes.push(customCSS); } this.DOM.initNode(view.getNode(), classes); }, // Method is invoked by remove destroy: function (view) { var node = view.getNode(), parentNode = node.parentNode; // remove custom events and notify children about removal this.undelegateAll(view); this.notifyDetach(view); if (parentNode) { parentNode.removeChild(node); } this.ViewManager.emptyView(view); node.innerHTML = ''; view.node = null; }, notifyAttach: function (view) { var viewManager = this.ViewManager; view.attached = true; view.onAttach(); viewManager.getChildViews(view.getID()).forEach( viewManager.notifyViewAboutAttach, viewManager); }, notifyDetach: function (view) { var viewManager = this.ViewManager; view.attached = false; view.onDetach(); viewManager.getChildViews(view.getID()).forEach( viewManager.notifyViewAboutDetach, viewManager); }, cleanupContext: function (view, context) { var expects = view.expects, prop, l; // remove default Wig specific properties delete context.id; delete context.css; delete context.node; if (typeof expects === 'object' && !Array.isArray(expects)) { expects = Object.keys(expects); } l = expects.length; while (l--) { prop = expects[l]; this.Insurer.is.defined( view[prop], '[' + prop + '] is already defined on the View instance!'); view[prop] = context[prop]; delete context[prop]; } }, _serializeAndRemoveView: function (view, childViewID) { this.ViewManager .serializeChildForView(view, childViewID); view.removeView(childViewID); }, _emptyAndPreserveChildContext: function (view) { var viewID = view.getID(), children = this.ViewManager.getChildViews(viewID); this.ViewManager .emptyContextRegistryForView(viewID); while (children.length > 0) { // method below will shift children out form the array this._serializeAndRemoveView(view, children[0]); } }, getSelectorForChild: function (view, id) { var childView = view.getView(id), childID = childView.getID().split('.').pop(); return (view.renderMap[childID] || view.renderMap['*']); }, initializeWithContext: function (view, context) { // update default/initial context this.cleanupContext(view, context); view.set(context); this.initialize(view); }, initialize: function (view) { var dataset = {}, classes = [view.className], attributes = view.getAttributes(); // data attributes dataset[DATA_ATTRIBUTE] = view.getID(); // add custom css if (view.css) { classes.push(view.css); } // assign classes and data context this.DOM.initNode(view.getNode(), classes, attributes, dataset); // apply event listeners Object.keys(view.events).forEach(view.listenFor, view); // initialize children this.initializeChildren(view); }, /** * Method contains logic to serialize the View into a context. * @returns {object} */ serialize: function (view) { return extend({}, view.defaults, view.context); }, /** * Used by the UIEventProxy to execute the event handler on the view. * @param {View} view * @param {Event} event */ fireDOMEvent: function (view, event) { var listener = view.events[event.type]; if (typeof listener !== 'function') { listener = view[listener]; } if (listener) { return listener.call(view, event); } }, /** * Used by the UIEventProxy to determine whether the view * has an event listener for the specified event type. * @param {View} view * @param {Event} event */ hasEvent: function (view, event) { return !!(view.events && view.events[event.type]); }, /** * @param {Registry} view * @param {string} type */ undelegateType: function (view, type) { var viewID = view.getID(), customEvents = this.ViewManager.getCustomEventsForView(viewID), selectors = customEvents[type], l = selectors.length, node; while (l--) { node = view.find(selectors[l]); this.UIEventProxy.removeListener(node, type); } }, /** * Undelegate all non-bubbling events registered for the View */ undelegateAll: function (view) { var viewID = view.getID(), customEvents = this.ViewManager.getCustomEventsForView(viewID); Object.keys(customEvents).forEach( this.undelegateType.bind(this, view)); } }); var ViewManager = module.ViewManager = Class.extend({ constructor: function (ViewRegistry, DOM, Selection) { this.DOM = DOM; this.Selection = Selection; this.ViewRegistry = ViewRegistry; }, getView: function (id) { var item = this.ViewRegistry.get(id); return (item && item.view); }, getParent: function (id) { var item = this.ViewRegistry.get(id); return (item && item.parent); }, getChildViews: function (id) { var item = this.ViewRegistry.get(id); return (item && item.children); }, getParentView: function (childView) { var childID = childView.getID(), parentID = this.getParent(childID); return this.getView(parentID); }, getViewAtNode: function (node) { return this.getView(node.dataset[DATA_ATTRIBUTE]); }, getRootNodeMapping: function (parentView, childView) { var viewID = childView.getID(), selector = env.viewHelper.getSelectorForChild(parentView, viewID), rootNode = parentView.getNode(); return env.getElement(rootNode, selector); }, compileTemplate: function (view) { var template = view.template, context = env.viewHelper.serialize(view); if (typeof template === 'function') { return view.template(context); } if (Array.isArray(template)) { template = template.join(''); } return env.compile(template, context); }, updateView: function (view) { var childNode = view.getNode(), parent = this.getParentView(view), rootNode = childNode.parentNode, childNodeIndex; env.viewHelper.undelegateAll(view); this.Selection.preserveSelectionInView(view); if (parent) { rootNode = this.getRootNodeMapping(parent, view); } childNodeIndex = arrayIndexOf.call(rootNode.children, childNode); if (childNodeIndex > -1) { rootNode.removeChild(childNode); } env.viewHelper.paint(view); this.DOM.attachNodeToParent(childNode, rootNode, childNodeIndex); this.Selection.restoreSelectionInView(view); }, notifyViewAboutAttach: function (viewID) { var view = this.getView(viewID); env.viewHelper.notifyAttach(view); }, notifyViewAboutDetach: function (viewID) { var view = this.getView(viewID); env.viewHelper.notifyDetach(view); }, removeViewFromParent: function (view) { var parentView = this.getParentView(view), childViewID = view.getID(); if (parentView) { parentView.removeView(childViewID); } else { env.viewHelper.destroy(view); } }, destroyViewAtNode: function (node) { var view = this.getViewAtNode(node); if (view) { view.remove(); } }, inheritCSS: function (superClassName, className) { if (className) { return superClassName + ' ' + className; } return superClassName; }, getCustomEventsForView: function (viewID) { return this.ViewRegistry.getCustomEventsForView(viewID); }, registerChildForView: function (view, childView) { this.ViewRegistry.registerView(childView, view); this.getChildViews(view.getID()) .push(childView.getID()); }, serializeChildForView: function (view, childViewID) { var childView = view.getView(childViewID), serializedChild = env.viewHelper.serialize(childView); this.ViewRegistry .setContextForChildView(view.getID(), childViewID, serializedChild); }, emptyContextRegistryForView: function (viewID) { // empty child context registry this.ViewRegistry .emptyViewContextRegistry(viewID); }, emptyView: function (view) { this.getChildViews(view.getID()) .forEach(view.removeView, view); this.ViewRegistry.removeView(view); } }); /** * Merges all argument objects into the first one. * @param {object} obj * @returns {object} */ function extend(obj) { var args = Array.prototype.slice.call(arguments, 1), argsLength = args.length, key, i; for (i = 0; i < argsLength; i += 1) { if (args[i] && typeof args[i] === 'object') { for (key in args[i]) { obj[key] = args[i][key]; } } } return obj; } wig.extend = extend; /** * Generates a new unique string based on the * provided prefix and the latest Id. * @param {string} prefix * @returns {string} */ env.generateID = function (prefix) { return ((prefix || 0) + Id++); }; // initialize wig wig.env.init = function () { this.dom = new DOM(); this.insurer = new Insurer(); this.compiler = new Compiler(); this.viewRegistry = new ViewRegistry(); this.selection = new Selection(this.dom); this.viewManager = new ViewManager( this.viewRegistry, this.dom, this.selection); this.uiEventProxy = new UIEventProxy( this.dom, this.viewManager); this.viewHelper = new ViewHelper(this.viewManager, this.uiEventProxy, this.dom, this.insurer); }; /** * Renders the provided View instance into a DOM node. * @param {View} view * @param {Element} node * @returns {View} */ function renderView(view, node) { node.appendChild(view.getNode()); env.viewHelper.paint(view); env.viewHelper.notifyAttach(view); return view; } wig.renderView = renderView; /** * @class * @param {object} options * @property {string} [id] - user defined or internal identifier * @property {string} [css] * @property {Node} [node] */ var View = wig.View = Class.extend({ constructor: function View(context) { context = (context || {}); // assign the ID and register the View this._ID = (context.id || env.generateID('v')); env.viewRegistry.registerView(this); this.css = (context.css || ''); this.node = (context.node || document.createElement(this.tagName)); this.context = {}; this.attached = false; env.viewHelper.initializeWithContext(this, context); }, // ////////// // // Properties // // ////////// // // strings tagName: 'div', className: 'View', // objects defaults: {}, renderMap: {}, events: {}, /** * @type {View} */ View: View, /** * @type {object|string[]} */ expects: {}, /** * @type {string|string[]|function} */ template: '', // //// // // View // // //// // get: function (key) { return (this.context[key] || this.defaults[key]); }, getID: function () { return this._ID; }, getNode: function () { return this.node; }, /** * Updates the View's context object - does not update the View itself * @param {object} newContext */ set: function (newContext) { var overrides; if (newContext && typeof newContext === 'object') { overrides = extend({}, this.defaults, this.context, newContext); extend(this.context, (this.parseContext(overrides) || overrides)); } }, /** * Sets the View for another Element and reinitializes it. * @param {Element} node */ setNode: function (node) { if (node) { this.node = node; this.initialize(); } }, /** * Finds an Element within the View's DOM Element. * @param {string} selector * @returns {Node} */ find: function (selector) { var node = this.getNode(); if (!selector) { return node; } return env.getElement(node, selector); }, /** * Updates (rerenders) the View and its children. * @param {object} [context] - context updates */ update: function (context) { env.viewHelper.notifyDetach(this); this.set(context); env.viewManager.updateView(this); env.viewHelper.notifyAttach(this); }, // Removes (destroys) the children. empty: function () { env.viewManager.getChildViews(this.getID()) .forEach(this.removeView, this); }, // Removes (destroys) the View and its children from the DOM. remove: function () { env.viewManager.removeViewFromParent(this); }, // ///// ////////// // // Child operations // // ///// ////////// // /** * Creates and adds the child view specified by the child view's _ID attribute. * @param {Function} [ViewClass] - child View type * @param {object} [childOptions] - options to create the child with * @returns {View} */ addView: function (ViewClass, childOptions) { var parentID = this.getID(), contextRegistry = env.viewRegistry.getContextRegistryForView(parentID), oldChildContext, newChildContext, options, childID, childView; // resolve arguments if (ViewClass && typeof ViewClass === 'object') { childOptions = ViewClass; ViewClass = (this.View || View); } childOptions = (childOptions || {}); // generate child id childID = parentID + '.' + (childOptions.id || env.generateID('v')); // apply previous context oldChildContext = contextRegistry.get(childID); newChildContext = extend({}, oldChildContext, childOptions); // create child view options = extend(newChildContext, { id: childID }); childView = env.viewHelper.createChildView( this, ViewClass, options); // render child view if parent (this) is attached if (this.attached) { env.viewHelper.paintChildView(this, childID); } return childView; }, /** * Returns the child view specified by the child view's _ID attribute. * @param {string|number} childViewID */ getView: function (childViewID) { var children = env.viewManager.getChildViews(this.getID()); // if id is an array index instead of a child's ID if (typeof childViewID === 'number' && childViewID < children.length) { childViewID = children[childViewID]; } // if id is not an absolute id if (children.indexOf(childViewID) === -1) { childViewID = this.getID() + '.' + childViewID; } return env.viewManager.getView(childViewID); }, /** * Removes a child view specified by the child view's _ID attribute. * @param {string} childViewID */ removeView: function (childViewID) { var childView = this.getView(childViewID), children = env.viewManager.getChildViews(this.getID()), index; if (childView) { index = children.indexOf(childView.getID()); if (index > -1) { env.viewHelper.destroy(childView); children.splice(index, 1); } } }, // /// ////// // // DOM Events // // /// ////// // /** * Delegate the UIEventProxy's listener to listen to * non-bubbling events on a node instead of the document * @param {string} type * @param {string} selector */ delegate: function (type, selector) { var viewID = this.getID(), customEvents = env.viewRegistry.getCustomEventsForView(viewID), node; if (!customEvents[type]) { customEvents[type] = []; } if (customEvents[type].indexOf(selector) === -1) { node = this.find(selector); env.uiEventProxy.addListener(node, type); customEvents[type].push(selector || ''); } }, /** * UIEventProxy listening to the specified event type. * @param {string} type */ listenFor: function (type) { env.uiEventProxy.startListenTo(type); }, // ///////// // // Overrides // // ///////// // /** * Returns additional, logic based CSS classes for the View's node. * @returns {string} */ getCSS: function () { return ''; }, /** * Returns additional, logic based attributes for the View's node. * @returns {string} */ getAttributes: function () { return {}; }, /** * Method contains logic to parse the new context for the View. * @returns {string} */ parseContext: function (newContext) { return newContext; }, // Method will be executed after the View is attached to the DOM. onAttach: NoOp, // Method will be executed before the View is detached from the DOM. onDetach: NoOp, // Method will be executed to create the View structure within the current View. render: NoOp }); View.extend = function (proto, statik) { statik = (statik || {}); statik.add = View.add; proto.className = env.viewManager.inheritCSS( this.prototype.className, proto.className ); return Class.extend.call(this, proto, statik); }; View.add = function (options, parentView) { env.insurer.exists.object( parentView, 'Parent View cannot be undefined!'); return parentView.addView(this, options); }; wig.env.init(); // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return wig; }));