UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

443 lines (404 loc) 13.6 kB
"use strict"; /** * @param {dom} element * @param {function} events - Shortcut for `{ tap: ... }`. * @param {function} events.tap { x, y } * @param {function} events.doubletap { x, y } * @param {function} events.drag { x, y, x0, y0, vx, vy } * @param {function} events.dragstart { x, y } * @param {function} events.dragend { x, y, x0, y0 } * @param {function} events.wheel { x, y, delta } */ module.exports = getGesture; const HANDLERS = { tap: onTap, doubletap: onDoubletap, drag: onDrag, dragstart: onDragStart, dragend: onDragEnd, move: onMove, down: onDown, up: onUp, wheel: onWheel }, SYMBOL = Symbol( '__tfw.gestures__' ), // Webkit and Opera still use 'mousewheel' event type. WHEEL_EVENT = ( function () { // Modern browsers support "wheel" if ( "onwheel" in document.createElement( "div" ) ) return "wheel"; // Webkit and IE support at least "mousewheel" if ( typeof document.onmousewheel !== 'undefined' ) return "mousewheel"; // let's assume that remaining browsers are older Firefox return "DOMMouseScroll"; }() ), SUPPORTED_EVENTS = Object.keys( HANDLERS ); const Hammer = require( "external.hammer" ); class Gesture { constructor( element ) { Object.defineProperty( this, '$', { writable: false, value: element, configurable: false } ); this._events = {}; // Last time a touch start occurs. This is used to prevent onmousedown to be triggered if a // touchstart has been processed. this._touchstart = 0; } /** * Two syntaxes: on( name, slot, args) or on( slot, args ). * In the second case, name is 'tap'. * * @param {string|function} arg1 - name or slot. * @param {function|any} arg2 - slot or args. * @param {any} arg3 - args or undefined. * @returns {object} this. */ on( arg1, arg2, arg3 ) { const syntax1 = typeof arg1 === 'function', name = syntax1 ? "tap" : arg1, slot = syntax1 ? arg1 : arg2, args = syntax1 ? arg2 : arg3; try { ensureValidGestureNameAndSlot( name, slot ); HANDLERS[ name ].call( this, doRegister.bind( this ), slot, args ); return this; } catch ( ex ) { console.error( "[Gesture.on( name, slot, args )]" ); console.error( " name", name ); console.error( " slot", slot ); console.error( " args", args ); console.error( " ERROR", ex ); throw Error( ex ); } } } /** * Return the associated gesture object. If it does not exist, create it. * * @param {[type]} element_ [description] * @returns {[type]} [description] */ function getGesture( element_ ) { const element = ensureDom( element_ ); if ( !element[ SYMBOL ] ) element[ SYMBOL ] = new Gesture( element ); return element[ SYMBOL ]; } /** * We map a chain of responsability to each hammer event we need to deal with. * When an item of that chain returns `true` that means it will take the responsability and we do * not ask the others. */ function handlerWithChainOfResponsability( eventName, evt ) { const chain = this._events[ eventName ].chain; for ( let i = 0; i < chain.length; i++ ) { const responsible = chain[ i ]; if ( responsible( evt ) === true ) return; } } /** * Add a responsability item in the chain of a hammer event. */ function doRegister( event, responsible ) { var hammerEvent = getEventNameForPrefix( event, "hammer." ); if ( hammerEvent && !this._hammer ) { this._hammer = new Hammer( this.$, { domEvents: true } ); // To get domEvents.stopPropagation() available. this._hammer.domEvents = true; } var eventDef = this._events[ event ]; if ( !eventDef ) { var handler = handlerWithChainOfResponsability.bind( this, event ); eventDef = { chain: [], handler: handler }; if ( hammerEvent ) this._hammer.on( hammerEvent, handler ); else this.$.addEventListener( event, handler ); this._events[ event ] = eventDef; } eventDef.chain.push( responsible ); } function ensureValidGestureNameAndSlot( name, slot ) { if ( typeof name !== 'string' ) { throw Error( "[Gestures.on] `name` must be a string: " + JSON( name ) + "!" ); } if ( SUPPORTED_EVENTS.indexOf( name ) === -1 ) { throw Error( "Unknown gesture's name `" + name + "`!\n" + "Available names are: " + SUPPORTED_EVENTS.join( ", " ) + "." ); } if ( typeof slot !== 'function' ) { throw Error( "Gesture `" + name + "` must have a function as slot!" ); } } function ensureDom( dom ) { if ( dom instanceof Node ) return dom; if ( dom === undefined || dom === null ) { throw Error( "Not a valid DOM element!", dom ); } if ( dom.$ instanceof Node ) return dom.$; if ( dom.element instanceof Node ) return dom.element; if ( typeof dom === 'string' ) { var elem = document.getElementById( dom ); if ( !elem ) { console.error( "There is no DOM element with this ID: `" + dom + "`" ); } return elem; } if ( typeof dom.element === 'function' ) return dom.element(); return dom; }; function setHammerXY( elem, evt ) { var x, y; // Hammer's attributes. x = evt.center.x; y = evt.center.y; var rect = elem.getBoundingClientRect(); evt.x = x - rect.left; evt.y = y - rect.top; } function setXY( elem, evt ) { var x, y; // Browser's attributes. x = evt.clientX; y = evt.clientY; var rect = elem.getBoundingClientRect(); return { x: x - rect.left, y: y - rect.top }; } function setHammerVxVy( elem, evt ) { evt.vx = evt.x - this._dragX; evt.vy = evt.y - this._dragY; evt.x0 = evt.x - evt.deltaX; evt.y0 = evt.y - evt.deltaY; this._dragX = evt.x; this._dragY = evt.y; } function onTap( register, slot, args ) { var that = this; register( 'hammer.tap', function ( evt ) { if ( evt.tapCount !== 1 ) return false; setHammerXY( that.$, evt ); slot( { x: evt.x, y: evt.y, target: evt.target, preventDefault: evt.preventDefault.bind( evt ) } ); return true; } ); } function onDoubletap( register, slot, args ) { var that = this; register( 'hammer.tap', function ( evt ) { if ( evt.tapCount !== 2 ) return false; setHammerXY( that.$, evt ); slot( { x: evt.x, y: evt.y, target: evt.target, preventDefault: evt.preventDefault.bind( evt ) } ); return true; } ); } function onWheel( register, slot, args ) { var that = this; register( WHEEL_EVENT, function ( evt ) { var newEvt = setXY( that.$, evt ); newEvt.delta = evt.deltaY; if ( typeof newEvt.delta !== 'number' ) { // IE 11. newEvt.delta = -evt.wheelDelta; } newEvt.preventDefault = evt.preventDefault.bind( evt ); newEvt.stopPropagation = evt.stopPropagation.bind( evt ); slot( newEvt ); } ); } function onMove( register, slot ) { var that = this; register( 'mousemove', function ( evt ) { var newEvt = setXY( that.$, evt ); newEvt.preventDefault = evt.preventDefault.bind( evt ); newEvt.stopPropagation = evt.stopPropagation.bind( evt ); newEvt.event = evt; slot( newEvt ); } ); } /** * Dragging is moving while touching. * * @this Gesture * @param {function} register - Hammer register function. * @param {function} slot - Function to call when the event occurs. * @param {any} args - Any argument to send to the slot. * @returns {undefined} */ function onDrag( register, slot, args ) { const that = this; register( 'hammer.pan', function ( evt ) { if ( evt.isFinal ) return false; setHammerXY( that.$, evt ); if ( typeof that._dragX === 'undefined' ) { that._dragX = evt.x; that._dragY = evt.y; that._dragStart = true; } setHammerVxVy.call( that, that.$, evt ); const domEvt = evt.srcEvent; slot( { x: evt.x, y: evt.y, x0: evt.x0, y0: evt.y0, vx: evt.vx, vy: evt.vy, sx: evt.velocityX, sy: evt.velocityY, event: evt, target: evt.target, buttons: getButtons( evt ), preventDefault: domEvt.preventDefault.bind( domEvt ), stopPropagation: domEvt.stopImmediatePropagation.bind( domEvt ) }, args ); return true; } ); } /** * Get the information on buttons pressed. * This is a cumulative value because several buttons can be pressed at the same time: * 1: left mouse button. * 2: right mouse button. * 4: middle mouse button. * Then, if the result is 3 (1+2), it means that the left and the right mouse buttons are pressed. * * @param {object} hammerEvent - Hammer event. * @param {integer} hammerEvent.srcEvent.buttons - The information we need. * @returns {integer} A betwise OR of the pressed buttons. */ function getButtons( hammerEvent ) { if ( !hammerEvent || !hammerEvent.srcEvent ) return 0; const buttons = hammerEvent.srcEvent.buttons; if ( typeof buttons === 'number' ) return buttons; return 0; } function onDragEnd( register, slot, args ) { var that = this; register( 'hammer.panend', function ( evt ) { setHammerXY( that.$, evt ); setHammerVxVy.call( that, that.$, evt ); var domEvt = evt.srcEvent; slot( { x: evt.x, y: evt.y, x0: evt.x0, y0: evt.y0, target: evt.target, preventDefault: domEvt.preventDefault.bind( domEvt ), stopPropagation: domEvt.stopImmediatePropagation.bind( domEvt ) } ); delete that._dragX; delete that._dragY; return true; } ); } function onDragStart( register, slot, args ) { var that = this; register( 'hammer.panstart', function ( evt ) { console.log( "START" ); setHammerXY.call( that, that.$, evt ); var domEvt = evt.srcEvent; slot( { x: evt.x, y: evt.y, target: evt.target, preventDefault: domEvt.preventDefault.bind( domEvt ), stopPropagation: domEvt.stopImmediatePropagation.bind( domEvt ) } ); return true; } ); } function onDown( register, slot, args ) { var that = this; register( "touchstart", function ( evt ) { if ( !evt.changedTouches || evt.changedTouches.length < 1 ) return false; var touch = evt.changedTouches[ 0 ]; var rect = that.$.getBoundingClientRect(); try { slot( { x: touch.clientX - rect.left, y: touch.clientY - rect.top, preventDefault: evt.preventDefault.bind( evt ), stopPropagation: evt.stopPropagation.bind( evt ) } ); } catch ( ex ) { console.error( ex ); } return true; } ); register( "mousedown", function ( evt ) { var now = Date.now(); if ( now - that._touchstart < 350 ) { evt.preventDefault(); evt.stopPropagation(); return false; } var rect = that.$.getBoundingClientRect(); try { slot( { x: evt.clientX - rect.left, y: evt.clientY - rect.top, preventDefault: evt.preventDefault.bind( evt ), stopPropagation: evt.stopPropagation.bind( evt ) } ); } catch ( ex ) { console.error( ex ); } that._touchstart = 0; return true; } ); } function onUp( register, slot, args ) { var that = this; register( "touchend", function ( evt ) { if ( !evt.changedTouches || evt.changedTouches.length < 1 ) return false; var touch = evt.changedTouches[ 0 ]; var rect = that.$.getBoundingClientRect(); try { slot( { x: touch.clientX - rect.left, y: touch.clientY - rect.top, preventDefault: evt.preventDefault.bind( evt ), stopPropagation: evt.stopPropagation.bind( evt ) } ); } catch ( ex ) { console.error( ex ); } that._touchstart = Date.now(); return true; } ); register( "mouseup", function ( evt ) { if ( that._touchstart > 0 ) { evt.preventDefault(); evt.stopPropagation(); return false; } var rect = that.$.getBoundingClientRect(); try { slot( { x: evt.clientX - rect.left, y: evt.clientY - rect.top, preventDefault: evt.preventDefault.bind( evt ), stopPropagation: evt.stopPropagation.bind( evt ) } ); } catch ( ex ) { console.error( ex ); } that._touchstart = 0; return true; } ); } function getEventNameForPrefix( text, prefix ) { if ( text.substr( 0, prefix.length ) == prefix ) { return text.substr( prefix.length ); } return null; }