'use strict'; /** * A Clay config Item * @typedef {Object} Clay~ConfigItem * @property {string} type * @property {string|boolean|number} defaultValue * @property {string} [appKey] * @property {string} [id] * @property {string} [label] * @property {Object} [attributes] * @property {Array} [options] * @property {Array} [items] * @property {Array} [capabilities] */ var HTML = require('../vendor/minified').HTML; var _ = require('../vendor/minified')._; var ClayItem = require('./clay-item'); var utils = require('../lib/utils'); var ClayEvents = require('./clay-events'); var componentStore = require('./component-registry'); var manipulators = require('./manipulators'); /** * @extends ClayEvents * @param {Object} settings - setting that were set from a previous session * @param {Array|Object} config * @param {M} $rootContainer * @param {Object} meta * @constructor */ function ClayConfig(settings, config, $rootContainer, meta) { var self = this; var _settings = _.copyObj(settings); var _items; var _itemsById; var _itemsByAppKey; var _isBuilt; /** * Initialize the item arrays and objects * @private * @return {void} */ function _initializeItems() { _items = []; _itemsById = {}; _itemsByAppKey = {}; _isBuilt = false; } /** * Add item(s) to the config * @param {Clay~ConfigItem|Array} item * @param {M} $container * @return {void} */ function _addItems(item, $container) { if (Array.isArray(item)) { item.forEach(function(item) { _addItems(item, $container); }); } else if (utils.includesCapability(meta.activeWatchInfo, item.capabilities)) { if (item.type === 'section') { var $wrapper = HTML('
'); $container.add($wrapper); _addItems(item.items, $wrapper); } else { var _item = _.copyObj(item); _item.clayId = _items.length; var clayItem = new ClayItem(_item).initialize(self); if (_item.id) { _itemsById[_item.id] = clayItem; } if (_item.appKey) { _itemsByAppKey[_item.appKey] = clayItem; } _items.push(clayItem); // set the value of the item via the manipulator to ensure consistency var value = typeof _settings[_item.appKey] !== 'undefined' ? _settings[_item.appKey] : (_item.defaultValue || ''); clayItem.set(value); $container.add(clayItem.$element); } } } /** * Throws if the config has not been built yet. * @param {string} fnName * @returns {boolean} * @private */ function _checkBuilt(fnName) { if (!_isBuilt) { throw new Error( 'ClayConfig not built. build() must be run before ' + 'you can run ' + fnName + '()' ); } return true; } self.meta = meta; self.$rootContainer = $rootContainer; self.EVENTS = { /** * Called before framework has initialized. This is when you would attach your * custom components. * @const */ BEFORE_BUILD: 'BEFORE_BUILD', /** * Called after the config has been parsed and all components have their initial * value set * @const */ AFTER_BUILD: 'AFTER_BUILD', /** * Called if .build() is executed after the page has already been built and * before the existing content is destroyed * @const */ BEFORE_DESTROY: 'BEFORE_DESTROY', /** * Called if .build() is executed after the page has already been built and after * the existing content is destroyed * @const */ AFTER_DESTROY: 'AFTER_DESTROY' }; utils.updateProperties(self.EVENTS, {writable: false}); /** * @returns {Array.} */ self.getAllItems = function() { _checkBuilt('getAllItems'); return _items; }; /** * @param {string} appKey * @returns {ClayItem} */ self.getItemByAppKey = function(appKey) { _checkBuilt('getItemByAppKey'); return _itemsByAppKey[appKey]; }; /** * @param {string} id * @returns {ClayItem} */ self.getItemById = function(id) { _checkBuilt('getItemById'); return _itemsById[id]; }; /** * @param {string} type * @returns {Array.} */ self.getItemsByType = function(type) { _checkBuilt('getItemsByType'); return _items.filter(function(item) { return item.config.type === type; }); }; /** * @returns {Object} */ self.serialize = function() { _checkBuilt('serialize'); _settings = {}; _.eachObj(_itemsByAppKey, function(appKey, item) { _settings[appKey] = { value: item.get() }; if (item.precision) { _settings[appKey].precision = item.precision; } }); return _settings; }; // @todo maybe don't do this and force the static method self.registerComponent = ClayConfig.registerComponent; /** * Empties the root container * @returns {ClayConfig} */ self.destroy = function() { var el = $rootContainer[0]; self.trigger(self.EVENTS.BEFORE_DESTROY); while (el.firstChild) { el.removeChild(el.firstChild); } _initializeItems(); self.trigger(self.EVENTS.AFTER_DESTROY); return self; }; /** * Build the config page. This must be run before any of the get methods can be run * If you call this method after the page has already been built, teh page will be * destroyed and built again. * @returns {ClayConfig} */ self.build = function() { if (_isBuilt) { self.destroy(); } self.trigger(self.EVENTS.BEFORE_BUILD); _addItems(self.config, $rootContainer); _isBuilt = true; self.trigger(self.EVENTS.AFTER_BUILD); return self; }; _initializeItems(); // attach event methods ClayEvents.call(self, $rootContainer); // prevent external modifications of properties utils.updateProperties(self, { writable: false, configurable: false }); // expose the config to allow developers to update it before the build is run self.config = config; } /** * Register a component to Clay. This must be called prior to .build(); * @param {Object} component - the clay component to register * @param {string} component.name - the name of the component * @param {string} component.template - HTML template to use for the component * @param {string|Object} component.manipulator - methods to attach to the component * @param {function} component.manipulator.set - set manipulator method * @param {function} component.manipulator.get - get manipulator method * @param {Object} [component.defaults] - template defaults * @param {function} [component.initialize] - method to scaffold the component * @return {boolean} - Returns true if component was registered correctly */ ClayConfig.registerComponent = function(component) { var _component = _.copyObj(component); if (componentStore[_component.name]) { console.warn('Component: ' + _component.name + ' is already registered. If you wish to override the existing' + ' functionality, you must provide a new name'); return false; } if (typeof _component.manipulator === 'string') { _component.manipulator = manipulators[component.manipulator]; if (!_component.manipulator) { throw new Error('The manipulator: ' + component.manipulator + ' does not exist in the built-in manipulators.'); } } if (!_component.manipulator) { throw new Error('The manipulator must be defined'); } if (typeof _component.manipulator.set !== 'function' || typeof _component.manipulator.get !== 'function') { throw new Error('The manipulator must have both a `get` and `set` method'); } if (_component.style) { var style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(_component.style)); document.head.appendChild(style); } componentStore[_component.name] = _component; return true; }; module.exports = ClayConfig;