Comparing version 1.0.0-beta.4 to 1.0.0-beta.5
2063
dist/panzoom.js
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | ||
typeof define === 'function' && define.amd ? define(['exports'], factory) : | ||
(factory((global.panzoom = {}))); | ||
}(this, (function (exports) { 'use strict'; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.panzoom = factory()); | ||
}(this, (function () { 'use strict'; | ||
var classCallCheck = function (instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
}; | ||
// Copyright (C) 2010 Google Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
var createClass = function () { | ||
function defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
// See https://github.com/traitsjs/traits.js#readme | ||
// for background on traits and a description of this library | ||
var Trait = (function(){ | ||
// == Ancillary functions == | ||
var SUPPORTS_DEFINEPROP = (function() { | ||
try { | ||
var test = {}; | ||
Object.defineProperty(test, 'x', {get: function() { return 0; } } ); | ||
return test.x === 0; | ||
} catch(e) { | ||
return false; | ||
} | ||
})(); | ||
// IE8 implements Object.defineProperty and Object.getOwnPropertyDescriptor | ||
// only for DOM objects. These methods don't work on plain objects. | ||
// Hence, we need a more elaborate feature-test to see whether the | ||
// browser truly supports these methods: | ||
function supportsGOPD() { | ||
try { | ||
if (Object.getOwnPropertyDescriptor) { | ||
var test = {x:0}; | ||
return !!Object.getOwnPropertyDescriptor(test,'x'); | ||
} | ||
} catch(e) {} | ||
return false; | ||
} function supportsDP() { | ||
try { | ||
if (Object.defineProperty) { | ||
var test = {}; | ||
Object.defineProperty(test,'x',{value:0}); | ||
return test.x === 0; | ||
} | ||
} catch(e) {} | ||
return false; | ||
} | ||
var call = Function.prototype.call; | ||
return function (Constructor, protoProps, staticProps) { | ||
if (protoProps) defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
/** | ||
* An ad hoc version of bind that only binds the 'this' parameter. | ||
*/ | ||
var bindThis = Function.prototype.bind ? | ||
function(fun, self) { return Function.prototype.bind.call(fun, self); } : | ||
function(fun, self) { | ||
function funcBound(var_args) { | ||
return fun.apply(self, arguments); | ||
} | ||
return funcBound; | ||
}; | ||
var hasOwnProperty = bindThis(call, Object.prototype.hasOwnProperty); | ||
var slice = bindThis(call, Array.prototype.slice); | ||
// feature testing such that traits.js runs on both ES3 and ES5 | ||
var forEach = function(arr, fun) { | ||
for (var i = 0, len = arr.length; i < len; i++) { fun(arr[i]); } | ||
}; | ||
}(); | ||
var inherits = function (subClass, superClass) { | ||
if (typeof superClass !== "function" && superClass !== null) { | ||
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); | ||
var freeze = Object.freeze || function(obj) { return obj; }; | ||
var getOwnPropertyNames = Object.getOwnPropertyNames || | ||
function(obj) { | ||
var props = []; | ||
for (var p in obj) { if (hasOwnProperty(obj,p)) { props.push(p); } } | ||
return props; | ||
}; | ||
var getOwnPropertyDescriptor = supportsGOPD() ? | ||
Object.getOwnPropertyDescriptor : | ||
function(obj, name) { | ||
return { | ||
value: obj[name], | ||
enumerable: true, | ||
writable: true, | ||
configurable: true | ||
}; | ||
}; | ||
var defineProperty = supportsDP() ? Object.defineProperty : | ||
function(obj, name, pd) { | ||
obj[name] = pd.value; | ||
}; | ||
var defineProperties = Object.defineProperties || | ||
function(obj, propMap) { | ||
for (var name in propMap) { | ||
if (hasOwnProperty(propMap, name)) { | ||
defineProperty(obj, name, propMap[name]); | ||
} | ||
} | ||
}; | ||
var Object_create = Object.create || | ||
function(proto, propMap) { | ||
var self; | ||
function dummy() {} dummy.prototype = proto || Object.prototype; | ||
self = new dummy(); | ||
if (propMap) { | ||
defineProperties(self, propMap); | ||
} | ||
return self; | ||
}; | ||
var getOwnProperties = Object.getOwnProperties || | ||
function(obj) { | ||
var map = {}; | ||
forEach(getOwnPropertyNames(obj), function (name) { | ||
map[name] = getOwnPropertyDescriptor(obj, name); | ||
}); | ||
return map; | ||
}; | ||
// end of ES3 - ES5 compatibility functions | ||
function makeConflictAccessor(name) { | ||
var accessor = function(var_args) { | ||
throw new Error("Conflicting property: "+name); | ||
}; | ||
freeze(accessor.prototype); | ||
return freeze(accessor); | ||
} | ||
function makeRequiredPropDesc(name) { | ||
return freeze({ | ||
value: undefined, | ||
enumerable: false, | ||
required: true | ||
}); | ||
} | ||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true | ||
function makeConflictingPropDesc(name) { | ||
var conflict = makeConflictAccessor(name); | ||
if (SUPPORTS_DEFINEPROP) { | ||
return freeze({ | ||
get: conflict, | ||
set: conflict, | ||
enumerable: false, | ||
conflict: true | ||
}); | ||
} else { | ||
return freeze({ | ||
value: conflict, | ||
enumerable: false, | ||
conflict: true | ||
}); | ||
} | ||
} | ||
/** | ||
* Are x and y not observably distinguishable? | ||
*/ | ||
function identical(x, y) { | ||
if (x === y) { | ||
// 0 === -0, but they are not identical | ||
return x !== 0 || 1/x === 1/y; | ||
} else { | ||
// NaN !== NaN, but they are identical. | ||
// NaNs are the only non-reflexive value, i.e., if x !== x, | ||
// then x is a NaN. | ||
return x !== x && y !== y; | ||
} | ||
} | ||
// Note: isSameDesc should return true if both | ||
// desc1 and desc2 represent a 'required' property | ||
// (otherwise two composed required properties would be turned into | ||
// a conflict) | ||
function isSameDesc(desc1, desc2) { | ||
// for conflicting properties, don't compare values because | ||
// the conflicting property values are never equal | ||
if (desc1.conflict && desc2.conflict) { | ||
return true; | ||
} else { | ||
return ( desc1.get === desc2.get | ||
&& desc1.set === desc2.set | ||
&& identical(desc1.value, desc2.value) | ||
&& desc1.enumerable === desc2.enumerable | ||
&& desc1.required === desc2.required | ||
&& desc1.conflict === desc2.conflict); | ||
} | ||
} | ||
function freezeAndBind(meth, self) { | ||
return freeze(bindThis(meth, self)); | ||
} | ||
/* makeSet(['foo', ...]) => { foo: true, ...} | ||
* | ||
* makeSet returns an object whose own properties represent a set. | ||
* | ||
* Each string in the names array is added to the set. | ||
* | ||
* To test whether an element is in the set, perform: | ||
* hasOwnProperty(set, element) | ||
*/ | ||
function makeSet(names) { | ||
var set = {}; | ||
forEach(names, function (name) { | ||
set[name] = true; | ||
}); | ||
return freeze(set); | ||
} | ||
// == singleton object to be used as the placeholder for a required | ||
// property == | ||
var required = freeze({ | ||
toString: function() { return '<Trait.required>'; } | ||
}); | ||
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; | ||
}; | ||
var possibleConstructorReturn = function (self, call) { | ||
if (!self) { | ||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); | ||
// == The public API methods == | ||
/** | ||
* var newTrait = trait({ foo:required, ... }) | ||
* | ||
* @param object an object record (in principle an object literal) | ||
* @returns a new trait describing all of the own properties of the object | ||
* (both enumerable and non-enumerable) | ||
* | ||
* As a general rule, 'trait' should be invoked with an object | ||
* literal, since the object merely serves as a record | ||
* descriptor. Both its identity and its prototype chain are | ||
* irrelevant. | ||
* | ||
* Data properties bound to function objects in the argument will be | ||
* flagged as 'method' properties. The prototype of these function | ||
* objects is frozen. | ||
* | ||
* Data properties bound to the 'required' singleton exported by | ||
* this module will be marked as 'required' properties. | ||
* | ||
* The <tt>trait</tt> function is pure if no other code can witness | ||
* the side-effects of freezing the prototypes of the methods. If | ||
* <tt>trait</tt> is invoked with an object literal whose methods | ||
* are represented as in-place anonymous functions, this should | ||
* normally be the case. | ||
*/ | ||
function trait(obj) { | ||
var map = {}; | ||
forEach(getOwnPropertyNames(obj), function (name) { | ||
var pd = getOwnPropertyDescriptor(obj, name); | ||
if (pd.value === required) { | ||
pd = makeRequiredPropDesc(name); | ||
} else if (typeof pd.value === 'function') { | ||
pd.method = true; | ||
pd.enumerable = false; | ||
if ('prototype' in pd.value) { | ||
freeze(pd.value.prototype); | ||
} | ||
} else { | ||
if (pd.get && pd.get.prototype) { freeze(pd.get.prototype); } | ||
if (pd.set && pd.set.prototype) { freeze(pd.set.prototype); } | ||
} | ||
map[name] = pd; | ||
}); | ||
return map; | ||
} | ||
return call && (typeof call === "object" || typeof call === "function") ? call : self; | ||
}; | ||
/** | ||
* var newTrait = compose(trait_1, trait_2, ..., trait_N) | ||
* | ||
* @param trait_i a trait object | ||
* @returns a new trait containing the combined own properties of | ||
* all the trait_i. | ||
* | ||
* If two or more traits have own properties with the same name, the new | ||
* trait will contain a 'conflict' property for that name. 'compose' is | ||
* a commutative and associative operation, and the order of its | ||
* arguments is not significant. | ||
* | ||
* If 'compose' is invoked with < 2 arguments, then: | ||
* compose(trait_1) returns a trait equivalent to trait_1 | ||
* compose() returns an empty trait | ||
*/ | ||
function compose(var_args) { | ||
var traits = slice(arguments, 0); | ||
var newTrait = {}; | ||
var LengthUnit = function LengthUnit(name) { | ||
classCallCheck(this, LengthUnit); | ||
forEach(traits, function (trait) { | ||
forEach(getOwnPropertyNames(trait), function (name) { | ||
var pd = trait[name]; | ||
if (hasOwnProperty(newTrait, name) && | ||
!newTrait[name].required) { | ||
this.name = name; | ||
}; | ||
// a non-required property with the same name was previously | ||
// defined this is not a conflict if pd represents a | ||
// 'required' property itself: | ||
if (pd.required) { | ||
return; // skip this property, the required property is | ||
// now present | ||
} | ||
var PixelUnit = function (_LengthUnit) { | ||
inherits(PixelUnit, _LengthUnit); | ||
if (!isSameDesc(newTrait[name], pd)) { | ||
// a distinct, non-required property with the same name | ||
// was previously defined by another trait => mark as | ||
// conflicting property | ||
newTrait[name] = makeConflictingPropDesc(name); | ||
} // else, | ||
// properties are not in conflict if they refer to the same value | ||
function PixelUnit() { | ||
classCallCheck(this, PixelUnit); | ||
return possibleConstructorReturn(this, (PixelUnit.__proto__ || Object.getPrototypeOf(PixelUnit)).call(this, 'px')); | ||
} else { | ||
newTrait[name] = pd; | ||
} | ||
}); | ||
}); | ||
return freeze(newTrait); | ||
} | ||
createClass(PixelUnit, [{ | ||
key: 'valueOf', | ||
value: function valueOf() { | ||
return this.name; | ||
/* var newTrait = exclude(['name', ...], trait) | ||
* | ||
* @param names a list of strings denoting property names. | ||
* @param trait a trait some properties of which should be excluded. | ||
* @returns a new trait with the same own properties as the original trait, | ||
* except that all property names appearing in the first argument | ||
* are replaced by required property descriptors. | ||
* | ||
* Note: exclude(A, exclude(B,t)) is equivalent to exclude(A U B, t) | ||
*/ | ||
function exclude(names, trait) { | ||
var exclusions = makeSet(names); | ||
var newTrait = {}; | ||
forEach(getOwnPropertyNames(trait), function (name) { | ||
// required properties are not excluded but ignored | ||
if (!hasOwnProperty(exclusions, name) || trait[name].required) { | ||
newTrait[name] = trait[name]; | ||
} else { | ||
// excluded properties are replaced by required properties | ||
newTrait[name] = makeRequiredPropDesc(name); | ||
} | ||
}); | ||
return freeze(newTrait); | ||
} | ||
/** | ||
* var newTrait = override(trait_1, trait_2, ..., trait_N) | ||
* | ||
* @returns a new trait with all of the combined properties of the | ||
* argument traits. In contrast to 'compose', 'override' | ||
* immediately resolves all conflicts resulting from this | ||
* composition by overriding the properties of later | ||
* traits. Trait priority is from left to right. I.e. the | ||
* properties of the leftmost trait are never overridden. | ||
* | ||
* override is associative: | ||
* override(t1,t2,t3) is equivalent to override(t1, override(t2, t3)) or | ||
* to override(override(t1, t2), t3) | ||
* override is not commutative: override(t1,t2) is not equivalent | ||
* to override(t2,t1) | ||
* | ||
* override() returns an empty trait | ||
* override(trait_1) returns a trait equivalent to trait_1 | ||
*/ | ||
function override(var_args) { | ||
var traits = slice(arguments, 0); | ||
var newTrait = {}; | ||
forEach(traits, function (trait) { | ||
forEach(getOwnPropertyNames(trait), function (name) { | ||
var pd = trait[name]; | ||
// add this trait's property to the composite trait only if | ||
// - the trait does not yet have this property | ||
// - or, the trait does have the property, but it's a required property | ||
if (!hasOwnProperty(newTrait, name) || newTrait[name].required) { | ||
newTrait[name] = pd; | ||
} | ||
}); | ||
}); | ||
return freeze(newTrait); | ||
} | ||
/** | ||
* var newTrait = override(dominantTrait, recessiveTrait) | ||
* | ||
* @returns a new trait with all of the properties of dominantTrait | ||
* and all of the properties of recessiveTrait not in dominantTrait | ||
* | ||
* Note: override is associative: | ||
* override(t1, override(t2, t3)) is equivalent to | ||
* override(override(t1, t2), t3) | ||
*/ | ||
/*function override(frontT, backT) { | ||
var newTrait = {}; | ||
// first copy all of backT's properties into newTrait | ||
forEach(getOwnPropertyNames(backT), function (name) { | ||
newTrait[name] = backT[name]; | ||
}); | ||
// now override all these properties with frontT's properties | ||
forEach(getOwnPropertyNames(frontT), function (name) { | ||
var pd = frontT[name]; | ||
// frontT's required property does not override the provided property | ||
if (!(pd.required && hasOwnProperty(newTrait, name))) { | ||
newTrait[name] = pd; | ||
} | ||
}); | ||
return freeze(newTrait); | ||
}*/ | ||
/** | ||
* var newTrait = rename(map, trait) | ||
* | ||
* @param map an object whose own properties serve as a mapping from | ||
old names to new names. | ||
* @param trait a trait object | ||
* @returns a new trait with the same properties as the original trait, | ||
* except that all properties whose name is an own property | ||
* of map will be renamed to map[name], and a 'required' property | ||
* for name will be added instead. | ||
* | ||
* rename({a: 'b'}, t) eqv compose(exclude(['a'],t), | ||
* { a: { required: true }, | ||
* b: t[a] }) | ||
* | ||
* For each renamed property, a required property is generated. If | ||
* the map renames two properties to the same name, a conflict is | ||
* generated. If the map renames a property to an existing | ||
* unrenamed property, a conflict is generated. | ||
* | ||
* Note: rename(A, rename(B, t)) is equivalent to rename(\n -> | ||
* A(B(n)), t) Note: rename({...},exclude([...], t)) is not eqv to | ||
* exclude([...],rename({...}, t)) | ||
*/ | ||
function rename(map, trait) { | ||
var renamedTrait = {}; | ||
forEach(getOwnPropertyNames(trait), function (name) { | ||
// required props are never renamed | ||
if (hasOwnProperty(map, name) && !trait[name].required) { | ||
var alias = map[name]; // alias defined in map | ||
if (hasOwnProperty(renamedTrait, alias) && | ||
!renamedTrait[alias].required) { | ||
// could happen if 2 props are mapped to the same alias | ||
renamedTrait[alias] = makeConflictingPropDesc(alias); | ||
} else { | ||
// add the property under an alias | ||
renamedTrait[alias] = trait[name]; | ||
} | ||
// add a required property under the original name | ||
// but only if a property under the original name does not exist | ||
// such a prop could exist if an earlier prop in the trait was | ||
// previously aliased to this name | ||
if (!hasOwnProperty(renamedTrait, name)) { | ||
renamedTrait[name] = makeRequiredPropDesc(name); | ||
} | ||
} else { // no alias defined | ||
if (hasOwnProperty(renamedTrait, name)) { | ||
// could happen if another prop was previously aliased to name | ||
if (!trait[name].required) { | ||
renamedTrait[name] = makeConflictingPropDesc(name); | ||
} | ||
// else required property overridden by a previously aliased | ||
// property and otherwise ignored | ||
} else { | ||
renamedTrait[name] = trait[name]; | ||
} | ||
} | ||
}); | ||
return freeze(renamedTrait); | ||
} | ||
/** | ||
* var newTrait = resolve({ oldName: 'newName', excludeName: | ||
* undefined, ... }, trait) | ||
* | ||
* This is a convenience function combining renaming and | ||
* exclusion. It can be implemented as <tt>rename(map, | ||
* exclude(exclusions, trait))</tt> where map is the subset of | ||
* mappings from oldName to newName and exclusions is an array of | ||
* all the keys that map to undefined (or another falsy value). | ||
* | ||
* @param resolutions an object whose own properties serve as a | ||
mapping from old names to new names, or to undefined if | ||
the property should be excluded | ||
* @param trait a trait object | ||
* @returns a resolved trait with the same own properties as the | ||
* original trait. | ||
* | ||
* In a resolved trait, all own properties whose name is an own property | ||
* of resolutions will be renamed to resolutions[name] if it is truthy, | ||
* or their value is changed into a required property descriptor if | ||
* resolutions[name] is falsy. | ||
* | ||
* Note, it's important to _first_ exclude, _then_ rename, since exclude | ||
* and rename are not associative, for example: | ||
* rename({a: 'b'}, exclude(['b'], trait({ a:1,b:2 }))) eqv trait({b:1}) | ||
* exclude(['b'], rename({a: 'b'}, trait({ a:1,b:2 }))) eqv | ||
* trait({b:Trait.required}) | ||
* | ||
* writing resolve({a:'b', b: undefined},trait({a:1,b:2})) makes it | ||
* clear that what is meant is to simply drop the old 'b' and rename | ||
* 'a' to 'b' | ||
*/ | ||
function resolve(resolutions, trait) { | ||
var renames = {}; | ||
var exclusions = []; | ||
// preprocess renamed and excluded properties | ||
for (var name in resolutions) { | ||
if (hasOwnProperty(resolutions, name)) { | ||
if (resolutions[name]) { // old name -> new name | ||
renames[name] = resolutions[name]; | ||
} else { // name -> undefined | ||
exclusions.push(name); | ||
} | ||
} | ||
} | ||
}]); | ||
return PixelUnit; | ||
}(LengthUnit); | ||
return rename(renames, exclude(exclusions, trait)); | ||
} | ||
var Translate3d = function () { | ||
function Translate3d() { | ||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { tx: 0, ty: 0, tz: 0 }, | ||
tx = _ref.tx, | ||
ty = _ref.ty, | ||
tz = _ref.tz; | ||
/** | ||
* var obj = create(proto, trait) | ||
* | ||
* @param proto denotes the prototype of the completed object | ||
* @param trait a trait object to be turned into a complete object | ||
* @returns an object with all of the properties described by the trait. | ||
* @throws 'Missing required property' the trait still contains a | ||
* required property. | ||
* @throws 'Remaining conflicting property' if the trait still | ||
* contains a conflicting property. | ||
* | ||
* Trait.create is like Object.create, except that it generates | ||
* high-integrity or final objects. In addition to creating a new object | ||
* from a trait, it also ensures that: | ||
* - an exception is thrown if 'trait' still contains required properties | ||
* - an exception is thrown if 'trait' still contains conflicting | ||
* properties | ||
* - the object is and all of its accessor and method properties are frozen | ||
* - the 'this' pseudovariable in all accessors and methods of | ||
* the object is bound to the composed object. | ||
* | ||
* Use Object.create instead of Trait.create if you want to create | ||
* abstract or malleable objects. Keep in mind that for such objects: | ||
* - no exception is thrown if 'trait' still contains required properties | ||
* (the properties are simply dropped from the composite object) | ||
* - no exception is thrown if 'trait' still contains conflicting | ||
* properties (these properties remain as conflicting | ||
* properties in the composite object) | ||
* - neither the object nor its accessor and method properties are frozen | ||
* - the 'this' pseudovariable in all accessors and methods of | ||
* the object is left unbound. | ||
*/ | ||
function create(proto, trait) { | ||
var self = Object_create(proto); | ||
var properties = {}; | ||
classCallCheck(this, Translate3d); | ||
forEach(getOwnPropertyNames(trait), function (name) { | ||
var pd = trait[name]; | ||
// check for remaining 'required' properties | ||
// Note: it's OK for the prototype to provide the properties | ||
if (pd.required) { | ||
if (!(name in proto)) { | ||
throw new Error('Missing required property: '+name); | ||
} | ||
} else if (pd.conflict) { // check for remaining conflicting properties | ||
throw new Error('Remaining conflicting property: '+name); | ||
} else if ('value' in pd) { // data property | ||
// freeze all function properties and their prototype | ||
if (pd.method) { // the property is meant to be used as a method | ||
// bind 'this' in trait method to the composite object | ||
properties[name] = { | ||
value: freezeAndBind(pd.value, self), | ||
enumerable: pd.enumerable, | ||
configurable: pd.configurable, | ||
writable: pd.writable | ||
}; | ||
} else { | ||
properties[name] = pd; | ||
} | ||
} else { // accessor property | ||
properties[name] = { | ||
get: pd.get ? freezeAndBind(pd.get, self) : undefined, | ||
set: pd.set ? freezeAndBind(pd.set, self) : undefined, | ||
enumerable: pd.enumerable, | ||
configurable: pd.configurable | ||
}; | ||
} | ||
}); | ||
this.tx = parseFloat(tx); | ||
this.ty = parseFloat(ty); | ||
this.tz = parseFloat(tz); | ||
defineProperties(self, properties); | ||
return freeze(self); | ||
} | ||
this.unit = new PixelUnit(); | ||
/** A shorthand for create(Object.prototype, trait({...}), options) */ | ||
function object(record, options) { | ||
return create(Object.prototype, trait(record), options); | ||
} | ||
createClass(Translate3d, [{ | ||
key: 'setUnit', | ||
value: function setUnit(unit) { | ||
this.unit = unit; | ||
/** | ||
* Tests whether two traits are equivalent. T1 is equivalent to T2 iff | ||
* both describe the same set of property names and for all property | ||
* names n, T1[n] is equivalent to T2[n]. Two property descriptors are | ||
* equivalent if they have the same value, accessors and attributes. | ||
* | ||
* @return a boolean indicating whether the two argument traits are | ||
* equivalent. | ||
*/ | ||
function eqv(trait1, trait2) { | ||
var names1 = getOwnPropertyNames(trait1); | ||
var names2 = getOwnPropertyNames(trait2); | ||
var name; | ||
if (names1.length !== names2.length) { | ||
return false; | ||
} | ||
}, { | ||
key: 'toString', | ||
value: function toString() { | ||
return 'translate3d(' + (this.tx + this.unit) + ', ' + (this.ty + this.unit) + ', ' + (this.tz + this.unit) + ');'; | ||
for (var i = 0; i < names1.length; i++) { | ||
name = names1[i]; | ||
if (!trait2[name] || !isSameDesc(trait1[name], trait2[name])) { | ||
return false; | ||
} | ||
} | ||
}]); | ||
return Translate3d; | ||
}(); | ||
return true; | ||
} | ||
// if this code is ran in ES3 without an Object.create function, this | ||
// library will define it on Object: | ||
if (!Object.create) { | ||
Object.create = Object_create; | ||
} | ||
// ES5 does not by default provide Object.getOwnProperties | ||
// if it's not defined, the Traits library defines this utility | ||
// function on Object | ||
if(!Object.getOwnProperties) { | ||
Object.getOwnProperties = getOwnProperties; | ||
} | ||
// expose the public API of this module | ||
function Trait(record) { | ||
// calling Trait as a function creates a new atomic trait | ||
return trait(record); | ||
} | ||
Trait.required = freeze(required); | ||
Trait.compose = freeze(compose); | ||
Trait.resolve = freeze(resolve); | ||
Trait.override = freeze(override); | ||
Trait.create = freeze(create); | ||
Trait.eqv = freeze(eqv); | ||
Trait.object = freeze(object); // not essential, cf. create + trait | ||
return freeze(Trait); | ||
}()); | ||
/** | ||
* Returns a new function that is a composition of supplied functions from right to left. | ||
* @example c = compose(x => x + 1, x => x.val), c({ val: 1 }) -> 2 | ||
* @param {...function} fs | ||
* @returns {function} | ||
*/ | ||
function compose() { | ||
for (var _len = arguments.length, fs = Array(_len), _key = 0; _key < _len; _key++) { | ||
fs[_key] = arguments[_key]; | ||
} | ||
return function (x) { | ||
return reduceRight(function (x, a) { | ||
return a(x); | ||
}, x, fs); | ||
}; | ||
} | ||
function reduceRight(f, init, list) { | ||
return reduce(f, init, list.reverse()); | ||
} | ||
/** | ||
* Returns a new function where some of the arguments are pre defined. | ||
* @param {function} f The function to partially apply arguments to | ||
* @param {number} [arity] Optionally specify how many arguments the function f will take, before being called. Useful for variadic functions | ||
* @returns {function} | ||
*/ | ||
function partial(f, context) { | ||
var arity = f.length; | ||
return function partial() { | ||
for (var _len2 = arguments.length, xs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { | ||
xs[_key2] = arguments[_key2]; | ||
} | ||
if (arity < xs.length) throw new TypeError((f.name || 'anonymous') + ' does not accept ' + xs.length + ' arguments'); | ||
return arity === xs.length ? f.apply(context, xs) : partial.bind.apply(partial, [context].concat(xs)); | ||
}; | ||
} | ||
function reduce(f, init, list) { | ||
var result = init; | ||
each(function (value, key) { | ||
result = f(result, value, key); | ||
}, list); | ||
return result; | ||
} | ||
function filter(f, list) { | ||
var seq = []; | ||
each(function (value, key) { | ||
if (f(value, key)) seq.push(value); | ||
}, list); | ||
return seq; | ||
} | ||
/** | ||
* map | ||
* @param {function} f | ||
* @param {array|object} list | ||
* @returns {array} | ||
*/ | ||
function map$1(f, list) { | ||
var seq = []; | ||
each(function (value, key) { | ||
seq.push(f(value, key)); | ||
}, list); | ||
return seq; | ||
} | ||
/** | ||
* | ||
* @param {function} f | ||
* @param {array|object} list | ||
* @returns {array} | ||
*/ | ||
function each(f, list) { | ||
for (var key in list) { | ||
f(list[key], key); | ||
} | ||
} | ||
var mapReduce = compose(partial(reduce)(function (a, b) { | ||
return a.indexOf(b) > -1 ? a : a.concat([b]); | ||
}, []), partial(map$1)(function (x) { | ||
return x[0]; | ||
})); | ||
function Observer() { | ||
@@ -113,7 +752,9 @@ var listeners = []; | ||
on: function on(eventName, f, reject) { | ||
if (!(f instanceof Function)) throw new TypeError('event handler is not a function'); | ||
if (!(reject instanceof Function)) reject = f; | ||
listeners.push([eventName, f, reject]); | ||
}, | ||
fire: function fire(eventName) { | ||
var _this = this; | ||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
@@ -123,6 +764,7 @@ args[_key - 1] = arguments[_key]; | ||
listeners.forEach(function (listener) { | ||
each(function (listener) { | ||
if (listener[0] === eventName) { | ||
try { | ||
listener[1].apply(listener[1], args); | ||
listener[1].apply(_this, args); | ||
// listener[1](...args) | ||
} catch (error) { | ||
@@ -132,12 +774,24 @@ listener[2](error); | ||
} | ||
}); | ||
}, listeners); | ||
}, | ||
/** | ||
* | ||
* @param {string} eventName The event type you want to remove | ||
* @param {function} f The function listening for the event type to | ||
* distinguish from other listeners for the same event type | ||
*/ | ||
off: function off(eventName, f) { | ||
listeners = listeners.filter(function (listener) { | ||
var notRemoved = true; | ||
listeners = filter(function (listener) { | ||
if (listener[0] === eventName) { | ||
// if f is not defined then remove all listeners with the eventName | ||
if (!f) return false;else return listener[1] === f ? false : true; | ||
if (!f) return false;else return listener[1] === f ? notRemoved = false : true; | ||
} | ||
return true; | ||
}); | ||
}, listeners); | ||
return !notRemoved; | ||
}, | ||
@@ -152,3 +806,3 @@ once: function once(eventName, f, errorHandler) { | ||
promise: function promise(eventName) { | ||
var _this = this; | ||
var _this2 = this; | ||
@@ -158,3 +812,3 @@ var self = this; | ||
return new Promise(function (resolve, reject) { | ||
_this.on(eventName, function wrapper() { | ||
_this2.on(eventName, function wrapper() { | ||
resolve.apply(undefined, arguments); | ||
@@ -172,2 +826,7 @@ self.off(eventName, wrapper); | ||
listeners = null; | ||
}, | ||
get currentListenerTypes() { | ||
return mapReduce(listeners); | ||
} | ||
@@ -177,652 +836,882 @@ }; | ||
var Point = function Point(_ref) { | ||
var _ref$x = _ref.x, | ||
x = _ref$x === undefined ? 0 : _ref$x, | ||
_ref$y = _ref.y, | ||
y = _ref$y === undefined ? 0 : _ref$y; | ||
classCallCheck(this, Point); | ||
var eventTypes = ['wheel', 'mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend']; | ||
this.x = parseFloat(x); | ||
this.y = parseFloat(y); | ||
var classCallCheck = function (instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
}; | ||
var Swipe = function () { | ||
function Swipe(options) { | ||
classCallCheck(this, Swipe); | ||
var createClass = function () { | ||
function defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
} | ||
} | ||
this.el = null; | ||
this.lastTouches = null; | ||
this.detecting = false; | ||
return function (Constructor, protoProps, staticProps) { | ||
if (protoProps) defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
}; | ||
}(); | ||
var defineProperty = function (obj, key, value) { | ||
if (key in obj) { | ||
Object.defineProperty(obj, key, { | ||
value: value, | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} else { | ||
obj[key] = value; | ||
} | ||
createClass(Swipe, [{ | ||
key: 'listen', | ||
value: function listen(action) { | ||
// console.log('Swipe::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function'); | ||
return obj; | ||
}; | ||
this.action = Swipe.action(this, action); | ||
var Point = function () { | ||
function Point(_ref) { | ||
var _ref$x = _ref.x, | ||
x = _ref$x === undefined ? 0 : _ref$x, | ||
_ref$y = _ref.y, | ||
y = _ref$y === undefined ? 0 : _ref$y; | ||
classCallCheck(this, Point); | ||
this.el.addEventListener('mousedown', this.action); | ||
this.el.addEventListener('touchstart', this.action); | ||
} | ||
}, { | ||
key: 'unlisten', | ||
value: function unlisten() { | ||
// console.log('Swipe::unlisten') | ||
this.el.removeEventListener('mousedown', this.action); | ||
this.el.removeEventListener('touchstart', this.action); | ||
} | ||
}, { | ||
key: 'setElement', | ||
value: function setElement(el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el; | ||
} | ||
}, { | ||
key: 'destroy', | ||
value: function destroy() { | ||
this.el = null; | ||
this.action = null; | ||
this.lastTouches = null; | ||
} | ||
}], [{ | ||
key: 'action', | ||
value: function action(swipe, _action) { | ||
var _this = this; | ||
this.x = parseFloat(x); | ||
this.y = parseFloat(y); | ||
} | ||
return function (event) { | ||
swipe.unlisten(); | ||
/** | ||
* Substract point from this point | ||
* @param {Point} point | ||
* @returns {Point} | ||
*/ | ||
var startEvent = normalizeEvent(event); | ||
swipe.lastTouches = startEvent; | ||
// console.log(startEvent) | ||
_this.detecting = true; | ||
createClass(Point, [{ | ||
key: "delta", | ||
value: function delta(point) { | ||
return new Point({ | ||
x: this.x - point.x, | ||
y: this.y - point.y | ||
}); | ||
} | ||
swipe.el.addEventListener(startEvent.type.move, moveHandler); | ||
swipe.el.addEventListener(startEvent.type.end, endHandler); // removing event listeners from DOM via this | ||
/** | ||
* Calculate the distance bewteen 2 points | ||
* @param {Point} point | ||
* @returns {number} | ||
*/ | ||
function moveHandler(event) { | ||
var currentEvent = normalizeEvent(event); | ||
// console.log('getting moves!', currentEvent) | ||
}, { | ||
key: "distance", | ||
value: function distance(point) { | ||
var delta = this.delta(point); | ||
return Math.sqrt(Math.pow(delta.x, 2) + Math.pow(delta.y, 2)); | ||
} | ||
}]); | ||
return Point; | ||
}(); | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
var GestureEvent = function () { | ||
function GestureEvent(nativeEvent) { | ||
classCallCheck(this, GestureEvent); | ||
var distance = Math.sqrt( // TODO: abstract this somewhere | ||
Math.pow(currentEvent.touches[0].x - swipe.lastTouches.touches[0].x, 2) + Math.pow(currentEvent.touches[0].y - swipe.lastTouches.touches[0].y, 2)); | ||
var event = GestureEvent.normalizeEvent(nativeEvent); | ||
this.touches = event.touches; | ||
this.page = event.page; | ||
this.type = event.type; | ||
this.deltaY = event.deltaY; | ||
this.target = event.target; | ||
this.preventDefault = event.preventDefault; | ||
} | ||
// console.log(distance) | ||
createClass(GestureEvent, [{ | ||
key: 'getDirection', | ||
value: function getDirection(event) { | ||
// debugger | ||
// https://stackoverflow.com/questions/2264072/detect-a-finger-swipe-through-javascript-on-the-iphone-and-android | ||
var delta = this.touches[0].delta(event.touches[0]); | ||
if (distance > 100) { | ||
// TODO: make 100 a relative value and consider zoom | ||
var direction = diff(swipe.lastTouches, currentEvent); | ||
// console.log(direction) | ||
// TODO: return an enum instead | ||
// console.log(delta.x, delta.y) | ||
if (Math.abs(delta.x) > Math.abs(delta.y)) { | ||
if (delta.x > 0) return 'right';else return 'left'; | ||
} else { | ||
if (delta.y > 0) return 'down';else return 'up'; | ||
} | ||
} | ||
// tell subscribers | ||
_action('swipe', direction); | ||
/** | ||
* Detect mouse or touch move and end event names. | ||
* getEventTypeNames will return an | ||
* object, where move is touchmove/mousemove and | ||
* end is touchend/mouseup. | ||
* @returns {object} { move: String, end: String } | ||
*/ | ||
// tell event listeners | ||
var swipeEvent = new CustomEvent('swipe', { detail: direction }); | ||
if (!swipe.el.dispatchEvent(swipeEvent)) { | ||
console.log('Swipe::action - event was cancelled'); | ||
} | ||
}, { | ||
key: 'getEventTypeNames', | ||
value: function getEventTypeNames() { | ||
return this.type === 'touchstart' ? { move: 'touchmove', end: 'touchend' } : { move: 'mousemove', end: 'mouseup' }; | ||
} | ||
}], [{ | ||
key: 'normalizeEvent', | ||
value: function normalizeEvent(nativeEvent) { | ||
var event = { | ||
touches: nativeEvent.touches ? map(function (t) { | ||
return new Point({ x: t.clientX, y: t.clientY }); | ||
}, nativeEvent.touches) : [new Point({ x: nativeEvent.clientX, y: nativeEvent.clientY })], | ||
// debugger | ||
endHandler(); // removing event listeners from DOM via this (before touchend/mouseup) | ||
} | ||
page: nativeEvent.touches ? map(function (t) { | ||
return new Point({ x: t.pageX, y: t.pageY }); | ||
}, nativeEvent.touches) : [new Point({ x: nativeEvent.pageX, y: nativeEvent.pageY })], | ||
type: nativeEvent.type, | ||
deltaY: nativeEvent.deltaY, | ||
target: nativeEvent.target, | ||
preventDefault: function preventDefault() { | ||
nativeEvent.preventDefault(); | ||
}, | ||
stopPropagation: function stopPropagation() { | ||
nativeEvent.stopPropagation(); | ||
} | ||
}; | ||
function endHandler() { | ||
swipe.el.removeEventListener(startEvent.type.move, moveHandler); | ||
swipe.el.removeEventListener(startEvent.type.end, endHandler); | ||
swipe.detecting = false; | ||
swipe.listen(_action); | ||
} | ||
}; | ||
return event; | ||
} | ||
}, { | ||
key: 'addEvent', | ||
value: function addEvent(el, type, listener, options) { | ||
el.addEventListener(type, listener, options || true); | ||
} | ||
}, { | ||
key: 'removeEvent', | ||
value: function removeEvent(el, type, listener) { | ||
el.removeEventListener(type, listener, true); | ||
} | ||
}]); | ||
return Swipe; | ||
return GestureEvent; | ||
}(); | ||
function normalizeEvent(ev) { | ||
var event = {}; | ||
function percentToPixel(percentage) { | ||
var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'width'; | ||
// console.log(ev) | ||
var box = this.el.getBoundingClientRect(); | ||
return box[side] * percentage * .01; | ||
} | ||
event.touches = [ev.touches ? new Point({ x: ev.touches[0].pageX, y: ev.touches[0].pageY }) : new Point({ x: ev.pageX, y: ev.pageY })]; | ||
event.type = ev.type === 'touchstart' // TODO: use a proper enum | ||
? { move: 'touchmove', end: 'touchend' } : { move: 'mousemove', end: 'mouseup' }; | ||
event.timeStamp = Date.now(); | ||
function percentToPixelMatrix() { | ||
var box = this.el.getBoundingClientRect(); | ||
return { | ||
tx: box.width * (this.tx * .01), | ||
ty: box.height * (this.ty * .01), | ||
tz: this.tz | ||
}; | ||
} | ||
return event; | ||
function remToPixel(rem) { | ||
var fontSize = window.getComputedStyle(documentElement).fontSize; | ||
return rem * fontSize; | ||
} | ||
// TODO: might also be useful in Pan.js | ||
function diff(event1, event2) { | ||
// https://stackoverflow.com/questions/2264072/detect-a-finger-swipe-through-javascript-on-the-iphone-and-android | ||
var deltaX = event1.touches[0].x - event2.touches[0].x; // TODO: make addition and substraction easier for Point | ||
var deltaY = event1.touches[0].y - event2.touches[0].y; | ||
function emToPixel(em) { | ||
var fontSize = window.getComputedStyle(this.el).fontSize; | ||
return em * fontSize; | ||
} | ||
// TODO: return an enum instead | ||
// console.log(deltaX, deltaY) | ||
if (Math.abs(deltaX) > Math.abs(deltaY)) { | ||
if (deltaX > 0) return 'left';else return 'right'; | ||
} else { | ||
if (deltaY > 0) return 'up';else return 'down'; | ||
} | ||
function cmToPixel(cm) { | ||
return cm * 96 / 2.54; | ||
} | ||
var Pinch = function () { | ||
function Pinch(options) { | ||
classCallCheck(this, Pinch); | ||
function mmToPixel(mm) { | ||
return cmToPixel(mm) / 10; | ||
} | ||
this.threshold = options.threshold; | ||
this.el = null; | ||
this.lastTouches = null; | ||
this.lastDistance = null; | ||
this.detecting = false; | ||
} | ||
function getUnit(value) { | ||
return String(value).indexOf('%') > -1 ? '%' : 'px'; | ||
} | ||
createClass(Pinch, [{ | ||
key: 'listen', | ||
value: function listen(action) { | ||
// console.log('Pinch::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function'); | ||
var units = /*#__PURE__*/Object.freeze({ | ||
percentToPixel: percentToPixel, | ||
percentToPixelMatrix: percentToPixelMatrix, | ||
remToPixel: remToPixel, | ||
emToPixel: emToPixel, | ||
cmToPixel: cmToPixel, | ||
mmToPixel: mmToPixel, | ||
getUnit: getUnit | ||
}); | ||
this.action = Pinch.action(this, action); | ||
this.el.addEventListener('touchstart', this.action); | ||
/* detect passive option for event listeners */ | ||
var supportsPassiveOption = false; | ||
try { | ||
var opts = Object.defineProperty({}, 'passive', { | ||
get: function get$$1() { | ||
supportsPassiveOption = true; | ||
} | ||
}, { | ||
key: 'unlisten', | ||
value: function unlisten() { | ||
// console.log('Pinch::unlisten') | ||
this.el.removeEventListener('touchstart', this.action); | ||
} | ||
}, { | ||
key: 'setElement', | ||
value: function setElement(el) { | ||
this.el = el; | ||
} | ||
}, { | ||
key: 'destroy', | ||
value: function destroy() { | ||
this.el = null; | ||
this.action = null; | ||
this.lastTouches = null; | ||
} | ||
}], [{ | ||
key: 'action', | ||
value: function action(pinch, _action) { | ||
var _this = this; | ||
}); | ||
window.addEventListener('test', null, opts); | ||
} catch (e) {} | ||
/* end detect */ | ||
// console.log('supportsPassiveOption', supportsPassiveOption) | ||
return function (event) { | ||
event.preventDefault(); | ||
var matchPassive = /\.passive$/; | ||
function isPassive(eventName) { | ||
return matchPassive.test(eventName); | ||
} | ||
pinch.unlisten(); | ||
function isValidEventType(eventName) { | ||
return eventTypes.indexOf(eventName.replace(matchPassive, '')) > -1; | ||
} | ||
var startEvent = normalizeEvent$1(event); | ||
// console.log(startEvent) | ||
if (startEvent.touches.length < 2) { | ||
endHandler(); | ||
return; | ||
} | ||
pinch.lastTouches = startEvent; | ||
/** | ||
* Returns the correct eventName and options object. | ||
* @param {string} eventName Removes '.passive' from the eventName if passive is not supported | ||
* @return {object} passive options if supported or true for options to use capture. | ||
*/ | ||
function normalisePassive(eventName) { | ||
return supportsPassiveOption && isPassive(eventName) ? { | ||
realEventName: eventName.replace(matchPassive, ''), | ||
options: { | ||
capture: true, | ||
passive: true | ||
} | ||
} : { | ||
realEventName: eventName.replace(matchPassive, ''), | ||
options: true | ||
}; | ||
} | ||
_this.detecting = true; | ||
function NativeEvents() { | ||
var _Trait; | ||
pinch.el.addEventListener(startEvent.type.move, moveHandler); | ||
pinch.el.addEventListener(startEvent.type.end, endHandler); // removing event listeners from DOM via this | ||
var nativeListeners = new Map(); | ||
function moveHandler(event) { | ||
event.preventDefault(); // prevent native zoom TODO: perhaps we should not make that decision... | ||
var currentEvent = normalizeEvent$1(event); | ||
// console.log('getting moves!', currentEvent) | ||
return Trait.compose(Trait.resolve({ | ||
on: 'observerOn', | ||
off: 'observerOff', | ||
destroy: 'observerDestroy' | ||
}, Trait(Observer())), Trait((_Trait = { | ||
el: Trait.required, | ||
currentListenerTypes: Trait.required, | ||
on: Trait.required, | ||
off: Trait.required, | ||
fire: Trait.required, | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
eventNotifier: function eventNotifier(event) { | ||
this.fire(event.type, new GestureEvent(event)); | ||
}, | ||
eventNotifierPassive: function eventNotifierPassive(event) { | ||
this.fire(event.type + '.passive', new GestureEvent(event)); | ||
} | ||
}, defineProperty(_Trait, 'on', function on(eventName, f) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
// movement (translate) | ||
var distanceFromFirstTouch = Math.sqrt( // TODO: abstract this somewhere | ||
Math.pow(currentEvent.touches[0].x - pinch.lastTouches.touches[0].x, 2) + Math.pow(currentEvent.touches[0].y - pinch.lastTouches.touches[0].y, 2)); | ||
// distance between two first fingers | ||
var distanceBetweenTwoFingers = Math.sqrt( // TODO: abstract this somewhere | ||
Math.pow(currentEvent.touches[0].x - pinch.lastTouches.touches[1].x, 2) + Math.pow(currentEvent.touches[0].y - pinch.lastTouches.touches[1].y, 2)); | ||
this.observerOn(eventName, f, options.reject); | ||
var pinchOutwards = pinch.lastDistance && distanceBetweenTwoFingers > pinch.lastDistance ? true : false; | ||
// console.log(pinchOutwards ? 'zoom in' : 'zoom out') | ||
if (!isValidEventType(eventName)) return; | ||
pinch.lastDistance = distanceBetweenTwoFingers; | ||
var realEventName = void 0; | ||
// console.log('distance', distanceFromFirstTouch, distanceBetweenTwoFingers) | ||
var _normalisePassive = normalisePassive(eventName); | ||
var scale = distanceFromFirstTouch / distanceBetweenTwoFingers; | ||
// console.log('scale', scale) | ||
options = _normalisePassive.options; | ||
realEventName = _normalisePassive.realEventName; | ||
if (scale > pinch.threshold) { | ||
// Focus formular ported from svg.panzoom.js - ask Ulrich why it's like that | ||
var currentFocus = new Point({ | ||
x: currentEvent.touches[0].x + .5 * (currentEvent.touches[1].x - currentEvent.touches[0].x), | ||
y: currentEvent.touches[0].y + .5 * (currentEvent.touches[1].y - currentEvent.touches[0].y) | ||
}); | ||
var lastFocus = new Point({ | ||
x: pinch.lastTouches.touches[0].x + 0.5 * (pinch.lastTouches.touches[1].x - pinch.lastTouches.touches[0].x), | ||
y: pinch.lastTouches.touches[0].y + 0.5 * (pinch.lastTouches.touches[1].y - pinch.lastTouches.touches[0].y) | ||
}); | ||
var eventNotifier = options.passive ? this.eventNotifierPassive : this.eventNotifier; | ||
// console.log(scale) | ||
var pinchEventData = { | ||
focus: currentFocus, | ||
scale: pinchOutwards ? scale : -scale, | ||
focusAfterScale: new Point({ x: -lastFocus.x, y: -lastFocus.y }) | ||
var counter = nativeListeners.get(eventName); | ||
if (counter) { | ||
nativeListeners.set(eventName, counter + 1); | ||
} else { | ||
GestureEvent.addEvent(this.el, realEventName, eventNotifier, options); | ||
nativeListeners.set(eventName, 1); | ||
} | ||
}), defineProperty(_Trait, 'off', function off(eventName, f) { | ||
if (!this.observerOff(eventName, f)) return; | ||
// console.dir(pinchEventData) | ||
if (!isValidEventType(eventName)) return; | ||
// tell subscribers | ||
};_action('pinch', pinchEventData); | ||
var _normalisePassive2 = normalisePassive(eventName), | ||
options = _normalisePassive2.options, | ||
realEventName = _normalisePassive2.realEventName; | ||
// tell event listeners | ||
var pinchEvent = new CustomEvent('pinch', { detail: pinchEventData }); | ||
if (!pinch.el.dispatchEvent(pinchEvent)) { | ||
endHandler(); | ||
console.log('Pinch::action - event was cancelled'); | ||
} | ||
} | ||
} | ||
function endHandler() { | ||
pinch.el.removeEventListener(startEvent.type.move, moveHandler); | ||
pinch.el.removeEventListener(startEvent.type.end, endHandler); | ||
pinch.detecting = false; | ||
pinch.listen(_action); | ||
} | ||
}; | ||
var counter = void 0; | ||
if (counter = nativeListeners.get(eventName)) { | ||
nativeListeners.set(eventName, counter - 1); | ||
if (counter === 1) GestureEvent.removeEvent(this.el, realEventName, this.eventNotifier, options); | ||
} else { | ||
GestureEvent.removeEvent(this.el, realEventName, this.eventNotifier, options); | ||
} | ||
}]); | ||
return Pinch; | ||
}(); | ||
}), defineProperty(_Trait, 'destroy', function destroy() { | ||
this.removeNativeEventHandlers(); | ||
nativeListeners = null; | ||
this.observerDestroy(); | ||
}), defineProperty(_Trait, 'addNativeEventHandlers', function addNativeEventHandlers() { | ||
var _this = this; | ||
function normalizeEvent$1(ev) { | ||
var event = {}; | ||
each(function (eventName) { | ||
if (isValidEventType(eventName) && !nativeListeners.has(eventName)) { | ||
var _normalisePassive3 = normalisePassive(eventName), | ||
_normalisePassive3$op = _normalisePassive3.options, | ||
options = _normalisePassive3$op === undefined ? {} : _normalisePassive3$op, | ||
realEventName = _normalisePassive3.realEventName; | ||
// console.log(ev) | ||
var eventNotifier = options.passive ? _this.eventNotifierPassive : _this.eventNotifier; | ||
// debugger | ||
GestureEvent.addEvent(_this.el, realEventName, eventNotifier, options); | ||
nativeListeners.set(eventName, 1); | ||
} | ||
}, this.currentListenerTypes); | ||
}), defineProperty(_Trait, 'removeNativeEventHandlers', function removeNativeEventHandlers() { | ||
var _this2 = this; | ||
event.touches = Array.prototype.map.call(ev.touches, function (t) { | ||
return new Point({ x: t.pageX, y: t.pageY }); | ||
}); | ||
event.type = { move: 'touchmove', end: 'touchend' // TODO: use a proper enum | ||
};event.timeStamp = Date.now(); | ||
each(function (eventName) { | ||
if (isValidEventType(eventName) && nativeListeners.has(eventName)) { | ||
var _normalisePassive4 = normalisePassive(eventName), | ||
options = _normalisePassive4.options, | ||
realEventName = _normalisePassive4.realEventName; | ||
return event; | ||
GestureEvent.removeEvent(_this2.el, realEventName, _this2.eventNotifier, options); | ||
nativeListeners.delete(eventName); | ||
} | ||
}, this.currentListenerTypes); | ||
}), _Trait))); | ||
} | ||
var Pan = function () { | ||
function Pan(options) { | ||
classCallCheck(this, Pan); | ||
var FLOATING = '(\\-?[\\d\\.e]+)'; | ||
var COMMA_SPACE = '\\,?\\s*'; | ||
var R_MATRIX = new RegExp('^matrix\\(' + FLOATING + COMMA_SPACE + FLOATING + COMMA_SPACE + FLOATING + COMMA_SPACE + FLOATING + COMMA_SPACE + FLOATING + COMMA_SPACE + FLOATING + '\\)$'); | ||
var Translate3d = function () { | ||
/** | ||
* @param {number} tx | ||
* @param {number} tx | ||
* @param {number} tz Is a <length> representing the z | ||
* component of the translating vector. It can't be a | ||
* <percentage> value; in that case the property | ||
* containing the transform is considered invalid. | ||
*/ | ||
function Translate3d(tx, ty, tz) { | ||
classCallCheck(this, Translate3d); | ||
Object.assign(this, units); | ||
this.tx = parseFloat(tx || 0); | ||
this.ty = parseFloat(ty || 0); | ||
this.tz = parseFloat(tz || 1); | ||
this.unit = this.getUnit(tx); | ||
this.el = null; | ||
this.lastTouches = null; | ||
this.detecting = false; | ||
} | ||
createClass(Pan, [{ | ||
key: 'listen', | ||
value: function listen(action) { | ||
// console.log('Pan::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function'); | ||
this.action = Pan.action(this, action); | ||
this.el.addEventListener('mousedown', this.action); | ||
this.el.addEventListener('touchstart', this.action); | ||
createClass(Translate3d, [{ | ||
key: 'setUnit', | ||
value: function setUnit(unit) { | ||
this.unit = this.getUnit(unit); | ||
} | ||
}, { | ||
key: 'unlisten', | ||
value: function unlisten() { | ||
// console.log('Pan::unlisten') | ||
this.el.removeEventListener('mousedown', this.action); | ||
this.el.removeEventListener('touchstart', this.action); | ||
} | ||
}, { | ||
key: 'setElement', | ||
value: function setElement(el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el; | ||
} | ||
}, { | ||
key: 'destroy', | ||
value: function destroy() { | ||
this.el = null; | ||
this.action = null; | ||
this.lastTouches = null; | ||
key: 'toString', | ||
value: function toString() { | ||
return Translate3d.getMatrixString(this.toObject()); | ||
} | ||
}, { | ||
key: 'toObject', | ||
value: function toObject() { | ||
var matrix = { | ||
tx: this.tx, | ||
ty: this.ty, | ||
tz: this.tz | ||
}; | ||
return this.unit === '%' ? this.percentToPixelMatrix() : matrix; | ||
} | ||
}], [{ | ||
key: 'action', | ||
value: function action(pan, _action) { | ||
var _this = this; | ||
key: 'getMatrixString', | ||
value: function getMatrixString(transform) { | ||
return 'matrix(' + transform.tz + ', 0, 0, ' + transform.tz + ', ' + transform.tx + ', ' + transform.ty + ')'; | ||
} | ||
}, { | ||
key: 'parse', | ||
value: function parse(cssTransform) { | ||
var matrix = R_MATRIX.exec(cssTransform); | ||
return matrix ? new Translate3d(matrix[5], matrix[6], matrix[1]) : null; | ||
} | ||
}]); | ||
return Translate3d; | ||
}(); | ||
return function (event) { | ||
event.preventDefault(); | ||
var lastTouches = null; | ||
var lastDistance = null; | ||
var eventNames = null; | ||
pan.unlisten(); | ||
var Pinch = { | ||
// required custom properties from option object | ||
options: { | ||
pinchThreshold: .2, | ||
preventDefault: true | ||
}, | ||
var startEvent = normalizeEvent$2(event); | ||
pan.lastTouches = startEvent; | ||
// life cycle handlers | ||
listen: function listen() { | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler }); | ||
console.log('Pinch::listen'); | ||
}, | ||
unlisten: function unlisten() { | ||
this.off('touchstart.passive', this.startHandler, { reject: errorHandler }); | ||
console.log('Pinch::unlisten'); | ||
}, | ||
// console.log(startEvent) | ||
_this.detecting = true; | ||
pan.el.addEventListener(startEvent.type.move, moveHandler); | ||
pan.el.addEventListener(startEvent.type.end, endHandler); // removing event listeners from DOM via this | ||
// custom methods | ||
startHandler: function startHandler(event) { | ||
console.log('Pinch::moveHandler', event, this); | ||
function moveHandler(event) { | ||
var currentEvent = normalizeEvent$2(event); | ||
// console.log('getting moves!', currentEvent) | ||
this.unlisten(); | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
if (event.touches.length < 2) { | ||
this.endHandler(); | ||
return; | ||
} | ||
// TODO: abstract this somewhere | ||
var delta = new Point({ | ||
x: currentEvent.touches[0].x - pan.lastTouches.touches[0].x, | ||
y: currentEvent.touches[0].y - pan.lastTouches.touches[0].y | ||
}); | ||
// console.log(delta) | ||
lastTouches = event; | ||
eventNames = event.getEventTypeNames(); | ||
// tell subscribers | ||
_action('pan', delta); | ||
this.on(preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler); | ||
this.on(eventNames.end, this.endHandler); | ||
}, | ||
moveHandler: function moveHandler(event) { | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
// tell event listeners | ||
var panEvent = new CustomEvent('pan', { detail: delta }); | ||
if (!pan.el.dispatchEvent(panEvent)) { | ||
endHandler(); | ||
console.log('Pan::action - event was cancelled'); | ||
} | ||
} | ||
// movement (translate) | ||
var distanceFromFirstTouch = event.touches[0].distance(lastTouches.touches[0]); | ||
function endHandler() { | ||
pan.el.removeEventListener(startEvent.type.move, moveHandler); | ||
pan.el.removeEventListener(startEvent.type.end, endHandler); | ||
pan.detecting = false; | ||
pan.listen(_action); | ||
} | ||
}; | ||
} | ||
}]); | ||
return Pan; | ||
}(); | ||
// distance between two first fingers | ||
var distanceBetweenTwoFingers = event.touches[0].distance(lastTouches.touches[1]); | ||
function normalizeEvent$2(ev) { | ||
var event = {}; | ||
var pinchOutwards = lastDistance && distanceBetweenTwoFingers > lastDistance ? true : false; | ||
// console.log(pinchOutwards ? 'zoom in' : 'zoom out') | ||
// console.log(ev) | ||
lastDistance = distanceBetweenTwoFingers; | ||
event.touches = [ev.touches ? new Point({ x: ev.touches[0].pageX, y: ev.touches[0].pageY }) : new Point({ x: ev.pageX, y: ev.pageY })]; | ||
event.type = ev.type === 'touchstart' // TODO: use a proper enum | ||
? { move: 'touchmove', end: 'touchend' } : { move: 'mousemove', end: 'mouseup' }; | ||
event.timeStamp = Date.now(); | ||
var scale = distanceFromFirstTouch / distanceBetweenTwoFingers; | ||
// console.log('scale', scale) | ||
return event; | ||
if (scale > this.options.pinchThreshold) { | ||
// Focus formular ported from svg.panzoom.js - ask Ulrich why it's like that | ||
var currentFocus = new Point({ | ||
x: event.touches[0].x + .5 * (event.touches[1].x - event.touches[0].x), | ||
y: event.touches[0].y + .5 * (event.touches[1].y - event.touches[0].y) | ||
}); | ||
var lastFocus = new Point({ | ||
x: lastTouches.touches[0].x + 0.5 * (lastTouches.touches[1].x - lastTouches.touches[0].x), | ||
y: lastTouches.touches[0].y + 0.5 * (lastTouches.touches[1].y - lastTouches.touches[0].y) | ||
}); | ||
// console.log(scale) | ||
event.point = currentFocus; | ||
event.scale = pinchOutwards ? scale : -scale, event.focusAfterScale = new Point({ x: -lastFocus.x, y: -lastFocus.y }); | ||
this.fire('pinch', event); | ||
} | ||
}, | ||
endHandler: function endHandler() { | ||
this.off(preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler); | ||
this.off(eventNames.end, this.endHandler); | ||
this.listen(); | ||
} | ||
}; | ||
function errorHandler(error) { | ||
console.error(error, 'error happen in listener'); | ||
} | ||
var Wheel = function () { | ||
function Wheel(options) { | ||
classCallCheck(this, Wheel); | ||
var Wheel = { | ||
options: { | ||
preventDefault: false | ||
}, | ||
this.el = null; | ||
this.lastTouches = null; | ||
this.detecting = false; | ||
this.zoomFactor = options.zoomFactor; | ||
// life cycle handlers | ||
listen: function listen() { | ||
this.on(this.options.preventDefault ? 'wheel' : 'wheel.passive', this.moveHandler); | ||
console.log('Wheel::listen'); | ||
}, | ||
unlisten: function unlisten() { | ||
console.log('Wheel::unlisten'); | ||
this.off(this.options.preventDefault ? 'wheel' : 'wheel.passive', this.moveHandler); | ||
}, | ||
moveHandler: function moveHandler(event) { | ||
// touchpads can give event.deltaY == 0, which is something we never want to handle | ||
if (event.deltaY === 0) return; | ||
event.point = event.touches[0]; | ||
this.fire('wheelEvent', event); | ||
} | ||
}; | ||
createClass(Wheel, [{ | ||
key: 'listen', | ||
value: function listen(action) { | ||
// console.log('Wheel::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function'); | ||
var translate3d = null; | ||
this.action = Wheel.action(this, action); | ||
var Zoom = { | ||
/** | ||
* @property gestures | ||
* Supplied gestures can be an object or array | ||
* but note that gestures are changed into | ||
* observable gestures and the gestures property | ||
* will always be an array after initialization. | ||
*/ | ||
gestures: [Pinch, Wheel], | ||
this.el.addEventListener('wheel', this.action, true); | ||
} | ||
}, { | ||
key: 'unlisten', | ||
value: function unlisten() { | ||
// console.log('Wheel::unlisten') | ||
this.el.removeEventListener('wheel', this.action, true); | ||
} | ||
}, { | ||
key: 'setElement', | ||
value: function setElement(el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el; | ||
} | ||
}, { | ||
key: 'destroy', | ||
value: function destroy() { | ||
this.el = null; | ||
this.action = null; | ||
this.lastTouches = null; | ||
} | ||
}], [{ | ||
key: 'action', | ||
value: function action(wheel, _action) { | ||
var _this = this; | ||
// default options | ||
options: { | ||
zoomFactor: 0.03, | ||
domEvents: false, | ||
preventDefault: true | ||
}, | ||
return function (event) { | ||
// event.preventDefault() | ||
// event.stopImmediatePropagation() | ||
// event.stopPropagation() | ||
// event.returnValue = false | ||
// life cycle handlers | ||
listen: function listen() { | ||
console.log('zoom::listen'); | ||
// touchpads can give event.deltaY == 0, which is something we never want to handle | ||
if (event.deltaY === 0) return; | ||
translate3d = Translate3d.parse(this.el.style.transform); | ||
if (!translate3d) translate3d = new Translate3d(); | ||
wheel.unlisten(); | ||
_this.detecting = true; | ||
translate3d.setElement(this.el); // enable percentage stuff | ||
var wheelEventData = { | ||
point: normalizeEvent$3(event).touches[0], | ||
scale: wheel.zoomFactor * -event.deltaY | ||
// console.log(wheelEventData) | ||
this.el.style.transformOrigin = 'top left'; | ||
};_action('wheelDelta', wheelEventData); | ||
this.on('wheelEvent', this.eventHandler, function (error) { | ||
console.error(error); | ||
}); | ||
}, | ||
unlisten: function unlisten() { | ||
console.log('zoom::unlisten'); | ||
// tell event listeners | ||
var wheelEvent = new CustomEvent('wheelDelta', { detail: wheelEventData }); | ||
if (!wheel.el.dispatchEvent(wheelEvent)) { | ||
endHandler(); | ||
console.log('Wheel::action - event was cancelled'); | ||
} | ||
this.off('wheelEvent', this.eventHandler); | ||
}, | ||
destroy: function destroy() { | ||
translate3d = null; | ||
}, | ||
eventHandler: function eventHandler(event) { | ||
if (this.options.domEvents) { | ||
var zoomEvent = new CustomEvent('zoom', { | ||
detail: event, | ||
bubbles: true, | ||
cancelable: true | ||
}); | ||
endHandler(); | ||
function endHandler() { | ||
wheel.detecting = false; | ||
wheel.listen(_action); | ||
} | ||
}; | ||
if (!this.el.dispatchEvent(zoomEvent)) { | ||
console.log('Zoom::eventHandler - event was cancelled'); | ||
return; | ||
} | ||
} | ||
}]); | ||
return Wheel; | ||
}(); | ||
function normalizeEvent$3(ev) { | ||
var event = {}; | ||
if (this.options.preventDefault) event.preventDefault(); | ||
// TODO: add async zoom operation here? | ||
this.zoom(event.point, getScaleMultiplier(event.deltaY, this.options.zoomFactor)); | ||
}, | ||
zoom: function zoom(point, multiplier) { | ||
translate3d.tx = point.x - multiplier * (point.x - translate3d.tx); | ||
translate3d.ty = point.y - multiplier * (point.y - translate3d.ty); | ||
translate3d.tz *= multiplier; | ||
this.el.style.transform = Translate3d.getMatrixString(translate3d); | ||
} | ||
}; | ||
// console.log(ev) | ||
function getScaleMultiplier(delta, zoomFactor) { | ||
var scaleMultiplier = 1; | ||
if (delta > 0) { | ||
// zoom out | ||
scaleMultiplier = 1 - zoomFactor; | ||
} else if (delta < 0) { | ||
// zoom in | ||
scaleMultiplier = 1 + zoomFactor; | ||
} | ||
event.touches = [new Point({ x: ev.pageX, y: ev.pageY })]; | ||
event.type = null; | ||
event.timeStamp = Date.now(); | ||
return scaleMultiplier; | ||
} | ||
return event; | ||
var lastTouches$1 = null; | ||
var eventNames$1 = null; | ||
function errorHandler$1(error) { | ||
console.error(error); | ||
} | ||
var Options = function () { | ||
function Options(_ref) { | ||
var Pan = { | ||
options: { | ||
preventDefault: false | ||
}, | ||
// life cycle handlers | ||
listen: function listen() { | ||
this.on('mousedown', this.startHandler, { reject: errorHandler$1 }); | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler$1 }); | ||
console.log('Pan::listen'); | ||
}, | ||
unlisten: function unlisten() { | ||
this.off('mousedown', this.startHandler); | ||
this.off('touchstart.passive', this.startHandler); | ||
console.log('Pan::unlisten'); | ||
}, | ||
// custom methods | ||
startHandler: function startHandler(event) { | ||
var _this = this; | ||
var gestures = _ref.gestures, | ||
_ref$min = _ref.min, | ||
min = _ref$min === undefined ? .5 : _ref$min, | ||
_ref$max = _ref.max, | ||
max = _ref$max === undefined ? 20 : _ref$max; | ||
classCallCheck(this, Options); | ||
this.unlisten(); | ||
this.min = parseFloat(min); | ||
this.max = parseFloat(max); | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
lastTouches$1 = event; | ||
if (gestures && !Array.isArray(gestures)) throw new TypeError('gestures most be an array'); | ||
eventNames$1 = event.getEventTypeNames(); | ||
this.configurations = gestures ? new Map(gestures) : new Map([['swipe', {}], ['pinch', { threshold: .2 }], ['pan', {}], ['wheel', { zoomFactor: 0.03 }]]); | ||
this.on(this.options.preventDefault ? eventNames$1.move : eventNames$1.move + '.passive', this.moveHandler, function (error) { | ||
console.error(error); | ||
_this.unlisten(); | ||
}); | ||
this.on(eventNames$1.end, this.endHandler, { reject: errorHandler$1 }); | ||
}, | ||
moveHandler: function moveHandler(event) { | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
this.factories = new Map([['swipe', function (opt) { | ||
return new Swipe(opt || _this.configurations.get('swipe')); | ||
}], ['pinch', function (opt) { | ||
return new Pinch(opt || _this.configurations.get('pinch')); | ||
}], ['pan', function (opt) { | ||
return new Pan(opt || _this.configurations.get('pan')); | ||
}], ['wheel', function (opt) { | ||
return new Wheel(opt || _this.configurations.get('wheel')); | ||
}]]); | ||
event.delta = event.touches[0].delta(lastTouches$1.touches[0]); | ||
event.direction = event.getDirection(lastTouches$1); | ||
// console.log(event.delta) | ||
this.fire('pan', event); | ||
}, | ||
endHandler: function endHandler() { | ||
this.off(this.options.preventDefault ? eventNames$1.move : eventNames$1.move + '.passive', this.moveHandler); | ||
this.off(eventNames$1.end, this.endHandler); | ||
this.listen(); | ||
} | ||
}; | ||
createClass(Options, [{ | ||
key: 'getFactory', | ||
value: function getFactory(name) { | ||
return this.factories.get(name); | ||
var minDistance$1 = ''; | ||
var lastTouches$2 = null; | ||
var eventNames$2 = null; | ||
var isPassive$1 = false; | ||
function getEventName(eventName) { | ||
return isPassive$1 ? eventName + '.passive' : eventName; | ||
} | ||
var Swipe = Object.assign(percentToPixel, { | ||
// required option(s) | ||
options: { | ||
distance: { | ||
required: true | ||
}, | ||
get preventDefault() { | ||
return !isPassive$1; | ||
}, | ||
set preventDefault(bool) { | ||
isPassive$1 = !bool; | ||
} | ||
}]); | ||
return Options; | ||
}(); | ||
}, | ||
/* detect passive option for event listeners */ | ||
var supportsPassiveOption = false; | ||
try { | ||
var opts = Object.defineProperty({}, 'passive', { | ||
get: function get$$1() { | ||
supportsPassiveOption = true; | ||
// life cycle handlers | ||
listen: function listen() { | ||
if (getUnit(this.options.distance) === '%') { | ||
minDistance$1 = Math.min(this.percentToPixel(parseFloat(this.options.distance), 'width'), this.percentToPixel(parseFloat(this.options.distance), 'height')); | ||
} | ||
}); | ||
window.addEventListener('test', null, opts); | ||
} catch (e) {} | ||
/* end detect */ | ||
console.log('supportsPassiveOption', supportsPassiveOption); | ||
var PanZoom = function () { | ||
function PanZoom(options) { | ||
classCallCheck(this, PanZoom); | ||
this.on('mousedown', this.startHandler, { reject: errorHandler$2 }); | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler$2 }); | ||
console.log('Swipe::listen'); | ||
}, | ||
unlisten: function unlisten() { | ||
this.off('mousedown', this.startHandler); | ||
this.off('touchstart.passive', this.startHandler); | ||
console.log('Swipe::unlisten'); | ||
}, | ||
this.options = options; | ||
this.el = null; | ||
this.swipe = null; | ||
this.pinch = null; | ||
this.pan = null; | ||
this.isListening = false; | ||
} | ||
createClass(PanZoom, [{ | ||
key: 'listen', | ||
value: function listen() { | ||
// console.log('PanZoom::listen') | ||
// custom methods | ||
/** | ||
* | ||
* @param {GestureEvent} event | ||
*/ | ||
startHandler: function startHandler(event) { | ||
this.unlisten(); | ||
if (this.isListening) return; | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
lastTouches$2 = event; | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
eventNames$2 = event.getEventTypeNames(); | ||
try { | ||
for (var _iterator = this.options.configurations.keys()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var name = _step.value; | ||
this.on(getEventName(eventNames$2.move), this.moveHandler, { reject: errorHandler$2 }); | ||
this.on(eventNames$2.end, this.endHandler, { reject: errorHandler$2 }); | ||
}, | ||
var factory = this.options.getFactory(name); | ||
this[name] = new factory(); | ||
this[name].setElement(this.el); | ||
this[name].listen(this.fire); | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
/** | ||
* | ||
* @param {GestureEvent} event | ||
*/ | ||
moveHandler: function moveHandler(event) { | ||
var distance = event.touches[0].distance(lastTouches$2.touches[0]); | ||
// console.log(distance) | ||
this.isListening = true; | ||
if (distance > minDistance$1) { | ||
// TODO: consider zoom in distance? | ||
event.direction = event.getDirection(lastTouches$2); | ||
this.fire('swipe', event); | ||
this.endHandler(); | ||
} | ||
}, { | ||
key: 'unlisten', | ||
value: function unlisten() { | ||
// console.log('PanZoom::unlisten') | ||
if (!this.isListening) return; | ||
}, | ||
endHandler: function endHandler() { | ||
this.off(getEventName(eventNames$2.move), this.moveHandler); | ||
this.off(eventNames$2.end, this.endHandler); | ||
this.listen(); | ||
} | ||
}); | ||
var _iteratorNormalCompletion2 = true; | ||
var _didIteratorError2 = false; | ||
var _iteratorError2 = undefined; | ||
function errorHandler$2(error) { | ||
console.error(error, 'error happen in listener'); | ||
} | ||
try { | ||
for (var _iterator2 = this.options.configurations.keys()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { | ||
var name = _step2.value; | ||
/** | ||
* | ||
* @param {HTMLElement} el | ||
* @param {object} referent | ||
* @param {object} options | ||
*/ | ||
function panzoom(el) { | ||
var referent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Zoom; | ||
var options = arguments[2]; | ||
this.off(name); | ||
this[name].unlisten(); | ||
this[name] = null; | ||
if (!el) throw new TypeError('the first argument to panzoom must be an Element'); | ||
var initializedReferent = initReferent(referent, el, options); | ||
console.log(initializedReferent); | ||
initializedReferent.listen(); | ||
return initializedReferent; | ||
} | ||
function initReferent(referent, el, options) { | ||
if (!(referent.gestures && Object.values(referent.gestures)[0])) { | ||
throw new Error('Referent must have gestures'); | ||
} | ||
// mixin options from referent with option argument | ||
options = Object.assign({}, referent.options, options); | ||
// if (referent.isInitialized) { | ||
// throw new Error('Referent is initialized - use .listen() or destroy()') | ||
// } | ||
// referent.isInitialized = true | ||
// shared observer between a referent and all of its gestures | ||
var hivemind = NativeEvents(); | ||
var isListening = false; | ||
var proxy = Trait.create(Object.prototype, Trait.compose(Trait.resolve({ destroy: 'destroyListeners' }, hivemind), Trait.resolve({ | ||
'listen': 'listenReferent', | ||
'unlisten': 'unlistenReferent', | ||
'destroy': 'destroyReferent', | ||
'options': undefined, | ||
'gestures': undefined | ||
}, Trait(referent)), Trait({ | ||
el: el, | ||
options: options, | ||
get isListening() { | ||
return isListening; | ||
}, | ||
gestures: map$1(function (gesture, name) { | ||
gesture.el = el; | ||
var defaultOptions = {}; | ||
each(function (value, key) { | ||
// better error message than the one from traits.js | ||
if (value.required && !options[key]) { | ||
throw new Error('options is missing ' + key + ' - required by ' + (isNaN(name) ? name : 'a') + ' gesture'); | ||
} | ||
} catch (err) { | ||
_didIteratorError2 = true; | ||
_iteratorError2 = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion2 && _iterator2.return) { | ||
_iterator2.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError2) { | ||
throw _iteratorError2; | ||
} | ||
} | ||
if (value.required) defaultOptions[key] = Trait.required;else defaultOptions[key] = value; | ||
}, gesture.options); | ||
// convert a gesture's options record into a trait instance where | ||
// `options` overwrite a gesture's default options | ||
try { | ||
gesture.options = Trait.object(Object.assign(defaultOptions, options)); | ||
} catch (error) { | ||
throw new Error(error.message + ' in options'); | ||
} | ||
this.isListening = false; | ||
} | ||
}, { | ||
key: 'setElement', | ||
value: function setElement(el) { | ||
this.el = el; | ||
} | ||
}]); | ||
return PanZoom; | ||
}(); | ||
// convert gesture record into a trait instance | ||
var t = Trait.create(Object.prototype, Trait.compose(Trait(gesture), hivemind)); | ||
return t; | ||
}, referent.gestures), | ||
function createPanzoom(el) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
listen: function listen(arg) { | ||
if (this.isListening) return; | ||
if (!el) throw new TypeError('the first argument to createPanzoom must be an Element'); | ||
each(function (gesture) { | ||
gesture.listen(arg); | ||
}, this.gestures); | ||
var panzoom = new PanZoom(new Options(options)); | ||
Object.assign(panzoom, Observer()); // add mixins | ||
panzoom.setElement(el); | ||
panzoom.listen(); | ||
this.listenReferent(arg); | ||
return panzoom; | ||
// TODO: maybe not needed since we call listen on all gestures and they add the native event handler via NativeEvents.js | ||
this.addNativeEventHandlers(); | ||
isListening = true; | ||
}, | ||
unlisten: function unlisten(arg) { | ||
this.removeNativeEventHandlers(); | ||
each(function (gesture) { | ||
gesture.unlisten(arg); | ||
}, this.gestures); | ||
if (this.unlistenReferent) this.unlistenReferent(arg); | ||
isListening = false; | ||
}, | ||
destroy: function destroy() { | ||
this.destroyListeners(); | ||
if (this.destroyReferent) this.destroyReferent(); | ||
return proxy = hivemind = null; | ||
} | ||
}))); | ||
return proxy; | ||
} | ||
exports.createPanzoom = createPanzoom; | ||
exports.PanZoom = PanZoom; | ||
exports.Translate3d = Translate3d; | ||
exports.Observer = Observer; | ||
panzoom.Point = Point; | ||
panzoom.Translate3d = Translate3d; | ||
panzoom.Observer = Observer; | ||
panzoom.gestures = {}; | ||
panzoom.gestures.Pinch = Pinch; | ||
panzoom.gestures.Pan = Pan; | ||
panzoom.gestures.Wheel = Wheel; | ||
panzoom.gestures.Swipe = Swipe; | ||
panzoom.referents = {}; | ||
panzoom.referents.Zoom = Zoom; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
return panzoom; | ||
}))); | ||
//# sourceMappingURL=panzoom.js.map |
'use strict' | ||
// const Translate3d = panzoom.Translate3d | ||
const scene = panzoom.createPanzoom(document.querySelector('.scene')) | ||
// scene.unlisten() | ||
const listenButton = document.getElementById('listenButton') | ||
const initializeButton = document.getElementById('initializeButton') | ||
const destroyButton = document.getElementById('destroyButton') | ||
// const translate3d = new Translate3d() | ||
// console.log(String(translate3d)) | ||
// console.log(translate3d) | ||
const elements = { | ||
container: document.querySelector('.container'), | ||
scene: document.querySelector('.scene') | ||
} | ||
let zoom | ||
initializeButton.onclick = initialize | ||
function initialize () { | ||
const el = getEl() | ||
// el.addEventListener('zoom', zoomHandler) | ||
// zoom = panzoom(el, panzoom.referents.Zoom, { domEvents: true }) | ||
zoom = panzoom(el) | ||
listenButton.textContent = getButtonText(zoom) | ||
} | ||
// function zoomHandler (event) { | ||
// event.preventDefault() | ||
// } | ||
listenButton.onclick = function () { | ||
if (zoom && zoom.isListening) { | ||
zoom.unlisten() | ||
} else if (zoom && !zoom.isListening) { | ||
zoom.listen() | ||
} else { | ||
initialize() | ||
} | ||
listenButton.textContent = getButtonText(zoom) | ||
} | ||
destroyButton.onclick = function () { | ||
// const el = getEl() | ||
// el.removeEventListener('zoom', zoomHandler) | ||
zoom.destroy() | ||
zoom = null | ||
listenButton.textContent = 'Listen' | ||
} | ||
function getEl () { | ||
const elValue = document.querySelector('input[type="radio"]:checked').value | ||
return elements[elValue] | ||
} | ||
function getButtonText (referent) { | ||
return referent.isListening ? 'Unlisten' : 'Listen' | ||
} |
/* global panzoom */ | ||
const allGesturesReferent = { | ||
gestures: { | ||
Pinch: panzoom.gestures.Pinch, | ||
Pan: panzoom.gestures.Pan, | ||
Wheel: panzoom.gestures.Wheel, | ||
Swipe: panzoom.gestures.Swipe | ||
}, | ||
options: { | ||
domEvents: true, | ||
distance: '200px' | ||
}, | ||
listen: function () { | ||
if (this.options.domEvents) { | ||
this.on('pinch', this.fireEvent('pinch'), { reject: function (error) { console.log(error) } }) | ||
this.on('pan', this.fireEvent('pan'), { reject: function (error) { console.log(error) } }) | ||
this.on('wheelEvent', this.fireEvent('wheelEvent'), { reject: function (error) { console.log(error) } }) | ||
this.on('swipe', this.fireEvent('swipe'), { reject: function (error) { console.log(error) } }) | ||
} | ||
}, | ||
fireEvent: function (name) { | ||
return function (event) { | ||
const customEvent = new CustomEvent(name, { | ||
detail: event, | ||
bubbles: true, | ||
cancelable: true, | ||
}) | ||
if (!this.el.dispatchEvent(customEvent)) { | ||
console.log(name + ' - event was cancelled') | ||
return | ||
} | ||
} | ||
} | ||
} | ||
const listenButton = document.getElementById('listenButton') | ||
const domMessages = document.querySelectorAll('.messages__message') | ||
const scene = panzoom.createPanzoom(document.querySelector('.scene')) | ||
const scene = panzoom(document.querySelector('.scene'), allGesturesReferent) | ||
@@ -17,3 +50,3 @@ const messages = setupMessages() | ||
if (messageEl.dataset.method === 'event') { | ||
switch (gesture) { | ||
switch (gesture) { // for DOM events | ||
case 'pinch': | ||
@@ -25,9 +58,12 @@ unpack = pinchEventUnpack | ||
break | ||
case 'wheelDelta': | ||
case 'wheelEvent': | ||
unpack = wheelEventUnpack | ||
break | ||
case 'swipe': | ||
unpack = swipeEventUnpack | ||
break | ||
default: | ||
unpack = eventDetail | ||
} | ||
} else { | ||
} else { // for panzoom2 events | ||
switch (gesture) { | ||
@@ -40,5 +76,8 @@ case 'pinch': | ||
break | ||
case 'wheelDelta': | ||
case 'wheelEvent': | ||
unpack = wheelUnpack | ||
break | ||
case 'swipe': | ||
unpack = swipeUnpack | ||
break | ||
default: | ||
@@ -67,3 +106,3 @@ unpack = identity | ||
return pinchEvent.scale.toFixed(2) | ||
+ ' (' + pinchEvent.focus.x.toFixed(1) + ', ' + pinchEvent.focus.y.toFixed(1) + ') (' | ||
+ ' (' + pinchEvent.point.x.toFixed(1) + ', ' + pinchEvent.point.y.toFixed(1) + ') (' | ||
+ pinchEvent.focusAfterScale.x.toFixed(1) + ', ' + pinchEvent.focusAfterScale.y.toFixed(1) + ')' | ||
@@ -77,3 +116,3 @@ } | ||
function panUnpack (panEvent) { | ||
return '(' + panEvent.x.toFixed(2) + ', ' + panEvent.y.toFixed(2) + ')' | ||
return '(' + panEvent.touches[0].x.toFixed(2) + ', ' + panEvent.touches[0].y.toFixed(2) + ')' | ||
} | ||
@@ -86,5 +125,13 @@ | ||
function wheelUnpack (wheelEvent) { | ||
return 'scale: ' + wheelEvent.scale.toFixed(2) + ' (' + wheelEvent.point.x + ', ' + wheelEvent.point.y + ')' | ||
return 'deltaY: ' + wheelEvent.deltaY.toFixed(2) + ' (' + wheelEvent.point.x + ', ' + wheelEvent.point.y + ') in page (' + wheelEvent.page[0].x + ', ' + wheelEvent.page[0].y + ')' | ||
} | ||
function swipeEventUnpack (event) { | ||
return swipeUnpack(eventDetail(event)) | ||
} | ||
function swipeUnpack (swipeEvent) { | ||
return 'direction: ' + swipeEvent.direction | ||
} | ||
function messagesFactory (gesture, method, title, messageEl, unpack) { | ||
@@ -97,3 +144,3 @@ const message = { | ||
handler: function (payload) { | ||
// if (gesture === 'wheelDelta') console.log('payload', payload) | ||
// if (gesture === 'wheelEvent') console.log('payload', payload) | ||
message.messageEl.textContent = title + ' ' + ++message.counter + ': ' + unpack(payload) | ||
@@ -133,4 +180,4 @@ } | ||
// If you have unlisten then you have to enable listen again | ||
// , but it's irrelevant when you start to listen again. | ||
// If you have unlisten then you have to enable listen again, | ||
// but it's irrelevant when you start to listen again. | ||
// .listen() is idempotent | ||
@@ -137,0 +184,0 @@ scene.listen() |
'use strict' | ||
const listenButton = document.getElementById('listenButton') | ||
const messages = document.querySelectorAll('.message') | ||
const scene = panzoom.createPanzoom(document.querySelector('.scene')) | ||
// our custom referent | ||
const catchSwipe = { | ||
gestures: [panzoom.gestures.Swipe], | ||
listen() | ||
// default options to referent and gestures | ||
// - can be overriden via the options argument to panzoom() | ||
options: { | ||
distance: '70%' | ||
}, | ||
function listen () { | ||
// life-cycle method | ||
listen: function () { | ||
// use promise | ||
this.promise('swipe').then(swipeHandler1) | ||
// use promise | ||
scene.promise('swipe').then(swipeHandler1) | ||
// listen to event | ||
document.body.addEventListener('swipe', swipeHandler2, true) | ||
// listen to event | ||
document.body.addEventListener('swipe', swipeHandler2, true) | ||
// subscribe | ||
this.on('swipe', swipeHandler3) | ||
// subscribe | ||
scene.on('swipe', swipeHandler3) | ||
// subscribe once | ||
this.once('swipe', swipeHandler4) | ||
// subscribe once | ||
scene.once('swipe', swipeHandler4) | ||
// If you have unlisten then you have to enable listen again | ||
// , but it's irrelevant when you start to listen again. | ||
scene.listen() | ||
listenButton.textContent = getButtonText(scene) | ||
listenButton.onclick = function () { | ||
// unlisten will remove all event listeners and nullify the swipe module | ||
scene.unlisten('swipe') | ||
if (this.options.domEvents) { | ||
this.on('swipe', this.delegateEvent) | ||
} | ||
}, | ||
// life-cycle method | ||
unlisten: function () { | ||
document.body.removeEventListener('swipe', swipeHandler2, true) | ||
listenButton.textContent = getButtonText(scene) | ||
listenButton.onclick = listen | ||
// scene.off('swipe) // will remove all event listeners for the 'swipe' event | ||
this.off('swipe') // will remove all event listeners for the 'swipe' event | ||
// scene.off('swipe', swipeHandler1) // promise can not be unlisten to - off all swipe event listeners or turn off swipe completely | ||
// scene.off('swipe', swipeHandler3) // this will work | ||
// scene.off('swipe', swipeHandler4) // once can not be unlisten to - off all swipe event listeners or turn off swipe completely | ||
}, | ||
// custom method | ||
delegateEvent: function (event) { | ||
const swipeEvent = new CustomEvent('swipe', { detail: event.direction }) | ||
if (!this.el.dispatchEvent(swipeEvent)) { | ||
console.log('Swipe::action - event was cancelled') | ||
} | ||
} | ||
} | ||
const initializeButton = document.getElementById('initializeButton') | ||
const listenButton = document.getElementById('listenButton') | ||
const messages = document.querySelectorAll('.message') | ||
let scene | ||
initializeButton.onclick = function () { | ||
if (scene) { | ||
scene.destroy() | ||
console.log(scene) | ||
scene = null | ||
initializeButton.textContent = 'Initialize' | ||
listenButton.style.display = 'none' | ||
} else { | ||
scene = panzoom(document.querySelector('.scene'), catchSwipe, { domEvents: true, preventDefault: true }) | ||
// scene.listen() is called automatically | ||
initializeButton.textContent = 'Destroy' | ||
listenButton.style.display = 'inline-block' | ||
} | ||
} | ||
listenButton.onclick = function () { | ||
// unlisten will remove all event listeners | ||
if (scene.isListening) scene.unlisten() | ||
else scene.listen() | ||
listenButton.textContent = getButtonText(scene) | ||
} | ||
function getButtonText (scene) { | ||
@@ -47,8 +82,9 @@ return scene.isListening ? 'Unlisten' : 'Listen' | ||
let counter1 = 0 | ||
const title1 = 'promise (once)' | ||
messages[0].textContent = title1 | ||
function swipeHandler1 (direction) { | ||
messages[0].textContent = title1 + ' ' + ++counter1 + ' - ' + direction | ||
console.log(direction) | ||
function swipeHandler1 (event) { | ||
messages[0].textContent = title1 + ' ' + ++counter1 + ' - ' + event.direction | ||
console.log(event) | ||
} | ||
@@ -69,5 +105,5 @@ | ||
messages[2].textContent = title3 | ||
function swipeHandler3(direction) { | ||
messages[2].textContent = title3 + ' ' + ++counter3 + ' - ' + direction | ||
console.log(direction) | ||
function swipeHandler3(event) { | ||
messages[2].textContent = title3 + ' ' + ++counter3 + ' - ' + event.direction | ||
console.log(event) | ||
} | ||
@@ -78,5 +114,5 @@ | ||
messages[3].textContent = title4 | ||
function swipeHandler4(direction) { | ||
messages[3].textContent = title4 + ' ' + ++counter4 + ' - ' + direction | ||
console.log(direction) | ||
function swipeHandler4(event) { | ||
messages[3].textContent = title4 + ' ' + ++counter4 + ' - ' + event.direction | ||
console.log(event) | ||
} |
{ | ||
"name": "panzoom2", | ||
"version": "1.0.0-beta.4", | ||
"version": "1.0.0-beta.5", | ||
"description": "Work In Progess - Hopefully a panzoom lib you can finally depend on", | ||
"main": "dist/panzoom.js", | ||
"scripts": { | ||
"test": "tap tests/*.test.js", | ||
"test": "tap tests/*.test.*", | ||
"dev": "parallelshell -v 'npm run build -- --watch' 'npm run browsersync'", | ||
"build": "rollup --config rollup.config.js", | ||
"browsersync": "browser-sync start --server --files 'dist/*.js' --startPath docs/" | ||
"browsersync": "browser-sync start --server --files 'dist/*.js' --startPath docs/", | ||
"debug": "node --inspect-brk" | ||
}, | ||
@@ -23,15 +24,18 @@ "keywords": [ | ||
"devDependencies": { | ||
"babel-core": "^6.26.0", | ||
"babel-core": "^6.26.3", | ||
"babel-plugin-external-helpers": "^6.22.0", | ||
"babel-preset-env": "^1.6.1", | ||
"browser-sync": "^2.23.6", | ||
"browser-sync": "^2.24.1", | ||
"eslint": "^4.19.1", | ||
"jsdom": "^11.6.2", | ||
"jsdom": "^11.10.0", | ||
"parallelshell": "^3.0.2", | ||
"rollup": "^0.57.1", | ||
"rollup-plugin-babel": "^3.0.3", | ||
"rollup-plugin-commonjs": "^9.1.0", | ||
"rollup-plugin-babel": "^3.0.4", | ||
"rollup-plugin-commonjs": "^9.1.3", | ||
"rollup-plugin-node-resolve": "^3.3.0", | ||
"tap": "^11.1.3" | ||
"tap": "^11.1.4" | ||
}, | ||
"dependencies": { | ||
"traits.js": "^1.1.4" | ||
} | ||
} |
@@ -13,2 +13,7 @@ [![Build Status](https://travis-ci.org/dotnetCarpenter/panzoom2.svg?branch=master)](https://travis-ci.org/dotnetCarpenter/panzoom2) | ||
## Supported browsers | ||
The project aim to support every [browser that has more than 1% market share, plus IE11](http://browserl.ist/?q=IE+11%2C+%3E+1%25). | ||
## Internet Explorer 11 | ||
@@ -31,3 +36,3 @@ | ||
- [swipe](https://dotnetcarpenter.github.io/panzoom2/swipe) `Showcase swipe detection and all the ways you can listen and unlisten to an event with panzoom.` | ||
- [gestures](https://dotnetcarpenter.github.io/panzoom2/gestures) `Showcase gesture recognition. - INCOMPLETE` | ||
- [gestures](https://dotnetcarpenter.github.io/panzoom2/gestures) `Showcase gesture recognition.` | ||
@@ -39,1 +44,2 @@ ## Inspiration drawn from | ||
- Eight Media's [HAMMER.JS](http://hammerjs.github.io/) | ||
- [User-defined gestures for surface computing](https://faculty.washington.edu/wobbrock/pubs/chi-09.02.pdf) |
@@ -15,3 +15,6 @@ // rollup.config.js | ||
plugins: [ | ||
resolve(), | ||
resolve({ | ||
extensions: ['.js', '.mjs'], | ||
module: true | ||
}), | ||
babel({ | ||
@@ -22,2 +25,2 @@ // only transpile our source code | ||
] | ||
} | ||
} |
import Point from '../models/Point' | ||
class Pan { | ||
constructor (options) { | ||
this.el = null | ||
this.lastTouches = null | ||
this.detecting = false | ||
} | ||
let lastTouches = null | ||
let eventNames = null | ||
listen (action) { | ||
// console.log('Pan::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function') | ||
function errorHandler (error) { | ||
console.error(error) | ||
} | ||
this.action = Pan.action(this, action) | ||
export default { | ||
options: { | ||
preventDefault: false | ||
}, | ||
this.el.addEventListener('mousedown', this.action) | ||
this.el.addEventListener('touchstart', this.action) | ||
} | ||
// life cycle handlers | ||
listen () { | ||
this.on('mousedown', this.startHandler, { reject: errorHandler }) | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler }) | ||
console.log('Pan::listen') | ||
}, | ||
unlisten () { | ||
// console.log('Pan::unlisten') | ||
this.el.removeEventListener('mousedown', this.action) | ||
this.el.removeEventListener('touchstart', this.action) | ||
} | ||
this.off('mousedown', this.startHandler) | ||
this.off('touchstart.passive', this.startHandler) | ||
console.log('Pan::unlisten') | ||
}, | ||
setElement (el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el | ||
} | ||
// custom methods | ||
startHandler (event) { | ||
this.unlisten() | ||
destroy () { | ||
this.el = null | ||
this.action = null | ||
this.lastTouches = null | ||
} | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
lastTouches = event | ||
static action (pan, action) { | ||
return event => { | ||
event.preventDefault() | ||
eventNames = event.getEventTypeNames() | ||
pan.unlisten() | ||
this.on(this.options.preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler, error => { | ||
console.error(error) | ||
this.unlisten() | ||
}) | ||
this.on(eventNames.end, this.endHandler, { reject: errorHandler }) | ||
}, | ||
const startEvent = normalizeEvent(event) | ||
pan.lastTouches = startEvent | ||
moveHandler (event) { | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
// console.log(startEvent) | ||
this.detecting = true | ||
event.delta = event.touches[0].delta(lastTouches.touches[0]) | ||
event.direction = event.getDirection(lastTouches) | ||
// console.log(event.delta) | ||
pan.el.addEventListener(startEvent.type.move, moveHandler) | ||
pan.el.addEventListener(startEvent.type.end, endHandler) // removing event listeners from DOM via this | ||
this.fire('pan', event) | ||
}, | ||
function moveHandler (event) { | ||
const currentEvent = normalizeEvent(event) | ||
// console.log('getting moves!', currentEvent) | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
// TODO: abstract this somewhere | ||
const delta = new Point({ | ||
x: currentEvent.touches[0].x - pan.lastTouches.touches[0].x, | ||
y: currentEvent.touches[0].y - pan.lastTouches.touches[0].y | ||
}) | ||
// console.log(delta) | ||
// tell subscribers | ||
action('pan', delta) | ||
// tell event listeners | ||
const panEvent = new CustomEvent('pan', { detail: delta }) | ||
if (!pan.el.dispatchEvent(panEvent)) { | ||
endHandler() | ||
console.log('Pan::action - event was cancelled') | ||
} | ||
} | ||
function endHandler () { | ||
pan.el.removeEventListener(startEvent.type.move, moveHandler) | ||
pan.el.removeEventListener(startEvent.type.end, endHandler) | ||
pan.detecting = false | ||
pan.listen(action) | ||
} | ||
} | ||
endHandler () { | ||
this.off(this.options.preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler) | ||
this.off(eventNames.end, this.endHandler) | ||
this.listen() | ||
} | ||
} | ||
function normalizeEvent(ev) { | ||
const event = {} | ||
// console.log(ev) | ||
event.touches = [ | ||
ev.touches | ||
? new Point({ x: ev.touches[0].pageX, y: ev.touches[0].pageY }) | ||
: new Point({ x: ev.pageX, y: ev.pageY }) | ||
] | ||
event.type = ev.type === 'touchstart' // TODO: use a proper enum | ||
? { move: 'touchmove', end: 'touchend' } | ||
: { move: 'mousemove', end: 'mouseup' } | ||
event.timeStamp = Date.now() | ||
return event | ||
} | ||
export default Pan |
import Point from '../models/Point' | ||
class Pinch { | ||
constructor (options) { | ||
this.threshold = options.threshold | ||
this.el = null | ||
this.lastTouches = null | ||
this.lastDistance = null | ||
this.detecting = false | ||
} | ||
let minDistance = '' | ||
let lastTouches = null | ||
let lastDistance = null | ||
let eventNames = null | ||
listen (action) { | ||
// console.log('Pinch::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function') | ||
export default { | ||
// required custom properties from option object | ||
options: { | ||
pinchThreshold: .2, | ||
preventDefault: true | ||
}, | ||
this.action = Pinch.action(this, action) | ||
this.el.addEventListener('touchstart', this.action) | ||
} | ||
// life cycle handlers | ||
listen () { | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler }) | ||
console.log('Pinch::listen') | ||
}, | ||
unlisten () { | ||
// console.log('Pinch::unlisten') | ||
this.el.removeEventListener('touchstart', this.action) | ||
} | ||
this.off('touchstart.passive', this.startHandler, { reject: errorHandler }) | ||
console.log('Pinch::unlisten') | ||
}, | ||
setElement (el) { | ||
this.el = el | ||
} | ||
// custom methods | ||
startHandler (event) { | ||
console.log('Pinch::moveHandler', event, this) | ||
destroy () { | ||
this.el = null | ||
this.action = null | ||
this.lastTouches = null | ||
} | ||
this.unlisten() | ||
static action (pinch, action) { | ||
return event => { | ||
event.preventDefault() | ||
if (event.touches.length < 2) { | ||
this.endHandler() | ||
return | ||
} | ||
pinch.unlisten() | ||
lastTouches = event | ||
eventNames = event.getEventTypeNames() | ||
const startEvent = normalizeEvent(event) | ||
// console.log(startEvent) | ||
if (startEvent.touches.length < 2) { | ||
endHandler() | ||
return | ||
} | ||
pinch.lastTouches = startEvent | ||
this.on(preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler) | ||
this.on(eventNames.end, this.endHandler) | ||
}, | ||
this.detecting = true | ||
moveHandler (event) { | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
pinch.el.addEventListener(startEvent.type.move, moveHandler) | ||
pinch.el.addEventListener(startEvent.type.end, endHandler) // removing event listeners from DOM via this | ||
// movement (translate) | ||
const distanceFromFirstTouch = event.touches[0].distance(lastTouches.touches[0]) | ||
function moveHandler (event) { | ||
event.preventDefault() // prevent native zoom TODO: perhaps we should not make that decision... | ||
const currentEvent = normalizeEvent(event) | ||
// console.log('getting moves!', currentEvent) | ||
// distance between two first fingers | ||
const distanceBetweenTwoFingers = event.touches[0].distance(lastTouches.touches[1]) | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
const pinchOutwards = lastDistance && distanceBetweenTwoFingers > lastDistance ? true : false | ||
// console.log(pinchOutwards ? 'zoom in' : 'zoom out') | ||
// movement (translate) | ||
const distanceFromFirstTouch = Math.sqrt( // TODO: abstract this somewhere | ||
(currentEvent.touches[0].x - pinch.lastTouches.touches[0].x) ** 2 | ||
+ | ||
(currentEvent.touches[0].y - pinch.lastTouches.touches[0].y) ** 2 | ||
) | ||
// distance between two first fingers | ||
const distanceBetweenTwoFingers = Math.sqrt( // TODO: abstract this somewhere | ||
(currentEvent.touches[0].x - pinch.lastTouches.touches[1].x) ** 2 | ||
+ | ||
(currentEvent.touches[0].y - pinch.lastTouches.touches[1].y) ** 2 | ||
) | ||
lastDistance = distanceBetweenTwoFingers | ||
const pinchOutwards = pinch.lastDistance && distanceBetweenTwoFingers > pinch.lastDistance ? true : false | ||
// console.log(pinchOutwards ? 'zoom in' : 'zoom out') | ||
pinch.lastDistance = distanceBetweenTwoFingers | ||
// console.log('distance', distanceFromFirstTouch, distanceBetweenTwoFingers) | ||
const scale = distanceFromFirstTouch / distanceBetweenTwoFingers | ||
const scale = distanceFromFirstTouch / distanceBetweenTwoFingers | ||
// console.log('scale', scale) | ||
if (scale > pinch.threshold) { | ||
// Focus formular ported from svg.panzoom.js - ask Ulrich why it's like that | ||
const currentFocus = new Point({ | ||
x: currentEvent.touches[0].x + .5 * (currentEvent.touches[1].x - currentEvent.touches[0].x), | ||
y: currentEvent.touches[0].y + .5 * (currentEvent.touches[1].y - currentEvent.touches[0].y) | ||
}) | ||
if (scale > this.options.pinchThreshold) { | ||
// Focus formular ported from svg.panzoom.js - ask Ulrich why it's like that | ||
const currentFocus = new Point({ | ||
x: event.touches[0].x + .5 * (event.touches[1].x - event.touches[0].x), | ||
y: event.touches[0].y + .5 * (event.touches[1].y - event.touches[0].y) | ||
}) | ||
const lastFocus = new Point({ | ||
x: pinch.lastTouches.touches[0].x + 0.5 * (pinch.lastTouches.touches[1].x - pinch.lastTouches.touches[0].x), | ||
y: pinch.lastTouches.touches[0].y + 0.5 * (pinch.lastTouches.touches[1].y - pinch.lastTouches.touches[0].y) | ||
}) | ||
const lastFocus = new Point({ | ||
x: lastTouches.touches[0].x + 0.5 * (lastTouches.touches[1].x - lastTouches.touches[0].x), | ||
y: lastTouches.touches[0].y + 0.5 * (lastTouches.touches[1].y - lastTouches.touches[0].y) | ||
}) | ||
// console.log(scale) | ||
const pinchEventData = { | ||
focus: currentFocus, | ||
scale: pinchOutwards ? scale : -scale, | ||
focusAfterScale: new Point({ x: -lastFocus.x, y: -lastFocus.y }) | ||
} | ||
// console.log(scale) | ||
event.point = currentFocus | ||
event.scale = pinchOutwards ? scale : -scale, | ||
event.focusAfterScale = new Point({ x: -lastFocus.x, y: -lastFocus.y }) | ||
// console.dir(pinchEventData) | ||
// tell subscribers | ||
action('pinch', pinchEventData) | ||
// tell event listeners | ||
const pinchEvent = new CustomEvent('pinch', { detail: pinchEventData }) | ||
if (!pinch.el.dispatchEvent(pinchEvent)) { | ||
endHandler() | ||
console.log('Pinch::action - event was cancelled') | ||
} | ||
} | ||
} | ||
function endHandler () { | ||
pinch.el.removeEventListener(startEvent.type.move, moveHandler) | ||
pinch.el.removeEventListener(startEvent.type.end, endHandler) | ||
pinch.detecting = false | ||
pinch.listen(action) | ||
} | ||
this.fire('pinch', event) | ||
} | ||
}, | ||
endHandler () { | ||
this.off(preventDefault ? eventNames.move : eventNames.move + '.passive', this.moveHandler) | ||
this.off(eventNames.end, this.endHandler) | ||
this.listen() | ||
} | ||
} | ||
function normalizeEvent(ev) { | ||
const event = {} | ||
// console.log(ev) | ||
event.touches = Array.prototype.map.call( | ||
ev.touches, | ||
t => new Point({ x: t.pageX, y: t.pageY }) | ||
) | ||
event.type = { move: 'touchmove', end: 'touchend' } // TODO: use a proper enum | ||
event.timeStamp = Date.now() | ||
return event | ||
function errorHandler (error) { | ||
console.error(error, 'error happen in listener') | ||
} | ||
export default Pinch |
import Point from '../models/Point' | ||
import { percentToPixel, getUnit } from '../mixins/LengthUnits' | ||
import GestureEvent from '../models/GestureEvent'; | ||
class Swipe { | ||
constructor (options) { | ||
this.el = null | ||
this.lastTouches = null | ||
this.detecting = false | ||
} | ||
let minDistance = '' | ||
let lastTouches = null | ||
let eventNames = null | ||
let isPassive = false | ||
listen (action) { | ||
// console.log('Swipe::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function') | ||
function getEventName(eventName) { | ||
return isPassive ? eventName + '.passive' : eventName | ||
} | ||
this.action = Swipe.action(this, action) | ||
export default Object.assign(percentToPixel, { | ||
// required option(s) | ||
options: { | ||
distance: { | ||
required: true | ||
}, | ||
get preventDefault () { | ||
return !isPassive | ||
}, | ||
set preventDefault (bool) { | ||
isPassive = !bool | ||
} | ||
}, | ||
this.el.addEventListener('mousedown', this.action) | ||
this.el.addEventListener('touchstart', this.action) | ||
} | ||
// life cycle handlers | ||
listen () { | ||
if (getUnit(this.options.distance) === '%') { | ||
minDistance = Math.min( | ||
this.percentToPixel(parseFloat(this.options.distance), 'width'), | ||
this.percentToPixel(parseFloat(this.options.distance), 'height') | ||
) | ||
} | ||
this.on('mousedown', this.startHandler, { reject: errorHandler }) | ||
this.on('touchstart.passive', this.startHandler, { reject: errorHandler }) | ||
console.log('Swipe::listen') | ||
}, | ||
unlisten () { | ||
// console.log('Swipe::unlisten') | ||
this.el.removeEventListener('mousedown', this.action) | ||
this.el.removeEventListener('touchstart', this.action) | ||
} | ||
this.off('mousedown', this.startHandler) | ||
this.off('touchstart.passive', this.startHandler) | ||
console.log('Swipe::unlisten') | ||
}, | ||
setElement (el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el | ||
} | ||
// custom methods | ||
/** | ||
* | ||
* @param {GestureEvent} event | ||
*/ | ||
startHandler (event) { | ||
this.unlisten() | ||
destroy () { | ||
this.el = null | ||
this.action = null | ||
this.lastTouches = null | ||
} | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
lastTouches = event | ||
static action (swipe, action) { | ||
return event => { | ||
swipe.unlisten() | ||
eventNames = event.getEventTypeNames() | ||
const startEvent = normalizeEvent(event) | ||
swipe.lastTouches = startEvent | ||
this.on(getEventName(eventNames.move), this.moveHandler, { reject: errorHandler }) | ||
this.on(eventNames.end, this.endHandler, { reject: errorHandler }) | ||
}, | ||
// console.log(startEvent) | ||
this.detecting = true | ||
/** | ||
* | ||
* @param {GestureEvent} event | ||
*/ | ||
moveHandler (event) { | ||
const distance = event.touches[0].distance(lastTouches.touches[0]) | ||
// console.log(distance) | ||
swipe.el.addEventListener(startEvent.type.move, moveHandler) | ||
swipe.el.addEventListener(startEvent.type.end, endHandler) // removing event listeners from DOM via this | ||
if (distance > minDistance) { // TODO: consider zoom in distance? | ||
event.direction = event.getDirection(lastTouches) | ||
function moveHandler (event) { | ||
const currentEvent = normalizeEvent(event) | ||
// console.log('getting moves!', currentEvent) | ||
this.fire('swipe', event) | ||
// TODO: take timestamp into consideration - call endHandler if enough time has passed | ||
this.endHandler() | ||
} | ||
}, | ||
const distance = Math.sqrt( // TODO: abstract this somewhere | ||
(currentEvent.touches[0].x - swipe.lastTouches.touches[0].x) ** 2 | ||
+ | ||
(currentEvent.touches[0].y - swipe.lastTouches.touches[0].y) ** 2 | ||
) | ||
// console.log(distance) | ||
if (distance > 100) { // TODO: make 100 a relative value and consider zoom | ||
const direction = diff(swipe.lastTouches, currentEvent) | ||
// console.log(direction) | ||
// tell subscribers | ||
action('swipe', direction) | ||
// tell event listeners | ||
const swipeEvent = new CustomEvent('swipe', { detail: direction }) | ||
if (!swipe.el.dispatchEvent(swipeEvent)) { | ||
console.log('Swipe::action - event was cancelled') | ||
} | ||
// debugger | ||
endHandler() // removing event listeners from DOM via this (before touchend/mouseup) | ||
} | ||
} | ||
function endHandler () { | ||
swipe.el.removeEventListener(startEvent.type.move, moveHandler) | ||
swipe.el.removeEventListener(startEvent.type.end, endHandler) | ||
swipe.detecting = false | ||
swipe.listen(action) | ||
} | ||
} | ||
endHandler () { | ||
this.off(getEventName(eventNames.move), this.moveHandler) | ||
this.off(eventNames.end, this.endHandler) | ||
this.listen() | ||
} | ||
} | ||
}) | ||
function normalizeEvent(ev) { | ||
const event = {} | ||
// console.log(ev) | ||
event.touches = [ | ||
ev.touches | ||
? new Point({ x: ev.touches[0].pageX, y: ev.touches[0].pageY }) | ||
: new Point({ x: ev.pageX, y: ev.pageY }) | ||
] | ||
event.type = ev.type === 'touchstart' // TODO: use a proper enum | ||
? { move: 'touchmove', end: 'touchend' } | ||
: { move: 'mousemove', end: 'mouseup' } | ||
event.timeStamp = Date.now() | ||
return event | ||
function errorHandler (error) { | ||
console.error(error, 'error happen in listener') | ||
} | ||
// TODO: might also be useful in Pan.js | ||
function diff (event1, event2) { | ||
// https://stackoverflow.com/questions/2264072/detect-a-finger-swipe-through-javascript-on-the-iphone-and-android | ||
const deltaX = event1.touches[0].x - event2.touches[0].x // TODO: make addition and substraction easier for Point | ||
const deltaY = event1.touches[0].y - event2.touches[0].y | ||
// TODO: return an enum instead | ||
// console.log(deltaX, deltaY) | ||
if (Math.abs(deltaX) > Math.abs(deltaY)) { | ||
if (deltaX > 0) return 'left' | ||
else return 'right' | ||
} else { | ||
if (deltaY > 0) return 'up' | ||
else return 'down' | ||
} | ||
} | ||
export default Swipe |
import Point from '../models/Point' | ||
class Wheel { | ||
constructor (options) { | ||
this.el = null | ||
this.lastTouches = null | ||
this.detecting = false | ||
this.zoomFactor = options.zoomFactor | ||
} | ||
export default { | ||
options: { | ||
preventDefault: false | ||
}, | ||
listen (action) { | ||
// console.log('Wheel::listen') | ||
if (!(action instanceof Function)) throw new TypeError('action must be a function') | ||
this.action = Wheel.action(this, action) | ||
this.el.addEventListener('wheel', this.action, true) | ||
} | ||
// life cycle handlers | ||
listen () { | ||
this.on(this.options.preventDefault ? 'wheel' : 'wheel.passive', this.moveHandler) | ||
console.log('Wheel::listen') | ||
}, | ||
unlisten () { | ||
// console.log('Wheel::unlisten') | ||
this.el.removeEventListener('wheel', this.action, true) | ||
} | ||
console.log('Wheel::unlisten') | ||
this.off(this.options.preventDefault ? 'wheel' : 'wheel.passive', this.moveHandler) | ||
}, | ||
setElement (el) { | ||
// TODO: unlisten on el before changing | ||
this.el = el | ||
} | ||
moveHandler (event) { | ||
// touchpads can give event.deltaY == 0, which is something we never want to handle | ||
if (event.deltaY === 0) return | ||
destroy () { | ||
this.el = null | ||
this.action = null | ||
this.lastTouches = null | ||
event.point = event.touches[0] | ||
this.fire('wheelEvent', event) | ||
} | ||
static action (wheel, action) { | ||
return event => { | ||
// event.preventDefault() | ||
// event.stopImmediatePropagation() | ||
// event.stopPropagation() | ||
// event.returnValue = false | ||
// touchpads can give event.deltaY == 0, which is something we never want to handle | ||
if (event.deltaY === 0) return | ||
wheel.unlisten() | ||
this.detecting = true | ||
const wheelEventData = { | ||
point: normalizeEvent(event).touches[0], | ||
scale: wheel.zoomFactor * -event.deltaY | ||
} | ||
// console.log(wheelEventData) | ||
action('wheelDelta', wheelEventData) | ||
// tell event listeners | ||
const wheelEvent = new CustomEvent('wheelDelta', { detail: wheelEventData }) | ||
if (!wheel.el.dispatchEvent(wheelEvent)) { | ||
endHandler() | ||
console.log('Wheel::action - event was cancelled') | ||
} | ||
endHandler() | ||
function endHandler () { | ||
wheel.detecting = false | ||
wheel.listen(action) | ||
} | ||
} | ||
} | ||
} | ||
function normalizeEvent(ev) { | ||
const event = {} | ||
// console.log(ev) | ||
event.touches = [ | ||
new Point({ x: ev.pageX, y: ev.pageY }) | ||
] | ||
event.type = null | ||
event.timeStamp = Date.now() | ||
return event | ||
} | ||
export default Wheel |
159
src/index.js
@@ -0,10 +1,157 @@ | ||
import Trait from 'traits.js' | ||
import NativeEvents from './mixins/NativeEvents' | ||
import Translate3d from './models/Translate3d' | ||
import Point from './models/Point' | ||
import { map, each } from './utils' | ||
import Zoom from './referents/Zoom' | ||
// import Move from './referents/Move' | ||
// library exports | ||
import Observer from './mixins/Observer' | ||
import { createPanzoom, PanZoom } from './PanZoom' | ||
import Pinch from './gestures/Pinch' | ||
import Pan from './gestures/Pan' | ||
import Wheel from './gestures/Wheel' | ||
import Swipe from './gestures/Swipe' | ||
export { | ||
createPanzoom, | ||
PanZoom, // only exposed to test | ||
Translate3d, // only exposed to test | ||
Observer // only exposed to test | ||
/** | ||
* | ||
* @param {HTMLElement} el | ||
* @param {object} referent | ||
* @param {object} options | ||
*/ | ||
function panzoom (el, referent = Zoom, options) { | ||
if (!el) throw new TypeError('the first argument to panzoom must be an Element') | ||
const initializedReferent = initReferent(referent, el, options) | ||
console.log(initializedReferent) | ||
initializedReferent.listen() | ||
return initializedReferent | ||
} | ||
function initReferent (referent, el, options) { | ||
if (!(referent.gestures && Object.values(referent.gestures)[0])) { | ||
throw new Error('Referent must have gestures') | ||
} | ||
// mixin options from referent with option argument | ||
options = Object.assign({}, referent.options, options) | ||
// if (referent.isInitialized) { | ||
// throw new Error('Referent is initialized - use .listen() or destroy()') | ||
// } | ||
// referent.isInitialized = true | ||
// shared observer between a referent and all of its gestures | ||
let hivemind = NativeEvents() | ||
let isListening = false | ||
let proxy = Trait.create(Object.prototype, | ||
Trait.compose( | ||
Trait.resolve({ destroy: 'destroyListeners' }, hivemind), | ||
Trait.resolve( | ||
{ | ||
'listen': 'listenReferent', | ||
'unlisten': 'unlistenReferent', | ||
'destroy': 'destroyReferent', | ||
'options': undefined, | ||
'gestures': undefined | ||
}, | ||
Trait(referent) | ||
), | ||
Trait({ | ||
el, | ||
options, | ||
get isListening () { return isListening }, | ||
gestures: map( | ||
(gesture, name) => { | ||
gesture.el = el | ||
const defaultOptions = {} | ||
each((value, key) => { | ||
// better error message than the one from traits.js | ||
if (value.required && !options[key]) { | ||
throw new Error(`options is missing ${key} - required by ${isNaN(name) ? name : 'a'} gesture`) | ||
} | ||
if (value.required) defaultOptions[key] = Trait.required | ||
else defaultOptions[key] = value | ||
}, gesture.options) | ||
// convert a gesture's options record into a trait instance where | ||
// `options` overwrite a gesture's default options | ||
try { | ||
gesture.options = Trait.object(Object.assign((defaultOptions), options)) | ||
} catch (error) { | ||
throw new Error(`${error.message} in options`) | ||
} | ||
// convert gesture record into a trait instance | ||
const t = Trait.create( | ||
Object.prototype, | ||
Trait.compose(Trait(gesture), hivemind) | ||
) | ||
return t | ||
}, | ||
referent.gestures | ||
), | ||
listen (arg) { | ||
if (this.isListening) return | ||
each(gesture => { | ||
gesture.listen(arg) | ||
}, this.gestures) | ||
this.listenReferent(arg) | ||
// TODO: maybe not needed since we call listen on all gestures and they add the native event handler via NativeEvents.js | ||
this.addNativeEventHandlers() | ||
isListening = true | ||
}, | ||
unlisten (arg) { | ||
this.removeNativeEventHandlers() | ||
each(gesture => { | ||
gesture.unlisten(arg) | ||
}, this.gestures) | ||
if (this.unlistenReferent) this.unlistenReferent(arg) | ||
isListening = false | ||
}, | ||
destroy () { | ||
this.destroyListeners() | ||
if (this.destroyReferent) this.destroyReferent() | ||
return proxy = hivemind = null | ||
} | ||
}) | ||
) | ||
) | ||
return proxy | ||
} | ||
panzoom.Point = Point | ||
panzoom.Translate3d = Translate3d | ||
panzoom.Observer = Observer | ||
panzoom.gestures = {} | ||
panzoom.gestures.Pinch = Pinch | ||
panzoom.gestures.Pan = Pan | ||
panzoom.gestures.Wheel = Wheel | ||
panzoom.gestures.Swipe = Swipe | ||
panzoom.referents = {} | ||
panzoom.referents.Zoom = Zoom | ||
export default panzoom |
@@ -1,4 +0,9 @@ | ||
'use strict' | ||
import { compose, partial, each, filter, reduce, map } from '../utils' | ||
function Observer () { | ||
const mapReduce = compose( | ||
partial(reduce)((a, b) => a.indexOf(b) > -1 ? a : a.concat([b]), []), | ||
partial(map)(x => x[0]) | ||
) | ||
export default function Observer () { | ||
let listeners = [] | ||
@@ -8,4 +13,4 @@ | ||
on (eventName, f, reject) { | ||
if (!(f instanceof Function)) throw new TypeError('event handler is not a function') | ||
if (!(reject instanceof Function)) reject = f | ||
listeners.push([eventName, f, reject]) | ||
@@ -15,6 +20,7 @@ }, | ||
fire (eventName, ...args) { | ||
listeners.forEach(listener => { | ||
each(listener => { | ||
if (listener[0] === eventName) { | ||
try { | ||
listener[1].apply(listener[1], args) | ||
listener[1].apply(this, args) | ||
// listener[1](...args) | ||
} catch (error) { | ||
@@ -24,7 +30,15 @@ listener[2](error) | ||
} | ||
}) | ||
}, listeners) | ||
}, | ||
/** | ||
* | ||
* @param {string} eventName The event type you want to remove | ||
* @param {function} f The function listening for the event type to | ||
* distinguish from other listeners for the same event type | ||
*/ | ||
off (eventName, f) { | ||
listeners = listeners.filter( | ||
let notRemoved = true | ||
listeners = filter( | ||
listener => { | ||
@@ -34,7 +48,9 @@ if (listener[0] === eventName) { | ||
if (!f) return false | ||
else return listener[1] === f ? false : true | ||
else return listener[1] === f ? notRemoved = false : true | ||
} | ||
return true | ||
} | ||
) | ||
, listeners) | ||
return !notRemoved | ||
}, | ||
@@ -72,6 +88,8 @@ | ||
listeners = null | ||
}, | ||
get currentListenerTypes () { | ||
return mapReduce(listeners) | ||
} | ||
} | ||
} | ||
export default Observer |
@@ -1,2 +0,1 @@ | ||
class Point { | ||
@@ -7,4 +6,28 @@ constructor ({x = 0, y = 0}) { | ||
} | ||
/** | ||
* Substract point from this point | ||
* @param {Point} point | ||
* @returns {Point} | ||
*/ | ||
delta (point) { | ||
return new Point({ | ||
x: this.x - point.x, | ||
y: this.y - point.y | ||
}) | ||
} | ||
/** | ||
* Calculate the distance bewteen 2 points | ||
* @param {Point} point | ||
* @returns {number} | ||
*/ | ||
distance (point) { | ||
const delta = this.delta(point) | ||
return Math.sqrt( | ||
delta.x ** 2 + delta.y ** 2 | ||
) | ||
} | ||
} | ||
export default Point |
@@ -1,21 +0,70 @@ | ||
import { PixelUnit } from './LengthUnit' | ||
import * as units from '../mixins/LengthUnits' | ||
const FLOATING = '(\\-?[\\d\\.e]+)' | ||
const COMMA_SPACE = '\\,?\\s*' | ||
const R_MATRIX = new RegExp( | ||
'^matrix\\(' + | ||
FLOATING + COMMA_SPACE + | ||
FLOATING + COMMA_SPACE + | ||
FLOATING + COMMA_SPACE + | ||
FLOATING + COMMA_SPACE + | ||
FLOATING + COMMA_SPACE + | ||
FLOATING + '\\)$' | ||
) | ||
class Translate3d { | ||
constructor ({tx, ty, tz} = {tx: 0, ty: 0, tz: 0}) { | ||
this.tx = parseFloat(tx) | ||
this.ty = parseFloat(ty) | ||
this.tz = parseFloat(tz) | ||
/** | ||
* @param {number} tx | ||
* @param {number} tx | ||
* @param {number} tz Is a <length> representing the z | ||
* component of the translating vector. It can't be a | ||
* <percentage> value; in that case the property | ||
* containing the transform is considered invalid. | ||
*/ | ||
constructor (tx, ty, tz) { | ||
Object.assign(this, units) | ||
this.unit = new PixelUnit() | ||
this.tx = parseFloat(tx || 0) | ||
this.ty = parseFloat(ty || 0) | ||
this.tz = parseFloat(tz || 1) | ||
this.unit = this.getUnit(tx) | ||
this.el = null | ||
} | ||
setUnit (unit) { | ||
this.unit = unit | ||
this.unit = this.getUnit(unit) | ||
} | ||
setElement (el) { | ||
this.el = el | ||
} | ||
toString () { | ||
return `translate3d(${this.tx + this.unit}, ${this.ty + this.unit}, ${this.tz + this.unit});` | ||
return Translate3d.getMatrixString(this.toObject()) | ||
} | ||
toObject () { | ||
const matrix = { | ||
tx: this.tx, | ||
ty: this.ty, | ||
tz: this.tz | ||
} | ||
return this.unit === '%' | ||
? this.percentToPixelMatrix(/* this.el, matrix */) | ||
: matrix | ||
} | ||
static getMatrixString (transform) { | ||
return `matrix(${transform.tz}, 0, 0, ${transform.tz}, ${transform.tx}, ${transform.ty})` | ||
} | ||
static parse (cssTransform) { | ||
const matrix = R_MATRIX.exec(cssTransform) | ||
return matrix | ||
? new Translate3d(matrix[5], matrix[6], matrix[1]) | ||
: null | ||
} | ||
} | ||
export default Translate3d |
@@ -1,2 +0,1 @@ | ||
class ZoomLevel { | ||
@@ -3,0 +2,0 @@ constructor (levels = []) { |
@@ -118,1 +118,20 @@ const test = require('tap').test | ||
}) | ||
test('Observer returns a list of listener types via currentListenerTypes', t => { | ||
t.plan(3) | ||
const observer = createObserver() | ||
const expected = ['wheel'] | ||
observer.on('wheel', noop) | ||
t.match(observer.currentListenerTypes, expected, 'should match [wheel]') | ||
observer.on('mousedown', noop) | ||
expected.push('mousedown') | ||
t.match(observer.currentListenerTypes.sort(), expected.sort(), 'should match [wheel, mousedown]') | ||
observer.on('wheel', noop) | ||
t.strictSame(observer.currentListenerTypes.sort(), expected.sort(), 'should match [wheel, mousedown]') | ||
function noop () {} | ||
}) |
const test = require('tap').test | ||
const JsDom = require('jsdom').JSDOM | ||
const JSDOM = require('jsdom').JSDOM | ||
const panzoom = require('../dist/panzoom') | ||
const globalDom = new JsDom('', { pretendToBeVisual: true }) | ||
global.window = globalDom.window; | ||
global.document = globalDom.window.document; | ||
global.HTMLElement = globalDom.window.HTMLElement; | ||
const globalDom = new JSDOM('', { pretendToBeVisual: true }) | ||
global.window = globalDom.window | ||
global.document = globalDom.window.document | ||
global.HTMLElement = globalDom.window.HTMLElement | ||
@@ -21,7 +21,7 @@ const PanZoom = panzoom.PanZoom | ||
const dom = new JsDom(`<body><div class='content'></div></body>`); | ||
const document = dom.window.document; | ||
const content = document.querySelector('.content'); | ||
const dom = new JSDOM(`<body><div class='content'></div></body>`) | ||
const document = dom.window.document | ||
const content = document.querySelector('.content') | ||
t.ok(panzoom.createPanzoom(content) instanceof PanZoom) | ||
}) | ||
}, {skip: true}) |
const test = require('tap').test | ||
const panzoom = require('../dist/panzoom') | ||
const JSDOM = require('jsdom').JSDOM | ||
const Translate3d = require('../dist/panzoom').Translate3d | ||
const Translate3d = panzoom.Translate3d | ||
// const test = test | ||
// const JSDOM = jsdom.default.JSDOM | ||
// console.log(tap) | ||
const globalDom = new JSDOM('', { pretendToBeVisual: true }) | ||
global.window = globalDom.window | ||
global.document = globalDom.window.document | ||
test('Translate3d returns a CSS string', t => { | ||
test('Translate3d returns a CSS string width default values', t => { | ||
t.plan(1) | ||
const translate3d = new Translate3d() | ||
const expected = 'translate3d(0px, 0px, 0px);' | ||
const expected = 'matrix(1, 0, 0, 1, 0, 0)' | ||
const actual = String(translate3d) | ||
@@ -15,1 +21,81 @@ | ||
}) | ||
test('Translate3d returns a CSS string with parameter values', t => { | ||
t.plan(1) | ||
const translate3d = new Translate3d(100, 200, 0.5) | ||
const expected = 'matrix(0.5, 0, 0, 0.5, 100, 200)' | ||
const actual = String(translate3d) | ||
t.equal(actual, expected) | ||
}) | ||
test('Translate3d will throw if you ask for percentage to pixel values and have not set a container', t => { | ||
t.plan(1) | ||
const translate3d = new Translate3d('100%', '200%', 0.5) | ||
t.throws(() => { String(translate3d) }, Error) | ||
}) | ||
test('Translate3d returns a CSS string with percentage to pixel values', t => { | ||
t.plan(1) | ||
const div = createMockDiv(200, 200) | ||
const translate3d = new Translate3d('100%', '200%', 0.5) | ||
translate3d.setElement(div) | ||
const expected = 'matrix(0.5, 0, 0, 0.5, 200, 400)' | ||
const actual = String(translate3d) | ||
t.equal(actual, expected) | ||
}) | ||
test('Translate3d can translate percentage values to pixel values', t => { | ||
t.plan(1) | ||
const div = createMockDiv(200, 200) | ||
const translate3d = new Translate3d('50%', '100%', '0.5') | ||
translate3d.setElement(div) | ||
const expected = { tx: 100, ty: 200, tz: .5 } | ||
const actual = translate3d.toObject() | ||
t.match(actual, expected) | ||
}) | ||
test('Translate3d can parse a css style string', t => { | ||
t.plan(3) | ||
const translate3d = Translate3d.parse('matrix(1.3, 0, 0, 1.3, 0, 0)') | ||
t.ok(translate3d instanceof Translate3d, '.parse() should return an instance of Translate3d') | ||
let expected = { tx: 0, ty: 0, tz: 1.3 } | ||
let actual = translate3d | ||
t.match(actual, expected, ' should return a string identical to the parsed string') | ||
expected = null | ||
actual = Translate3d.parse('matrix(something very wrong)') | ||
t.equal(actual, expected, '.parse() should return null for incorrect transform matrix') | ||
}) | ||
function createMockDiv (width, height) { | ||
const div = global.document.createElement('div') | ||
Object.assign(div.style, { | ||
width: width + '', | ||
height: height + '', | ||
}) | ||
// we have to mock this for jsdom. | ||
div.getBoundingClientRect = () => ({ | ||
width, | ||
height, | ||
top: 0, | ||
left: 0, | ||
right: width, | ||
bottom: height, | ||
}) | ||
return div | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
1194769
47
3718
43
1
2
80
1
+ Addedtraits.js@^1.1.4
+ Addedtraits.js@1.1.5(transitive)