'use strict'; /** * Allows to drag and zoom svg elements */ var wheel = require('wheel'); var animate = require('amator'); var eventify = require('ngraph.events'); var kinetic = require('./lib/kinetic.js'); var createTextSelectionInterceptor = require('./lib/createTextSelectionInterceptor.js'); var domTextSelectionInterceptor = createTextSelectionInterceptor(); var fakeTextSelectorInterceptor = createTextSelectionInterceptor(true); var Transform = require('./lib/transform.js'); var makeSvgController = require('./lib/svgController.js'); var makeDomController = require('./lib/domController.js'); var defaultZoomSpeed = 1; var defaultDoubleTapZoomSpeed = 1.75; var doubleTapSpeedInMS = 300; var clickEventTimeInMS = 200; module.exports = createPanZoom; /** * Creates a new instance of panzoom, so that an object can be panned and zoomed * * @param {DOMElement} domElement where panzoom should be attached. * @param {Object} options that configure behavior. */ function createPanZoom(domElement, options) { options = options || {}; var panController = options.controller; if (!panController) { if (makeSvgController.canAttach(domElement)) { panController = makeSvgController(domElement, options); } else if (makeDomController.canAttach(domElement)) { panController = makeDomController(domElement, options); } } if (!panController) { throw new Error( 'Cannot create panzoom for the current type of dom element' ); } var owner = panController.getOwner(); // just to avoid GC pressure, every time we do intermediate transform // we return this object. For internal use only. Never give it back to the consumer of this library var storedCTMResult = { x: 0, y: 0 }; var isDirty = false; var transform = new Transform(); if (panController.initTransform) { panController.initTransform(transform); } var filterKey = typeof options.filterKey === 'function' ? options.filterKey : noop; // TODO: likely need to unite pinchSpeed with zoomSpeed var pinchSpeed = typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1; var bounds = options.bounds; var maxZoom = typeof options.maxZoom === 'number' ? options.maxZoom : Number.POSITIVE_INFINITY; var minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0; var boundsPadding = typeof options.boundsPadding === 'number' ? options.boundsPadding : 0.05; var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; var beforeWheel = options.beforeWheel || noop; var beforeMouseDown = options.beforeMouseDown || noop; var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed; var transformOrigin = parseTransformOrigin(options.transformOrigin); var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor; validateBounds(bounds); if (options.autocenter) { autocenter(); } var frameAnimation; var lastTouchEndTime = 0; var lastTouchStartTime = 0; var pendingClickEventTimeout = 0; var lastMouseDownedEvent = null; var lastMouseDownTime = new Date(); var lastSingleFingerOffset; var touchInProgress = false; // We only need to fire panstart when actual move happens var panstartFired = false; // cache mouse coordinates here var mouseX; var mouseY; // Where the first click has happened, so that we can differentiate // between pan and click var clickX; var clickY; var pinchZoomLength; var smoothScroll; if ('smoothScroll' in options && !options.smoothScroll) { // If user explicitly asked us not to use smooth scrolling, we obey smoothScroll = rigidScroll(); } else { // otherwise we use forward smoothScroll settings to kinetic API // which makes scroll smoothing. smoothScroll = kinetic(getPoint, scroll, options.smoothScroll); } var moveByAnimation; var zoomToAnimation; var multiTouch; var paused = false; listenForEvents(); var api = { dispose: dispose, moveBy: internalMoveBy, moveTo: moveTo, smoothMoveTo: smoothMoveTo, centerOn: centerOn, zoomTo: publicZoomTo, zoomAbs: zoomAbs, smoothZoom: smoothZoom, smoothZoomAbs: smoothZoomAbs, showRectangle: showRectangle, pause: pause, resume: resume, isPaused: isPaused, getTransform: getTransformModel, getMinZoom: getMinZoom, setMinZoom: setMinZoom, getMaxZoom: getMaxZoom, setMaxZoom: setMaxZoom, getTransformOrigin: getTransformOrigin, setTransformOrigin: setTransformOrigin, getZoomSpeed: getZoomSpeed, setZoomSpeed: setZoomSpeed }; eventify(api); var initialX = typeof options.initialX === 'number' ? options.initialX : transform.x; var initialY = typeof options.initialY === 'number' ? options.initialY : transform.y; var initialZoom = typeof options.initialZoom === 'number' ? options.initialZoom : transform.scale; if(initialX != transform.x || initialY != transform.y || initialZoom != transform.scale){ zoomAbs(initialX, initialY, initialZoom); } return api; function pause() { releaseEvents(); paused = true; } function resume() { if (paused) { listenForEvents(); paused = false; } } function isPaused() { return paused; } function showRectangle(rect) { // TODO: this duplicates autocenter. I think autocenter should go. var clientRect = owner.getBoundingClientRect(); var size = transformToScreen(clientRect.width, clientRect.height); var rectWidth = rect.right - rect.left; var rectHeight = rect.bottom - rect.top; if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) { throw new Error('Invalid rectangle'); } var dw = size.x / rectWidth; var dh = size.y / rectHeight; var scale = Math.min(dw, dh); transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2; transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2; transform.scale = scale; } function transformToScreen(x, y) { if (panController.getScreenCTM) { var parentCTM = panController.getScreenCTM(); var parentScaleX = parentCTM.a; var parentScaleY = parentCTM.d; var parentOffsetX = parentCTM.e; var parentOffsetY = parentCTM.f; storedCTMResult.x = x * parentScaleX - parentOffsetX; storedCTMResult.y = y * parentScaleY - parentOffsetY; } else { storedCTMResult.x = x; storedCTMResult.y = y; } return storedCTMResult; } function autocenter() { var w; // width of the parent var h; // height of the parent var left = 0; var top = 0; var sceneBoundingBox = getBoundingBox(); if (sceneBoundingBox) { // If we have bounding box - use it. left = sceneBoundingBox.left; top = sceneBoundingBox.top; w = sceneBoundingBox.right - sceneBoundingBox.left; h = sceneBoundingBox.bottom - sceneBoundingBox.top; } else { // otherwise just use whatever space we have var ownerRect = owner.getBoundingClientRect(); w = ownerRect.width; h = ownerRect.height; } var bbox = panController.getBBox(); if (bbox.width === 0 || bbox.height === 0) { // we probably do not have any elements in the SVG // just bail out; return; } var dh = h / bbox.height; var dw = w / bbox.width; var scale = Math.min(dw, dh); transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left; transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top; transform.scale = scale; } function getTransformModel() { // TODO: should this be read only? return transform; } function getMinZoom() { return minZoom; } function setMinZoom(newMinZoom) { minZoom = newMinZoom; } function getMaxZoom() { return maxZoom; } function setMaxZoom(newMaxZoom) { maxZoom = newMaxZoom; } function getTransformOrigin() { return transformOrigin; } function setTransformOrigin(newTransformOrigin) { transformOrigin = parseTransformOrigin(newTransformOrigin); } function getZoomSpeed() { return speed; } function setZoomSpeed(newSpeed) { if (!Number.isFinite(newSpeed)) { throw new Error('Zoom speed should be a number'); } speed = newSpeed; } function getPoint() { return { x: transform.x, y: transform.y }; } function moveTo(x, y) { transform.x = x; transform.y = y; keepTransformInsideBounds(); triggerEvent('pan'); makeDirty(); } function moveBy(dx, dy) { moveTo(transform.x + dx, transform.y + dy); } function keepTransformInsideBounds() { var boundingBox = getBoundingBox(); if (!boundingBox) return; var adjusted = false; var clientRect = getClientRect(); var diff = boundingBox.left - clientRect.right; if (diff > 0) { transform.x += diff; adjusted = true; } // check the other side: diff = boundingBox.right - clientRect.left; if (diff < 0) { transform.x += diff; adjusted = true; } // y axis: diff = boundingBox.top - clientRect.bottom; if (diff > 0) { // we adjust transform, so that it matches exactly our bounding box: // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale => // transform.y = boundingBox.top - (clientRect.bottom - transform.y) => // transform.y = diff + transform.y => transform.y += diff; adjusted = true; } diff = boundingBox.bottom - clientRect.top; if (diff < 0) { transform.y += diff; adjusted = true; } return adjusted; } /** * Returns bounding box that should be used to restrict scene movement. */ function getBoundingBox() { if (!bounds) return; // client does not want to restrict movement if (typeof bounds === 'boolean') { // for boolean type we use parent container bounds var ownerRect = owner.getBoundingClientRect(); var sceneWidth = ownerRect.width; var sceneHeight = ownerRect.height; return { left: sceneWidth * boundsPadding, top: sceneHeight * boundsPadding, right: sceneWidth * (1 - boundsPadding), bottom: sceneHeight * (1 - boundsPadding) }; } return bounds; } function getClientRect() { var bbox = panController.getBBox(); var leftTop = client(bbox.left, bbox.top); return { left: leftTop.x, top: leftTop.y, right: bbox.width * transform.scale + leftTop.x, bottom: bbox.height * transform.scale + leftTop.y }; } function client(x, y) { return { x: x * transform.scale + transform.x, y: y * transform.scale + transform.y }; } function makeDirty() { isDirty = true; frameAnimation = window.requestAnimationFrame(frame); } function zoomByRatio(clientX, clientY, ratio) { if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { throw new Error('zoom requires valid numbers'); } var newScale = transform.scale * ratio; if (newScale < minZoom) { if (transform.scale === minZoom) return; ratio = minZoom / transform.scale; } if (newScale > maxZoom) { if (transform.scale === maxZoom) return; ratio = maxZoom / transform.scale; } var size = transformToScreen(clientX, clientY); transform.x = size.x - ratio * (size.x - transform.x); transform.y = size.y - ratio * (size.y - transform.y); // TODO: https://github.com/anvaka/panzoom/issues/112 if (bounds && boundsPadding === 1 && minZoom === 1) { transform.scale *= ratio; keepTransformInsideBounds(); } else { var transformAdjusted = keepTransformInsideBounds(); if (!transformAdjusted) transform.scale *= ratio; } triggerEvent('zoom'); makeDirty(); } function zoomAbs(clientX, clientY, zoomLevel) { var ratio = zoomLevel / transform.scale; zoomByRatio(clientX, clientY, ratio); } function centerOn(ui) { var parent = ui.ownerSVGElement; if (!parent) throw new Error('ui element is required to be within the scene'); // TODO: should i use controller's screen CTM? var clientRect = ui.getBoundingClientRect(); var cx = clientRect.left + clientRect.width / 2; var cy = clientRect.top + clientRect.height / 2; var container = parent.getBoundingClientRect(); var dx = container.width / 2 - cx; var dy = container.height / 2 - cy; internalMoveBy(dx, dy, true); } function smoothMoveTo(x, y){ internalMoveBy(x - transform.x, y - transform.y, true); } function internalMoveBy(dx, dy, smooth) { if (!smooth) { return moveBy(dx, dy); } if (moveByAnimation) moveByAnimation.cancel(); var from = { x: 0, y: 0 }; var to = { x: dx, y: dy }; var lastX = 0; var lastY = 0; moveByAnimation = animate(from, to, { step: function (v) { moveBy(v.x - lastX, v.y - lastY); lastX = v.x; lastY = v.y; } }); } function scroll(x, y) { cancelZoomAnimation(); moveTo(x, y); } function dispose() { releaseEvents(); } function listenForEvents() { owner.addEventListener('mousedown', onMouseDown, { passive: false }); owner.addEventListener('dblclick', onDoubleClick, { passive: false }); owner.addEventListener('touchstart', onTouch, { passive: false }); owner.addEventListener('keydown', onKeyDown, { passive: false }); // Need to listen on the owner container, so that we are not limited // by the size of the scrollable domElement wheel.addWheelListener(owner, onMouseWheel, { passive: false }); makeDirty(); } function releaseEvents() { wheel.removeWheelListener(owner, onMouseWheel); owner.removeEventListener('mousedown', onMouseDown); owner.removeEventListener('keydown', onKeyDown); owner.removeEventListener('dblclick', onDoubleClick); owner.removeEventListener('touchstart', onTouch); if (frameAnimation) { window.cancelAnimationFrame(frameAnimation); frameAnimation = 0; } smoothScroll.cancel(); releaseDocumentMouse(); releaseTouches(); textSelection.release(); triggerPanEnd(); } function frame() { if (isDirty) applyTransform(); } function applyTransform() { isDirty = false; // TODO: Should I allow to cancel this? panController.applyTransform(transform); triggerEvent('transform'); frameAnimation = 0; } function onKeyDown(e) { var x = 0, y = 0, z = 0; if (e.keyCode === 38) { y = 1; // up } else if (e.keyCode === 40) { y = -1; // down } else if (e.keyCode === 37) { x = 1; // left } else if (e.keyCode === 39) { x = -1; // right } else if (e.keyCode === 189 || e.keyCode === 109) { // DASH or SUBTRACT z = 1; // `-` - zoom out } else if (e.keyCode === 187 || e.keyCode === 107) { // EQUAL SIGN or ADD z = -1; // `=` - zoom in (equal sign on US layout is under `+`) } if (filterKey(e, x, y, z)) { // They don't want us to handle the key: https://github.com/anvaka/panzoom/issues/45 return; } if (x || y) { e.preventDefault(); e.stopPropagation(); var clientRect = owner.getBoundingClientRect(); // movement speed should be the same in both X and Y direction: var offset = Math.min(clientRect.width, clientRect.height); var moveSpeedRatio = 0.05; var dx = offset * moveSpeedRatio * x; var dy = offset * moveSpeedRatio * y; // TODO: currently we do not animate this. It could be better to have animation internalMoveBy(dx, dy); } if (z) { var scaleMultiplier = getScaleMultiplier(z * 100); var offset = transformOrigin ? getTransformOriginOffset() : midPoint(); publicZoomTo(offset.x, offset.y, scaleMultiplier); } } function midPoint() { var ownerRect = owner.getBoundingClientRect(); return { x: ownerRect.width / 2, y: ownerRect.height / 2 }; } function onTouch(e) { // let them override the touch behavior beforeTouch(e); clearPendingClickEventTimeout(); if (e.touches.length === 1) { return handleSingleFingerTouch(e, e.touches[0]); } else if (e.touches.length === 2) { // handleTouchMove() will care about pinch zoom. pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); multiTouch = true; startTouchListenerIfNeeded(); } } function beforeTouch(e) { // TODO: Need to unify this filtering names. E.g. use `beforeTouch` if (options.onTouch && !options.onTouch(e)) { // if they return `false` from onTouch, we don't want to stop // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 return; } e.stopPropagation(); e.preventDefault(); } function beforeDoubleClick(e) { clearPendingClickEventTimeout(); // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` if (options.onDoubleClick && !options.onDoubleClick(e)) { // if they return `false` from onTouch, we don't want to stop // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 return; } e.preventDefault(); e.stopPropagation(); } function handleSingleFingerTouch(e) { lastTouchStartTime = new Date(); var touch = e.touches[0]; var offset = getOffsetXY(touch); lastSingleFingerOffset = offset; var point = transformToScreen(offset.x, offset.y); mouseX = point.x; mouseY = point.y; clickX = mouseX; clickY = mouseY; smoothScroll.cancel(); startTouchListenerIfNeeded(); } function startTouchListenerIfNeeded() { if (touchInProgress) { // no need to do anything, as we already listen to events; return; } touchInProgress = true; document.addEventListener('touchmove', handleTouchMove); document.addEventListener('touchend', handleTouchEnd); document.addEventListener('touchcancel', handleTouchEnd); } function handleTouchMove(e) { if (e.touches.length === 1) { e.stopPropagation(); var touch = e.touches[0]; var offset = getOffsetXY(touch); var point = transformToScreen(offset.x, offset.y); var dx = point.x - mouseX; var dy = point.y - mouseY; if (dx !== 0 && dy !== 0) { triggerPanStart(); } mouseX = point.x; mouseY = point.y; internalMoveBy(dx, dy); } else if (e.touches.length === 2) { // it's a zoom, let's find direction multiTouch = true; var t1 = e.touches[0]; var t2 = e.touches[1]; var currentPinchLength = getPinchZoomLength(t1, t2); // since the zoom speed is always based on distance from 1, we need to apply // pinch speed only on that distance from 1: var scaleMultiplier = 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; var firstTouchPoint = getOffsetXY(t1); var secondTouchPoint = getOffsetXY(t2); mouseX = (firstTouchPoint.x + secondTouchPoint.x) / 2; mouseY = (firstTouchPoint.y + secondTouchPoint.y) / 2; if (transformOrigin) { var offset = getTransformOriginOffset(); mouseX = offset.x; mouseY = offset.y; } publicZoomTo(mouseX, mouseY, scaleMultiplier); pinchZoomLength = currentPinchLength; e.stopPropagation(); e.preventDefault(); } } function clearPendingClickEventTimeout() { if (pendingClickEventTimeout) { clearTimeout(pendingClickEventTimeout); pendingClickEventTimeout = 0; } } function handlePotentialClickEvent(e) { // we could still be in the double tap mode, let's wait until double tap expires, // and then notify: if (!options.onClick) return; clearPendingClickEventTimeout(); var dx = mouseX - clickX; var dy = mouseY - clickY; var l = Math.sqrt(dx * dx + dy * dy); if (l > 5) return; // probably they are panning, ignore it pendingClickEventTimeout = setTimeout(function() { pendingClickEventTimeout = 0; options.onClick(e); }, doubleTapSpeedInMS); } function handleTouchEnd(e) { clearPendingClickEventTimeout(); if (e.touches.length > 0) { var offset = getOffsetXY(e.touches[0]); var point = transformToScreen(offset.x, offset.y); mouseX = point.x; mouseY = point.y; } else { var now = new Date(); if (now - lastTouchEndTime < doubleTapSpeedInMS) { // They did a double tap here if (transformOrigin) { var offset = getTransformOriginOffset(); smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); } else { // We want untransformed x/y here. smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed); } } else if (now - lastTouchStartTime < clickEventTimeInMS) { handlePotentialClickEvent(e); } lastTouchEndTime = now; triggerPanEnd(); releaseTouches(); } } function getPinchZoomLength(finger1, finger2) { var dx = finger1.clientX - finger2.clientX; var dy = finger1.clientY - finger2.clientY; return Math.sqrt(dx * dx + dy * dy); } function onDoubleClick(e) { beforeDoubleClick(e); var offset = getOffsetXY(e); if (transformOrigin) { // TODO: looks like this is duplicated in the file. // Need to refactor offset = getTransformOriginOffset(); } smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); } function onMouseDown(e) { clearPendingClickEventTimeout(); // if client does not want to handle this event - just ignore the call if (beforeMouseDown(e)) return; lastMouseDownedEvent = e; lastMouseDownTime = new Date(); if (touchInProgress) { // modern browsers will fire mousedown for touch events too // we do not want this: touch is handled separately. e.stopPropagation(); return false; } // for IE, left click == 1 // for Firefox, left click == 0 var isLeftButton = (e.button === 1 && window.event !== null) || e.button === 0; if (!isLeftButton) return; smoothScroll.cancel(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); clickX = mouseX = point.x; clickY = mouseY = point.y; // We need to listen on document itself, since mouse can go outside of the // window, and we will loose it document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); textSelection.capture(e.target || e.srcElement); return false; } function onMouseMove(e) { // no need to worry about mouse events when touch is happening if (touchInProgress) return; triggerPanStart(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); var dx = point.x - mouseX; var dy = point.y - mouseY; mouseX = point.x; mouseY = point.y; internalMoveBy(dx, dy); } function onMouseUp() { var now = new Date(); if (now - lastMouseDownTime < clickEventTimeInMS) handlePotentialClickEvent(lastMouseDownedEvent); textSelection.release(); triggerPanEnd(); releaseDocumentMouse(); } function releaseDocumentMouse() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); panstartFired = false; } function releaseTouches() { document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); document.removeEventListener('touchcancel', handleTouchEnd); panstartFired = false; multiTouch = false; touchInProgress = false; } function onMouseWheel(e) { // if client does not want to handle this event - just ignore the call if (beforeWheel(e)) return; smoothScroll.cancel(); var delta = e.deltaY; if (e.deltaMode > 0) delta *= 100; var scaleMultiplier = getScaleMultiplier(delta); if (scaleMultiplier !== 1) { var offset = transformOrigin ? getTransformOriginOffset() : getOffsetXY(e); publicZoomTo(offset.x, offset.y, scaleMultiplier); e.preventDefault(); } } function getOffsetXY(e) { var offsetX, offsetY; // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path. var ownerRect = owner.getBoundingClientRect(); offsetX = e.clientX - ownerRect.left; offsetY = e.clientY - ownerRect.top; return { x: offsetX, y: offsetY }; } function smoothZoom(clientX, clientY, scaleMultiplier) { var fromValue = transform.scale; var from = { scale: fromValue }; var to = { scale: scaleMultiplier * fromValue }; smoothScroll.cancel(); cancelZoomAnimation(); zoomToAnimation = animate(from, to, { step: function (v) { zoomAbs(clientX, clientY, v.scale); }, done: triggerZoomEnd }); } function smoothZoomAbs(clientX, clientY, toScaleValue) { var fromValue = transform.scale; var from = { scale: fromValue }; var to = { scale: toScaleValue }; smoothScroll.cancel(); cancelZoomAnimation(); zoomToAnimation = animate(from, to, { step: function (v) { zoomAbs(clientX, clientY, v.scale); } }); } function getTransformOriginOffset() { var ownerRect = owner.getBoundingClientRect(); return { x: ownerRect.width * transformOrigin.x, y: ownerRect.height * transformOrigin.y }; } function publicZoomTo(clientX, clientY, scaleMultiplier) { smoothScroll.cancel(); cancelZoomAnimation(); return zoomByRatio(clientX, clientY, scaleMultiplier); } function cancelZoomAnimation() { if (zoomToAnimation) { zoomToAnimation.cancel(); zoomToAnimation = null; } } function getScaleMultiplier(delta) { var sign = Math.sign(delta); var deltaAdjustedSpeed = Math.min(0.25, Math.abs(speed * delta / 128)); return 1 - sign * deltaAdjustedSpeed; } function triggerPanStart() { if (!panstartFired) { triggerEvent('panstart'); panstartFired = true; smoothScroll.start(); } } function triggerPanEnd() { if (panstartFired) { // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): if (!multiTouch) smoothScroll.stop(); triggerEvent('panend'); } } function triggerZoomEnd() { triggerEvent('zoomend'); } function triggerEvent(name) { api.fire(name, api); } } function parseTransformOrigin(options) { if (!options) return; if (typeof options === 'object') { if (!isNumber(options.x) || !isNumber(options.y)) failTransformOrigin(options); return options; } failTransformOrigin(); } function failTransformOrigin(options) { console.error(options); throw new Error( [ 'Cannot parse transform origin.', 'Some good examples:', ' "center center" can be achieved with {x: 0.5, y: 0.5}', ' "top center" can be achieved with {x: 0.5, y: 0}', ' "bottom right" can be achieved with {x: 1, y: 1}' ].join('\n') ); } function noop() { } function validateBounds(bounds) { var boundsType = typeof bounds; if (boundsType === 'undefined' || boundsType === 'boolean') return; // this is okay // otherwise need to be more thorough: var validBounds = isNumber(bounds.left) && isNumber(bounds.top) && isNumber(bounds.bottom) && isNumber(bounds.right); if (!validBounds) throw new Error( 'Bounds object is not valid. It can be: ' + 'undefined, boolean (true|false) or an object {left, top, right, bottom}' ); } function isNumber(x) { return Number.isFinite(x); } // IE 11 does not support isNaN: function isNaN(value) { if (Number.isNaN) { return Number.isNaN(value); } return value !== value; } function rigidScroll() { return { start: noop, stop: noop, cancel: noop }; } function autoRun() { if (typeof document === 'undefined') return; var scripts = document.getElementsByTagName('script'); if (!scripts) return; var panzoomScript; for (var i = 0; i < scripts.length; ++i) { var x = scripts[i]; if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { panzoomScript = x; break; } } if (!panzoomScript) return; var query = panzoomScript.getAttribute('query'); if (!query) return; var globalName = panzoomScript.getAttribute('name') || 'pz'; var started = Date.now(); tryAttach(); function tryAttach() { var el = document.querySelector(query); if (!el) { var now = Date.now(); var elapsed = now - started; if (elapsed < 2000) { // Let's wait a bit setTimeout(tryAttach, 100); return; } // If we don't attach within 2 seconds to the target element, consider it a failure console.error('Cannot find the panzoom element', globalName); return; } var options = collectOptions(panzoomScript); console.log(options); window[globalName] = createPanZoom(el, options); } function collectOptions(script) { var attrs = script.attributes; var options = {}; for (var j = 0; j < attrs.length; ++j) { var attr = attrs[j]; var nameValue = getPanzoomAttributeNameValue(attr); if (nameValue) { options[nameValue.name] = nameValue.value; } } return options; } function getPanzoomAttributeNameValue(attr) { if (!attr.name) return; var isPanZoomAttribute = attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; if (!isPanZoomAttribute) return; var name = attr.name.substr(3); var value = JSON.parse(attr.value); return { name: name, value: value }; } } autoRun();