From 31abec5d1c1b80af05d9c9e6cb2528e28994b921 Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 30 Oct 2018 20:22:05 +0100 Subject: [PATCH] zoom icons --- src/public/images/icons/zoom-in-24.png | Bin 0 -> 455 bytes src/public/images/icons/zoom-out-24.png | Bin 0 -> 455 bytes .../services/note_detail_relation_map.js | 89 +- src/public/libraries/panzoom.js | 3066 +++++++++-------- src/public/stylesheets/style.css | 6 + src/views/index.ejs | 14 + 6 files changed, 1631 insertions(+), 1544 deletions(-) create mode 100644 src/public/images/icons/zoom-in-24.png create mode 100644 src/public/images/icons/zoom-out-24.png diff --git a/src/public/images/icons/zoom-in-24.png b/src/public/images/icons/zoom-in-24.png new file mode 100644 index 0000000000000000000000000000000000000000..7c963e7f6d882e7f2a219015bbd590f713d29fc4 GIT binary patch literal 455 zcmV;&0XY7NP)Iwh=0aQsu zK~zY`-Ic#i!cY{(f9NO(i%r}`M?s^Tnh1`(5&9m;8yI{9BB^mPPMQF(AdNWG(NLGK z$xYMV-j+D%Nlx1IeCIp&{JVw!E?2-1m;+zZ8E^}9*CE~qd?3ov3D7J`egP~+@DB8W z7O*94>)S$@C2;0SegJ&R@eZh2u-m{$*bmoCe1p8=fUf}e@_BG+Xk3=GlRrw6Qa8%g zu>}|k)X%WTjZKpq*#@=%bAb*^1Zc}PvjqqRswT>kHKJy@YWfx?NjEYYS5#u3l}m|Q zB}#N8+tikUuV=4B2E7DFs&sXVg_g*5OdCKT-^;v{OavS`_F@xwlF!&B)uz@y4pO&Z z8wU9|aOzsmb0bIqT&syyjpMNvD=^3-;0^FxgY4;U$-Az{zzOhbEZT07>F8+vr&^*= x?}J1`ch(8wDxS&|0nfkni}iq)jB);>@&maWdmQ@?u)P2P002ovPDHLkV1nKPz^VWM literal 0 HcmV?d00001 diff --git a/src/public/images/icons/zoom-out-24.png b/src/public/images/icons/zoom-out-24.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d16ce7259192bf8e24a1513396ca7959563dd3 GIT binary patch literal 455 zcmV;&0XY7NP)0+p=f$G}7c z@4z`w1JVr^OF&!L59>|>hrD3G7k~%(JX#tWmBq_Y(y~*o zM*QcB^L?iE3J5a002ovPDHLkV1kVLyp;d| literal 0 HcmV?d00001 diff --git a/src/public/javascripts/services/note_detail_relation_map.js b/src/public/javascripts/services/note_detail_relation_map.js index 45675bb43..49b00c2ea 100644 --- a/src/public/javascripts/services/note_detail_relation_map.js +++ b/src/public/javascripts/services/note_detail_relation_map.js @@ -6,6 +6,8 @@ import libraryLoader from "./library_loader.js"; const $noteDetailRelationMap = $("#note-detail-relation-map"); const $relationMapCanvas = $("#relation-map-canvas"); const $addChildNotesButton = $("#relation-map-add-child-notes"); +const $zoomInButton = $("#relation-map-zoom-in"); +const $zoomOutButton = $("#relation-map-zoom-out"); let mapData; let instance; @@ -87,25 +89,10 @@ async function loadNotesAndRelations() { mapData.notes = mapData.notes.filter(note => note.id in data.noteTitles); instance.batch(async function () { - const maxY = mapData.notes.filter(note => !!note.y).map(note => note.y).reduce((a, b) => Math.max(a, b), 0); - let curX = 100; - let curY = maxY + 200; - for (const note of mapData.notes) { const title = data.noteTitles[note.id]; - if (note.x && note.y) { - await createNoteBox(note.id, title, note.x, note.y); - } else { - await createNoteBox(note.id, title, curX, curY); - - if (curX > 1000) { - curX = 100; - curY += 200; - } else { - curX += 200; - } - } + await createNoteBox(note.id, title, note.x, note.y); } for (const relation of relations) { @@ -134,19 +121,37 @@ function initPanZoom() { }); if (mapData.transform) { + console.log(mapData.transform); + pz.moveTo(mapData.transform.x, mapData.transform.y); pz.zoomTo(0, 0, mapData.transform.scale); } - $relationMapCanvas[0].addEventListener('zoom', function (e) { + pz.on('zoom', function (e) { mapData.transform = pz.getTransform(); + + console.log(mapData.transform); + saveData(); }); - $relationMapCanvas[0].addEventListener('panend', function (e) { + pz.on('panend', function (e) { mapData.transform = pz.getTransform(); + saveData(); }, true); + + $zoomInButton.click(() => { + const transform = pz.getTransform(); + + pz.zoomTo(0, 0, 1.2); + }); + + $zoomOutButton.click(() => { + const transform = pz.getTransform(); + + pz.zoomTo(0, 0, 0.8); + }); } async function initJsPlumb () { @@ -206,6 +211,7 @@ async function connectionCreatedHandler(info, originalEvent) { return; } + const connection = info.connection; const name = prompt("Specify new relation name:"); if (!name || !name.trim()) { @@ -214,8 +220,6 @@ async function connectionCreatedHandler(info, originalEvent) { return; } - const connection = info.connection; - const targetNoteId = connection.target.id; const sourceNoteId = connection.source.id; @@ -300,7 +304,7 @@ async function createNoteBox(id, title, x, y) { .addClass("note-box") .prop("id", id) .append($("").addClass("title").html(await linkService.createNoteLink(id, title))) - .append($("
").addClass("endpoint")) + .append($("
").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) .css("left", x + "px") .css("top", y + "px"); @@ -340,12 +344,16 @@ async function createNoteBox(id, title, x, y) { }); } +function getFreePosition() { + const maxY = mapData.notes.filter(note => !!note.y).map(note => note.y).reduce((a, b) => Math.max(a, b), 0); + + return [100, maxY + 200]; +} + $addChildNotesButton.click(async () => { const children = await server.get("notes/" + noteDetailService.getCurrentNoteId() + "/children"); - const maxY = mapData.notes.filter(note => !!note.y).map(note => note.y).reduce((a, b) => Math.max(a, b), 0); - let curX = 100; - let curY = maxY + 200; + let [curX, curY] = getFreePosition(); for (const child of children) { if (mapData.notes.some(note => note.id === child.noteId)) { @@ -353,11 +361,11 @@ $addChildNotesButton.click(async () => { continue; } - const note = { id: child.noteId }; - - mapData.notes.push(note); - - await createNoteBox(note.id, note.title, curX, curY); + mapData.notes.push({ + id: child.noteId, + x: curX, + y: curY + }); if (curX > 1000) { curX = 100; @@ -368,25 +376,12 @@ $addChildNotesButton.click(async () => { } } - for (const child of children) { - for (const relation of child.relations) { - const connection = instance.connect({ - id: relation.attributeId, - source: child.noteId, - target: relation.targetNoteId, - type: "basic" - }); - - if (!connection) { - continue; - } - - connection.getOverlay("label").setLabel(relation.name); - connection.canvas.setAttribute("data-connection-id", connection.id); - } - } - saveData(); + + // delete all endpoints and connections + instance.deleteEveryEndpoint(); + + await loadNotesAndRelations(); }); export default { diff --git a/src/public/libraries/panzoom.js b/src/public/libraries/panzoom.js index 52e3f5a6e..387f7d3cc 100644 --- a/src/public/libraries/panzoom.js +++ b/src/public/libraries/panzoom.js @@ -1,1573 +1,1645 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.panzoom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 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 = domController.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) - - 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 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) - owner.addEventListener('dblclick', onDoubleClick) - owner.addEventListener('touchstart', onTouch) - owner.addEventListener('keydown', onKeyDown) - - // Need to listen on the owner container, so that we are not limited - // by the size of the scrollable domElement - wheel.addWheelListener(owner, onMouseWheel) - - 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() - - triggerPanEnd() - } - - - function frame() { - if (isDirty) applyTransform() - } - - function applyTransform() { - isDirty = false - - // TODO: Should I allow to cancel this? - domController.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) - var ownerRect = owner.getBoundingClientRect() - publicZoomTo(ownerRect.width/2, ownerRect.height/2, scaleMultiplier) - } - } - - function onTouch(e) { - // let the override the touch behavior - beforeTouch(e); - - 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) { - 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) { - 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) { - var touch = e.touches[0] - var offset = getOffsetXY(touch) - mouseX = offset.x - mouseY = offset.y - - startTouchListenerIfNeeded() - } - - function startTouchListenerIfNeeded() { - if (!touchInProgress) { - 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 dx = offset.x - mouseX - var dy = offset.y - mouseY - - if (dx !== 0 && dy !== 0) { - triggerPanStart() - } - mouseX = offset.x - mouseY = offset.y - var point = transformToScreen(dx, dy) - internalMoveBy(point.x, point.y) - } 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) - - var scaleMultiplier = 1 - - if (realPinch) { - scaleMultiplier = currentPinchLength / pinchZoomLength - } else { - var delta = 0 - if (currentPinchLength < pinchZoomLength) { - delta = 1 - } else if (currentPinchLength > pinchZoomLength) { - delta = -1 - } - - scaleMultiplier = getScaleMultiplier(delta) - } - - mouseX = (t1.clientX + t2.clientX)/2 - mouseY = (t1.clientY + t2.clientY)/2 - - publicZoomTo(mouseX, mouseY, scaleMultiplier) - - 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 - } else { - var now = new Date() - if (now - lastTouchEndTime < doubleTapSpeedInMS) { - smoothZoom(mouseX, mouseY, zoomDoubleClickSpeed) - } - - lastTouchEndTime = now - - touchInProgress = false - triggerPanEnd() - releaseTouches() - } - } - - function getPinchZoomLength(finger1, finger2) { - return Math.sqrt((finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + - (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY)) - } - - function onDoubleClick(e) { - beforeDoubleClick(e); - var offset = getOffsetXY(e) - smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed) - } - - function onMouseDown(e) { - 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 - - var offset = getOffsetXY(e); - 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) - - preventTextSelection.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() { - preventTextSelection.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 - } - - function onMouseWheel(e) { - // if client does not want to handle this event - just ignore the call - if (beforeWheel(e)) return - - smoothScroll.cancel() - - var scaleMultiplier = getScaleMultiplier(e.deltaY) - - if (scaleMultiplier !== 1) { - var offset = 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() - - // TODO: should consolidate this and publicZoomTo - triggerEvent('zoom') - - zoomToAnimation = animate(from, to, { - step: function(v) { - zoomAbs(clientX, clientY, v.scale) - } - }) - } - - 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 scaleMultiplier = 1 - if (delta > 0) { // zoom out - scaleMultiplier = (1 - speed) - } else if (delta < 0) { // zoom in - scaleMultiplier = (1 + speed) - } - - return scaleMultiplier - } - - 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 triggerEvent(name) { - var event = createEvent(name) - domElement.dispatchEvent(event) - } +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.panzoom = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 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) + + 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 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) + owner.addEventListener('dblclick', onDoubleClick) + owner.addEventListener('touchstart', onTouch) + owner.addEventListener('keydown', onKeyDown) + + // Need to listen on the owner container, so that we are not limited + // by the size of the scrollable domElement + wheel.addWheelListener(owner, onMouseWheel) + + 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() + + 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) + var ownerRect = owner.getBoundingClientRect() + publicZoomTo(ownerRect.width/2, ownerRect.height/2, scaleMultiplier) + } + } + + function onTouch(e) { + // let the override the touch behavior + beforeTouch(e); + + 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) { + 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) { + 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) { + var touch = e.touches[0] + var offset = getOffsetXY(touch) + mouseX = offset.x + mouseY = offset.y + + smoothScroll.cancel() + startTouchListenerIfNeeded() + } + + function startTouchListenerIfNeeded() { + if (!touchInProgress) { + 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 dx = offset.x - mouseX + var dy = offset.y - mouseY + + if (dx !== 0 && dy !== 0) { + triggerPanStart() + } + mouseX = offset.x + mouseY = offset.y + var point = transformToScreen(dx, dy) + internalMoveBy(point.x, point.y) + } 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) + + var scaleMultiplier = 1 + + if (realPinch) { + scaleMultiplier = currentPinchLength / pinchZoomLength + } else { + var delta = 0 + if (currentPinchLength < pinchZoomLength) { + delta = 1 + } else if (currentPinchLength > pinchZoomLength) { + delta = -1 } - function noop() { } + scaleMultiplier = getScaleMultiplier(delta) + } - 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) + mouseX = (t1.clientX + t2.clientX)/2 + mouseY = (t1.clientY + t2.clientY)/2 - if (!validBounds) throw new Error('Bounds object is not valid. It can be: ' + - 'undefined, boolean (true|false) or an object {left, top, right, bottom}') + publicZoomTo(mouseX, mouseY, scaleMultiplier) + + 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 + } else { + var now = new Date() + if (now - lastTouchEndTime < doubleTapSpeedInMS) { + smoothZoom(mouseX, mouseY, zoomDoubleClickSpeed) + } + + lastTouchEndTime = now + + touchInProgress = false + triggerPanEnd() + releaseTouches() + } + } + + function getPinchZoomLength(finger1, finger2) { + return Math.sqrt((finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + + (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY)) + } + + function onDoubleClick(e) { + beforeDoubleClick(e); + var offset = getOffsetXY(e) + smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed) + } + + function onMouseDown(e) { + 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) + 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) + + preventTextSelection.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() { + preventTextSelection.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 + } + + function onMouseWheel(e) { + // if client does not want to handle this event - just ignore the call + if (beforeWheel(e)) return + + smoothScroll.cancel() + + var scaleMultiplier = getScaleMultiplier(e.deltaY) + + if (scaleMultiplier !== 1) { + var offset = 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) } + }) + } - function isNumber(x) { - return Number.isFinite(x) - } + 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 scaleMultiplier = 1 + if (delta > 0) { // zoom out + scaleMultiplier = (1 - speed) + } else if (delta < 0) { // zoom in + scaleMultiplier = (1 + speed) + } + + return scaleMultiplier + } + + 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 triggerEvent(name) { + api.fire(name, api); + } +} + +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; - - Array.from(scripts).forEach(function(x) { - if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { - panzoomScript = x - } - }) - - 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 i = 0; i < attrs.length; ++i) { - var attr = attrs[i]; - 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(); - - },{"./lib/createEvent.js":2,"./lib/domController.js":3,"./lib/kinetic.js":4,"./lib/svgController.js":5,"./lib/textSelectionInterceptor.js":6,"./lib/transform.js":7,"amator":8,"wheel":10}],2:[function(require,module,exports){ - /* global Event */ - module.exports = createEvent; - - var isIE = typeof Event !== 'function' - - /** - * Constructs custom event. Works in IE too - */ - function createEvent(name) { - if (isIE) { - var evt = document.createEvent('CustomEvent') - evt.initCustomEvent(name, true, true, undefined) - return evt - } else { - return new Event(name, { - bubbles: true - }) - } - } - - },{}],3:[function(require,module,exports){ - module.exports = makeDomController - - function makeDomController(domElement) { - var elementValid = (domElement instanceof HTMLElement) - if (!elementValid) { - throw new Error('svg element is required for svg.panzoom to work') - } - - var owner = domElement.parentElement - if (!owner) { - throw new Error( - 'Do not apply panzoom to the detached DOM element. ' - ) - } - - domElement.scrollTop = 0; - owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite - - var api = { - getBBox: getBBox, - getOwner: getOwner, - applyTransform: applyTransform, - } - - return api - - function getOwner() { - return owner - } - - function getBBox() { - // TODO: We should probably cache this? - return { - left: 0, - top: 0, - width: domElement.clientWidth, - height: domElement.clientHeight - } - } - - function applyTransform(transform) { - // TODO: Should we cache this? - domElement.style.transformOrigin = '0 0 0'; - domElement.style.transform = 'matrix(' + - transform.scale + ', 0, 0, ' + - transform.scale + ', ' + - transform.x + ', ' + transform.y + ')' - } - } - - },{}],4:[function(require,module,exports){ - /** - * Allows smooth kinetic scrolling of the surface - */ - 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 = {} - } - - var minVelocity = (typeof settings.minVelocity === 'number') ? settings.minVelocity : 5 - var amplitude = (typeof settings.amplitude === 'number') ? settings.amplitude : 0.25 - - var lastPoint - var timestamp - var timeConstant = 342 - - var ticker - var vx, targetX, ax; - var vy, targetY, ay; - - var raf - - return { - start: start, - stop: stop, - cancel: dispose - } - - function dispose() { - window.clearInterval(ticker) - window.cancelAnimationFrame(raf) - } - - function start() { - lastPoint = getPoint() - - ax = ay = vx = vy = 0 - timestamp = new Date() - - window.clearInterval(ticker) - window.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); - } - - function track() { - var now = Date.now(); - var elapsed = now - timestamp; - timestamp = now; - - var currentPoint = getPoint() - - var dx = currentPoint.x - lastPoint.x - var dy = currentPoint.y - lastPoint.y - - lastPoint = currentPoint - - var dt = 1000 / (1 + elapsed) - - // moving average - vx = 0.8 * dx * dt + 0.2 * vx - vy = 0.8 * dy * dt + 0.2 * vy - } - - function stop() { - window.clearInterval(ticker); - window.cancelAnimationFrame(raf) - - var currentPoint = getPoint() - - targetX = currentPoint.x - targetY = currentPoint.y - timestamp = Date.now() - - if (vx < -minVelocity || vx > minVelocity) { - ax = amplitude * vx - targetX += ax - } - - if (vy < -minVelocity || vy > minVelocity) { - ay = amplitude * vy - targetY += ay - } - - raf = window.requestAnimationFrame(autoScroll); - } - - function autoScroll() { - var elapsed = Date.now() - timestamp - - var moving = false - var dx = 0 - var dy = 0 - - if (ax) { - dx = -ax * Math.exp(-elapsed / timeConstant) - - if (dx > 0.5 || dx < -0.5) moving = true - else dx = ax = 0 - } - - if (ay) { - dy = -ay * Math.exp(-elapsed / timeConstant) - - if (dy > 0.5 || dy < -0.5) moving = true - else dy = ay = 0 - } - - if (moving) { - scroll(targetX + dx, targetY + dy) - raf = window.requestAnimationFrame(autoScroll); - } - } - - } - - },{}],5:[function(require,module,exports){ - module.exports = makeSvgController - - function makeSvgController(svgElement) { - var elementValid = (svgElement instanceof SVGElement) - if (!elementValid) { - throw new Error('svg element is required for svg.panzoom to work') - } - - var owner = svgElement.ownerSVGElement - if (!owner) { - throw new Error( - 'Do not apply panzoom to the root element. ' + - 'Use its child instead (e.g. ). ' + - 'As of March 2016 only FireFox supported transform on the root element') - } - - owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite - - var api = { - getBBox: getBBox, - getScreenCTM: getScreenCTM, - getOwner: getOwner, - applyTransform: applyTransform, - initTransform: initTransform - } - - return api - - function getOwner() { - return owner - } - - function getBBox() { - var bbox = svgElement.getBBox() - return { - left: bbox.x, - top: bbox.y, - width: bbox.width, - height: bbox.height, - } - } - - function getScreenCTM() { - return owner.getScreenCTM() - } - - function initTransform(transform) { - var screenCTM = svgElement.getScreenCTM() - transform.x = screenCTM.e; - transform.y = screenCTM.f; - transform.scale = screenCTM.a; - owner.removeAttributeNS(null, 'viewBox'); - } - - function applyTransform(transform) { - svgElement.setAttribute('transform', 'matrix(' + - transform.scale + ' 0 0 ' + - transform.scale + ' ' + - transform.x + ' ' + transform.y + ')') - } - } - },{}],6:[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 - } - - },{}],7:[function(require,module,exports){ - module.exports = Transform; - - function Transform() { - this.x = 0; - this.y = 0; - this.scale = 1; - } - - },{}],8:[function(require,module,exports){ - var BezierEasing = require('bezier-easing') +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; + + Array.from(scripts).forEach(function(x) { + if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { + panzoomScript = x + } + }) + + 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 i = 0; i < attrs.length; ++i) { + var attr = attrs[i]; + 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(); + +},{"./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){ +module.exports = makeDomController + +function makeDomController(domElement) { + var elementValid = (domElement instanceof HTMLElement) + if (!elementValid) { + throw new Error('svg element is required for svg.panzoom to work') + } + + var owner = domElement.parentElement + if (!owner) { + throw new Error( + 'Do not apply panzoom to the detached DOM element. ' + ) + } + + domElement.scrollTop = 0; + owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite + + var api = { + getBBox: getBBox, + getOwner: getOwner, + applyTransform: applyTransform, + } + + return api + + function getOwner() { + return owner + } + + function getBBox() { + // TODO: We should probably cache this? + return { + left: 0, + top: 0, + width: domElement.clientWidth, + height: domElement.clientHeight + } + } + + function applyTransform(transform) { + // TODO: Should we cache this? + domElement.style.transformOrigin = '0 0 0'; + domElement.style.transform = 'matrix(' + + transform.scale + ', 0, 0, ' + + transform.scale + ', ' + + transform.x + ', ' + transform.y + ')' + } +} + +},{}],3:[function(require,module,exports){ +/** + * Allows smooth kinetic scrolling of the surface + */ +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 = {} + } + + var minVelocity = (typeof settings.minVelocity === 'number') ? settings.minVelocity : 5 + var amplitude = (typeof settings.amplitude === 'number') ? settings.amplitude : 0.25 + + var lastPoint + var timestamp + var timeConstant = 342 + + var ticker + var vx, targetX, ax; + var vy, targetY, ay; + + var raf + + return { + start: start, + stop: stop, + cancel: dispose + } + + function dispose() { + window.clearInterval(ticker) + window.cancelAnimationFrame(raf) + } + + function start() { + lastPoint = getPoint() + + ax = ay = vx = vy = 0 + timestamp = new Date() + + window.clearInterval(ticker) + window.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); + } + + function track() { + var now = Date.now(); + var elapsed = now - timestamp; + timestamp = now; + + var currentPoint = getPoint() + + var dx = currentPoint.x - lastPoint.x + var dy = currentPoint.y - lastPoint.y + + lastPoint = currentPoint + + var dt = 1000 / (1 + elapsed) + + // moving average + vx = 0.8 * dx * dt + 0.2 * vx + vy = 0.8 * dy * dt + 0.2 * vy + } + + function stop() { + window.clearInterval(ticker); + window.cancelAnimationFrame(raf) + + var currentPoint = getPoint() + + targetX = currentPoint.x + targetY = currentPoint.y + timestamp = Date.now() + + if (vx < -minVelocity || vx > minVelocity) { + ax = amplitude * vx + targetX += ax + } + + if (vy < -minVelocity || vy > minVelocity) { + ay = amplitude * vy + targetY += ay + } + + raf = window.requestAnimationFrame(autoScroll); + } + + function autoScroll() { + var elapsed = Date.now() - timestamp + + var moving = false + var dx = 0 + var dy = 0 + + if (ax) { + dx = -ax * Math.exp(-elapsed / timeConstant) + + if (dx > 0.5 || dx < -0.5) moving = true + else dx = ax = 0 + } + + if (ay) { + dy = -ay * Math.exp(-elapsed / timeConstant) + + if (dy > 0.5 || dy < -0.5) moving = true + else dy = ay = 0 + } + + if (moving) { + scroll(targetX + dx, targetY + dy) + raf = window.requestAnimationFrame(autoScroll); + } + } + +} + +},{}],4:[function(require,module,exports){ +module.exports = makeSvgController + +function makeSvgController(svgElement) { + var elementValid = (svgElement instanceof SVGElement) + if (!elementValid) { + throw new Error('svg element is required for svg.panzoom to work') + } + + var owner = svgElement.ownerSVGElement + if (!owner) { + throw new Error( + 'Do not apply panzoom to the root element. ' + + 'Use its child instead (e.g. ). ' + + 'As of March 2016 only FireFox supported transform on the root element') + } + + owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite + + var api = { + getBBox: getBBox, + getScreenCTM: getScreenCTM, + getOwner: getOwner, + applyTransform: applyTransform, + initTransform: initTransform + } + + return api + + function getOwner() { + return owner + } + + function getBBox() { + var bbox = svgElement.getBBox() + return { + left: bbox.x, + top: bbox.y, + width: bbox.width, + height: bbox.height, + } + } + + function getScreenCTM() { + return owner.getScreenCTM() + } + + function initTransform(transform) { + var screenCTM = svgElement.getScreenCTM() + transform.x = screenCTM.e; + transform.y = screenCTM.f; + transform.scale = screenCTM.a; + owner.removeAttributeNS(null, 'viewBox'); + } + + function applyTransform(transform) { + svgElement.setAttribute('transform', 'matrix(' + + transform.scale + ' 0 0 ' + + transform.scale + ' ' + + 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; + +function Transform() { + this.x = 0; + this.y = 0; + this.scale = 1; +} + +},{}],7:[function(require,module,exports){ +var BezierEasing = require('bezier-easing') // Predefined set of animations. Similar to CSS easing functions - var animations = { - ease: BezierEasing(0.25, 0.1, 0.25, 1), - easeIn: BezierEasing(0.42, 0, 1, 1), - easeOut: BezierEasing(0, 0, 0.58, 1), - easeInOut: BezierEasing(0.42, 0, 0.58, 1), - linear: BezierEasing(0, 0, 1, 1) - } +var animations = { + ease: BezierEasing(0.25, 0.1, 0.25, 1), + easeIn: BezierEasing(0.42, 0, 1, 1), + easeOut: BezierEasing(0, 0, 0.58, 1), + easeInOut: BezierEasing(0.42, 0, 0.58, 1), + linear: BezierEasing(0, 0, 1, 1) +} - module.exports = animate; - module.exports.makeAggregateRaf = makeAggregateRaf; - module.exports.sharedScheduler = makeAggregateRaf(); +module.exports = animate; +module.exports.makeAggregateRaf = makeAggregateRaf; +module.exports.sharedScheduler = makeAggregateRaf(); - function animate(source, target, options) { - var start = Object.create(null) - var diff = Object.create(null) - options = options || {} - // We let clients specify their own easing function - var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] +function animate(source, target, options) { + var start = Object.create(null) + var diff = Object.create(null) + options = options || {} + // We let clients specify their own easing function + var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] - // if nothing is specified, default to ease (similar to CSS animations) - if (!easing) { - if (options.easing) { - console.warn('Unknown easing function in amator: ' + options.easing); - } - easing = animations.ease - } + // if nothing is specified, default to ease (similar to CSS animations) + if (!easing) { + if (options.easing) { + console.warn('Unknown easing function in amator: ' + options.easing); + } + easing = animations.ease + } - var step = typeof options.step === 'function' ? options.step : noop - var done = typeof options.done === 'function' ? options.done : noop + var step = typeof options.step === 'function' ? options.step : noop + var done = typeof options.done === 'function' ? options.done : noop - var scheduler = getScheduler(options.scheduler) + var scheduler = getScheduler(options.scheduler) - var keys = Object.keys(target) - keys.forEach(function(key) { - start[key] = source[key] - diff[key] = target[key] - source[key] - }) + var keys = Object.keys(target) + keys.forEach(function(key) { + start[key] = source[key] + diff[key] = target[key] - source[key] + }) - var durationInMs = typeof options.duration === 'number' ? options.duration : 400 - var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms - var previousAnimationId - var frame = 0 + var durationInMs = typeof options.duration === 'number' ? options.duration : 400 + var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms + var previousAnimationId + var frame = 0 - previousAnimationId = scheduler.next(loop) + previousAnimationId = scheduler.next(loop) - return { - cancel: cancel - } + return { + cancel: cancel + } - function cancel() { - scheduler.cancel(previousAnimationId) - previousAnimationId = 0 - } + function cancel() { + scheduler.cancel(previousAnimationId) + previousAnimationId = 0 + } - function loop() { - var t = easing(frame/durationInFrames) - frame += 1 - setValues(t) - if (frame <= durationInFrames) { - previousAnimationId = scheduler.next(loop) - step(source) - } else { - previousAnimationId = 0 - setTimeout(function() { done(source) }, 0) - } - } + function loop() { + var t = easing(frame/durationInFrames) + frame += 1 + setValues(t) + if (frame <= durationInFrames) { + previousAnimationId = scheduler.next(loop) + step(source) + } else { + previousAnimationId = 0 + setTimeout(function() { done(source) }, 0) + } + } - function setValues(t) { - keys.forEach(function(key) { - source[key] = diff[key] * t + start[key] - }) - } - } + function setValues(t) { + keys.forEach(function(key) { + source[key] = diff[key] * t + start[key] + }) + } +} - function noop() { } +function noop() { } - function getScheduler(scheduler) { - if (!scheduler) { - var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame - return canRaf ? rafScheduler() : timeoutScheduler() - } - if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') - if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') +function getScheduler(scheduler) { + if (!scheduler) { + var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame + return canRaf ? rafScheduler() : timeoutScheduler() + } + if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') + if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') - return scheduler - } + return scheduler +} - function rafScheduler() { - return { - next: window.requestAnimationFrame.bind(window), - cancel: window.cancelAnimationFrame.bind(window) - } - } +function rafScheduler() { + return { + next: window.requestAnimationFrame.bind(window), + cancel: window.cancelAnimationFrame.bind(window) + } +} - function timeoutScheduler() { - return { - next: function(cb) { - return setTimeout(cb, 1000/60) - }, - cancel: function (id) { - return clearTimeout(id) - } - } - } +function timeoutScheduler() { + return { + next: function(cb) { + return setTimeout(cb, 1000/60) + }, + cancel: function (id) { + return clearTimeout(id) + } + } +} - function makeAggregateRaf() { - var frontBuffer = new Set(); - var backBuffer = new Set(); - var frameToken = 0; +function makeAggregateRaf() { + var frontBuffer = new Set(); + var backBuffer = new Set(); + var frameToken = 0; - return { - next: next, - cancel: next, - clearAll: clearAll - } + return { + next: next, + cancel: next, + clearAll: clearAll + } - function clearAll() { - frontBuffer.clear(); - backBuffer.clear(); - cancelAnimationFrame(frameToken); - frameToken = 0; - } + function clearAll() { + frontBuffer.clear(); + backBuffer.clear(); + cancelAnimationFrame(frameToken); + frameToken = 0; + } - function next(callback) { - backBuffer.add(callback); - renderNextFrame(); - } + function next(callback) { + backBuffer.add(callback); + renderNextFrame(); + } - function renderNextFrame() { - if (!frameToken) frameToken = requestAnimationFrame(renderFrame); - } + function renderNextFrame() { + if (!frameToken) frameToken = requestAnimationFrame(renderFrame); + } - function renderFrame() { - frameToken = 0; + function renderFrame() { + frameToken = 0; - var t = backBuffer; - backBuffer = frontBuffer; - frontBuffer = t; + var t = backBuffer; + backBuffer = frontBuffer; + frontBuffer = t; - frontBuffer.forEach(function(callback) { - callback(); - }); - frontBuffer.clear(); - } + frontBuffer.forEach(function(callback) { + callback(); + }); + frontBuffer.clear(); + } - function cancel(callback) { - backBuffer.delete(callback); - } - } + function cancel(callback) { + backBuffer.delete(callback); + } +} - },{"bezier-easing":9}],9:[function(require,module,exports){ - /** - * https://github.com/gre/bezier-easing - * BezierEasing - use bezier curve for transition easing function - * by Gaëtan Renaudeau 2014 - 2015 – MIT License - */ +},{"bezier-easing":8}],8:[function(require,module,exports){ +/** + * https://github.com/gre/bezier-easing + * BezierEasing - use bezier curve for transition easing function + * by Gaëtan Renaudeau 2014 - 2015 – MIT License + */ // These values are established by empiricism with tests (tradeoff: performance VS precision) - var NEWTON_ITERATIONS = 4; - var NEWTON_MIN_SLOPE = 0.001; - var SUBDIVISION_PRECISION = 0.0000001; - var SUBDIVISION_MAX_ITERATIONS = 10; +var NEWTON_ITERATIONS = 4; +var NEWTON_MIN_SLOPE = 0.001; +var SUBDIVISION_PRECISION = 0.0000001; +var SUBDIVISION_MAX_ITERATIONS = 10; - var kSplineTableSize = 11; - var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); +var kSplineTableSize = 11; +var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); - var float32ArraySupported = typeof Float32Array === 'function'; +var float32ArraySupported = typeof Float32Array === 'function'; - function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } - function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } - function C (aA1) { return 3.0 * aA1; } +function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } +function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } +function C (aA1) { return 3.0 * aA1; } // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. - function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } +function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. - function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } +function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } - function binarySubdivide (aX, aA, aB, mX1, mX2) { - var currentX, currentT, i = 0; - do { - currentT = aA + (aB - aA) / 2.0; - currentX = calcBezier(currentT, mX1, mX2) - aX; - if (currentX > 0.0) { - aB = currentT; - } else { - aA = currentT; - } - } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); - return currentT; +function binarySubdivide (aX, aA, aB, mX1, mX2) { + var currentX, currentT, i = 0; + do { + currentT = aA + (aB - aA) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - aX; + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + return currentT; +} + +function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + if (currentSlope === 0.0) { + return aGuessT; + } + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; +} + +function LinearEasing (x) { + return x; +} + +module.exports = function bezier (mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { + throw new Error('bezier x values must be in [0, 1] range'); + } + + if (mX1 === mY1 && mX2 === mY2) { + return LinearEasing; + } + + // Precompute samples table + var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + for (var i = 0; i < kSplineTableSize; ++i) { + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + + function getTForX (aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; + + // Interpolate to provide an initial guess for t + var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); + var guessForT = intervalStart + dist * kSampleStepSize; + + var initialSlope = getSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT, mX1, mX2); + } else if (initialSlope === 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); + } + } + + return function BezierEasing (x) { + // Because JavaScript number are imprecise, we should guarantee the extremes are right. + if (x === 0) { + return 0; + } + if (x === 1) { + return 1; + } + return calcBezier(getTForX(x), mY1, mY2); + }; +}; + +},{}],9:[function(require,module,exports){ +module.exports = function(subject) { + validateSubject(subject); + + var eventsStorage = createEventsStorage(subject); + subject.on = eventsStorage.on; + subject.off = eventsStorage.off; + subject.fire = eventsStorage.fire; + return subject; +}; + +function createEventsStorage(subject) { + // Store all event listeners to this hash. Key is event name, value is array + // of callback records. + // + // A callback record consists of callback function and its optional context: + // { 'eventName' => [{callback: function, ctx: object}] } + var registeredEvents = Object.create(null); + + return { + on: function (eventName, callback, ctx) { + if (typeof callback !== 'function') { + throw new Error('callback is expected to be a function'); + } + var handlers = registeredEvents[eventName]; + if (!handlers) { + handlers = registeredEvents[eventName] = []; + } + handlers.push({callback: callback, ctx: ctx}); + + return subject; + }, + + off: function (eventName, callback) { + var wantToRemoveAll = (typeof eventName === 'undefined'); + if (wantToRemoveAll) { + // Killing old events storage should be enough in this case: + registeredEvents = Object.create(null); + return subject; + } + + if (registeredEvents[eventName]) { + var deleteAllCallbacksForEvent = (typeof callback !== 'function'); + if (deleteAllCallbacksForEvent) { + delete registeredEvents[eventName]; + } else { + var callbacks = registeredEvents[eventName]; + for (var i = 0; i < callbacks.length; ++i) { + if (callbacks[i].callback === callback) { + callbacks.splice(i, 1); + } + } } + } - function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { - for (var i = 0; i < NEWTON_ITERATIONS; ++i) { - var currentSlope = getSlope(aGuessT, mX1, mX2); - if (currentSlope === 0.0) { - return aGuessT; - } - var currentX = calcBezier(aGuessT, mX1, mX2) - aX; - aGuessT -= currentX / currentSlope; - } - return aGuessT; - } + return subject; + }, - function LinearEasing (x) { - return x; - } + fire: function (eventName) { + var callbacks = registeredEvents[eventName]; + if (!callbacks) { + return subject; + } - module.exports = function bezier (mX1, mY1, mX2, mY2) { - if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { - throw new Error('bezier x values must be in [0, 1] range'); - } + var fireArguments; + if (arguments.length > 1) { + fireArguments = Array.prototype.splice.call(arguments, 1); + } + for(var i = 0; i < callbacks.length; ++i) { + var callbackInfo = callbacks[i]; + callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); + } - if (mX1 === mY1 && mX2 === mY2) { - return LinearEasing; - } + return subject; + } + }; +} - // Precompute samples table - var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); - for (var i = 0; i < kSplineTableSize; ++i) { - sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); - } +function validateSubject(subject) { + if (!subject) { + throw new Error('Eventify cannot use falsy object as events subject'); + } + var reservedWords = ['on', 'fire', 'off']; + for (var i = 0; i < reservedWords.length; ++i) { + if (subject.hasOwnProperty(reservedWords[i])) { + throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); + } + } +} - function getTForX (aX) { - var intervalStart = 0.0; - var currentSample = 1; - var lastSample = kSplineTableSize - 1; - - for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { - intervalStart += kSampleStepSize; - } - --currentSample; - - // Interpolate to provide an initial guess for t - var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); - var guessForT = intervalStart + dist * kSampleStepSize; - - var initialSlope = getSlope(guessForT, mX1, mX2); - if (initialSlope >= NEWTON_MIN_SLOPE) { - return newtonRaphsonIterate(aX, guessForT, mX1, mX2); - } else if (initialSlope === 0.0) { - return guessForT; - } else { - return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); - } - } - - return function BezierEasing (x) { - // Because JavaScript number are imprecise, we should guarantee the extremes are right. - if (x === 0) { - return 0; - } - if (x === 1) { - return 1; - } - return calcBezier(getTForX(x), mY1, mY2); - }; - }; - - },{}],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 - * - * Usage: - * var addWheelListener = require('wheel').addWheelListener; - * var removeWheelListener = require('wheel').removeWheelListener; - * addWheelListener(domElement, function (e) { - * // mouse wheel event - * }); - * removeWheelListener(domElement, function); - */ +},{}],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 + * + * Usage: + * var addWheelListener = require('wheel').addWheelListener; + * var removeWheelListener = require('wheel').removeWheelListener; + * addWheelListener(domElement, function (e) { + * // mouse wheel event + * }); + * removeWheelListener(domElement, function); + */ // by default we shortcut to 'addEventListener': - module.exports = addWheelListener; +module.exports = addWheelListener; // But also expose "advanced" api with unsubscribe: - module.exports.addWheelListener = addWheelListener; - module.exports.removeWheelListener = removeWheelListener; +module.exports.addWheelListener = addWheelListener; +module.exports.removeWheelListener = removeWheelListener; - var prefix = "", _addEventListener, _removeEventListener, support; +var prefix = "", _addEventListener, _removeEventListener, support; - detectEventModel(typeof window !== 'undefined' && window, - typeof document !== 'undefined' && document); +detectEventModel(typeof window !== 'undefined' && window, + typeof document !== 'undefined' && document); - function addWheelListener( elem, callback, useCapture ) { - _addWheelListener( elem, support, callback, useCapture ); +function addWheelListener( elem, callback, useCapture ) { + _addWheelListener( elem, support, callback, useCapture ); - // handle MozMousePixelScroll in older Firefox - if( support == "DOMMouseScroll" ) { - _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); - } - } + // handle MozMousePixelScroll in older Firefox + if( support == "DOMMouseScroll" ) { + _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); + } +} - function removeWheelListener( elem, callback, useCapture ) { - _removeWheelListener( elem, support, callback, useCapture ); +function removeWheelListener( elem, callback, useCapture ) { + _removeWheelListener( elem, support, callback, useCapture ); - // handle MozMousePixelScroll in older Firefox - if( support == "DOMMouseScroll" ) { - _removeWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); - } - } + // handle MozMousePixelScroll in older Firefox + if( support == "DOMMouseScroll" ) { + _removeWheelListener( elem, "MozMousePixelScroll", callback, 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 ); + // 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(); - } - }; + // 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; - } + // 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 ); + // it's time to fire the callback + return callback( event ); - }, useCapture || false ); - } + }, useCapture || false ); +} - function _removeWheelListener( elem, eventName, callback, useCapture ) { - elem[ _removeEventListener ]( prefix + eventName, callback, 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"; - } +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"; - } - } + 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) -}); \ No newline at end of file +},{}]},{},[1])(1) +}); diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 51a93b27a..eff35dbea 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -433,6 +433,12 @@ button.icon-button { background: no-repeat center; } +button.icon-button24 { + height: 32px; + width: 32px; + background: no-repeat center; +} + #note-actions { margin-left: 10px; margin-right: 10px; diff --git a/src/views/index.ejs b/src/views/index.ejs index ec22255a6..2d3af3197 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -265,6 +265,20 @@
+
+
+