1(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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 2module.exports = { 3 enable: function(instance) { 4 // Select (and create if necessary) defs 5 var defs = instance.svg.querySelector('defs') 6 if (!defs) { 7 defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs') 8 instance.svg.appendChild(defs) 9 } 10 11 // Create style element 12 var style = document.createElementNS('http://www.w3.org/2000/svg', 'style') 13 style.setAttribute('type', 'text/css') 14 style.textContent = '.svg-pan-zoom-control { cursor: pointer; fill: black; fill-opacity: 0.333; } .svg-pan-zoom-control:hover { fill-opacity: 0.8; } .svg-pan-zoom-control-background { fill: white; fill-opacity: 0.5; } .svg-pan-zoom-control-background { fill-opacity: 0.8; }' 15 defs.appendChild(style) 16 17 // Zoom Group 18 var zoomGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 19 zoomGroup.setAttribute('id', 'svg-pan-zoom-controls'); 20 zoomGroup.setAttribute('transform', 'translate(' + ( instance.width - 70 ) + ' ' + ( instance.height - 76 ) + ') scale(0.75)'); 21 zoomGroup.setAttribute('class', 'svg-pan-zoom-control'); 22 23 // Control elements 24 zoomGroup.appendChild(this._createZoomIn(instance)) 25 zoomGroup.appendChild(this._createZoomReset(instance)) 26 zoomGroup.appendChild(this._createZoomOut(instance)) 27 28 // Finally append created element 29 instance.svg.appendChild(zoomGroup) 30 31 // Cache control instance 32 instance.controlIcons = zoomGroup 33 } 34 35, _createZoomIn: function(instance) { 36 var zoomIn = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 37 zoomIn.setAttribute('id', 'svg-pan-zoom-zoom-in'); 38 zoomIn.setAttribute('transform', 'translate(30.5 5) scale(0.015)'); 39 zoomIn.setAttribute('class', 'svg-pan-zoom-control'); 40 zoomIn.addEventListener('click', function() {instance.getPublicInstance().zoomIn()}, false) 41 42 var zoomInBackground = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier 43 zoomInBackground.setAttribute('x', '0'); 44 zoomInBackground.setAttribute('y', '0'); 45 zoomInBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down 46 zoomInBackground.setAttribute('height', '1400'); 47 zoomInBackground.setAttribute('class', 'svg-pan-zoom-control-background'); 48 zoomIn.appendChild(zoomInBackground); 49 50 var zoomInShape = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 51 zoomInShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z'); 52 zoomInShape.setAttribute('class', 'svg-pan-zoom-control-element'); 53 zoomIn.appendChild(zoomInShape); 54 55 return zoomIn 56 } 57 58, _createZoomReset: function(instance){ 59 // reset 60 var resetPanZoomControl = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 61 resetPanZoomControl.setAttribute('id', 'svg-pan-zoom-reset-pan-zoom'); 62 resetPanZoomControl.setAttribute('transform', 'translate(5 35) scale(0.4)'); 63 resetPanZoomControl.setAttribute('class', 'svg-pan-zoom-control'); 64 resetPanZoomControl.addEventListener('click', function() {instance.getPublicInstance().resetZoom()}, false); 65 66 var resetPanZoomControlBackground = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier 67 resetPanZoomControlBackground.setAttribute('x', '2'); 68 resetPanZoomControlBackground.setAttribute('y', '2'); 69 resetPanZoomControlBackground.setAttribute('width', '182'); // larger than expected because the whole group is transformed to scale down 70 resetPanZoomControlBackground.setAttribute('height', '58'); 71 resetPanZoomControlBackground.setAttribute('class', 'svg-pan-zoom-control-background'); 72 resetPanZoomControl.appendChild(resetPanZoomControlBackground); 73 74 var resetPanZoomControlShape1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 75 resetPanZoomControlShape1.setAttribute('d', 'M33.051,20.632c-0.742-0.406-1.854-0.609-3.338-0.609h-7.969v9.281h7.769c1.543,0,2.701-0.188,3.473-0.562c1.365-0.656,2.048-1.953,2.048-3.891C35.032,22.757,34.372,21.351,33.051,20.632z'); 76 resetPanZoomControlShape1.setAttribute('class', 'svg-pan-zoom-control-element'); 77 resetPanZoomControl.appendChild(resetPanZoomControlShape1); 78 79 var resetPanZoomControlShape2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 80 resetPanZoomControlShape2.setAttribute('d', 'M170.231,0.5H15.847C7.102,0.5,0.5,5.708,0.5,11.84v38.861C0.5,56.833,7.102,61.5,15.847,61.5h154.384c8.745,0,15.269-4.667,15.269-10.798V11.84C185.5,5.708,178.976,0.5,170.231,0.5z M42.837,48.569h-7.969c-0.219-0.766-0.375-1.383-0.469-1.852c-0.188-0.969-0.289-1.961-0.305-2.977l-0.047-3.211c-0.03-2.203-0.41-3.672-1.142-4.406c-0.732-0.734-2.103-1.102-4.113-1.102h-7.05v13.547h-7.055V14.022h16.524c2.361,0.047,4.178,0.344,5.45,0.891c1.272,0.547,2.351,1.352,3.234,2.414c0.731,0.875,1.31,1.844,1.737,2.906s0.64,2.273,0.64,3.633c0,1.641-0.414,3.254-1.242,4.84s-2.195,2.707-4.102,3.363c1.594,0.641,2.723,1.551,3.387,2.73s0.996,2.98,0.996,5.402v2.32c0,1.578,0.063,2.648,0.19,3.211c0.19,0.891,0.635,1.547,1.333,1.969V48.569z M75.579,48.569h-26.18V14.022h25.336v6.117H56.454v7.336h16.781v6H56.454v8.883h19.125V48.569z M104.497,46.331c-2.44,2.086-5.887,3.129-10.34,3.129c-4.548,0-8.125-1.027-10.731-3.082s-3.909-4.879-3.909-8.473h6.891c0.224,1.578,0.662,2.758,1.316,3.539c1.196,1.422,3.246,2.133,6.15,2.133c1.739,0,3.151-0.188,4.236-0.562c2.058-0.719,3.087-2.055,3.087-4.008c0-1.141-0.504-2.023-1.512-2.648c-1.008-0.609-2.607-1.148-4.796-1.617l-3.74-0.82c-3.676-0.812-6.201-1.695-7.576-2.648c-2.328-1.594-3.492-4.086-3.492-7.477c0-3.094,1.139-5.664,3.417-7.711s5.623-3.07,10.036-3.07c3.685,0,6.829,0.965,9.431,2.895c2.602,1.93,3.966,4.73,4.093,8.402h-6.938c-0.128-2.078-1.057-3.555-2.787-4.43c-1.154-0.578-2.587-0.867-4.301-0.867c-1.907,0-3.428,0.375-4.565,1.125c-1.138,0.75-1.706,1.797-1.706,3.141c0,1.234,0.561,2.156,1.682,2.766c0.721,0.406,2.25,0.883,4.589,1.43l6.063,1.43c2.657,0.625,4.648,1.461,5.975,2.508c2.059,1.625,3.089,3.977,3.089,7.055C108.157,41.624,106.937,44.245,104.497,46.331z M139.61,48.569h-26.18V14.022h25.336v6.117h-18.281v7.336h16.781v6h-16.781v8.883h19.125V48.569z M170.337,20.14h-10.336v28.43h-7.266V20.14h-10.383v-6.117h27.984V20.14z'); 81 resetPanZoomControlShape2.setAttribute('class', 'svg-pan-zoom-control-element'); 82 resetPanZoomControl.appendChild(resetPanZoomControlShape2); 83 84 return resetPanZoomControl 85 } 86 87, _createZoomOut: function(instance){ 88 // zoom out 89 var zoomOut = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 90 zoomOut.setAttribute('id', 'svg-pan-zoom-zoom-out'); 91 zoomOut.setAttribute('transform', 'translate(30.5 70) scale(0.015)'); 92 zoomOut.setAttribute('class', 'svg-pan-zoom-control'); 93 zoomOut.addEventListener('click', function() {instance.getPublicInstance().zoomOut()}, false); 94 95 var zoomOutBackground = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); // TODO change these background space fillers to rounded rectangles so they look prettier 96 zoomOutBackground.setAttribute('x', '0'); 97 zoomOutBackground.setAttribute('y', '0'); 98 zoomOutBackground.setAttribute('width', '1500'); // larger than expected because the whole group is transformed to scale down 99 zoomOutBackground.setAttribute('height', '1400'); 100 zoomOutBackground.setAttribute('class', 'svg-pan-zoom-control-background'); 101 zoomOut.appendChild(zoomOutBackground); 102 103 var zoomOutShape = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 104 zoomOutShape.setAttribute('d', 'M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z'); 105 zoomOutShape.setAttribute('class', 'svg-pan-zoom-control-element'); 106 zoomOut.appendChild(zoomOutShape); 107 108 return zoomOut 109 } 110 111, disable: function(instance) { 112 if (instance.controlIcons) { 113 instance.controlIcons.parentNode.removeChild(instance.controlIcons) 114 instance.controlIcons = null 115 } 116 } 117} 118 119},{}],2:[function(require,module,exports){ 120// Cross-browser wheel event, from: https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel 121if (!window.hasOwnProperty('addWheelListener')) { 122 // creates a global "addWheelListener" method 123 // example: addWheelListener( elem, function( e ) { console.log( e.deltaY ); e.preventDefault(); } ); 124 (function(window,document) { 125 126 var prefix = "", _addEventListener, onwheel, support; 127 128 // detect event model 129 if ( window.addEventListener ) { 130 _addEventListener = "addEventListener"; 131 } else { 132 _addEventListener = "attachEvent"; 133 prefix = "on"; 134 } 135 136 // detect available wheel event 137 support = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" 138 document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" 139 "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox 140 141 window.addWheelListener = function( elem, callback, useCapture ) { 142 _addWheelListener( elem, support, callback, useCapture ); 143 144 // handle MozMousePixelScroll in older Firefox 145 if( support == "DOMMouseScroll" ) { 146 _addWheelListener( elem, "MozMousePixelScroll", callback, useCapture ); 147 } 148 }; 149 150 function _addWheelListener( elem, eventName, callback, useCapture ) { 151 elem[ _addEventListener ]( prefix + eventName, support == "wheel" ? callback : function( originalEvent ) { 152 !originalEvent && ( originalEvent = window.event ); 153 154 // create a normalized event object 155 var event = { 156 // keep a ref to the original event object 157 originalEvent: originalEvent, 158 // NOTE: clientX and clientY are not in Mozilla example, but are needed for svg-pan-zoom 159 clientX: originalEvent.clientX, 160 clientY: originalEvent.clientY, 161 target: originalEvent.target || originalEvent.srcElement, 162 type: "wheel", 163 deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, 164 deltaX: 0, 165 deltaZ: 0, 166 preventDefault: function() { 167 originalEvent.preventDefault ? 168 originalEvent.preventDefault() : 169 originalEvent.returnValue = false; 170 } 171 }; 172 173 // calculate deltaY (and deltaX) according to the event 174 if ( support == "mousewheel" ) { 175 event.deltaY = - 1/40 * originalEvent.wheelDelta; 176 // Webkit also support wheelDeltaX 177 originalEvent.wheelDeltaX && ( event.deltaX = - 1/40 * originalEvent.wheelDeltaX ); 178 } else { 179 event.deltaY = originalEvent.detail; 180 } 181 182 // it's time to fire the callback 183 return callback(event); 184 185 }, useCapture || false ); 186 } 187 188 })(window,document); 189} 190 191},{}],3:[function(require,module,exports){ 192var Mousewheel = require('./mousewheel') // Keep it here so that mousewheel is initialised 193 , ControlIcons = require('./control-icons') 194 , Utils = require('./utilities') 195 , SvgUtils = require('./svg-utilities') 196 197;(function(window, document){ 198 'use strict'; 199 200 var SvgPanZoom = function(svg, options) { 201 this.init(svg, options) 202 } 203 204 var optionsDefaults = { 205 panEnabled: true // enable or disable panning (default enabled) 206 , dragEnabled: false // enable or disable dragging (default disabled) 207 , controlIconsEnabled: false // insert icons to give user an option in addition to mouse events to control pan/zoom (default disabled) 208 , zoomEnabled: true // enable or disable zooming (default enabled) 209 , zoomScaleSensitivity: 0.2 // Zoom sensitivity 210 , minZoom: 0.5 // Minimum Zoom level 211 , maxZoom: 10 // Maximum Zoom level 212 , fit: true // enable or disable viewport fit in SVG (default true) 213 , center: true // enable or disable viewport centering in SVG (default true) 214 , beforeZoom: null 215 , onZoom: function(){} 216 , beforePan: null 217 , onPan: function(){} 218 } 219 220 SvgPanZoom.prototype.init = function(svg, options) { 221 this.svg = svg 222 223 // Set options 224 this.options = Utils.extend(Utils.extend({}, optionsDefaults), options) 225 226 // Set default state 227 this.state = 'none' 228 229 // Get dimensions 230 var dimensions = SvgUtils.getSvgDimensions(svg) 231 this.width = dimensions.width 232 this.height = dimensions.height 233 234 // Get viewport 235 this.viewport = SvgUtils.getOrCreateViewport(svg) 236 237 // Create zoom and pan cache 238 this._zoom = 1 239 this._pan = {x: 0, y: 0} 240 241 // Sets initialCTM 242 this.processCTM() 243 244 if (this.options.controlIconsEnabled && this.options.zoomEnabled) { 245 ControlIcons.enable(this) 246 } 247 248 // Add default attributes to SVG 249 SvgUtils.setupSvgAttributes(this.svg) 250 251 // Init events handlers 252 this.setupHandlers() 253 254 // TODO what for do we need this? 255 // It is replacing window.svgPanZoom constructor with this instance 256 // 257 // if (this.svg.ownerDocument.documentElement.tagName.toLowerCase() !== 'svg') { 258 // this.svg.ownerDocument.defaultView.svgPanZoom = this 259 // } 260 } 261 262 /** 263 * Get CTM and save it into initialCTM attribute 264 * Parse viewBox and if any update initialCTM based on this values 265 */ 266 SvgPanZoom.prototype.processCTM = function() { 267 var svgViewBox = this.svg.getAttribute('viewBox') 268 269 this.cacheViewBox() 270 this.svg.removeAttribute('viewBox') 271 272 if (this.options.fit) { 273 var newCTM = this.viewport.getCTM() 274 , newScale = Math.min(this.width/(this._viewBox.width - this._viewBox.x), this.height/(this._viewBox.height - this._viewBox.y)); 275 276 newCTM.a = newCTM.a * newScale; //x-scale 277 newCTM.d = newCTM.d * newScale; //y-scale 278 newCTM.e = (newCTM.e - this._viewBox.x) * newScale; //x-transform 279 newCTM.f = (newCTM.f - this._viewBox.y) * newScale; //y-transform 280 this.initialCTM = newCTM; 281 282 // Update viewport CTM 283 SvgUtils.setCTM(this.viewport, this.initialCTM); 284 } else { 285 // Leave sizes as they are 286 this.svg.removeAttribute('viewBox') 287 this.initialCTM = this.viewport.getCTM(); 288 } 289 290 // Cache zoom level 291 this._zoom = this.initialCTM.a 292 293 // Cache pan level 294 this._pan.x = this.initialCTM.e 295 this._pan.y = this.initialCTM.f 296 297 if (this.options.center) { 298 this.center() 299 } 300 } 301 302 /** 303 * Cache initial viewBox value 304 * If nok viewBox is defined than use viewport sizes as viewBox values 305 */ 306 SvgPanZoom.prototype.cacheViewBox = function() { 307 // ViewBox cache 308 this._viewBox = {x: 0, y: 0, width: 0, height: 0} 309 310 var svgViewBox = this.svg.getAttribute('viewBox') 311 312 if (svgViewBox) { 313 var viewBoxValues = svgViewBox.split(' ').map(parseFloat) 314 315 // Cache viewbox x and y offset 316 this._viewBox.x = viewBoxValues[0] 317 this._viewBox.y = viewBoxValues[1] 318 this._viewBox.width = viewBoxValues[2] 319 this._viewBox.height = viewBoxValues[3] 320 } else { 321 var boundingClientRect = this.viewport.getBoundingClientRect() 322 323 // Cache viewbox sizes 324 this._viewBox.width = boundingClientRect.width 325 this._viewBox.height = boundingClientRect.height 326 } 327 } 328 329 /** 330 * Recalculate viewport sizes and update viewBox cache 331 */ 332 SvgPanZoom.prototype.recacheViewBox = function() { 333 var boundingClientRect = this.viewport.getBoundingClientRect() 334 , viewBoxWidth = boundingClientRect.width / this.getZoom() 335 , viewBoxHeight = boundingClientRect.height / this.getZoom() 336 337 // Cache viewbox 338 this._viewBox.x = 0 339 this._viewBox.y = 0 340 this._viewBox.width = viewBoxWidth 341 this._viewBox.height = viewBoxHeight 342 } 343 344 /** 345 * Register event handlers 346 */ 347 SvgPanZoom.prototype.setupHandlers = function() { 348 var that = this 349 350 // Mouse down group 351 this.svg.addEventListener("mousedown", function(evt) { 352 return that.handleMouseDown(evt); 353 }, false); 354 this.svg.addEventListener("touchstart", function(evt) { 355 return that.handleMouseDown(evt); 356 }, false); 357 358 // Mouse up group 359 this.svg.addEventListener("mouseup", function(evt) { 360 return that.handleMouseUp(evt); 361 }, false); 362 this.svg.addEventListener("touchend", function(evt) { 363 return that.handleMouseUp(evt); 364 }, false); 365 366 // Mouse move group 367 this.svg.addEventListener("mousemove", function(evt) { 368 return that.handleMouseMove(evt); 369 }, false); 370 this.svg.addEventListener("touchmove", function(evt) { 371 return that.handleMouseMove(evt); 372 }, false); 373 374 // Mouse leave group 375 this.svg.addEventListener("mouseleave", function(evt) { 376 return that.handleMouseUp(evt); 377 }, false); 378 this.svg.addEventListener("touchleave", function(evt) { 379 return that.handleMouseUp(evt); 380 }, false); 381 this.svg.addEventListener("touchcancel", function(evt) { 382 return that.handleMouseUp(evt); 383 }, false); 384 385 // Mouse wheel listener 386 window.addWheelListener(this.svg, function(evt) { 387 return that.handleMouseWheel(evt); 388 }) 389 } 390 391 /** 392 * Handle mouse wheel event 393 * 394 * @param {object} evt Event object 395 */ 396 SvgPanZoom.prototype.handleMouseWheel = function(evt) { 397 if (!this.options.zoomEnabled) { 398 return; 399 } 400 401 if (evt.preventDefault) { 402 evt.preventDefault(); 403 } else { 404 evt.returnValue = false; 405 } 406 407 var delta = 0 408 409 if ('deltaMode' in evt && evt.deltaMode === 0) { 410 // Make empirical adjustments for browsers that give deltaY in pixels (deltaMode=0) 411 412 if (evt.wheelDelta) { 413 // Normalizer for Chrome 414 delta = evt.deltaY / Math.abs(evt.wheelDelta/3) 415 } else { 416 // Others. Possibly tablets? Use a value just in case 417 delta = evt.deltaY / 120 418 } 419 } else if ('mozPressure' in evt) { 420 // Normalizer for newer Firefox 421 // NOTE: May need to change detection at some point if mozPressure disappears. 422 delta = evt.deltaY / 3; 423 } else { 424 // Others should be reasonably normalized by the mousewheel code at the end of the file. 425 delta = evt.deltaY; 426 } 427 428 var svg = (evt.target.tagName === 'svg' || evt.target.tagName === 'SVG') ? evt.target : evt.target.ownerSVGElement || evt.target.correspondingElement.ownerSVGElement 429 , relativeMousePoint = SvgUtils.getRelativeMousePoint(svg, evt) 430 , zoom = Math.pow(1 + this.options.zoomScaleSensitivity, (-1) * delta); // multiplying by neg. 1 so as to make zoom in/out behavior match Google maps behavior 431 432 this.zoomAtPoint(svg, relativeMousePoint, zoom) 433 } 434 435 /** 436 * Zoom in at an SVG point 437 * 438 * @param {object} svg SVG Element 439 * @param {object} point SVG Point 440 * @param {float} zoomScale Number representing how much to zoom 441 * @param {bool} zoomAbsolute [description] 442 * @return {[type]} Default false. If true, zoomScale is treated as an absolute value. 443 * Otherwise, zoomScale is treated as a multiplied (e.g. 1.10 would zoom in 10%) 444 */ 445 SvgPanZoom.prototype.zoomAtPoint = function(svg, point, zoomScale, zoomAbsolute) { 446 if (Utils.isFunction(this.options.beforeZoom)) { 447 this.options.beforeZoom() 448 } 449 450 var viewportCTM = this.viewport.getCTM() 451 452 point = point.matrixTransform(viewportCTM.inverse()) 453 454 var k = svg.createSVGMatrix().translate(point.x, point.y).scale(zoomScale).translate(-point.x, -point.y) 455 , wasZoom = viewportCTM 456 , setZoom = viewportCTM.multiply(k) 457 458 if (zoomAbsolute) { 459 setZoom.a = setZoom.d = zoomScale 460 } 461 462 if (setZoom.a < this.options.minZoom * this.initialCTM.a) {setZoom.a = setZoom.d = wasZoom.a} 463 if (setZoom.a > this.options.maxZoom * this.initialCTM.a) {setZoom.a = setZoom.d = wasZoom.a} 464 if (setZoom.a !== wasZoom.a) { 465 SvgUtils.setCTM(this.viewport, setZoom) 466 467 // Cache zoom level 468 this._zoom = setZoom.a 469 } 470 471 if (!this.stateTf) { 472 this.stateTf = setZoom.inverse() 473 } 474 475 this.stateTf = this.stateTf.multiply(k.inverse()) 476 477 if (this.options.onZoom) { 478 this.options.onZoom(setZoom.a) 479 } 480 } 481 482 SvgPanZoom.prototype.publicZoomAtPoint = function(scale, point, absolute) { 483 // If not a SVGPoint but has x and y than create new point 484 if (Utils.getType(point) !== 'SVGPoint' && 'x' in point && 'y' in point) { 485 var _point = this.svg.createSVGPoint() 486 _point.x = point.x 487 _point.y = point.y 488 point = _point 489 } else { 490 throw new Error('Given point is invalid') 491 return 492 } 493 494 this.zoomAtPoint(this.svg, point, scale, absolute) 495 } 496 497 /** 498 * Get zoom scale/level 499 * 500 * @return {float} zoom scale 501 */ 502 SvgPanZoom.prototype.getZoom = function() { 503 return this._zoom 504 } 505 506 SvgPanZoom.prototype.resetZoom = function() { 507 this.getPublicInstance().zoom(this.initialCTM.a) 508 this.getPublicInstance().pan({x: this.initialCTM.e, y: this.initialCTM.f}) 509 510 // Cache zoom level 511 this._zoom = this.initialCTM.a 512 513 // Cache pan level 514 this._pan.x = this.initialCTM.e 515 this._pan.y = this.initialCTM.f 516 } 517 518 /** 519 * Handle mouse move event 520 * 521 * @param {object} evt Event 522 */ 523 SvgPanZoom.prototype.handleMouseMove = function(evt) { 524 if (evt.preventDefault) { 525 evt.preventDefault() 526 } else { 527 evt.returnValue = false 528 } 529 530 var svg = (evt.target.tagName === 'svg' || evt.target.tagName === 'SVG') ? evt.target : evt.target.ownerSVGElement || evt.target.correspondingElement.ownerSVGElement 531 532 var point; 533 if (this.state === 'pan' && this.options.panEnabled) { 534 // Trigger beforePan 535 if (Utils.isFunction(this.options.beforePan)) { 536 this.options.beforePan() 537 } 538 539 // Pan mode 540 point = SvgUtils.getEventPoint(evt).matrixTransform(this.stateTf) 541 var viewportCTM = this.stateTf.inverse().translate(point.x - this.stateOrigin.x, point.y - this.stateOrigin.y) 542 543 SvgUtils.setCTM(this.viewport, viewportCTM) 544 545 // Cache pan level 546 this._pan.x = viewportCTM.e 547 this._pan.y = viewportCTM.f 548 549 // Trigger onPan 550 this.options.onPan(this._pan.x, this._pan.y) 551 } else if (this.state === 'drag' && this.options.dragEnabled) { 552 // Drag mode 553 point = SvgUtils.getEventPoint(evt).matrixTransform(this.viewport.getCTM().inverse()) 554 555 SvgUtils.setCTM(this.stateTarget, svg.createSVGMatrix().translate(point.x - this.stateOrigin.x, point.y - this.stateOrigin.y).multiply(this.viewport.getCTM().inverse()).multiply(this.stateTarget.getCTM())) 556 557 this.stateOrigin = point; 558 } 559 } 560 561 /** 562 * Handle double click event 563 * See handleMouseDown() for alternate detection method 564 * 565 * @param {object} evt Event 566 */ 567 SvgPanZoom.prototype.handleDblClick = function(evt) { 568 var target = evt.target 569 , svg = (target.tagName === 'svg' || target.tagName === 'SVG') ? target : target.ownerSVGElement || target.correspondingElement.ownerSVGElement 570 571 if (evt.preventDefault) { 572 evt.preventDefault() 573 } else { 574 evt.returnValue = false 575 } 576 577 // Check if target was a control button 578 if (this.options.controlIconsEnabled) { 579 var targetClass = target.getAttribute('class') || '' 580 if (targetClass.indexOf('svg-pan-zoom-control') > -1) { 581 return false 582 } 583 } 584 585 var zoomFactor 586 587 if (evt.shiftKey) { 588 zoomFactor = 1/((1 + this.options.zoomScaleSensitivity) * 2) // zoom out when shift key pressed 589 } 590 else { 591 zoomFactor = (1 + this.options.zoomScaleSensitivity) * 2 592 } 593 594 var point = SvgUtils.getRelativeMousePoint(svg, evt) 595 this.zoomAtPoint(svg, point, zoomFactor) 596 } 597 598 /** 599 * Handle click event 600 * 601 * @param {object} evt Event 602 */ 603 SvgPanZoom.prototype.handleMouseDown = function(evt) { 604 // Double click detection; more consistent than ondblclick 605 if (evt.detail === 2){ 606 this.handleDblClick(evt) 607 } 608 609 if (evt.preventDefault) { 610 evt.preventDefault() 611 } else { 612 evt.returnValue = false 613 } 614 615 var svg = (evt.target.tagName === 'svg' || evt.target.tagName === 'SVG') ? evt.target : evt.target.ownerSVGElement || evt.target.correspondingElement.ownerSVGElement 616 617 if (evt.target.tagName === 'svg' || !this.options.dragEnabled) { // Pan anyway when drag is disabled and the user clicked on an element 618 // Pan mode 619 this.state = 'pan' 620 this.stateTf = this.viewport.getCTM().inverse() 621 this.stateOrigin = SvgUtils.getEventPoint(evt).matrixTransform(this.stateTf) 622 } else { 623 // Drag mode 624 this.state = 'drag' 625 this.stateTarget = evt.target 626 this.stateTf = this.viewport.getCTM().inverse() 627 this.stateOrigin = SvgUtils.getEventPoint(evt).matrixTransform(this.stateTf) 628 } 629 } 630 631 /** 632 * Handle mouse button release event 633 * 634 * @param {object} evt Event 635 */ 636 SvgPanZoom.prototype.handleMouseUp = function(evt) { 637 if (evt.preventDefault) { 638 evt.preventDefault() 639 } else { 640 evt.returnValue = false 641 } 642 643 var svg = (evt.target.tagName === 'svg' || evt.target.tagName === 'SVG') ? evt.target : evt.target.ownerSVGElement || evt.target.correspondingElement.ownerSVGElement 644 645 if (this.state === 'pan' || this.state === 'drag') { 646 // Quit pan mode 647 this.state = 'none' 648 } 649 } 650 651 /** 652 * Adjust viewport size (only) so it will fit in SVG 653 * Does not center image 654 * 655 * @param {bool} dropCache drop viewBox cache and recalculate SVG's viewport sizes. Default false 656 */ 657 SvgPanZoom.prototype.fit = function(dropCache) { 658 if (dropCache) { 659 this.recacheViewBox() 660 } 661 662 var newScale = Math.min(this.width/(this._viewBox.width - this._viewBox.x), this.height/(this._viewBox.height - this._viewBox.y)) 663 664 this.getPublicInstance().zoom(newScale) 665 } 666 667 /** 668 * Adjust viewport pan (only) so it will be centered in SVG 669 * Does not zoom/fit image 670 * 671 * @param {bool} dropCache drop viewBox cache and recalculate SVG's viewport sizes. Default false 672 */ 673 SvgPanZoom.prototype.center = function(dropCache) { 674 if (dropCache) { 675 this.recacheViewBox() 676 } 677 678 var offsetX = (this.width - (this._viewBox.width + this._viewBox.x) * this.getZoom()) * 0.5 679 , offsetY = (this.height - (this._viewBox.height + this._viewBox.y) * this.getZoom()) * 0.5 680 681 this.getPublicInstance().pan({x: offsetX, y: offsetY}) 682 } 683 684 /** 685 * Pan to a rendered position 686 * 687 * @param {object} point {x: 0, y: 0} 688 */ 689 SvgPanZoom.prototype.pan = function(point) { 690 // Trigger beforePan 691 if (Utils.isFunction(this.options.beforePan)) { 692 this.options.beforePan() 693 } 694 695 var viewportCTM = this.viewport.getCTM() 696 viewportCTM.e = point.x 697 viewportCTM.f = point.y 698 SvgUtils.setCTM(this.viewport, viewportCTM) 699 700 // Cache pan level 701 this._pan.x = viewportCTM.e 702 this._pan.y = viewportCTM.f 703 704 // Trigger onPan 705 this.options.onPan(this._pan.x, this._pan.y) 706 } 707 708 /** 709 * Relatively pan the graph by a specified rendered position vector 710 * 711 * @param {object} point {x: 0, y: 0} 712 */ 713 SvgPanZoom.prototype.panBy = function(point) { 714 // Trigger beforePan 715 if (Utils.isFunction(this.options.beforePan)) { 716 this.options.beforePan() 717 } 718 719 var viewportCTM = this.viewport.getCTM() 720 viewportCTM.e += point.x 721 viewportCTM.f += point.y 722 SvgUtils.setCTM(this.viewport, viewportCTM) 723 724 // Cache pan level 725 this._pan.x = viewportCTM.e 726 this._pan.y = viewportCTM.f 727 728 // Trigger onPan 729 this.options.onPan(this._pan.x, this._pan.y) 730 } 731 732 /** 733 * Get pan vector 734 * 735 * @return {object} {x: 0, y: 0} 736 */ 737 SvgPanZoom.prototype.getPan = function() { 738 // Do not return object directly because it will be possible to modify it using the reference 739 return {x: this._pan.x, y: this._pan.y} 740 } 741 742 /** 743 * Returns a public instance object 744 * @return {object} Public instance object 745 */ 746 SvgPanZoom.prototype.getPublicInstance = function() { 747 var that = this 748 749 // Create cache 750 if (!this.publicInstance) { 751 this.publicInstance = { 752 // Pan 753 enablePan: function() {that.options.panEnabled = true} 754 , disablePan: function() {that.options.panEnabled = false} 755 , isPanEnabled: function() {return !!that.options.panEnabled} 756 , pan: function(point) {that.pan(point)} 757 , panBy: function(point) {that.panBy(point)} 758 , getPan: function() {return that.getPan()} 759 // Pan event 760 , setBeforePan: function(fn) {that.options.beforePan = Utils.proxy(fn, that.publicInstance)} 761 , setOnPan: function(fn) {that.options.onPan = Utils.proxy(fn, that.publicInstance)} 762 // Drag 763 , enableDrag: function() {that.options.dragEnabled = true} 764 , disableDrag: function() {that.options.dragEnabled = false} 765 , isDragEnabled: function() {return !!that.options.dragEnabled} 766 // Zoom and Control Icons 767 , enableZoom: function() { 768 if (that.options.controlIconsEnabled && !that.options.zoomEnabled) { 769 ControlIcons.enable(that) 770 } 771 that.options.zoomEnabled = true; 772 } 773 , disableZoom: function() { 774 if (that.options.controlIconsEnabled && that.options.zoomEnabled) { 775 ControlIcons.disable(that) 776 } 777 that.options.zoomEnabled = false; 778 } 779 , isZoomEnabled: function() {return !!that.options.zoomEnabled} 780 , enableControlIcons: function() { 781 if (that.options.zoomEnabled && !that.options.controlIconsEnabled) { 782 that.options.controlIconsEnabled = true 783 ControlIcons.enable(that) 784 } 785 } 786 , disableControlIcons: function() { 787 if (that.options.controlIconsEnabled) { 788 that.options.controlIconsEnabled = false; 789 ControlIcons.disable(that) 790 } 791 } 792 , isControlIconsEnabled: function() {return !!that.options.controlIconsEnabled} 793 // Zoom scale and bounds 794 , setZoomScaleSensitivity: function(scale) {that.options.zoomScaleSensitivity = scale} 795 , setMinZoom: function(zoom) {that.options.minZoom = zoom} 796 , setMaxZoom: function(zoom) {that.options.maxZoom = zoom} 797 // Zoom event 798 , setBeforeZoom: function(fn) {that.options.beforeZoom = Utils.proxy(fn, that.publicInstance)} 799 , setOnZoom: function(fn) {that.options.onZoom = Utils.proxy(fn, that.publicInstance)} 800 // Zooming 801 , zoom: function(scale) { 802 that.zoomAtPoint(that.svg, SvgUtils.getSvgCenterPoint(that.svg), scale, true) 803 } 804 , zoomBy: function(scale) { 805 that.zoomAtPoint(that.svg, SvgUtils.getSvgCenterPoint(that.svg), scale, false) 806 } 807 , zoomAtPoint: function(scale, point) { 808 that.publicZoomAtPoint(scale, point, true) 809 } 810 , zoomAtPointBy: function(scale, point) { 811 that.publicZoomAtPoint(scale, point, false) 812 } 813 , zoomIn: function() { 814 this.zoomBy(1 + that.options.zoomScaleSensitivity) 815 } 816 , zoomOut: function() { 817 this.zoomBy(1 / (1 + that.options.zoomScaleSensitivity)) 818 } 819 , resetZoom: function() {that.resetZoom()} 820 , getZoom: function() {return that.getZoom()} 821 , fit: function(dropCache) {return that.fit(dropCache)} 822 , center: function(dropCache) {return that.center(dropCache)} 823 } 824 } 825 826 return this.publicInstance 827 } 828 829 /** 830 * Stores pairs of instances of SvgPanZoom and SVG 831 * Each pair is represented by an object {svg: SVG, instance: SvgPanZoom} 832 * 833 * @type {Array} 834 */ 835 var instancesStore = [] 836 837 window.svgPanZoom = function(elementOrSelector, options){ 838 var svg = Utils.getSvg(elementOrSelector) 839 840 if (svg === null) { 841 return null 842 } else { 843 // Look for existent instance 844 for(var i = instancesStore.length - 1; i >= 0; i--) { 845 if (instancesStore[i].svg === svg) { 846 return instancesStore[i].instance.getPublicInstance() 847 } 848 } 849 850 // If instance not found - create one 851 instancesStore.push({ 852 svg: svg 853 , instance: new SvgPanZoom(svg, options) 854 }) 855 856 // Return just pushed instance 857 return instancesStore[instancesStore.length - 1].instance.getPublicInstance() 858 } 859 } 860})(window, document) 861 862},{"./control-icons":1,"./mousewheel":2,"./svg-utilities":4,"./utilities":5}],4:[function(require,module,exports){ 863module.exports = { 864 /** 865 * Get svg dimensions: width and height 866 * 867 * @param {object} svg 868 * @return {object} {width: 0, height: 0} 869 */ 870 getSvgDimensions: function(svg) { 871 var width = 0 872 , height = 0 873 , svgClientRects = svg.getClientRects() 874 875 // thanks to http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser 876 var isFirefox = typeof InstallTrigger !== 'undefined'; 877 878 // Firefox has no nice way of detecting SVG size, so we'll check for 879 // width/height from getComputedStyle, specified in pixels, 880 // and if they don't exist, we'll use the parent dimensions. 881 // TODO: check whether this method would be a better method for other browsers too. 882 if (isFirefox) { 883 var svgComputedStyle = window.getComputedStyle(svg, null); 884 width = parseFloat(svgComputedStyle.width) - (parseFloat(svgComputedStyle.borderLeftWidth) + parseFloat(svgComputedStyle.paddingLeft) + parseFloat(svgComputedStyle.borderRightWidth) + parseFloat(svgComputedStyle.paddingRight)); 885 height = parseFloat(svgComputedStyle.height) - (parseFloat(svgComputedStyle.borderTopWidth) + parseFloat(svgComputedStyle.paddingTop) + parseFloat(svgComputedStyle.borderBottomWidth) + parseFloat(svgComputedStyle.paddingBottom)); 886 if (!width || !height) { 887 var parentStyle = window.getComputedStyle(svg.parentElement, null); 888 var parentDimensions = svg.parentElement.getBoundingClientRect(); 889 width = parentDimensions.width - (parseFloat(parentStyle.borderLeftWidth) + parseFloat(parentStyle.paddingLeft) + parseFloat(parentStyle.borderRightWidth) + parseFloat(parentStyle.paddingRight)); 890 height = parentDimensions.height - (parseFloat(parentStyle.borderTopWidth) + parseFloat(parentStyle.paddingTop) + parseFloat(parentStyle.borderBottomWidth) + parseFloat(parentStyle.paddingBottom)); 891 } 892 } else { 893 if (typeof svgClientRects !== 'undefined' && svgClientRects.length > 0) { 894 var svgClientRect = svgClientRects[0]; 895 896 width = parseFloat(svgClientRect.width); 897 height = parseFloat(svgClientRect.height); 898 } else { 899 var svgBoundingClientRect = svg.getBoundingClientRect(); 900 901 if (!!svgBoundingClientRect) { 902 width = parseFloat(svgBoundingClientRect.width); 903 height = parseFloat(svgBoundingClientRect.height); 904 } else { 905 throw new Error('Cannot determine SVG width and height.'); 906 } 907 } 908 } 909 910 return { 911 width: width 912 , height: height 913 } 914 } 915 916 /** 917 * Gets g.viewport element or creates it if it doesn't exist 918 * @param {object} svg 919 * @return {object} g element 920 */ 921, getOrCreateViewport: function(svg) { 922 var viewport = svg.querySelector('g.viewport') 923 924 // If no g container with id 'viewport' exists, create one 925 if (!viewport) { 926 var viewport = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 927 viewport.setAttribute('class', 'viewport'); 928 929 var svgChildren = svg.childNodes || svg.children; 930 do { 931 viewport.appendChild(svgChildren[0]); 932 } while (svgChildren.length > 0); 933 svg.appendChild(viewport); 934 } 935 936 return viewport 937 } 938 939, setupSvgAttributes: function(svg) { 940 // Setting default attributes 941 svg.setAttribute('xmlns', 'http://www.w3.org/1999/xlink'); 942 svg.setAttributeNS('xmlns', 'xlink', 'http://www.w3.org/1999/xlink'); 943 svg.setAttributeNS('xmlns', 'ev', 'http://www.w3.org/2001/xml-events'); 944 945 // Needed for Internet Explorer, otherwise the viewport overflows 946 if (svg.parentNode !== null) { 947 var style = svg.getAttribute('style') || ''; 948 if (style.toLowerCase().indexOf('overflow') === -1) { 949 svg.setAttribute('style', 'overflow: hidden; ' + style); 950 } 951 } 952 } 953 954 /** 955 * Sets the current transform matrix of an element 956 * @param {object} element SVG Element 957 * @param {object} matrix CTM 958 */ 959, setCTM: function(element, matrix) { 960 var s = 'matrix(' + matrix.a + ',' + matrix.b + ',' + matrix.c + ',' + matrix.d + ',' + matrix.e + ',' + matrix.f + ')'; 961 element.setAttribute('transform', s); 962 } 963 964 /** 965 * Time-based cache for svg.getScreenCTM(). 966 * Needed because getScreenCTM() is very slow on Firefox (FF 28 at time of writing). 967 * The cache expires every 300ms... this is a pretty safe time because it's only called 968 * when we're zooming, when the screenCTM is unlikely/impossible to change. 969 * 970 * @param {object} svg SVG Element 971 * @return {[type]} [description] 972 */ 973, getScreenCTMCached: (function() { 974 var svgs = {}; 975 return function(svg) { 976 var cur = Date.now(); 977 if (svgs.hasOwnProperty(svg)) { 978 var cached = svgs[svg]; 979 if (cur - cached.time > 300) { 980 // Cache expired 981 cached.time = cur; 982 cached.ctm = svg.getScreenCTM(); 983 } 984 return cached.ctm; 985 } else { 986 var ctm = svg.getScreenCTM(); 987 svgs[svg] = {time: cur, ctm: ctm}; 988 return ctm; 989 } 990 }; 991 })() 992 993 /** 994 * Get an SVGPoint of the mouse co-ordinates of the event, relative to the SVG element 995 * 996 * @param {object} svg SVG Element 997 * @param {object} evt Event 998 * @return {object} point 999 */ 1000, getRelativeMousePoint: function(svg, evt) { 1001 var point = svg.createSVGPoint() 1002 1003 point.x = evt.clientX 1004 point.y = evt.clientY 1005 1006 return point.matrixTransform(this.getScreenCTMCached(svg).inverse()) 1007 } 1008 1009 /** 1010 * Instantiate an SVGPoint object with given event coordinates 1011 * 1012 * @param {object} evt Event 1013 */ 1014, getEventPoint: function(evt) { 1015 var svg = (evt.target.tagName === 'svg' || evt.target.tagName === 'SVG') ? evt.target : evt.target.ownerSVGElement || evt.target.correspondingElement.ownerSVGElement 1016 , point = svg.createSVGPoint() 1017 1018 point.x = evt.clientX 1019 point.y = evt.clientY 1020 1021 return point 1022 } 1023 1024 /** 1025 * Get SVG center point 1026 * 1027 * @param {object} svg SVG Element 1028 * @return {object} SVG Point 1029 */ 1030, getSvgCenterPoint: function(svg) { 1031 var boundingClientRect = svg.getBoundingClientRect() 1032 , width = boundingClientRect.width 1033 , height = boundingClientRect.height 1034 , point = svg.createSVGPoint() 1035 1036 point.x = width / 2 1037 point.y = height / 2 1038 1039 return point 1040 } 1041} 1042 1043},{}],5:[function(require,module,exports){ 1044module.exports = { 1045 /** 1046 * Extends an object 1047 * 1048 * @param {object} target object to extend 1049 * @param {object} source object to take properties from 1050 * @return {object} extended object 1051 */ 1052 extend: function(target, source) { 1053 target = target || {}; 1054 for (var prop in source) { 1055 // Go recursively 1056 if (this.isObject(source[prop])) { 1057 target[prop] = this.extend(target[prop], source[prop]) 1058 } else { 1059 target[prop] = source[prop] 1060 } 1061 } 1062 return target; 1063 } 1064 1065 /** 1066 * Checks if an object is a DOM element 1067 * 1068 * @param {object} o HTML element or String 1069 * @return {Boolean} returns true if object is a DOM element 1070 */ 1071, isElement: function(o){ 1072 return ( 1073 typeof HTMLElement === "object" ? (o instanceof HTMLElement || o instanceof SVGElement || o instanceof SVGSVGElement) : //DOM2 1074 o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string" 1075 ); 1076 } 1077 1078 /** 1079 * Checks if an object is an Object 1080 * 1081 * @param {object} o Object 1082 * @return {Boolean} returns true if object is an Object 1083 */ 1084, isObject: function(o){ 1085 return Object.prototype.toString.call(o) === '[object Object]'; 1086 } 1087 1088 /** 1089 * Checks if an object is a Function 1090 * 1091 * @param {object} f Function 1092 * @return {Boolean} returns true if object is a Function 1093 */ 1094, isFunction: function(f){ 1095 return Object.prototype.toString.call(f) === '[object Function]'; 1096 } 1097 1098 /** 1099 * Search for an SVG element 1100 * 1101 * @param {object|string} elementOrSelector DOM Element or selector String 1102 * @return {object|null} SVG or null 1103 */ 1104, getSvg: function(elementOrSelector) { 1105 var element 1106 , svg; 1107 1108 if (!this.isElement(elementOrSelector)) { 1109 // If selector provided 1110 if (typeof elementOrSelector == 'string' || elementOrSelector instanceof String) { 1111 // Try to find the element 1112 element = document.querySelector(elementOrSelector) 1113 1114 if (!element) { 1115 throw new Error('Provided selector did not find any elements') 1116 return null 1117 } 1118 1119 } else { 1120 throw new Error('Provided selector is not an HTML object nor String') 1121 return null 1122 } 1123 } else { 1124 element = elementOrSelector 1125 } 1126 1127 if (element.tagName.toLowerCase() === 'svg') { 1128 svg = element; 1129 } else { 1130 if (element.tagName.toLowerCase() === 'object') { 1131 svg = element.contentDocument.documentElement; 1132 } else { 1133 if (element.tagName.toLowerCase() === 'embed') { 1134 svg = element.getSVGDocument().documentElement; 1135 } else { 1136 if (element.tagName.toLowerCase() === 'img') { 1137 throw new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.'); 1138 } else { 1139 throw new Error('Cannot get SVG.'); 1140 } 1141 return null 1142 } 1143 } 1144 } 1145 1146 return svg 1147 } 1148 1149 /** 1150 * Attach a given context to a function 1151 * @param {Function} fn Function 1152 * @param {object} context Context 1153 * @return {Function} Function with certain context 1154 */ 1155, proxy: function(fn, context) { 1156 return function() { 1157 fn.apply(context, arguments) 1158 } 1159 } 1160 1161 /** 1162 * Returns object type 1163 * Uses toString that returns [object SVGPoint] 1164 * And than parses object type from string 1165 * 1166 * @param {object} o Any object 1167 * @return {string} Object type 1168 */ 1169, getType: function(o) { 1170 return Object.prototype.toString.apply(o).replace(/^\[object\s/, '').replace(/\]$/, '') 1171 } 1172} 1173 1174},{}]},{},[3])