UNPKG

jquery-toad

Version:

jQuery TOAD - O velho e querido jQuery

1,023 lines (795 loc) 31.2 kB
/*! * jquery-toad v1.0.35 * jQuery TOAD - O velho e querido jQuery (https://e5r.github.io/jquery-toad) * Copyright (c) Erlimar Silva Campos. All rights reserved. * Licensed under the Apache-2.0 License. More license information in LICENSE. */ (function(global, factory ) { "use strict"; if(typeof module === 'object' && typeof module.exports === 'object' && typeof require === 'function') { module.exports = function(w) { /* DOM - Document Object Model é pré-requisito */ if (typeof w !== 'object' || typeof w.document !== 'object') { throw new Error("jQuery TOAD\'s requires a DOM (Document Object Model)!"); } var jQuery = require('jquery')(w); return factory(w, jQuery, 'CommonJS'); } } else if(typeof define === 'function' && typeof define.amd === 'object') { define('jquery-toad', ['jquery'], function(jQuery) { return factory(global, jQuery, 'AMD'); }); } else { /* É necessário definir um valor para __TOAD__ explicitamente. Esse será o nome do objeto de aplicação disponível em window. */ if (typeof global.__TOAD__ !== 'string') { throw new Error('You have not set a value for __TOAD__!'); } global[global.__TOAD__] = factory(global, global.jQuery, 'Browser'); } })(typeof window !== "undefined" ? window : this, function(window, $, $ml) { /* jQuery 1.12.4 é pré-requisito */ (function () { 'use strict'; if (typeof $ !== 'function') { throw new Error('jQuery TOAD\'s requires jQuery!'); } var versionAll = $.fn.jquery.split(' ')[0].split('.'), vMajor = versionAll[0], vMinor = versionAll[1], vPath = versionAll[2]; if (vMajor > 1) return; if (vMajor == 1 && vMinor > 12) return; if (vMajor == 1 && vMinor == 12 && vPath >= 4) return; throw new Error('jQuery TOAD\'s requires jQuery version 1.12.4 or higher!'); })(); var $toad = { '$jq': $, '$toad': { version: '1.0.35', author: 'Erlimar Silva Campos', license: 'Apache-2.0', homepage: 'https://e5r.github.io/jquery-toad', moduleLoader: $ml } }; var document = window.document; var _NAMESPACES_ = []; var _APP_NAMESPACE_KEY_ = '_app_namespace_'; // Inicializa namespace exclusivo para aplicação do usuário var _APP_ = $toad[_APP_NAMESPACE_KEY_] = {}; var $require = function (name) { return $toad[name] = $toad[name] || {}; }; var $namespace = function (order, name, factory) { _NAMESPACES_.push({ idx: order, cb: function () { factory(($toad[name] = $toad[name] || {})); } }); }; /** * @code {__TOAD__}.namespace('utils', function(exports) { var myData = {} var myFunction = function(){} exports.data = myData exports.func = myFunction }) */ $toad.namespace = function (name, factory) { if (typeof name !== 'string') { throw new Error('Invalid namespace.'); } // TODO: Mudar para abordagem semelhante a $namespace factory((_APP_[name] = _APP_[name] || {})); } /** * @code var utils = {__TOAD__}.require('utils') utils.func(utils.data) */ $toad.require = function (name) { if (typeof name !== 'string' || $.trim(name).length < 1) { throw new Error('Invalid namespace to import.'); } var required = {}, exportGlobals = [ 'core', 'utils' ], atIgnore = [ '__internals__' ]; if (name.charAt(0) === '@') { var parts = name.split('@'); if (parts.length !== 2 || $.inArray(parts[1], atIgnore) >= 0) return; return $toad['@'][parts[1]]; } // Objetos globais exceto o namespace da aplicação if ($.inArray(name, exportGlobals) > -1) { for (var k in $toad[name]) { required[k] = $toad[name][k]; } } // Objetos da aplicação. Esses sobrescrevem os globais se existirem for (var k in _APP_[name]) { required[k] = _APP_[name][k]; } return required; } /** * @code var MY_CONST = {__TOAD__}.constant('MY_CONST', VALUE_FOR_MY_CONST) */ $toad.constant = function (constName, constValue) { var internals = $require('@').__internals__; return internals.setConstant(constName, constValue); } // ======================================================================== // app.js // ======================================================================== $namespace(9, 'core', function (exports) { var utils = $require('utils'), internals = $require('@').__internals__; var CONTROLLER_IDENTIFIER = 'controller', CONTROLLER_DATA_IDENTIFIER = 'data-' + CONTROLLER_IDENTIFIER, CONTROLLER_SELECTOR = '[' + CONTROLLER_DATA_IDENTIFIER + ']', CONTROLLER_ELEMENT_DATA = '$ctrl', CONTROLLER_VIEW_FIELD = '__view__', CONTROLLER_OPTIONS_FIELD = '$options', COMPONENT_SELECTOR_KEY = '$jqSelector', COMPONENT_NAME_KEY = '$name', BIND_DATA_IDENTIFIER = 'data-events', BIND_SELECTOR = '[' + BIND_DATA_IDENTIFIER + ']', BIND_EVENT_COLLECTION_SPLITER = ',', BIND_EVENT_SPLITER = '=>'; function _installControllers() { $(CONTROLLER_SELECTOR, document).each(function () { var el = $(this), name = el.attr(CONTROLLER_DATA_IDENTIFIER), ctor = internals.getController(name), options = {}, dataset = el.data(); // Lê opções dos elementos [data-*] exceto [data-controller] for (var opt in dataset) { if (opt === CONTROLLER_IDENTIFIER) continue; options[opt] = dataset[opt]; } var ctrl = new ctor(el, options); el.data(CONTROLLER_ELEMENT_DATA, ctrl); ctrl[CONTROLLER_VIEW_FIELD] = el; ctrl[CONTROLLER_OPTIONS_FIELD] = options; _setupEvents(el, ctrl) _setupComponents(el, ctrl); internals.callLazyTriggers(ctrl); }); } function _setupEvents(ctrlElm, ctrl) { $(BIND_SELECTOR, ctrlElm).each(function () { var el = $(this), binder = el.attr(BIND_DATA_IDENTIFIER); if (!utils.isString(binder) || 0 > binder.indexOf(BIND_EVENT_SPLITER)) return; binder = binder.split(BIND_EVENT_COLLECTION_SPLITER); if (!utils.isArray(binder) || binder.length < 1) return; for (var b in binder) { var binderExpr = $.trim(binder[b]), bind = binderExpr.split(BIND_EVENT_SPLITER); if (!utils.isArray(bind) || bind.length < 2) continue; var bEvent = bind[0], bHandler = ctrl[bind[1]]; if (!utils.isString(bEvent) || !utils.isFunction(bHandler)) return; el.on(bEvent, bHandler); el.data(CONTROLLER_ELEMENT_DATA, ctrl); } }); } function _setupComponents(ctrlElm, ctrl) { var cmpList = internals.listComponents(); for (var c in cmpList) { var cmp = cmpList[c]; if (!utils.isString(cmp.id)) return; if (!utils.isFunction(cmp.component)) return; if (!utils.isString(cmp.component[COMPONENT_SELECTOR_KEY])) return; if (!utils.isString(cmp.component[COMPONENT_NAME_KEY])) return; var jqSelector = cmp.component[COMPONENT_SELECTOR_KEY]; var jqFn = cmp.component[COMPONENT_NAME_KEY]; $(jqSelector, ctrlElm)[jqFn](ctrl); }; } function _installToad() { _installControllers(); } $(document).ready(_installToad); }) // ======================================================================== // config.js // ======================================================================== $namespace(1, '@', function (exports) { var CONFIG = {}, utils = $require('utils'); function _getConfig(key, defaultValue) { if (!utils.isString(key)) return; return utils.getObjectItemByPath(CONFIG, key) || defaultValue; } function _setConfig(key, newValue) { if (!utils.isString(key)) return; return utils.setObjectItemByPath(CONFIG, key, newValue); } exports.config = { get: _getConfig, set: _setConfig }; }) // ======================================================================== // constants.js // ======================================================================== $namespace(1, '@', function (exports) { var constants = {}, internals = exports.__internals__ = exports.__internals__ || {}; internals.setConstant = _setConstant; /** * Constantes */ exports.constants = constants; /** * Define uma constante global no sistema * * @param {string} constName - Nome da constante * @param {any} constValue - Valor da constante */ function _setConstant(constName, constValue) { if (typeof constName !== 'string') throw new Error('Invalid constName "' + constName + '"'); if (!constValue) throw new Error('constValue is required!'); if (typeof constants[constName] !== 'undefined') throw new Error('Constant "' + constName + '" already exists!'); // Quando [Object.defineProperty] não está disponível (ex: IE8 <) simplesmente // guardamos um valor para uso, porém sem nenhuma proteção de imutabilidade // // OBS: Apesar de usarmos [Object.defineProperty] para definir a constante, // testamos [Object.defineProperties] porque o IE8 implementa [Object.defineProperty] // mas somente para objetos DOM. Com suporte completo no IE9 junto com a // implementação de [Object.defineProperties] if (typeof Object.defineProperties !== 'function') { console.warn('WARNING!', 'Object.defineProperty is not supported!'); return constants[constName] = constValue; } return Object.defineProperty(constants, constName, { // TODO: Mudar para [false], isso irá impedir o uso de [for(var c in constants)] // porém será necessário fornecer algum meio para listar as constantes enumerable: true, configurable: false, writable: false, value: constValue })[constName]; } }) // ======================================================================== // controller-component.js // ======================================================================== $namespace(2, 'core', function (exports) { var CONTROLLER_ELEMENT_DATA = '$ctrl'; exports.ControllerComponent = $.fn['controller'] = function ControllerComponent() { return $(this).data(CONTROLLER_ELEMENT_DATA); } }) // ======================================================================== // plain-object-cloner.js // ======================================================================== $namespace(1, '@', function (exports) { var utils = $require('utils'), internals = exports.__internals__ = exports.__internals__ || {}; internals.PlainObjectCloner = PlainObjectCloner; /** * Clonador de objetos * * @param {object} target - Object to clone */ function PlainObjectCloner(target) { this.target = target; this.cloning = []; this.validTypes = [ (typeof true), (typeof 0), (typeof ''), (typeof {}) ]; } PlainObjectCloner.prototype.isValidProp = function (prop) { return this.validTypes.indexOf(typeof prop) >= 0; } PlainObjectCloner.prototype.cloneArray = function (target) { if (!utils.isArray(target)) return; var arr = []; for (var p in target) { var prop = target[p]; if (!this.isValidProp(prop)) continue; if (this.cloning.indexOf(prop) >= 0) throw new Error('Circular reference detected!'); this.cloning.push(prop); if (utils.isArray(prop)) { arr.push(this.cloneArray(prop)); } else if (utils.isObject(prop)) { arr.push(this.cloneObject(prop)); } else { arr.push(prop); } var cloningIdx = this.cloning.indexOf(prop); this.cloning.splice(cloningIdx, 1); } return arr; } PlainObjectCloner.prototype.cloneObject = function () { var target = arguments[0] || this.target; if (utils.isArray(target)) { return this.cloneArray(target); } if (!utils.isObject(target)) return; var clone = {}; for (var p in target) { var prop = target[p]; if (!this.isValidProp(prop)) continue; if (this.cloning.indexOf(prop) >= 0) throw new Error('Circular reference detected!'); this.cloning.push(prop); if (utils.isArray(prop)) { clone[p] = this.cloneArray(prop); } else if (utils.isObject(prop)) { clone[p] = this.cloneObject(prop); } else { clone[p] = prop; } var cloningIdx = this.cloning.indexOf(prop); this.cloning.splice(cloningIdx, 1); } return clone; } }) // ======================================================================== // register-component.js // ======================================================================== $namespace(3, '@', function (exports) { var NAME_FIELD = 'name', COMPONENT_IDENTIFIER = 'gui', CONSTRUCTOR_FIELD = 'ctor', EXPORT_NAME_FIELD = '$name', EXPORT_SELECTOR_FIELD = '$jqSelector'; var components = []; var internals = exports.__internals__ = exports.__internals__ || {}; internals.getComponent = _getComponent; internals.listComponents = _listComponents; /** * Registra um componente * * @param {string} name - Nome do componente * @param {function} ctor - Função construtora do componente */ exports.registerComponent = function (name, ctor) { var options = ensureOptions({ name: name, ctor: ctor }); var componentName = options[NAME_FIELD], componentJqName = 'gui-{name}'.replace('{name}', componentName), selector = '[data-gui="{name}"]'.replace('{name}', componentName); if (components[componentName]) { throw new Error('Component ' + componentName + ' already registered!'); } var fnCmp = function (ctrl) { return this.each(function (_, htmlEl) { var dataOptions = {}, el = $(htmlEl), dataset = el.data(); // Lê opções dos elementos [data-*] exceto [data-gui] for (var opt in dataset) { if (opt === COMPONENT_IDENTIFIER) continue; dataOptions[opt] = dataset[opt]; } // [apply] ao invés de [bind] por compatibilidade ao IE8 return options[CONSTRUCTOR_FIELD].apply(this, [ctrl, dataOptions]) }); }; fnCmp[EXPORT_NAME_FIELD] = componentJqName; fnCmp[EXPORT_SELECTOR_FIELD] = selector; components[componentName] = fnCmp; $.fn[componentJqName] = fnCmp; return fnCmp; } function ensureOptions(options) { options = options || {}; if (typeof options[NAME_FIELD] != 'string') throw invalidOptionMessage(NAME_FIELD, 'string'); if (typeof options[CONSTRUCTOR_FIELD] != 'function') throw invalidOptionMessage(CONSTRUCTOR_FIELD, 'function'); return options; } function invalidOptionMessage(fieldName, fieldType) { return 'Invalid @component.{name}. Must be a {type}.' .replace('{name}', fieldName) .replace('{type}', fieldType); } function _getComponent(componentName) { if (typeof componentName !== 'string' || componentName == '') { throw new Error('Parameter componentName is required.'); } if (!components[componentName]) { throw new Error('Controller ' + componentName + ' not registered!'); } return components[componentName]; } function _listComponents() { var list = [] for (var c in components) list.push({ id: c, component: components[c] }) return list } }) // ======================================================================== // register-controller.js // ======================================================================== $namespace(3, '@', function (exports) { var utils = $require('utils'); var NAME_FIELD = 'name', CONSTRUCTOR_FIELD = 'ctor', EXPORT_NAME_FIELD = '$name', CONTROLLER_VIEW_FIELD_PRIVATE = '__view__', CONTROLLER_VIEW_FIELD = '$view', CONTROLLER_MODEL_FIELD_PRIVATE = '__model__', CONTROLLER_MODEL_FIELD = '$model', CONTROLLER_TRIGGER_FIELD_PRIVATE = '__triggers__', CONTROLLER_TRIGGER_FIELD = '$onUpdateModel'; var lazyTriggers = []; var controllers = []; var internals = exports.__internals__ = exports.__internals__ || {}; internals.getController = _getController; internals.callLazyTriggers = _callLazyTriggers; // Registra constantes públicas internals.setConstant('VIEW_BY_ID', 1); /** * Registra um controlador * * @param {string} name - Nome do controlador * @param {function} ctor - Função construtora do controlador */ exports.registerController = function (name, ctor) { var options = ensureOptions({ name: name, ctor: ctor }); var controllerName = options[NAME_FIELD]; if (controllers[controllerName]) { throw new Error('Controller ' + controllerName + ' already registered!'); } var fnCtrl = options[CONSTRUCTOR_FIELD]; fnCtrl[EXPORT_NAME_FIELD] = options[NAME_FIELD]; controllers[controllerName] = fnCtrl; fnCtrl.prototype[CONTROLLER_VIEW_FIELD] = _getViewElement; fnCtrl.prototype[CONTROLLER_MODEL_FIELD] = _manageModel; fnCtrl.prototype[CONTROLLER_TRIGGER_FIELD] = _manageTriggers; return fnCtrl; } function ensureOptions(options) { options = options || {}; if (typeof options[NAME_FIELD] != 'string') throw invalidOptionMessage(NAME_FIELD, 'string'); if (typeof options[CONSTRUCTOR_FIELD] != 'function') throw invalidOptionMessage(CONSTRUCTOR_FIELD, 'function'); return options; } function invalidOptionMessage(fieldName, fieldType) { return 'Invalid @controller.{name}. Must be a {type}.' .replace('{name}', fieldName) .replace('{type}', fieldType); } function _getController(controllerName) { if (typeof controllerName !== 'string' || controllerName == '') { throw new Error('Parameter controllerName is required.'); } if (!controllers[controllerName]) { throw new Error('Controller ' + controllerName + ' not registered!'); } return controllers[controllerName]; } /** * Retorna uma coleção de elementos dentro do escopo da controller * * @param {DOM} elType - Elemento DOM * @param {string} selector - jQuery selector */ function _getViewElement(elType, selector) { var view = this[CONTROLLER_VIEW_FIELD_PRIVATE], VIEW_BY_ID = $require('@').constants.VIEW_BY_ID; if (!(view instanceof $)) return; if (typeof elType === 'string' && arguments.length === 1) selector = elType; else if (typeof selector !== 'string') throw new Error('Invalid view selector.'); else switch (elType) { case VIEW_BY_ID: selector = '[data-id="{id}"]'.replace('{id}', selector); break; default: throw new Error('Invalid view type "' + elType + '".'); } return $(selector, view); } /** * Gerencia o modelo */ function _manageModel() { var clonerCurrent = new internals.PlainObjectCloner(this[CONTROLLER_MODEL_FIELD_PRIVATE]); // this.$model(): Get a full model if (!arguments.length) { return clonerCurrent.cloneObject(); } // this.$model({ object }): Set a full model if (arguments.length === 1 && utils.isObject(arguments[0])) { var clonerNew = new internals.PlainObjectCloner(arguments[0]), newState = clonerNew.cloneObject(); this[CONTROLLER_MODEL_FIELD_PRIVATE] = newState; _callTriggers(clonerCurrent.cloneObject(), newState, null, this); return; } // this.$model('string'): Get path of model if (arguments.length === 1 && utils.isString(arguments[0])) { var path = $.trim(arguments[0]), stateFull = clonerCurrent.cloneObject(); if (path.length === 0) return stateFull; return utils.getObjectItemByPath(stateFull, path) } // this.$model('string', { object }): Get path of model if (arguments.length === 2 && utils.isString(arguments[0]) && utils.isObject(arguments[1])) { var path = arguments[0], stateFull = clonerCurrent.cloneObject(), clonerNew = new internals.PlainObjectCloner(arguments[1]), newState = clonerNew.cloneObject(); utils.setObjectItemByPath(stateFull, path, newState); this[CONTROLLER_MODEL_FIELD_PRIVATE] = stateFull; _callTriggers(clonerCurrent.cloneObject(), stateFull, path, this); return; } throw new Error('Call with invalid parameters for ' + CONTROLLER_MODEL_FIELD + '!'); } function _attachTrigger(ctrl, path, trigger) { if (!utils.isArray(ctrl[CONTROLLER_TRIGGER_FIELD_PRIVATE])) ctrl[CONTROLLER_TRIGGER_FIELD_PRIVATE] = []; var triggers = ctrl[CONTROLLER_TRIGGER_FIELD_PRIVATE]; for (var t in triggers) { var trg = triggers[t]; if (trg.path === path && trg.trigger === trigger) return; } triggers.push({ path: path, trigger: trigger }); } function _manageTriggers() { if (arguments.length === 1 && utils.isFunction(arguments[0])) { return _attachTrigger(this, null, arguments[0]); } if (arguments.length === 2 && utils.isString(arguments[0]) && utils.isFunction(arguments[1])) { return _attachTrigger(this, arguments[0], arguments[1]); } throw new Error('Call with invalid parameters for ' + CONTROLLER_TRIGGER_FIELD + '!'); } function _callTriggers(oldState, newState, modelPath, controller) { if (!controller[CONTROLLER_VIEW_FIELD_PRIVATE]) { _setLazyTriggers(oldState, newState, modelPath, controller); return; } var eligibleTriggers = [], path = (modelPath || ''), pathParts = path === '' ? [] : path.split('.'); for (var idx = pathParts.length - 1; idx >= 0; idx--) { var pathPartsBegin = pathParts.splice(0, idx + 1); pathParts = pathPartsBegin.concat(pathParts); eligibleTriggers.push(pathPartsBegin.join('.')); } eligibleTriggers.push(''); var triggers = controller[CONTROLLER_TRIGGER_FIELD_PRIVATE] || [], triggerFilter = function (prefix) { return $.grep(triggers, function (item) { return (item.path || '') === prefix; }); }; $.each(eligibleTriggers, function (_, itemPath) { $.each(triggerFilter(itemPath), function (_, tgr) { if (!utils.isFunction(tgr.trigger)) return; var _oldState = oldState, _newState = newState; if (tgr.path) { _oldState = utils.getObjectItemByPath(_oldState, tgr.path); _newState = utils.getObjectItemByPath(_newState, tgr.path); } // function(oldState, newState, modelPath, controller) { } tgr.trigger.call( null, /* this -> null */ _oldState, _newState, modelPath, controller); }); }); } function _setLazyTriggers(oldState, newState, modelPath, controller) { var triggerIdx = -1, triggerRecord = { controller: controller, trigger: { oldState: oldState, newState: newState, modelPath: modelPath } }; $.each(lazyTriggers, function (idx, item) { if (item.controller === controller) triggerIdx = idx; }); if (triggerIdx >= 0) lazyTriggers[triggerIdx] = triggerRecord; else lazyTriggers.push(triggerRecord); } function _callLazyTriggers(controller) { var triggerIdx = -1, triggerRecord; $.each(lazyTriggers, function (idx, record) { if (record.controller === controller) { triggerIdx = idx; triggerRecord = record; } }); if (triggerRecord) { try { _callTriggers( triggerRecord.trigger.oldState, triggerRecord.trigger.newState, triggerRecord.trigger.modelPath, triggerRecord.controller ); } catch (_) { } } if (triggerIdx >= 0) { lazyTriggers.splice(triggerIdx, 1); } } }) // ======================================================================== // utils.js // ======================================================================== $namespace(0, 'utils', function (exports) { /** * Extrai o valor da propridade de um objeto por um caminho informado * * @param {object} obj - Objeto com a propriedade * @param {string} path - Caminho da propriedade */ exports.getObjectItemByPath = function (obj, path) { var value = obj, keys = path.split('.'), k = 0; while (value && k < keys.length) { value = value[keys[k]]; k++; } return value; } /** * Altera o valor da propriedade de um objeto por um caminho informado * * @param {object} obj - Objeto com a propriedade * @param {string} path - Caminho da propriedade * @param {any} newValue - Novo valor da propriedade */ exports.setObjectItemByPath = function (obj, path, newValue) { var value = obj, keys = path.split('.'), k = 0; while (value && k < keys.length) { if (typeof value[keys[k]] !== 'object') value[keys[k]] = {}; if (k + 1 !== keys.length) value = value[keys[k]]; k++; } return value[keys[--k]] = newValue; } /** * Verifica se referencia uma string * * @param {any} value - Instância a verificar */ exports.isString = function (value) { return typeof value === 'string'; } /** * Verifica se referencia uma função * * @param {any} value - Instância a verificar */ exports.isFunction = function (value) { return typeof value === 'function'; } /** * Verifica se referencia uma indefinição * * @param {any} value - Instância a verificar */ exports.isUndefined = function (value) { return typeof value === 'undefined'; } /** * Verifica se referencia um objeto * * @param {any} value - Instância a verificar */ exports.isObject = function (value) { // http://jsperf.com/isobject4 return value !== null && typeof value === 'object'; } /** * Verifica se referencia um número * * @param {any} value - Instância a verificar */ exports.isNumber = function (value) { return typeof value === 'number'; } /** * Verifica se referencia um array * * @param {any} value - Instância a verificar */ exports.isArray = function (value) { return value instanceof Array || Object.prototype.toString.call(value) === "[object Array]"; } /** * Recupera o titulo do elemento principal (document) */ exports.getPageTitle = function (newTitle) { return $(document).attr('title'); } /** * Altera o titulo do elemento principal (document) * * @param {string} newTitle - Novo título */ exports.setPageTitle = function (title) { $(document).attr('title', title); } }) // Inicializa os namespaces na ordem especificada _NAMESPACES_.sort(function (a, b) { return a.idx - b.idx; }); // Não usamos (map) para compatibilidade com IE8 for (var n in _NAMESPACES_) { _NAMESPACES_[n].cb(); } return $toad; });