%PDF- %PDF-
Direktori : /home2/vacivi36/intranet.vacivitta.com.br/static/js/humhub/ |
Current File : //home2/vacivi36/intranet.vacivitta.com.br/static/js/humhub/humhub.action.js |
/** * This module can be used by humhub sub modules for registering handlers and serves as core module for executing actions triggered in the gui. * A module can either register global handler by using the registerHandler functions or use the component mechanism. * * @namespace humhub.modules.action */ humhub.module('action', function(module, require, $) { /** @module action **/ var _handler = {}; var util = require('util'); var object = util.object; var string = util.string; var loader = require('ui.loader'); /** * Used for non blocked actions. * * @constant module:action~BLOCK_NONE * @type {string} */ var BLOCK_NONE = 'none'; /** * Used for synchronous blocking of actions. * * @constant module:action~BLOCK_NONE * @type {string} */ var BLOCK_SYNC = 'sync'; /** * Used for asynchronous action blocking. * * @constant module:action~BLOCK_NONE * @type {string} */ var BLOCK_ASYNC = 'async'; /** * Used for manual action blocking. * * @constant module:action~BLOCK_NONE * @type {string} */ var BLOCK_MANUAL = 'manual'; var DATA_COMPONENT = 'action-component'; var processes = {}; /** * Base class for all action based components and widgets. * * This class uses the `object.extendable` feature which enables classes to be extended by calling: * * ``` * var Widget = Component.extend(); * * or with custom init logic * * var Widget = Component.extend(function(node, options) { * Component.call(this, node, options); * //... * }); * ``` * * @class module:action.Component * @property {jQuery} $ */ var Component = object.extendable({ name: 'Component', /** * Initializes the component and appends the component instance to the node as `data-component`. * Sub classes should call the parent constructor if providing an own `init` function. * * @function module:action.Component.init * @param {jQuery|Node} node * @param {array} options */ init: function(node, options) { if(!node) { return; } if(node instanceof $) { this.$ = node; } else if(object.isString(node)) { this.$ = $(node); if(!this.$.length && !string.startsWith(node, '#')) { this.$ = $('#' + node); } } this.base = Component.getNameSpace(this.$); this.$.data(this.static('component'), this); } }); Component._selectors = [DATA_COMPONENT]; Component.component = 'humhub-component'; Component._buildSelector = function() { Component._selector = Component._selectors.map(function(selector) { return '[data-' + selector + ']'; }).join(','); }; /** * Searches for the given data setting and forwards the search call to the parent component if not found. * * @function module:action.Component.data * @param {string} dataSuffix * @param {*} defaultValue * @returns {*} */ Component.prototype.data = function(dataSuffix, defaultValue) { var result = this.$.data(dataSuffix); if(!result) { var parentComponent = this.parent(); if(parentComponent) { result = parentComponent.data(dataSuffix); } } return object.defaultValue(result, defaultValue); }; /** * Sets the given data setting to the root node. * * @function module:action.Component.setData * @param {string} key * @param {string} value */ Component.prototype.setData = function(key, value) { this.$.data(key, value); }; /** * Searches for a parent component. * * @function module:action.Component.parent * @returns {Component} */ Component.prototype.parent = function() { var $parent = this.$.parent().closest(Component._selector); return Component.closest($parent); }; /** * Searches for component by means of a given dom selector. If includeSelf is set to true * the Component itself is included if it matches the given selector. * * @function module:action.Component.find * @param {jQuery|Node}node * @param {string} selector * @param {boolean} includeSelf * @returns {Component[]} */ Component.find = function(node, selector, includeSelf) { var result = []; var $node = (node instanceof $) ? node : $(node); var $query = $node.find(Component._selector); if(includeSelf) { $query.addBack(selector); } $query.each(function() { var component = Component.instance($(this)); if(component && (!selector || component.$.is(selector))) { result.push(component); } }); return result; }; /** * Returns child components which do match the given dom selector. * * @function module:action.Component.children * @param {string} selector * @returns {Component[]} */ Component.prototype.children = function(selector) { return Component.find(this.$, selector); }; /** * @param action * @returns {boolean} * @deprecated */ Component.prototype.hasAction = function(action) { return this.actions().indexOf(action) >= 0; }; /** * @returns {boolean} * @deprecated */ Component.prototype.actions = function() { return []; }; /** * Triggers a given evt on the root node. * * @returns {jQuery} root */ Component.prototype.trigger = function(eventType, extraParameters) { return this.$.trigger(eventType, extraParameters); }; /** * Finds the closest component of the given node (including the node itself). * * @function module:action.Component.closest * @param {jQuery|Node} node * @param {Array} options * @returns {Component} */ Component.closest = function(node, options) { //Determine closest component node (parent or or given node) var $node = _getNode(node); if(!$node.length) { return; } // Search the component root, which is either the node itself or a surrounding component var $componentRoot = Component.getComponentRoot($node); var ns = Component.getNameSpace($componentRoot); if(!$componentRoot.length || !ns) { return; } return Component._getInstance(require(ns), $componentRoot, options); }; Component.getComponentRoot = function($node) { var ns = Component.getNameSpace($node); if(ns) { return $node; } else { return Component.getClosestComponentNode($node); } }; Component.getNameSpace = function(node) { var $node = (node instanceof $) ? node : $(node); var base; $.each(Component._selectors, function(i, selector) { base = $node.data(selector); if(base) { return false; // leave foreach } }); return base; }; /** * Creates a component instance out of the given node. * * @function module:action.Component.instance * @param {jQuery|Node} node * @param {Array} options * @returns {Component} */ Component.instance = function(node, options) { //Determine closest component node (parent or or given node) try { var $node = _getNode(node); if(!$node || !$node.length) { return null; } var ns = Component.getNameSpace($node); var ComponentClass = (ns) ? require(ns) : this; return Component._getInstance(ComponentClass, $node, options); } catch(e) { module.log.warn(e); } return null; }; var _getNode = function(node) { try { var $node = (node instanceof $) ? node : $(node); if(!$node.length && object.isString(node) && node.length && !string.startsWith(node, '#')) { $node = $('#' + node); } return $node; } catch(e) { return null; } }; Component._getInstance = function(ComponentClass, $node, options) { if(!ComponentClass) { module.log.error('No valid component class found for given node ', $node, true); return; } else if($node.data(ComponentClass.component)) { return $node.data(ComponentClass.component); } else { return ComponentClass.createInstance(ComponentClass, $node, options); } }; Component.createInstance = function(ComponentClass, node, options) { return new ComponentClass(node, options); }; Component.addSelector = function(selector) { Component._selectors.push(selector); Component._buildSelector(); }; Component.getClosestComponentNode = function($element) { return $element.closest(Component._selector); }; /** * Thes method will search for a sorrounding component and try to execute * the event handler action on this component. * * If no component is found or the component does not provide the action handler * we'll return false else true. * * @param {object} event - event object * @returns {Boolean} true if the component action could be executed else false */ Component.handleAction = function(event) { var component = Component.closest(event.$target); if(component && string.startsWith(event.handler, 'parent.')) { component = component.parent(); event.handler = event.handler.split('.')[1]; } return (component) ? _executeAction(component, event.handler, event) : false; }; /** * Constructor for initializing the module. */ var init = function($isPjax) { if(!$isPjax) { Component._buildSelector(); //Binding default action types this.bindAction(document, 'click', '[data-action-click]'); this.bindAction(document, 'change', '[data-action-change]'); this.bindAction(document, 'keypress', '[data-action-keypress]', false); this.bindAction(document, 'keydown', '[data-action-keydown]', false); } // Handles data-action-confirm non action based links links $(document).on('click', '[data-action-confirm]', function(evt) { var $this = $(this); // Make sure we are not intercepting an action based link var data = $this.data(); for(var key in data) { if(string.startsWith(key, 'action') && !string.startsWith(key, 'actionConfirm') && !string.startsWith(key, 'actionCancel') && !string.startsWith(key, 'actionMethod')) { return; } } evt.preventDefault(); require('ui.modal').confirm($this).then(function(confirmed) { if(confirmed) { if ($this.attr('type') === 'submit' && $this.closest('form').length) { return $this.closest('form').submit(); } var client = require('client'); var url = $this.attr('href'); var method = $this.data('action-method') || 'GET'; if(method.toLocaleUpperCase() === 'POST') { return $this.is('[data-pjax-prevent]') ? client.submit({url:url}) : client.pjax.post({url: url}) ; } else { return $this.is('[data-pjax-prevent]') ? (document.location = url) : client.pjax.redirect(url); } } }).finally(function() { loader.reset($this); }); }); updateBindings(); }; /** * Registers a given handler with the given id. * * This handler will be called e.g. after clicking a button with the handler id as * data-action-click attribute. * * The handler can access additional event information through the argument event. * The this object within the handler will be the trigger of the event. * * @function module:action.registerHandler * @param {string} id handler id should contain the module namespace * @param {function} handler function with one event argument * @returns {undefined} */ var registerHandler = function(id, handler) { if(!id) { return; } if(handler) { _handler[id] = handler; } }; var actionBindings = []; var updateBindings = function() { module.log.debug('Update action bindings'); $.each(actionBindings, function(i, binding) { var $targets = $(binding.selector); $targets.off(binding.event).on(binding.event, function(evt) { module.log.debug('Handle direct trigger action', evt); return binding.handle({originalEvent: evt, $trigger: $(this)}); }); $targets.data('action-' + binding.event, true); }); }; var getProcessTrigger = function(id) { return processes[id]; }; /** * ActionBinding instances are used to store the binding settings and handling * binding events. * * @param {type} options * @returns {humhub_action_L5.ActionBinding} */ var ActionBinding = function(options) { options = options || {}; this.parent = options.parent; // e.g. click this.eventType = options.type; // namespaced event e.g. click.humhub-action this.event = options.event; this.selector = options.selector; this.directHandler = options.directHandler; this.preventDefault = options.preventDefault }; /** * Handles an action event for the given $trigger node. * * This handler searches for a valid action handler, by checking the following handler types in the given order: * * - **Direct-ActionHandler** is called if a directHandler was given when binding the action. * - **Component-ActionHandler** is called if $trigger is part of a component and the component handler can be resolved * - **Global-ActionHandler** is called if we find a handler in the _handler array. See registerHandler * - **Namespace-ActionHandler** is called if we can resolve an action by namespace e.g: data-action-click="myModule.myAction" * * Once triggered the handler can be blocked to prevent multiple click events. The block logic can be configured by setting * the data-action-block or more specific data-action-<eventType>-block on the $trigger node. The following block values are available: * * - `none`: No blocking at all * - `sync`: Synchronous blocking, the block will be removed after the actionhandler was executed. * - `async`: Asynchronous the block has to be manually removed by calling event.finish. * * If the action is provided with an url or is an submit action (data-action-submit or type="submit") the block value is set to 'async' by default, * otherwise its set to 'sync'. * * Note: When using humhub.modules.client for submitting a form or sending a request and providing the action event, the event.finish will be * called for you after we receive you response, so you do not have to call it manually. * * Once triggered the handler will block the event for this actionbinding until the actionevents .finish is called. * This is used to prevent multiple triggering of actions. This behaviour can be disabled by setting: * * data-action-prevent-block or data-action-prevent-block-<eventType> * * @param {type} evt the originalEvent * @param {type} $trigger the jQuery node which triggered the event * @returns {undefined} */ ActionBinding.prototype.handle = function(options) { var options = options || {}; var $trigger = options.$trigger; if(this.data($trigger, 'process')) { processes[this.data($trigger, 'process')] = $trigger; } if(this.preventDefault !== false && options.originalEvent) { options.originalEvent.preventDefault(); } module.log.debug('Handle Action', this); var event = this.createActionEvent(options); if(object.isDefined(this.data($trigger, 'confirm')) && !options.confirmed) { var that = this; require('ui.modal').confirm($trigger).then(function(confirmed) { if(confirmed) { options.confirmed = true; that.handle(options); } else { event.finish(); } }); return; } // Reset value just to get sure the options are not reused. options.confirmed = undefined; if(this.isBlocked($trigger)) { module.log.warn('Blocked action execution ', $trigger); return; } if(this.isBlockAction($trigger)) { this.block($trigger); } try { // Check for a direct action handler if(object.isFunction(this.directHandler)) { this.directHandler.apply($trigger, _getArgs(event)); return; } // Check for a component action handler if(Component.handleAction(event)) { return; } // Check for global registered handlers if(_handler[event.handler]) { _handler[event.handler].apply($trigger, _getArgs(event)); return; } // As last resort we try to call the action by namespace for handlers like humhub.modules.myModule.myAction var handlerAction = event.handler.split('.').pop(); var target = require(string.cutSuffix(event.handler, '.' + handlerAction)); if(!_executeAction(target, handlerAction, event)) { module.log.error('actionHandlerNotFound', event.handler, true); } } catch(e) { module.log.error('error.default', e, true); event.finish(); } finally { // Just to get sure the handler is not called twice. if(options.originalEvent) { options.originalEvent.actionHandled = true; } if(this.isBlockType($trigger, BLOCK_SYNC)) { event.finish(); } } }; var _executeAction = function(target, handlerAction, event) { // first try actionMyhandler var handlerCapitalized = 'action' + string.capitalize(handlerAction); if(object.isFunction(target[handlerCapitalized])) { handlerAction = handlerCapitalized; } if(object.isFunction(target[handlerAction])) { // Handler arguments target[handlerAction].apply(target, _getArgs(event)); return true; } return false; }; var _getArgs = function(event) { if(!event.params) { return [event]; } if(object.isArray(event.params)) { var args = event.params.slice(); args.unshift(event); return args; } else { return [event, event.params]; } }; /** * Returns the value of data-action-click-<name> over data-action-<name> * e.g.: * * If the $trigger sets a data-action-click-url and data-action-url and we call * * $actioNBinding.data($trigger, 'url'); * * We'll receive the data-action-click-url. * * If no data-action-click-url is set it will return the fallback data-action-url setting. * * @param {type} $trigger * @param {type} name * @param {type} def * @returns {mixed} */ ActionBinding.prototype.data = function($trigger, name, def) { var result = $trigger.data('action-' + this.eventType + '-' + name); if(!object.isDefined(result)) { result = $trigger.data('action-' + name); } return object.isDefined(result) ? result : def; }; ActionBinding.prototype.getUrl = function($trigger) { var url = this.data($trigger, 'url'); if(!url) { url = $trigger.attr('href'); } if(url !== '#' && url !== '') { return url; } return null; }; /** * Checks if the trigger should be blocked before running the action. * * @param {type} $trigger * @returns {Boolean} */ ActionBinding.prototype.isBlockAction = function($trigger) { return !this.isBlockType($trigger, BLOCK_NONE); }; /** * Checks the given block data setting of $trigger agains a blocktype. * * @param {type} $trigger * @param {type} type * @returns {Boolean} */ ActionBinding.prototype.isBlockType = function($trigger, type) { var defaultBlockType = (this.isSubmit($trigger) || this.getUrl($trigger)) ? BLOCK_ASYNC : BLOCK_SYNC; var blockType = this.data($trigger, 'block', defaultBlockType); return type === blockType; }; ActionBinding.prototype.isSubmit = function($trigger) { return $trigger.is('[type="submit"]') || object.isDefined($trigger.data('action-submit')); }; /** * Checks if $trigger is currently blocked. * * @param {type} $trigger * @returns {unresolved} */ ActionBinding.prototype.isBlocked = function($trigger) { return this.data($trigger, 'blocked'); }; /** * Blocks $trigger, which will disable further action calls. * * @param {type} $trigger * @returns {undefined} */ ActionBinding.prototype.block = function($trigger) { $trigger.data('action-' + this.eventType + '-blocked', true); }; ActionBinding.prototype.unblock = function($trigger) { $trigger.data('action-' + this.eventType + '-blocked', false); }; ActionBinding.prototype.createActionEvent = function(options) { var $trigger = options.$trigger; var event = $.Event(this.eventType); event.originalEvent = options.originalEvent; var settings = { $trigger : $trigger, $target: $(this.data($trigger, 'target', $trigger)), url: this.getUrl($trigger), params: this.data($trigger, 'params', {}), block: this.data($trigger, 'block'), handler: $trigger.data('action' + '-' + this.eventType) }; if(this.isSubmit($trigger)) { // Either use closest form or data-action-target if provided settings.$form = $(this.data($trigger, 'target', $trigger.closest('form'))); } $.extend(event, settings, options); var that = this; event.finish = function() { _removeLoaderFromEventTarget(event); _removeLoaderFromEventTarget(event.originalEvent); that.unblock($trigger); }; event.data = function(key, def) { return that.data($trigger, key, def); }; return event; }; var _removeLoaderFromEventTarget = function(evt) { if(evt && evt.target) { loader.reset(evt.target); } if(evt && evt.$trigger) { loader.reset(evt.$trigger); } }; /** * Binds a delegate wrapper event handler to the parent node. This is used to detect action handlers like * `data-action-click` events and map the call to either a stand alone handler or a content * action handler. The trigger of a contentAction has to be contained in a data-content-base node. * * This function uses the jQuery event delegation: * * ``` * $(parent).on(type, selector, function(){...}); * ``` * * This assures the event binding for dynamic content (ajax content etc..) * * @function module:action.bindAction * @param {Node|jQuery} parent - the event target * @param {string} type - event type e.g. click, change,... * @param {string} selector - jQuery selector * @param {function} directHandler * @param {boolean} preventDefault */ var bindAction = function(parent, type, selector, directHandler, preventDefault) { parent = parent || document; var $parent = (parent instanceof $) ? parent : $(parent); var actionEvent = type + '.humhub-action'; if(object.isBoolean(directHandler)) { preventDefault = directHandler; directHandler = null; } preventDefault = object.isDefined(preventDefault) ? preventDefault : true; var actionBinding = new ActionBinding({ parent: parent, type: type, event: actionEvent, selector: selector, preventDefault: preventDefault, directHandler: directHandler }); // Add new ActionBinding with given settings. actionBindings.push(actionBinding); $parent.off(actionEvent).on(actionEvent, selector, function(evt) { var test = evt.isDefaultPrevented(); if(preventDefault) { evt.preventDefault(); } var $this = $(this); // Get sure we don't call the handler twice if the event was already handled by the directly attached handler. // We have to rebind the handler only if we detect an unbound handler! // Note, since jquery object loses data after removed from dom, we also check if the trigger is still in dom, if not we do not execute the action. if($this.data('action-' + actionBinding.event) || !$this.closest('body').length || (evt.originalEvent && evt.originalEvent.actionHandled)) { module.log.debug('Action Handler already executed by direct handler' + actionEvent, actionBinding); module.log.debug('Handler event triggered by', $this); return; } module.log.debug('Detected unhandled action', actionBinding); updateBindings(); actionBinding.handle({originalEvent: evt, $trigger: $this}); }); return; }; /** * This function can be called to manually trigger an action event of the given $trigger. * This can be used for example for additional event types without actually binding the * event to $trigger. * * e.g manually trigger a custom data-action-done action of an ui component. * * @function module:action.trigger * @param {jQuery} $trigger * @param {string} type * @param {Array} options * @returns {undefined} */ var trigger = function($trigger, type, options) { options.$trigger = $trigger; // For manually triggered action events we do not need a block in most cases. if(!options.block) { options.block = BLOCK_NONE; } new ActionBinding({ type: type, event: type }).handle(options); }; module.export({ initOnPjax: true, init: init, sortOrder: 100, bindAction: bindAction, registerHandler: registerHandler, Component: Component, trigger: trigger, getProcessTrigger: getProcessTrigger, BLOCK_NONE: BLOCK_NONE, BLOCK_SYNC: BLOCK_SYNC, BLOCK_ASYNC: BLOCK_ASYNC, BLOCK_MANUAL: BLOCK_MANUAL }); });