// Commandor constructor : rootElement is the element // from wich we capture commands var Commandor = function Commandor(rootElement, prefix) { // Event handlers var _pointerDownListener; var _pointerUpListener; var _pointerClickListener; var _touchstartListener; var _touchendListener; var _clickListener; var _keydownListener; var _keyupListener; var _formChangeListener; var _formSubmitListener; // Commands hashmap var _commands = { '____internal': true }; // Command prefix this.prefix = prefix || 'app:'; // Testing rootElement if(!rootElement) { throw Error('No rootElement given'); } // keeping a reference to the rootElement this.rootElement = rootElement; // MS Pointer events : should unify pointers, but... read and see by yourself. if(!!('onmsgesturechange' in window)) { // event listeners for buttons (function() { var curElement = null; _pointerDownListener = function(event) { curElement = this.findButton(event.target) || this.findForm(event.target); curElement && event.preventDefault() || event.stopPropagation(); }.bind(this); _pointerUpListener = function(event) { if(curElement) { if(curElement === this.findButton(event.target)) { this.captureButton(event); } else if(curElement === this.findForm(event.target)) { this.captureForm(event); } event.preventDefault(); event.stopPropagation(); curElement = null; } }.bind(this); this.rootElement.addEventListener('MSPointerDown', _pointerDownListener, true); this.rootElement.addEventListener('MSPointerUp', _pointerUpListener, true); }).call(this); // fucking IE10 bug : it doesn't cancel click event // when gesture events are cancelled _pointerClickListener = function(event) { if(this.findButton(event.target)) { event.preventDefault(); event.stopPropagation(); } }.bind(this); this.rootElement.addEventListener('click', _pointerClickListener,true); } else { // Touch events if(!!('ontouchstart' in window)) { (function() { // a var keepin' the touchstart element var curElement = null; _touchstartListener = function(event) { curElement = this.findButton(event.target) || this.findForm(event.target); curElement && event.preventDefault() || event.stopPropagation(); }.bind(this); this.rootElement.addEventListener('touchstart', _touchstartListener, true); // checking it's the same at touchend, capturing command if so _touchendListener = function(event) { if(curElement == this.findButton(event.target)) { this.captureButton(event); } else if(curElement === this.findForm(event.target)) { this.captureForm(event); } else { curElement = null; } }.bind(this); this.rootElement.addEventListener('touchend', _touchendListener, true); }).call(this); } // Clic events _clickListener = this.captureButton.bind(this); this.rootElement.addEventListener('click', _clickListener, true); } // Keyboard events // Cancel keydown action (no click event) _keydownListener = function(event) { if( 13 === event.keyCode && ( this.findButton(event.target) || this.findForm(event.target) ) ) { event.preventDefault() && event.stopPropagation(); } }.bind(this); this.rootElement.addEventListener('keydown', _keydownListener, true); // Fire on keyup _keyupListener = function(event) { if( 13 === event.keyCode && !event.ctrlKey ) { if(this.findButton(event.target)) { this.captureButton.apply(this, arguments); } else { this.captureForm.apply(this, arguments); } } }.bind(this); this.rootElement.addEventListener('keyup', _keyupListener, true); // event listeners for forms submission _formSubmitListener = this.captureForm.bind(this); this.rootElement.addEventListener('submit', _formSubmitListener, true); // event listeners for form changes _formChangeListener = this.formChange.bind(this); this.rootElement.addEventListener('change', _formChangeListener, true); this.rootElement.addEventListener('select', _formChangeListener, true); // Common command executor this.executeCommand = function (event,command,element) { if(!_commands) { throw Error('Cannot execute command on a disposed Commandor object.'); } // checking for the prefix if(0 !== command.indexOf(this.prefix)) { return false; } // removing the prefix command = command.substr(this.prefix.length); var chunks = command.split('?'); // the first chunk is the command path var callback = _commands; var nodes = chunks[0].split('/'); for(var i = 0, j = nodes.length; i < j-1; i++) { if(!callback[nodes[i]]) { throw Error('Cannot execute the following command "' + command + '".'); } callback = callback[nodes[i]]; } if('function' !== typeof callback[nodes[i]]) { throw Error('Cannot execute the following command "' + command + '", not a function.'); } // Preparing arguments var args = {}; if(chunks[1]) { chunks = chunks[1].split('&'); for(var k = 0, l = chunks.length; k < l; k++) { var parts = chunks[k].split('='); if(undefined !== parts[0] && undefined !== parts[1]) { args[parts[0]] = decodeURIComponent(parts[1]); } } } // executing the command fallback if(callback.____internal) { return !!!((callback[nodes[i]])(event, args, element)); } else { return !!!(callback[nodes[i]](event, args, element)); } return !!!callback(event, args, element); }; // Add a callback or object for the specified path this.suscribe = function(path, callback) { if(!_commands) { throw Error('Cannot suscribe commands on a disposed Commandor object.'); } var nodes = path.split('/'); var command = _commands; for(var i = 0, j = nodes.length-1; i < j; i++) { if((!command[nodes[i]]) || !(command[nodes[i]] instanceof Object)) { if(!command.____internal) { throw Error('Cannot suscribe commands on an external object.'); } command[nodes[i]] = { '____internal': true }; } command = command[nodes[i]]; } if(!command.____internal) { throw Error('Cannot suscribe commands on an external object.'); } command[nodes[i]] = callback; }; // Delete callback for the specified path this.unsuscribe = function(path) { if(!_commands) { throw Error('Cannot unsuscribe commands of a disposed Commandor object.'); } var nodes = path.split('/'), command = _commands; for(var i = 0, j = nodes.length-1; i < j; i++) { command = command[nodes[i]] = {}; } if(!command.____internal) { throw Error('Cannot unsuscribe commands of an external object.'); } command[nodes[i]] = null; }; // Dispose the commandor object (remove event listeners) this.dispose = function() { _commands = null; if(_pointerDownListener) { this.rootElement.removeEventListener('MSPointerDown', _pointerDownListener, true); this.rootElement.removeEventListener('MSPointerUp', _pointerUpListener, true); this.rootElement.removeEventListener('click', _pointerClickListener, true); } if(_touchstartListener) { this.rootElement.removeEventListener('touchstart', _touchstartListener, true); this.rootElement.removeEventListener('touchend', _touchendListener, true); } this.rootElement.removeEventListener('click', _clickListener, true); this.rootElement.removeEventListener('keydown', _keydownListener, true); this.rootElement.removeEventListener('keyup', _keyupListener, true); this.rootElement.removeEventListener('change', _formChangeListener, true); this.rootElement.removeEventListener('select', _formChangeListener, true); this.rootElement.removeEventListener('submit', _formSubmitListener, true); }; }; // Look for a button Commandor.prototype.findButton = function(element) { while(element && element.parentNode) { if( 'A' === element.nodeName && element.getAttribute('href') && -1 !== element.getAttribute('href').indexOf(this.prefix) ) { return element; } if( 'INPUT' === element.nodeName && element.getAttribute('type') && ( element.getAttribute('type') == 'submit' || element.getAttribute('type') == 'button' ) && element.getAttribute('formaction') && -1 !== element.getAttribute('formaction').indexOf(this.prefix) ) { return element; } if(element === this.rootElement) { return null; } element = element.parentNode; } return null; }; // Look for a form Commandor.prototype.findForm = function(element) { if( 'FORM' === element.nodeName || ( 'INPUT' === element.nodeName && element.getAttribute('type') && 'submit' === element.getAttribute('type') ) ) { while(element && element.parentNode) { if( 'FORM' === element.nodeName && element.getAttribute('action') && -1 !== element.getAttribute('action').indexOf(this.prefix) ) { return element; } if(element === this.rootElement) { return null; } element = element.parentNode; } return element; } return null; }; // Look for form change Commandor.prototype.findFormChange = function(element) { while(element && element.parentNode) { if( 'FORM' === element.nodeName && element.getAttribute('action') && -1 !== element.getAttribute('action').indexOf(this.prefix) ) { return element; } if(element === this.rootElement) { return null; } element = element.parentNode; } return element; }; // Extract the command for a button Commandor.prototype.doCommandOfButton = function(element, event) { var command = ''; // looking for a button with formaction attribute if('INPUT' === element.nodeName) { command = element.getAttribute('formaction'); // looking for a link } else if('A' === element.nodeName) { command = element.getAttribute('href'); } // executing the command this.executeCommand(event, command, element); }; // Button event handler Commandor.prototype.captureButton = function(event) { var element = this.findButton(event.target); // if there is a button, stop event if(element) { // if the button is not disabled, run the command if('disabled' !== element.getAttribute('disabled')) { this.doCommandOfButton(element, event); } event.stopPropagation() || event.preventDefault(); } }; // Form change handler Commandor.prototype.formChange = function(event) { // find the evolved form var element = this.findFormChange(event.target); var command = ''; // searching the data-change attribute containing the command if(element && 'FORM' === element.nodeName) { command = element.getAttribute('data-change'); } // executing the command command && this.executeCommand(event, command, element); }; // Extract the command for a button Commandor.prototype.doCommandOfForm = function(element, event) { var command = ''; // looking for a button with formaction attribute if('FORM' === element.nodeName) { command=element.getAttribute('action'); } // executing the command this.executeCommand(event, command, element); }; // Form command handler Commandor.prototype.captureForm = function(event) { var element = this.findForm(event.target); // if there is a button, stop event if(element) { // if the button is not disabled, run the command if('disabled' !== element.getAttribute('disabled')) { this.doCommandOfForm(element, event); } event.stopPropagation() || event.preventDefault(); } }; module.exports = Commandor;