From 81ec85083acd5eb66147f5871dbe5af90d243f5e Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 7 Mar 2020 20:41:03 +0100 Subject: [PATCH] link map widget always rendered centered map --- libraries/panzoom.js | 1193 +++++++++-------- src/public/javascripts/services/link_map.js | 34 +- .../javascripts/services/tab_manager.js | 2 +- .../javascripts/widgets/collapsible_widget.js | 7 +- .../javascripts/widgets/desktop_layout.js | 1 + src/public/javascripts/widgets/link_map.js | 30 +- .../javascripts/widgets/tab_caching_widget.js | 17 +- src/public/javascripts/widgets/tab_row.js | 8 +- 8 files changed, 718 insertions(+), 574 deletions(-) diff --git a/libraries/panzoom.js b/libraries/panzoom.js index 587851041..0f8dce56c 100644 --- a/libraries/panzoom.js +++ b/libraries/panzoom.js @@ -5,20 +5,22 @@ /** * Allows to drag and zoom svg elements */ -var wheel = require('wheel') -var animate = require('amator') +var wheel = require('wheel'); +var animate = require('amator'); var eventify = require('ngraph.events'); -var kinetic = require('./lib/kinetic.js') -var preventTextSelection = require('./lib/textSelectionInterceptor.js')() +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 makeSvgController = require('./lib/svgController.js'); +var makeDomController = require('./lib/domController.js'); -var defaultZoomSpeed = 0.065 -var defaultDoubleTapZoomSpeed = 1.75 -var doubleTapSpeedInMS = 300 +var defaultZoomSpeed = 1; +var defaultDoubleTapZoomSpeed = 1.75; +var doubleTapSpeedInMS = 300; -module.exports = createPanZoom +module.exports = createPanZoom; /** * Creates a new instance of panzoom, so that an object can be panned and zoomed @@ -27,85 +29,89 @@ module.exports = createPanZoom * @param {Object} options that configure behavior. */ function createPanZoom(domElement, options) { - options = options || {} + options = options || {}; - var panController = options.controller + var panController = options.controller; if (!panController) { if (domElement instanceof SVGElement) { - panController = makeSvgController(domElement, options) + panController = makeSvgController(domElement, options); } if (domElement instanceof HTMLElement) { - panController = makeDomController(domElement, options) + panController = makeDomController(domElement, options); } } if (!panController) { - throw new Error('Cannot create panzoom for the current type of dom element') + throw new Error( + 'Cannot create panzoom for the current type of dom element' + ); } - var owner = panController.getOwner() + 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 storedCTMResult = { x: 0, y: 0 }; - var isDirty = false - var transform = new Transform() + var isDirty = false; + var transform = new Transform(); if (panController.initTransform) { - panController.initTransform(transform) + 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 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 speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed + 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) + validateBounds(bounds); if (options.autocenter) { - autocenter() + autocenter(); } - var frameAnimation - - var lastTouchEndTime = 0 - - var touchInProgress = false + var frameAnimation; + var lastTouchEndTime = 0; + var lastSingleFingerOffset; + var touchInProgress = false; // We only need to fire panstart when actual move happens - var panstartFired = false + var panstartFired = false; // cache mouse coordinates here - var mouseX - var mouseY + var mouseX; + var mouseY; - var pinchZoomLength + var pinchZoomLength; - var smoothScroll + var smoothScroll; if ('smoothScroll' in options && !options.smoothScroll) { // If user explicitly asked us not to use smooth scrolling, we obey - smoothScroll = rigidScroll() + smoothScroll = rigidScroll(); } else { // otherwise we use forward smoothScroll settings to kinetic API // which makes scroll smoothing. - smoothScroll = kinetic(getPoint, scroll, options.smoothScroll) + smoothScroll = kinetic(getPoint, scroll, options.smoothScroll); } - var moveByAnimation - var zoomToAnimation + var moveByAnimation; + var zoomToAnimation; - var multiTouch - var paused = false + var multiTouch; + var paused = false; - listenForEvents() + listenForEvents(); var api = { dispose: dispose, @@ -115,6 +121,7 @@ function createPanZoom(domElement, options) { zoomTo: publicZoomTo, zoomAbs: zoomAbs, smoothZoom: smoothZoom, + smoothZoomAbs: smoothZoomAbs, showRectangle: showRectangle, pause: pause, @@ -122,24 +129,35 @@ function createPanZoom(domElement, options) { isPaused: isPaused, getTransform: getTransformModel, + getMinZoom: getMinZoom, + setMinZoom: setMinZoom, + getMaxZoom: getMaxZoom, + setMaxZoom: setMaxZoom, + + getTransformOrigin: getTransformOrigin, + setTransformOrigin: setTransformOrigin, + + getZoomSpeed: getZoomSpeed, + setZoomSpeed: setZoomSpeed, + getOwner: () => owner - } + }; eventify(api); return api; function pause() { - releaseEvents() - paused = true + releaseEvents(); + paused = true; } function resume() { if (paused) { - listenForEvents() - paused = false + listenForEvents(); + paused = false; } } @@ -149,274 +167,302 @@ function createPanZoom(domElement, options) { function showRectangle(rect) { // TODO: this duplicates autocenter. I think autocenter should go. - var clientRect = owner.getBoundingClientRect() - var size = transformToScreen(clientRect.width, clientRect.height) + var clientRect = owner.getBoundingClientRect(); + var size = transformToScreen(clientRect.width, clientRect.height); - var rectWidth = rect.right - rect.left - var rectHeight = rect.bottom - rect.top + 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 + 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 + 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 + storedCTMResult.x = x; + storedCTMResult.y = y; } - return storedCTMResult + 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() + 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 + 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 + w = ownerRect.width; + h = ownerRect.height; } - var bbox = panController.getBBox() + 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 + 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 + 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 + transform.x = x; + transform.y = y; - keepTransformInsideBounds() + keepTransformInsideBounds(); - triggerEvent('pan') - makeDirty() + triggerEvent('pan'); + makeDirty(); } function moveBy(dx, dy) { - moveTo(transform.x + dx, transform.y + dy) + moveTo(transform.x + dx, transform.y + dy); } function keepTransformInsideBounds() { - var boundingBox = getBoundingBox() - if (!boundingBox) return + var boundingBox = getBoundingBox(); + if (!boundingBox) return; - var adjusted = false - var clientRect = getClientRect() + var adjusted = false; + var clientRect = getClientRect(); - var diff = boundingBox.left - clientRect.right + var diff = boundingBox.left - clientRect.right; if (diff > 0) { - transform.x += diff - adjusted = true + transform.x += diff; + adjusted = true; } // check the other side: - diff = boundingBox.right - clientRect.left + diff = boundingBox.right - clientRect.left; if (diff < 0) { - transform.x += diff - adjusted = true + transform.x += diff; + adjusted = true; } // y axis: - diff = boundingBox.top - clientRect.bottom + 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 + transform.y += diff; + adjusted = true; } - diff = boundingBox.bottom - clientRect.top + diff = boundingBox.bottom - clientRect.top; if (diff < 0) { - transform.y += diff - adjusted = true + transform.y += diff; + adjusted = true; } - return adjusted + 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 (!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 + 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), - } + bottom: sceneHeight * (1 - boundsPadding) + }; } - return bounds + return bounds; } function getClientRect() { - var bbox = panController.getBBox() - var leftTop = client(bbox.left, bbox.top) + 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 - } + x: x * transform.scale + transform.x, + y: y * transform.scale + transform.y + }; } function makeDirty() { - isDirty = true - frameAnimation = window.requestAnimationFrame(frame) + 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') + throw new Error('zoom requires valid numbers'); } - var newScale = transform.scale * ratio + var newScale = transform.scale * ratio; if (newScale < minZoom) { if (transform.scale === minZoom) return; - ratio = minZoom / transform.scale + ratio = minZoom / transform.scale; } if (newScale > maxZoom) { if (transform.scale === maxZoom) return; - ratio = maxZoom / transform.scale + ratio = maxZoom / transform.scale; } - var size = transformToScreen(clientX, clientY) + var size = transformToScreen(clientX, clientY); - transform.x = size.x - ratio * (size.x - transform.x) - transform.y = size.y - ratio * (size.y - transform.y) + 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() + transform.scale *= ratio; + keepTransformInsideBounds(); } else { - var transformAdjusted = keepTransformInsideBounds() - if (!transformAdjusted) transform.scale *= ratio + var transformAdjusted = keepTransformInsideBounds(); + if (!transformAdjusted) transform.scale *= ratio; } - triggerEvent('zoom') + triggerEvent('zoom'); - makeDirty() + makeDirty(); } function zoomAbs(clientX, clientY, zoomLevel) { - var ratio = zoomLevel / transform.scale - zoomByRatio(clientX, clientY, ratio) + 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') + 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 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 + var container = parent.getBoundingClientRect(); + var dx = container.width / 2 - cx; + var dy = container.height / 2 - cy; - internalMoveBy(dx, dy, true) + internalMoveBy(dx, dy, true); } function internalMoveBy(dx, dy, smooth) { if (!smooth) { - return moveBy(dx, dy) + return moveBy(dx, dy); } - if (moveByAnimation) moveByAnimation.cancel() + if (moveByAnimation) moveByAnimation.cancel(); - var from = { x: 0, y: 0 } - var to = { x: dx, y : dy } - var lastX = 0 - var lastY = 0 + 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) + step: function (v) { + moveBy(v.x - lastX, v.y - lastY); - lastX = v.x - lastY = v.y + lastX = v.x; + lastY = v.y; } - }) + }); } function scroll(x, y) { - cancelZoomAnimation() - moveTo(x, y) + cancelZoomAnimation(); + moveTo(x, y); } function dispose() { @@ -424,67 +470,71 @@ function createPanZoom(domElement, options) { } function listenForEvents() { - owner.addEventListener('mousedown', onMouseDown) - owner.addEventListener('dblclick', onDoubleClick) - owner.addEventListener('touchstart', onTouch) - owner.addEventListener('keydown', onKeyDown) + 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) + wheel.addWheelListener(owner, onMouseWheel, { passive: false }); - makeDirty() + makeDirty(); } function releaseEvents() { - wheel.removeWheelListener(owner, onMouseWheel) - owner.removeEventListener('mousedown', onMouseDown) - owner.removeEventListener('keydown', onKeyDown) - owner.removeEventListener('dblclick', onDoubleClick) - owner.removeEventListener('touchstart', onTouch) + 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 + window.cancelAnimationFrame(frameAnimation); + frameAnimation = 0; } - smoothScroll.cancel() + smoothScroll.cancel(); - releaseDocumentMouse() - releaseTouches() + releaseDocumentMouse(); + releaseTouches(); + textSelection.release(); - triggerPanEnd() + triggerPanEnd(); } - function frame() { - if (isDirty) applyTransform() + if (isDirty) applyTransform(); } function applyTransform() { - isDirty = false + isDirty = false; // TODO: Should I allow to cancel this? - panController.applyTransform(transform) + panController.applyTransform(transform); - triggerEvent('transform') - frameAnimation = 0 + triggerEvent('transform'); + frameAnimation = 0; } function onKeyDown(e) { - var x = 0, y = 0, z = 0 + var x = 0, + y = 0, + z = 0; if (e.keyCode === 38) { - y = 1 // up + y = 1; // up } else if (e.keyCode === 40) { - y = -1 // down + y = -1; // down } else if (e.keyCode === 37) { - x = 1 // left + 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 `+`) + 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)) { @@ -493,230 +543,272 @@ function createPanZoom(domElement, options) { } if (x || y) { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); - var clientRect = owner.getBoundingClientRect() + 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 + 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) + internalMoveBy(dx, dy); } if (z) { - var scaleMultiplier = getScaleMultiplier(z) - var ownerRect = owner.getBoundingClientRect() - publicZoomTo(ownerRect.width/2, ownerRect.height/2, scaleMultiplier) + 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 the override the touch behavior beforeTouch(e); if (e.touches.length === 1) { - return handleSingleFingerTouch(e, e.touches[0]) + 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() + 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 + return; } - e.stopPropagation() - e.preventDefault() + e.stopPropagation(); + e.preventDefault(); } function beforeDoubleClick(e) { + // 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 + return; } - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); } function handleSingleFingerTouch(e) { - var touch = e.touches[0] - var offset = getOffsetXY(touch) - mouseX = offset.x - mouseY = offset.y + var touch = e.touches[0]; + var offset = getOffsetXY(touch); + lastSingleFingerOffset = offset; + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + mouseY = point.y; - smoothScroll.cancel() - startTouchListenerIfNeeded() + smoothScroll.cancel(); + startTouchListenerIfNeeded(); } function startTouchListenerIfNeeded() { - if (!touchInProgress) { - touchInProgress = true - document.addEventListener('touchmove', handleTouchMove) - document.addEventListener('touchend', handleTouchEnd) - document.addEventListener('touchcancel', handleTouchEnd) + 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] + e.stopPropagation(); + var touch = e.touches[0]; - var offset = getOffsetXY(touch) + var offset = getOffsetXY(touch); + var point = transformToScreen(offset.x, offset.y); - var dx = offset.x - mouseX - var dy = offset.y - mouseY + var dx = point.x - mouseX; + var dy = point.y - mouseY; if (dx !== 0 && dy !== 0) { - triggerPanStart() + triggerPanStart(); } - mouseX = offset.x - mouseY = offset.y - var point = transformToScreen(dx, dy) - internalMoveBy(point.x, point.y) + 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) + 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 scaleMultiplier = + 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; - mouseX = (t1.clientX + t2.clientX)/2 - mouseY = (t1.clientY + t2.clientY)/2 + 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) + publicZoomTo(mouseX, mouseY, scaleMultiplier); - pinchZoomLength = currentPinchLength - e.stopPropagation() - e.preventDefault() + pinchZoomLength = currentPinchLength; + e.stopPropagation(); + e.preventDefault(); } } function handleTouchEnd(e) { if (e.touches.length > 0) { - var offset = getOffsetXY(e.touches[0]) - mouseX = offset.x - mouseY = offset.y + var offset = getOffsetXY(e.touches[0]); + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + mouseY = point.y; } else { - var now = new Date() + var now = new Date(); if (now - lastTouchEndTime < doubleTapSpeedInMS) { - smoothZoom(mouseX, mouseY, zoomDoubleClickSpeed) + if (transformOrigin) { + var offset = getTransformOriginOffset(); + smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); + } else { + smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed); + } } - lastTouchEndTime = now + lastTouchEndTime = now; - touchInProgress = false - triggerPanEnd() - releaseTouches() + touchInProgress = false; + 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) + 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) - smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed) + 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) { + // if client does not want to handle this event - just ignore the call + if (beforeMouseDown(e)) return; + if (touchInProgress) { // modern browsers will fire mousedown for touch events too // we do not want this: touch is handled separately. - e.stopPropagation() - return false + 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 + var isLeftButton = + (e.button === 1 && window.event !== null) || e.button === 0; + if (!isLeftButton) return; - smoothScroll.cancel() + smoothScroll.cancel(); var offset = getOffsetXY(e); - var point = transformToScreen(offset.x, offset.y) - mouseX = point.x - mouseY = point.y + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + 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) + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + textSelection.capture(e.target || e.srcElement); - preventTextSelection.capture(e.target || e.srcElement) - - return false + return false; } function onMouseMove(e) { // no need to worry about mouse events when touch is happening - if (touchInProgress) return + if (touchInProgress) return; - triggerPanStart() + triggerPanStart(); var offset = getOffsetXY(e); - var point = transformToScreen(offset.x, offset.y) - var dx = point.x - mouseX - var dy = point.y - mouseY + var point = transformToScreen(offset.x, offset.y); + var dx = point.x - mouseX; + var dy = point.y - mouseY; - mouseX = point.x - mouseY = point.y + mouseX = point.x; + mouseY = point.y; - internalMoveBy(dx, dy) + internalMoveBy(dx, dy); } function onMouseUp() { - preventTextSelection.release() - triggerPanEnd() - releaseDocumentMouse() + textSelection.release(); + triggerPanEnd(); + releaseDocumentMouse(); } function releaseDocumentMouse() { - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - panstartFired = false + 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 + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchEnd); + panstartFired = false; + multiTouch = false; } function onMouseWheel(e) { // if client does not want to handle this event - just ignore the call - if (beforeWheel(e)) return + if (beforeWheel(e)) return; - smoothScroll.cancel() + smoothScroll.cancel(); - var scaleMultiplier = getScaleMultiplier(e.deltaY) + var delta = e.deltaY; + if (e.deltaMode > 0) delta *= 100; + + var scaleMultiplier = getScaleMultiplier(delta); if (scaleMultiplier !== 1) { - var offset = getOffsetXY(e) - publicZoomTo(offset.x, offset.y, scaleMultiplier) - e.preventDefault() + var offset = transformOrigin + ? getTransformOriginOffset() + : getOffsetXY(e); + publicZoomTo(offset.x, offset.y, scaleMultiplier); + e.preventDefault(); } } @@ -724,96 +816,149 @@ function createPanZoom(domElement, options) { 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 + offsetX = e.clientX - ownerRect.left; + offsetY = e.clientY - ownerRect.top; - return {x: offsetX, y: offsetY}; + return { x: offsetX, y: offsetY }; } function smoothZoom(clientX, clientY, scaleMultiplier) { - var fromValue = transform.scale - var from = {scale: fromValue} - var to = {scale: scaleMultiplier * fromValue} + var fromValue = transform.scale; + var from = { scale: fromValue }; + var to = { scale: scaleMultiplier * fromValue }; - smoothScroll.cancel() - cancelZoomAnimation() + smoothScroll.cancel(); + cancelZoomAnimation(); - zoomToAnimation = animate(from, to, { - step: function(v) { - zoomAbs(clientX, clientY, v.scale) - } - }) + 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) + smoothScroll.cancel(); + cancelZoomAnimation(); + return zoomByRatio(clientX, clientY, scaleMultiplier); } function cancelZoomAnimation() { - if (zoomToAnimation) { - zoomToAnimation.cancel() - zoomToAnimation = null - } + if (zoomToAnimation) { + zoomToAnimation.cancel(); + zoomToAnimation = null; + } } function getScaleMultiplier(delta) { - var scaleMultiplier = 1 - if (delta > 0) { // zoom out - scaleMultiplier = (1 - speed) - } else if (delta < 0) { // zoom in - scaleMultiplier = (1 + speed) - } - - return scaleMultiplier + 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() + 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') + 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 + 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) + 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}') + 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) + return Number.isFinite(x); } // IE 11 does not support isNaN: function isNaN(value) { if (Number.isNaN) { - return Number.isNaN(value) + return Number.isNaN(value); } - return value !== value + return value !== value; } function rigidScroll() { @@ -821,12 +966,11 @@ function rigidScroll() { start: noop, stop: noop, cancel: noop - } + }; } - function autoRun() { - if (typeof document === 'undefined') return + if (typeof document === 'undefined') return; var scripts = document.getElementsByTagName('script'); if (!scripts) return; @@ -835,25 +979,25 @@ function autoRun() { for (var i = 0; i < scripts.length; ++i) { var x = scripts[i]; if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { - panzoomScript = x + panzoomScript = x; break; } } if (!panzoomScript) return; - var query = panzoomScript.getAttribute('query') + var query = panzoomScript.getAttribute('query'); if (!query) return; - var globalName = panzoomScript.getAttribute('name') || 'pz' - var started = Date.now() + var globalName = panzoomScript.getAttribute('name') || 'pz'; + var started = Date.now(); tryAttach(); function tryAttach() { - var el = document.querySelector(query) + var el = document.querySelector(query); if (!el) { - var now = Date.now() + var now = Date.now(); var elapsed = now - started; if (elapsed < 2000) { // Let's wait a bit @@ -861,22 +1005,22 @@ function autoRun() { 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 + console.error('Cannot find the panzoom element', globalName); + return; } - var options = collectOptions(panzoomScript) - console.log(options) + var options = collectOptions(panzoomScript); + console.log(options); window[globalName] = createPanZoom(el, options); } function collectOptions(script) { var attrs = script.attributes; var options = {}; - for(var i = 0; i < attrs.length; ++i) { + for (var i = 0; i < attrs.length; ++i) { var attr = attrs[i]; var nameValue = getPanzoomAttributeNameValue(attr); if (nameValue) { - options[nameValue.name] = nameValue.value + options[nameValue.name] = nameValue.value; } } @@ -885,19 +1029,71 @@ function autoRun() { function getPanzoomAttributeNameValue(attr) { if (!attr.name) return; - var isPanZoomAttribute = attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; + var isPanZoomAttribute = + attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; if (!isPanZoomAttribute) return; - var name = attr.name.substr(3) + var name = attr.name.substr(3); var value = JSON.parse(attr.value); - return {name: name, value: value}; + return { name: name, value: value }; } } autoRun(); -},{"./lib/domController.js":2,"./lib/kinetic.js":3,"./lib/svgController.js":4,"./lib/textSelectionInterceptor.js":5,"./lib/transform.js":6,"amator":7,"ngraph.events":9,"wheel":10}],2:[function(require,module,exports){ +},{"./lib/createTextSelectionInterceptor.js":2,"./lib/domController.js":3,"./lib/kinetic.js":4,"./lib/svgController.js":5,"./lib/transform.js":6,"amator":7,"ngraph.events":9,"wheel":10}],2:[function(require,module,exports){ +/** + * Disallows selecting text. + */ +module.exports = createTextSelectionInterceptor; + +function createTextSelectionInterceptor(useFake) { + if (useFake) { + return { + capture: noop, + release: noop + }; + } + + var dragObject; + var prevSelectStart; + var prevDragStart; + var wasCaptured = false; + + return { + capture: capture, + release: release + }; + + function capture(domObject) { + wasCaptured = true; + prevSelectStart = window.document.onselectstart; + prevDragStart = window.document.ondragstart; + + window.document.onselectstart = disabled; + + dragObject = domObject; + dragObject.ondragstart = disabled; + } + + function release() { + if (!wasCaptured) return; + + wasCaptured = false; + window.document.onselectstart = prevSelectStart; + if (dragObject) dragObject.ondragstart = prevDragStart; + } +} + +function disabled(e) { + e.stopPropagation(); + return false; +} + +function noop() {} + +},{}],3:[function(require,module,exports){ module.exports = makeDomController function makeDomController(domElement, options) { @@ -951,7 +1147,8 @@ function makeDomController(domElement, options) { } } -},{}],3:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ +(function (global){ /** * Allows smooth kinetic scrolling of the surface */ @@ -960,46 +1157,48 @@ module.exports = kinetic; function kinetic(getPoint, scroll, settings) { if (typeof settings !== 'object') { // setting could come as boolean, we should ignore it, and use an object. - settings = {} + settings = {}; } - var minVelocity = (typeof settings.minVelocity === 'number') ? settings.minVelocity : 5 - var amplitude = (typeof settings.amplitude === 'number') ? settings.amplitude : 0.25 + var minVelocity = typeof settings.minVelocity === 'number' ? settings.minVelocity : 5; + var amplitude = typeof settings.amplitude === 'number' ? settings.amplitude : 0.25; + var cancelAnimationFrame = typeof settings.cancelAnimationFrame === 'function' ? settings.cancelAnimationFrame : getCancelAnimationFrame(); + var requestAnimationFrame = typeof settings.requestAnimationFrame === 'function' ? settings.requestAnimationFrame : getRequestAnimationFrame(); - var lastPoint - var timestamp - var timeConstant = 342 + var lastPoint; + var timestamp; + var timeConstant = 342; - var ticker + var ticker; var vx, targetX, ax; var vy, targetY, ay; - var raf + var raf; return { start: start, stop: stop, cancel: dispose - } + }; function dispose() { - window.clearInterval(ticker) - window.cancelAnimationFrame(raf) + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); } function start() { - lastPoint = getPoint() + lastPoint = getPoint(); - ax = ay = vx = vy = 0 - timestamp = new Date() + ax = ay = vx = vy = 0; + timestamp = new Date(); - window.clearInterval(ticker) - window.cancelAnimationFrame(raf) + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); // we start polling the point position to accumulate velocity // Once we stop(), we will use accumulated velocity to keep scrolling // an object. - ticker = window.setInterval(track, 100); + ticker = requestAnimationFrame(track); } function track() { @@ -1007,73 +1206,88 @@ function kinetic(getPoint, scroll, settings) { var elapsed = now - timestamp; timestamp = now; - var currentPoint = getPoint() + var currentPoint = getPoint(); - var dx = currentPoint.x - lastPoint.x - var dy = currentPoint.y - lastPoint.y + var dx = currentPoint.x - lastPoint.x; + var dy = currentPoint.y - lastPoint.y; - lastPoint = currentPoint + lastPoint = currentPoint; - var dt = 1000 / (1 + elapsed) + var dt = 1000 / (1 + elapsed); // moving average - vx = 0.8 * dx * dt + 0.2 * vx - vy = 0.8 * dy * dt + 0.2 * vy + vx = 0.8 * dx * dt + 0.2 * vx; + vy = 0.8 * dy * dt + 0.2 * vy; + + ticker = requestAnimationFrame(track); } function stop() { - window.clearInterval(ticker); - window.cancelAnimationFrame(raf) + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); - var currentPoint = getPoint() + var currentPoint = getPoint(); - targetX = currentPoint.x - targetY = currentPoint.y - timestamp = Date.now() + targetX = currentPoint.x; + targetY = currentPoint.y; + timestamp = Date.now(); if (vx < -minVelocity || vx > minVelocity) { - ax = amplitude * vx - targetX += ax + ax = amplitude * vx; + targetX += ax; } if (vy < -minVelocity || vy > minVelocity) { - ay = amplitude * vy - targetY += ay + ay = amplitude * vy; + targetY += ay; } - raf = window.requestAnimationFrame(autoScroll); + raf = requestAnimationFrame(autoScroll); } function autoScroll() { - var elapsed = Date.now() - timestamp + var elapsed = Date.now() - timestamp; - var moving = false - var dx = 0 - var dy = 0 + var moving = false; + var dx = 0; + var dy = 0; if (ax) { - dx = -ax * Math.exp(-elapsed / timeConstant) + dx = -ax * Math.exp(-elapsed / timeConstant); - if (dx > 0.5 || dx < -0.5) moving = true - else dx = ax = 0 + if (dx > 0.5 || dx < -0.5) moving = true; + else dx = ax = 0; } if (ay) { - dy = -ay * Math.exp(-elapsed / timeConstant) + dy = -ay * Math.exp(-elapsed / timeConstant); - if (dy > 0.5 || dy < -0.5) moving = true - else dy = ay = 0 + if (dy > 0.5 || dy < -0.5) moving = true; + else dy = ay = 0; } if (moving) { - scroll(targetX + dx, targetY + dy) - raf = window.requestAnimationFrame(autoScroll); + scroll(targetX + dx, targetY + dy); + raf = requestAnimationFrame(autoScroll); } } - } -},{}],4:[function(require,module,exports){ +function getCancelAnimationFrame() { + if (typeof global.cancelAnimationFrame === 'function') return global.cancelAnimationFrame; + + return clearTimeout; +} + +function getRequestAnimationFrame() { + if (typeof global.requestAnimationFrame === 'function') return global.requestAnimationFrame; + + return function (handler) { + return setTimeout(handler, 16); + } +} +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],5:[function(require,module,exports){ module.exports = makeSvgController function makeSvgController(svgElement, options) { @@ -1119,11 +1333,17 @@ function makeSvgController(svgElement, options) { } function getScreenCTM() { - return owner.getScreenCTM() + var ctm = owner.getCTM(); + if (!ctm) { + // This is likely firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=873106 + // The code below is not entirely correct, but still better than nothing + return owner.getScreenCTM(); + } + return ctm; } function initTransform(transform) { - var screenCTM = svgElement.getScreenCTM() + var screenCTM = svgElement.getCTM() transform.x = screenCTM.e; transform.y = screenCTM.f; transform.scale = screenCTM.a; @@ -1137,43 +1357,6 @@ function makeSvgController(svgElement, options) { transform.x + ' ' + transform.y + ')') } } -},{}],5:[function(require,module,exports){ -/** - * Disallows selecting text. - */ -module.exports = createTextSelectionInterceptor - -function createTextSelectionInterceptor() { - var dragObject - var prevSelectStart - var prevDragStart - - return { - capture: capture, - release: release - } - - function capture(domObject) { - prevSelectStart = window.document.onselectstart - prevDragStart = window.document.ondragstart - - window.document.onselectstart = disabled - - dragObject = domObject - dragObject.ondragstart = disabled - } - - function release() { - window.document.onselectstart = prevSelectStart - if (dragObject) dragObject.ondragstart = prevDragStart - } -} - -function disabled(e) { - e.stopPropagation() - return false -} - },{}],6:[function(require,module,exports){ module.exports = Transform; @@ -1540,10 +1723,8 @@ function validateSubject(subject) { },{}],10:[function(require,module,exports){ /** - * This module unifies handling of mouse whee event across different browsers - * - * See https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel?redirectlocale=en-US&redirectslug=DOM%2FMozilla_event_reference%2Fwheel - * for more details + * This module used to unify mouse wheel behavior between different browsers in 2014 + * Now it's just a wrapper around addEventListener('wheel'); * * Usage: * var addWheelListener = require('wheel').addWheelListener; @@ -1553,7 +1734,6 @@ function validateSubject(subject) { * }); * removeWheelListener(domElement, function); */ -// by default we shortcut to 'addEventListener': module.exports = addWheelListener; @@ -1562,101 +1742,12 @@ module.exports.addWheelListener = addWheelListener; module.exports.removeWheelListener = removeWheelListener; -var prefix = "", _addEventListener, _removeEventListener, support; - -detectEventModel(typeof window !== 'undefined' && window, - typeof document !== 'undefined' && document); - -function addWheelListener( elem, callback, useCapture ) { - _addWheelListener( elem, support, callback, useCapture ); - - // handle MozMousePixelScroll in older Firefox - if( support == "DOMMouseScroll" ) { - _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); - } +function addWheelListener(element, listener, useCapture) { + element.addEventListener('wheel', listener, useCapture); } -function removeWheelListener( elem, callback, useCapture ) { - _removeWheelListener( elem, support, callback, useCapture ); - - // handle MozMousePixelScroll in older Firefox - if( support == "DOMMouseScroll" ) { - _removeWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); - } +function removeWheelListener( element, listener, useCapture ) { + element.removeEventListener('wheel', listener, useCapture); } - - // TODO: in theory this anonymous function may result in incorrect - // unsubscription in some browsers. But in practice, I don't think we should - // worry too much about it (those browsers are on the way out) -function _addWheelListener( elem, eventName, callback, useCapture ) { - elem[ _addEventListener ]( prefix + eventName, support == "wheel" ? callback : function( originalEvent ) { - !originalEvent && ( originalEvent = window.event ); - - // create a normalized event object - var event = { - // keep a ref to the original event object - originalEvent: originalEvent, - target: originalEvent.target || originalEvent.srcElement, - type: "wheel", - deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, - deltaX: 0, - deltaY: 0, - deltaZ: 0, - clientX: originalEvent.clientX, - clientY: originalEvent.clientY, - preventDefault: function() { - originalEvent.preventDefault ? - originalEvent.preventDefault() : - originalEvent.returnValue = false; - }, - stopPropagation: function() { - if(originalEvent.stopPropagation) - originalEvent.stopPropagation(); - }, - stopImmediatePropagation: function() { - if(originalEvent.stopImmediatePropagation) - originalEvent.stopImmediatePropagation(); - } - }; - - // calculate deltaY (and deltaX) according to the event - if ( support == "mousewheel" ) { - event.deltaY = - 1/40 * originalEvent.wheelDelta; - // Webkit also support wheelDeltaX - originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX ); - } else { - event.deltaY = originalEvent.detail; - } - - // it's time to fire the callback - return callback( event ); - - }, useCapture || false ); -} - -function _removeWheelListener( elem, eventName, callback, useCapture ) { - elem[ _removeEventListener ]( prefix + eventName, callback, useCapture || false ); -} - -function detectEventModel(window, document) { - if ( window && window.addEventListener ) { - _addEventListener = "addEventListener"; - _removeEventListener = "removeEventListener"; - } else { - _addEventListener = "attachEvent"; - _removeEventListener = "detachEvent"; - prefix = "on"; - } - - if (document) { - // detect available wheel event - support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" - document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" - "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox - } else { - support = "wheel"; - } -} - },{}]},{},[1])(1) }); diff --git a/src/public/javascripts/services/link_map.js b/src/public/javascripts/services/link_map.js index 7adf1a66e..22f5d56ed 100644 --- a/src/public/javascripts/services/link_map.js +++ b/src/public/javascripts/services/link_map.js @@ -184,18 +184,36 @@ export default class LinkMap { } moveToCenterOfElement(element) { - const elemBounds = element.getBoundingClientRect(); - const containerBounds = this.pzInstance.getOwner().getBoundingClientRect(); + const owner = this.pzInstance.getOwner(); - const centerX = -elemBounds.left + containerBounds.left + (containerBounds.width / 2) - (elemBounds.width / 2); - const centerY = -elemBounds.top + containerBounds.top + (containerBounds.height / 2) - (elemBounds.height / 2); + const center = () => { + const elemBounds = element.getBoundingClientRect(); + const containerBounds = owner.getBoundingClientRect(); - const transform = this.pzInstance.getTransform(); + const centerX = -elemBounds.left + containerBounds.left + (containerBounds.width / 2) - (elemBounds.width / 2); + const centerY = -elemBounds.top + containerBounds.top + (containerBounds.height / 2) - (elemBounds.height / 2); - const newX = transform.x + centerX; - const newY = transform.y + centerY; + const transform = this.pzInstance.getTransform(); - this.pzInstance.moveTo(newX, newY); + const newX = transform.x + centerX; + const newY = transform.y + centerY; + + this.pzInstance.moveTo(newX, newY); + }; + + let shown = false; + + const observer = new IntersectionObserver(entries => { + if (!shown && entries[0].isIntersecting) { + shown = true; + center(); + } + }, { + rootMargin: '0px', + threshold: 0.1 + }); + + observer.observe(owner); } initPanZoom() { diff --git a/src/public/javascripts/services/tab_manager.js b/src/public/javascripts/services/tab_manager.js index 1addaa6e0..5b50e6d36 100644 --- a/src/public/javascripts/services/tab_manager.js +++ b/src/public/javascripts/services/tab_manager.js @@ -244,7 +244,7 @@ export default class TabManager extends Component { if (this.tabContexts.length <= 1) { this.openAndActivateEmptyTab(); } - else { + else if (tabContextToRemove.isActive()) { const idx = this.tabContexts.findIndex(tc => tc.tabId === tabId); if (idx === this.tabContexts.length - 1) { diff --git a/src/public/javascripts/widgets/collapsible_widget.js b/src/public/javascripts/widgets/collapsible_widget.js index 5c509f675..c8ff3497d 100644 --- a/src/public/javascripts/widgets/collapsible_widget.js +++ b/src/public/javascripts/widgets/collapsible_widget.js @@ -18,8 +18,7 @@ const WIDGET_TPL = `
- -`; +`; export default class CollapsibleWidget extends TabAwareWidget { getWidgetTitle() { return "Untitled widget"; } @@ -68,10 +67,10 @@ export default class CollapsibleWidget extends TabAwareWidget { this.$headerActions = this.$widget.find('.widget-header-actions'); this.$headerActions.append(...this.getHeaderActions()); - this.decorateWidget(); - this.initialized = this.doRenderBody(); + this.decorateWidget(); + return this.$widget; } diff --git a/src/public/javascripts/widgets/desktop_layout.js b/src/public/javascripts/widgets/desktop_layout.js index 7edfc3d65..bd5ac4674 100644 --- a/src/public/javascripts/widgets/desktop_layout.js +++ b/src/public/javascripts/widgets/desktop_layout.js @@ -88,6 +88,7 @@ const RIGHT_PANE_CSS = ` border: 0; height: 100%; overflow: auto; + max-height: 300px; } #right-pane .card-body ul { diff --git a/src/public/javascripts/widgets/link_map.js b/src/public/javascripts/widgets/link_map.js index 0a7ab77fd..ef47e11c3 100644 --- a/src/public/javascripts/widgets/link_map.js +++ b/src/public/javascripts/widgets/link_map.js @@ -4,7 +4,7 @@ let linkMapContainerIdCtr = 1; const TPL = ` `; @@ -28,25 +28,43 @@ export default class LinkMapWidget extends CollapsibleWidget { return [$showFullButton]; } - noteSwitched() { + decorateWidget() { + this.$body.css("max-height", "400px"); + } + + async refreshWithNote(note) { const noteId = this.noteId; + let shown = false; + // avoid executing this expensive operation multiple times when just going through notes (with keyboard especially) // until the users settles on a note setTimeout(() => { if (this.noteId === noteId) { - this.refresh(); + // there's a problem with centering the rendered link map before it is actually shown on the screen + // that's why we make the whole process lazy and with the help of IntersectionObserver wait until the + // tab is really shown and only then render + const observer = new IntersectionObserver(entries => { + if (!shown && entries[0].isIntersecting) { + shown = true; + this.displayLinkMap(note); + } + }, { + rootMargin: '0px', + threshold: 0.1 + }); + + observer.observe(this.$body[0]); } }, 1000); } - async refreshWithNote(note) { + async displayLinkMap(note) { this.$body.css('opacity', 0); this.$body.html(TPL); const $linkMapContainer = this.$body.find('.link-map-container'); $linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++); - $linkMapContainer.css("height", "300px"); const LinkMapServiceClass = (await import('../services/link_map.js')).default; @@ -69,7 +87,7 @@ export default class LinkMapWidget extends CollapsibleWidget { entitiesReloadedEvent({loadResults}) { if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) { - this.refresh(); + this.noteSwitched(); } } } \ No newline at end of file diff --git a/src/public/javascripts/widgets/tab_caching_widget.js b/src/public/javascripts/widgets/tab_caching_widget.js index bfda627a2..044099072 100644 --- a/src/public/javascripts/widgets/tab_caching_widget.js +++ b/src/public/javascripts/widgets/tab_caching_widget.js @@ -17,9 +17,22 @@ export default class TabCachingWidget extends TabAwareWidget { // stop propagation of the event to the children, individual tab widget should not know about tab switching // since they are per-tab if (name === 'tabNoteSwitchedAndActivated') { - return super.handleEventInChildren('tabNoteSwitched', data); + name = 'tabNoteSwitched'; } - else if (name !== 'activeTabChanged') { + + if (name === 'tabNoteSwitched') { + // this event is propagated only to the widgets of a particular tab + const widget = this.widgets[data.tabContext.tabId]; + + if (widget) { + return widget.handleEvent(name, data); + } + else { + return Promise.resolve(); + } + } + + if (name !== 'activeTabChanged') { return super.handleEventInChildren(name, data); } diff --git a/src/public/javascripts/widgets/tab_row.js b/src/public/javascripts/widgets/tab_row.js index 24bcd66d7..a64d5b44c 100644 --- a/src/public/javascripts/widgets/tab_row.js +++ b/src/public/javascripts/widgets/tab_row.js @@ -411,9 +411,13 @@ export default class TabRowWidget extends BasicWidget { } activeTabChangedEvent() { - const newActiveTabId = appContext.tabManager.getActiveTabContext().tabId; + const activeTabContext = appContext.tabManager.getActiveTabContext(); - const tabEl = this.getTabById(newActiveTabId)[0]; + if (!activeTabContext) { + return; + } + + const tabEl = this.getTabById(activeTabContext.tabId)[0]; const activeTabEl = this.activeTabEl; if (activeTabEl === tabEl) return; if (activeTabEl) activeTabEl.removeAttribute('active');