//! openseadragon 5.0.0
//! Built on 2024-08-14
//! Git commit: v5.0.0-0-f28b7fc1
//! http://openseadragon.github.io
//! License: http://openseadragon.github.io/license/
/*
* OpenSeadragon
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Portions of this source file taken from jQuery:
*
* Copyright 2011 John Resig
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* Portions of this source file taken from mattsnider.com:
*
* Copyright (c) 2006-2013 Matt Snider
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
* OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* @namespace OpenSeadragon
* @version openseadragon 5.0.0
* @classdesc The root namespace for OpenSeadragon. All utility methods
* and classes are defined on or below this namespace.
*
*/
// Typedefs
/**
* All required and optional settings for instantiating a new instance of an OpenSeadragon image viewer.
*
* @typedef {Object} Options
* @memberof OpenSeadragon
*
* @property {String} id
* Id of the element to append the viewer's container element to. If not provided, the 'element' property must be provided.
* If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
*
* @property {Element} element
* The element to append the viewer's container element to. If not provided, the 'id' property must be provided.
* If both the element and id properties are specified, the viewer is appended to the element provided in the element property.
*
* @property {Array|String|Function|Object} [tileSources=null]
* Tile source(s) to open initially. This is a complex parameter; see
* {@link OpenSeadragon.Viewer#open} for details.
*
* @property {Number} [tabIndex=0]
* Tabbing order index to assign to the viewer element. Positive values are selected in increasing order. When tabIndex is 0
* source order is used. A negative value omits the viewer from the tabbing order.
*
* @property {Array} overlays Array of objects defining permanent overlays of
* the viewer. The overlays added via this option and later removed with
* {@link OpenSeadragon.Viewer#removeOverlay} will be added back when a new
* image is opened.
* To add overlays which can be definitively removed, one must use
* {@link OpenSeadragon.Viewer#addOverlay}
* If displaying a sequence of images, the overlays can be associated
* with a specific page by passing the overlays array to the page's
* tile source configuration.
* Expected properties:
* * x, y, (or px, py for pixel coordinates) to define the location.
* * width, height in point if using x,y or in pixels if using px,py. If width
* and height are specified, the overlay size is adjusted when zooming,
* otherwise the size stays the size of the content (or the size defined by CSS).
* * className to associate a class to the overlay
* * id to set the overlay element. If an element with this id already exists,
* it is reused, otherwise it is created. If not specified, a new element is
* created.
* * placement a string to define the relative position to the viewport.
* Only used if no width and height are specified. Default: 'TOP_LEFT'.
* See {@link OpenSeadragon.Placement} for possible values.
*
* @property {String} [xmlPath=null]
* <strong>DEPRECATED</strong>. A relative path to load a DZI file from the server.
* Prefer the newer Options.tileSources.
*
* @property {String} [prefixUrl='/images/']
* Prepends the prefixUrl to navImages paths, which is very useful
* since the default paths are rarely useful for production
* environments.
*
* @property {OpenSeadragon.NavImages} [navImages]
* An object with a property for each button or other built-in navigation
* control, eg the current 'zoomIn', 'zoomOut', 'home', and 'fullpage'.
* Each of those in turn provides an image path for each state of the button
* or navigation control, eg 'REST', 'GROUP', 'HOVER', 'PRESS'. Finally the
* image paths, by default assume there is a folder on the servers root path
* called '/images', eg '/images/zoomin_rest.png'. If you need to adjust
* these paths, prefer setting the option.prefixUrl rather than overriding
* every image path directly through this setting.
*
* @property {Boolean} [debugMode=false]
* TODO: provide an in-screen panel providing event detail feedback.
*
* @property {String} [debugGridColor=['#437AB2', '#1B9E77', '#D95F02', '#7570B3', '#E7298A', '#66A61E', '#E6AB02', '#A6761D', '#666666']]
* The colors of grids in debug mode. Each tiled image's grid uses a consecutive color.
* If there are more tiled images than provided colors, the color vector is recycled.
*
* @property {Boolean} [silenceMultiImageWarnings=false]
* Silences warnings when calling viewport coordinate functions with multi-image.
* Useful when you're overlaying multiple images on top of one another.
*
* @property {Number} [blendTime=0]
* Specifies the duration of animation as higher or lower level tiles are
* replacing the existing tile.
*
* @property {Boolean} [alwaysBlend=false]
* Forces the tile to always blend. By default the tiles skip blending
* when the blendTime is surpassed and the current animation frame would
* not complete the blend.
*
* @property {Boolean} [autoHideControls=true]
* If the user stops interacting with the viewport, fade the navigation
* controls. Useful for presentation since the controls are by default
* floated on top of the image the user is viewing.
*
* @property {Boolean} [immediateRender=false]
* Render the best closest level first, ignoring the lowering levels which
* provide the effect of very blurry to sharp. It is recommended to change
* setting to true for mobile devices.
*
* @property {Number} [defaultZoomLevel=0]
* Zoom level to use when image is first opened or the home button is clicked.
* If 0, adjusts to fit viewer.
*
* @property {String|DrawerImplementation|Array} [drawer = ['webgl', 'canvas', 'html']]
* Which drawer to use. Valid strings are 'webgl', 'canvas', and 'html'. Valid drawer
* implementations are constructors of classes that extend OpenSeadragon.DrawerBase.
* An array of strings and/or constructors can be used to indicate the priority
* of different implementations, which will be tried in order based on browser support.
*
* @property {Object} drawerOptions
* Options to pass to the selected drawer implementation. For details
* please see {@link OpenSeadragon.DrawerOptions}.
*
* @property {Number} [opacity=1]
* Default proportional opacity of the tiled images (1=opaque, 0=hidden)
* Hidden images do not draw and only load when preloading is allowed.
*
* @property {Boolean} [preload=false]
* Default switch for loading hidden images (true loads, false blocks)
*
* @property {String} [compositeOperation=null]
* Valid values are 'source-over', 'source-atop', 'source-in', 'source-out',
* 'destination-over', 'destination-atop', 'destination-in', 'destination-out',
* 'lighter', 'difference', 'copy', 'xor', etc.
* For complete list of modes, please @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation/ globalCompositeOperation}
*
* @property {Boolean} [imageSmoothingEnabled=true]
* Image smoothing for canvas rendering (only if the canvas drawer is used). Note: Ignored
* by some (especially older) browsers which do not support this canvas property.
* This property can be changed in {@link Viewer.DrawerBase.setImageSmoothingEnabled}.
*
* @property {String|CanvasGradient|CanvasPattern|Function} [placeholderFillStyle=null]
* Draws a colored rectangle behind the tile if it is not loaded yet.
* You can pass a CSS color value like "#FF8800".
* When passing a function the tiledImage and canvas context are available as argument which is useful when you draw a gradient or pattern.
*
* @property {Object} [subPixelRoundingForTransparency=null]
* Determines when subpixel rounding should be applied for tiles when rendering images that support transparency.
* This property is a subpixel rounding enum values dictionary [{@link BROWSERS}] --> {@link SUBPIXEL_ROUNDING_OCCURRENCES}.
* The key is a {@link BROWSERS} value, and the value is one of {@link SUBPIXEL_ROUNDING_OCCURRENCES},
* indicating, for a given browser, when to apply subpixel rounding.
* Key '*' is the fallback value for any browser not specified in the dictionary.
* This property has a simple mode, and one can set it directly to
* {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER}, {@link SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST} or {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS}
* in order to apply this rule for all browser. The values {@link SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS} would be equivalent to { '*', SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS }.
* The default is {@link SUBPIXEL_ROUNDING_OCCURRENCES.NEVER} for all browsers, for backward compatibility reason.
*
* @property {Number} [degrees=0]
* Initial rotation.
*
* @property {Boolean} [flipped=false]
* Initial flip state.
*
* @property {Boolean} [overlayPreserveContentDirection=true]
* When the viewport is flipped (by pressing 'f'), the overlay is flipped using ScaleX.
* Normally, this setting (default true) keeps the overlay's content readable by flipping it back.
* To make the content flip with the overlay, set overlayPreserveContentDirection to false.
*
* @property {Number} [minZoomLevel=null]
*
* @property {Number} [maxZoomLevel=null]
*
* @property {Boolean} [homeFillsViewer=false]
* Make the 'home' button fill the viewer and clip the image, instead
* of fitting the image to the viewer and letterboxing.
*
* @property {Boolean} [panHorizontal=true]
* Allow horizontal pan.
*
* @property {Boolean} [panVertical=true]
* Allow vertical pan.
*
* @property {Boolean} [constrainDuringPan=false]
*
* @property {Boolean} [wrapHorizontal=false]
* Set to true to force the image to wrap horizontally within the viewport.
* Useful for maps or images representing the surface of a sphere or cylinder.
*
* @property {Boolean} [wrapVertical=false]
* Set to true to force the image to wrap vertically within the viewport.
* Useful for maps or images representing the surface of a sphere or cylinder.
*
* @property {Number} [minZoomImageRatio=0.9]
* The minimum percentage ( expressed as a number between 0 and 1 ) of
* the viewport height or width at which the zoom out will be constrained.
* Setting it to 0, for example will allow you to zoom out infinity.
*
* @property {Number} [maxZoomPixelRatio=1.1]
* The maximum ratio to allow a zoom-in to affect the highest level pixel
* ratio. This can be set to Infinity to allow 'infinite' zooming into the
* image though it is less effective visually if the HTML5 Canvas is not
* available on the viewing device.
*
* @property {Number} [smoothTileEdgesMinZoom=1.1]
* A zoom percentage ( where 1 is 100% ) of the highest resolution level.
* When zoomed in beyond this value alternative compositing will be used to
* smooth out the edges between tiles. This will have a performance impact.
* Can be set to Infinity to turn it off.
* Note: This setting is ignored on iOS devices due to a known bug (See {@link https://github.com/openseadragon/openseadragon/issues/952})
*
* @property {Boolean} [iOSDevice=?]
* True if running on an iOS device, false otherwise.
* Used to disable certain features that behave differently on iOS devices.
*
* @property {Boolean} [autoResize=true]
* Set to false to prevent polling for viewer size changes. Useful for providing custom resize behavior.
*
* @property {Boolean} [preserveImageSizeOnResize=false]
* Set to true to have the image size preserved when the viewer is resized. This requires autoResize=true (default).
*
* @property {Number} [minScrollDeltaTime=50]
* Number of milliseconds between canvas-scroll events. This value helps normalize the rate of canvas-scroll
* events between different devices, causing the faster devices to slow down enough to make the zoom control
* more manageable.
*
* @property {Number} [rotationIncrement=90]
* The number of degrees to rotate right or left when the rotate buttons or keyboard shortcuts are activated.
*
* @property {Number} [maxTilesPerFrame=1]
* The number of tiles loaded per frame. As the frame rate of the client's machine is usually high (e.g., 50 fps),
* one tile per frame should be a good choice. However, for large screens or lower frame rates, the number of
* loaded tiles per frame can be adjusted here. Reasonable values might be 2 or 3 tiles per frame.
* (Note that the actual frame rate is given by the client's browser and machine).
*
* @property {Number} [pixelsPerWheelLine=40]
* For pixel-resolution scrolling devices, the number of pixels equal to one scroll line.
*
* @property {Number} [pixelsPerArrowPress=40]
* The number of pixels viewport moves when an arrow key is pressed.
*
* @property {Number} [visibilityRatio=0.5]
* The percentage ( as a number from 0 to 1 ) of the source image which
* must be kept within the viewport. If the image is dragged beyond that
* limit, it will 'bounce' back until the minimum visibility ratio is
* achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to
* true will provide the effect of an infinitely scrolling viewport.
*
* @property {Object} [viewportMargins={}]
* Pushes the "home" region in from the sides by the specified amounts.
* Possible subproperties (Numbers, in screen coordinates): left, top, right, bottom.
*
* @property {Number} [imageLoaderLimit=0]
* The maximum number of image requests to make concurrently. By default
* it is set to 0 allowing the browser to make the maximum number of
* image requests in parallel as allowed by the browsers policy.
*
* @property {Number} [clickTimeThreshold=300]
* The number of milliseconds within which a pointer down-up event combination
* will be treated as a click gesture.
*
* @property {Number} [clickDistThreshold=5]
* The maximum distance allowed between a pointer down event and a pointer up event
* to be treated as a click gesture.
*
* @property {Number} [dblClickTimeThreshold=300]
* The number of milliseconds within which two pointer down-up event combinations
* will be treated as a double-click gesture.
*
* @property {Number} [dblClickDistThreshold=20]
* The maximum distance allowed between two pointer click events
* to be treated as a double-click gesture.
*
* @property {Number} [springStiffness=6.5]
*
* @property {Number} [animationTime=1.2]
* Specifies the animation duration per each {@link OpenSeadragon.Spring}
* which occur when the image is dragged, zoomed or rotated.
*
* @property {OpenSeadragon.GestureSettings} [gestureSettingsMouse]
* Settings for gestures generated by a mouse pointer device. (See {@link OpenSeadragon.GestureSettings})
* @property {Boolean} [gestureSettingsMouse.dragToPan=true] - Pan on drag gesture
* @property {Boolean} [gestureSettingsMouse.scrollToZoom=true] - Zoom on scroll gesture
* @property {Boolean} [gestureSettingsMouse.clickToZoom=true] - Zoom on click gesture
* @property {Boolean} [gestureSettingsMouse.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsMouse.dblClickDragToZoom=false] - Zoom on dragging through
* double-click gesture ( single click and next click to drag). Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsMouse.pinchToZoom=false] - Zoom on pinch gesture
* @property {Boolean} [gestureSettingsMouse.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
* the zoom is centered at the canvas center.
* @property {Boolean} [gestureSettingsMouse.flickEnabled=false] - Enable flick gesture
* @property {Number} [gestureSettingsMouse.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
* @property {Number} [gestureSettingsMouse.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
* @property {Boolean} [gestureSettingsMouse.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
*
* @property {OpenSeadragon.GestureSettings} [gestureSettingsTouch]
* Settings for gestures generated by a touch pointer device. (See {@link OpenSeadragon.GestureSettings})
* @property {Boolean} [gestureSettingsTouch.dragToPan=true] - Pan on drag gesture
* @property {Boolean} [gestureSettingsTouch.scrollToZoom=false] - Zoom on scroll gesture
* @property {Boolean} [gestureSettingsTouch.clickToZoom=false] - Zoom on click gesture
* @property {Boolean} [gestureSettingsTouch.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsTouch.dblClickDragToZoom=true] - Zoom on dragging through
* double-click gesture ( single click and next click to drag). Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsTouch.pinchToZoom=true] - Zoom on pinch gesture
* @property {Boolean} [gestureSettingsTouch.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
* the zoom is centered at the canvas center.
* @property {Boolean} [gestureSettingsTouch.flickEnabled=true] - Enable flick gesture
* @property {Number} [gestureSettingsTouch.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
* @property {Number} [gestureSettingsTouch.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
* @property {Boolean} [gestureSettingsTouch.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
*
* @property {OpenSeadragon.GestureSettings} [gestureSettingsPen]
* Settings for gestures generated by a pen pointer device. (See {@link OpenSeadragon.GestureSettings})
* @property {Boolean} [gestureSettingsPen.dragToPan=true] - Pan on drag gesture
* @property {Boolean} [gestureSettingsPen.scrollToZoom=false] - Zoom on scroll gesture
* @property {Boolean} [gestureSettingsPen.clickToZoom=true] - Zoom on click gesture
* @property {Boolean} [gestureSettingsPen.dblClickToZoom=false] - Zoom on double-click gesture. Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsPen.pinchToZoom=false] - Zoom on pinch gesture
* @property {Boolean} [gestureSettingsPen.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
* the zoom is centered at the canvas center.
* @property {Boolean} [gestureSettingsPen.flickEnabled=false] - Enable flick gesture
* @property {Number} [gestureSettingsPen.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
* @property {Number} [gestureSettingsPen.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
* @property {Boolean} [gestureSettingsPen.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
*
* @property {OpenSeadragon.GestureSettings} [gestureSettingsUnknown]
* Settings for gestures generated by unknown pointer devices. (See {@link OpenSeadragon.GestureSettings})
* @property {Boolean} [gestureSettingsUnknown.dragToPan=true] - Pan on drag gesture
* @property {Boolean} [gestureSettingsUnknown.scrollToZoom=true] - Zoom on scroll gesture
* @property {Boolean} [gestureSettingsUnknown.clickToZoom=false] - Zoom on click gesture
* @property {Boolean} [gestureSettingsUnknown.dblClickToZoom=true] - Zoom on double-click gesture. Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsUnknown.dblClickDragToZoom=false] - Zoom on dragging through
* double-click gesture ( single click and next click to drag). Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
* @property {Boolean} [gestureSettingsUnknown.pinchToZoom=true] - Zoom on pinch gesture
* @property {Boolean} [gestureSettingsUnknown.zoomToRefPoint=true] - If zoomToRefPoint is true, the zoom is centered at the pointer position. Otherwise,
* the zoom is centered at the canvas center.
* @property {Boolean} [gestureSettingsUnknown.flickEnabled=true] - Enable flick gesture
* @property {Number} [gestureSettingsUnknown.flickMinSpeed=120] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second)
* @property {Number} [gestureSettingsUnknown.flickMomentum=0.25] - If flickEnabled is true, the momentum factor for the flick gesture
* @property {Boolean} [gestureSettingsUnknown.pinchRotate=false] - If pinchRotate is true, the user will have the ability to rotate the image using their fingers.
*
* @property {Number} [zoomPerClick=2.0]
* The "zoom distance" per mouse click or touch tap. <em><strong>Note:</strong> Setting this to 1.0 effectively disables the click-to-zoom feature (also see gestureSettings[Mouse|Touch|Pen].clickToZoom/dblClickToZoom).</em>
*
* @property {Number} [zoomPerScroll=1.2]
* The "zoom distance" per mouse scroll or touch pinch. <em><strong>Note:</strong> Setting this to 1.0 effectively disables the mouse-wheel zoom feature (also see gestureSettings[Mouse|Touch|Pen].scrollToZoom}).</em>
*
* @property {Number} [zoomPerDblClickDrag=1.2]
* The "zoom distance" per double-click mouse drag. <em><strong>Note:</strong> Setting this to 1.0 effectively disables the double-click-drag-to-Zoom feature (also see gestureSettings[Mouse|Touch|Pen].dblClickDragToZoom).</em>
*
* @property {Number} [zoomPerSecond=1.0]
* Sets the zoom amount per second when zoomIn/zoomOut buttons are pressed and held.
* The value is a factor of the current zoom, so 1.0 (the default) disables zooming when the zoomIn/zoomOut buttons
* are held. Higher values will increase the rate of zoom when the zoomIn/zoomOut buttons are held. Note that values
* < 1.0 will reverse the operation of the zoomIn/zoomOut buttons (zoomIn button will decrease the zoom, zoomOut will
* increase the zoom).
*
* @property {Boolean} [showNavigator=false]
* Set to true to make the navigator minimap appear.
*
* @property {Element} [navigatorElement=null]
* The element to hold the navigator minimap.
* If an element is specified, the Id option (see navigatorId) is ignored.
* If no element nor ID is specified, a div element will be generated accordingly.
*
* @property {String} [navigatorId=navigator-GENERATED DATE]
* The ID of a div to hold the navigator minimap.
* If an ID is specified, the navigatorPosition, navigatorSizeRatio, navigatorMaintainSizeRatio, navigator[Top|Left|Height|Width] and navigatorAutoFade options will be ignored.
* If an ID is not specified, a div element will be generated and placed on top of the main image.
*
* @property {String} [navigatorPosition='TOP_RIGHT']
* Valid values are 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', or 'ABSOLUTE'.<br>
* If 'ABSOLUTE' is specified, then navigator[Top|Left|Height|Width] determines the size and position of the navigator minimap in the viewer, and navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.<br>
* For 'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', and 'BOTTOM_RIGHT', the navigatorSizeRatio or navigator[Height|Width] values determine the size of the navigator minimap.
*
* @property {Number} [navigatorSizeRatio=0.2]
* Ratio of navigator size to viewer size. Ignored if navigator[Height|Width] are specified.
*
* @property {Boolean} [navigatorMaintainSizeRatio=false]
* If true, the navigator minimap is resized (using navigatorSizeRatio) when the viewer size changes.
*
* @property {Number|String} [navigatorTop=null]
* Specifies the location of the navigator minimap (see navigatorPosition).
*
* @property {Number|String} [navigatorLeft=null]
* Specifies the location of the navigator minimap (see navigatorPosition).
*
* @property {Number|String} [navigatorHeight=null]
* Specifies the size of the navigator minimap (see navigatorPosition).
* If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
*
* @property {Number|String} [navigatorWidth=null]
* Specifies the size of the navigator minimap (see navigatorPosition).
* If specified, navigatorSizeRatio and navigatorMaintainSizeRatio are ignored.
*
* @property {Boolean} [navigatorAutoResize=true]
* Set to false to prevent polling for navigator size changes. Useful for providing custom resize behavior.
* Setting to false can also improve performance when the navigator is configured to a fixed size.
*
* @property {Boolean} [navigatorAutoFade=true]
* If the user stops interacting with the viewport, fade the navigator minimap.
* Setting to false will make the navigator minimap always visible.
*
* @property {Boolean} [navigatorRotate=true]
* If true, the navigator will be rotated together with the viewer.
*
* @property {String} [navigatorBackground='#000']
* Specifies the background color of the navigator minimap
*
* @property {Number} [navigatorOpacity=0.8]
* Specifies the opacity of the navigator minimap.
*
* @property {String} [navigatorBorderColor='#555']
* Specifies the border color of the navigator minimap
*
* @property {String} [navigatorDisplayRegionColor='#900']
* Specifies the border color of the display region rectangle of the navigator minimap
*
* @property {Number} [controlsFadeDelay=2000]
* The number of milliseconds to wait once the user has stopped interacting
* with the interface before beginning to fade the controls. Assumes
* showNavigationControl and autoHideControls are both true.
*
* @property {Number} [controlsFadeLength=1500]
* The number of milliseconds to animate the controls fading out.
*
* @property {Number} [maxImageCacheCount=200]
* The max number of images we should keep in memory (per drawer).
*
* @property {Number} [timeout=30000]
* The max number of milliseconds that an image job may take to complete.
*
* @property {Number} [tileRetryMax=0]
* The max number of retries when a tile download fails. By default it's 0, so retries are disabled.
*
* @property {Number} [tileRetryDelay=2500]
* Milliseconds to wait after each tile retry if tileRetryMax is set.
*
* @property {Boolean} [useCanvas=true]
* Deprecated. Use the `drawer` option to specify preferred renderer.
*
* @property {Number} [minPixelRatio=0.5]
* The higher the minPixelRatio, the lower the quality of the image that
* is considered sufficient to stop rendering a given zoom level. For
* example, if you are targeting mobile devices with less bandwidth you may
* try setting this to 1.5 or higher.
*
* @property {Boolean} [mouseNavEnabled=true]
* Is the user able to interact with the image via mouse or touch. Default
* interactions include draging the image in a plane, and zooming in toward
* and away from the image.
*
* @property {Boolean} [showNavigationControl=true]
* Set to false to prevent the appearance of the default navigation controls.<br>
* Note that if set to false, the customs buttons set by the options
* zoomInButton, zoomOutButton etc, are rendered inactive.
*
* @property {OpenSeadragon.ControlAnchor} [navigationControlAnchor=TOP_LEFT]
* Placement of the default navigation controls.
* To set the placement of the sequence controls, see the
* sequenceControlAnchor option.
*
* @property {Boolean} [showZoomControl=true]
* If true then + and - buttons to zoom in and out are displayed.<br>
* Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
* this setting when set to false.
*
* @property {Boolean} [showHomeControl=true]
* If true then the 'Go home' button is displayed to go back to the original
* zoom and pan.<br>
* Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
* this setting when set to false.
*
* @property {Boolean} [showFullPageControl=true]
* If true then the 'Toggle full page' button is displayed to switch
* between full page and normal mode.<br>
* Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
* this setting when set to false.
*
* @property {Boolean} [showRotationControl=false]
* If true then the rotate left/right controls will be displayed as part of the
* standard controls. This is also subject to the browser support for rotate
* (e.g. viewer.drawer.canRotate()).<br>
* Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding
* this setting when set to false.
*
* @property {Boolean} [showFlipControl=false]
* If true then the flip controls will be displayed as part of the
* standard controls.
*
* @property {Boolean} [showSequenceControl=true]
* If sequenceMode is true, then provide buttons for navigating forward and
* backward through the images.
*
* @property {OpenSeadragon.ControlAnchor} [sequenceControlAnchor=TOP_LEFT]
* Placement of the default sequence controls.
*
* @property {Boolean} [navPrevNextWrap=false]
* If true then the 'previous' button will wrap to the last image when
* viewing the first image and the 'next' button will wrap to the first
* image when viewing the last image.
*
*@property {String|Element} zoomInButton
* Set the id or element of the custom 'Zoom in' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} zoomOutButton
* Set the id or element of the custom 'Zoom out' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} homeButton
* Set the id or element of the custom 'Go home' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} fullPageButton
* Set the id or element of the custom 'Toggle full page' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} rotateLeftButton
* Set the id or element of the custom 'Rotate left' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} rotateRightButton
* Set the id or element of the custom 'Rotate right' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} previousButton
* Set the id or element of the custom 'Previous page' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {String|Element} nextButton
* Set the id or element of the custom 'Next page' button to use.
* This is useful to have a custom button anywhere in the web page.<br>
* To only change the button images, consider using
* {@link OpenSeadragon.Options.navImages}
*
* @property {Boolean} [sequenceMode=false]
* Set to true to have the viewer treat your tilesources as a sequence of images to
* be opened one at a time rather than all at once.
*
* @property {Number} [initialPage=0]
* If sequenceMode is true, display this page initially.
*
* @property {Boolean} [preserveViewport=false]
* If sequenceMode is true, then normally navigating through each image resets the
* viewport to 'home' position. If preserveViewport is set to true, then the viewport
* position is preserved when navigating between images in the sequence.
*
* @property {Boolean} [preserveOverlays=false]
* If sequenceMode is true, then normally navigating through each image
* resets the overlays.
* If preserveOverlays is set to true, then the overlays added with {@link OpenSeadragon.Viewer#addOverlay}
* are preserved when navigating between images in the sequence.
* Note: setting preserveOverlays overrides any overlays specified in the global
* "overlays" option for the Viewer. It's also not compatible with specifying
* per-tileSource overlays via the options, as those overlays will persist
* even after the tileSource is closed.
*
* @property {Boolean} [showReferenceStrip=false]
* If sequenceMode is true, then display a scrolling strip of image thumbnails for
* navigating through the images.
*
* @property {String} [referenceStripScroll='horizontal']
*
* @property {Element} [referenceStripElement=null]
*
* @property {Number} [referenceStripHeight=null]
*
* @property {Number} [referenceStripWidth=null]
*
* @property {String} [referenceStripPosition='BOTTOM_LEFT']
*
* @property {Number} [referenceStripSizeRatio=0.2]
*
* @property {Boolean} [collectionMode=false]
* Set to true to have the viewer arrange your TiledImages in a grid or line.
*
* @property {Number} [collectionRows=3]
* If collectionMode is true, specifies how many rows the grid should have. Use 1 to make a line.
* If collectionLayout is 'vertical', specifies how many columns instead.
*
* @property {Number} [collectionColumns=0]
* If collectionMode is true, specifies how many columns the grid should have. Use 1 to make a line.
* If collectionLayout is 'vertical', specifies how many rows instead. Ignored if collectionRows is not set to a falsy value.
*
* @property {String} [collectionLayout='horizontal']
* If collectionMode is true, specifies whether to arrange vertically or horizontally.
*
* @property {Number} [collectionTileSize=800]
* If collectionMode is true, specifies the size, in viewport coordinates, for each TiledImage to fit into.
* The TiledImage will be centered within a square of the specified size.
*
* @property {Number} [collectionTileMargin=80]
* If collectionMode is true, specifies the margin, in viewport coordinates, between each TiledImage.
*
* @property {String|Boolean} [crossOriginPolicy=false]
* Valid values are 'Anonymous', 'use-credentials', and false. If false, canvas requests will
* not use CORS, and the canvas will be tainted.
*
* @property {Boolean} [ajaxWithCredentials=false]
* Whether to set the withCredentials XHR flag for AJAX requests.
* Note that this can be overridden at the {@link OpenSeadragon.TileSource} level.
*
* @property {Boolean} [loadTilesWithAjax=false]
* Whether to load tile data using AJAX requests.
* Note that this can be overridden at the {@link OpenSeadragon.TileSource} level.
*
* @property {Object} [ajaxHeaders={}]
* A set of headers to include when making AJAX requests for tile sources or tiles.
*
* @property {Boolean} [splitHashDataForPost=false]
* Allows to treat _first_ hash ('#') symbol as a separator for POST data:
* URL to be opened by a {@link OpenSeadragon.TileSource} can thus look like: http://some.url#postdata=here.
* The whole URL is used to fetch image info metadata and it is then split to 'http://some.url' and
* 'postdata=here'; post data is given to the {@link OpenSeadragon.TileSource} of the choice and can be further
* used within tile requests (see TileSource methods).
* NOTE: {@link OpenSeadragon.TileSource.prototype.configure} return value should contain the post data
* if you want to use it later - so that it is given to your constructor later.
* NOTE: usually, post data is expected to be ampersand-separated (just like GET parameters), and is NOT USED
* to fetch tile image data unless explicitly programmed, or if loadTilesWithAjax=false 4
* (but it is still used for the initial image info request).
* NOTE: passing POST data from URL by this feature only supports string values, however,
* TileSource can send any data using POST as long as the header is correct
* (@see OpenSeadragon.TileSource.prototype.getTilePostData)
*/
/**
* Settings for gestures generated by a pointer device.
*
* @typedef {Object} GestureSettings
* @memberof OpenSeadragon
*
* @property {Boolean} dragToPan
* Set to false to disable panning on drag gestures.
*
* @property {Boolean} scrollToZoom
* Set to false to disable zooming on scroll gestures.
*
* @property {Boolean} clickToZoom
* Set to false to disable zooming on click gestures.
*
* @property {Boolean} dblClickToZoom
* Set to false to disable zooming on double-click gestures. Note: If set to true
* then clickToZoom should be set to false to prevent multiple zooms.
*
* @property {Boolean} pinchToZoom
* Set to false to disable zooming on pinch gestures.
*
* @property {Boolean} flickEnabled
* Set to false to disable the kinetic panning effect (flick) at the end of a drag gesture.
*
* @property {Number} flickMinSpeed
* If flickEnabled is true, the minimum speed (in pixels-per-second) required to cause the kinetic panning effect (flick) at the end of a drag gesture.
*
* @property {Number} flickMomentum
* If flickEnabled is true, a constant multiplied by the velocity to determine the distance of the kinetic panning effect (flick) at the end of a drag gesture.
* A larger value will make the flick feel "lighter", while a smaller value will make the flick feel "heavier".
* Note: springStiffness and animationTime also affect the "spring" used to stop the flick animation.
*
*/
/**
* @typedef {Object} DrawerOptions
* @memberof OpenSeadragon
* @property {Object} webgl - options if the WebGLDrawer is used. No options are currently supported.
* @property {Object} canvas - options if the CanvasDrawer is used. No options are currently supported.
* @property {Object} html - options if the HTMLDrawer is used. No options are currently supported.
* @property {Object} custom - options if a custom drawer is used. No options are currently supported.
*/
/**
* The names for the image resources used for the image navigation buttons.
*
* @typedef {Object} NavImages
* @memberof OpenSeadragon
*
* @property {Object} zoomIn - Images for the zoom-in button.
* @property {String} zoomIn.REST
* @property {String} zoomIn.GROUP
* @property {String} zoomIn.HOVER
* @property {String} zoomIn.DOWN
*
* @property {Object} zoomOut - Images for the zoom-out button.
* @property {String} zoomOut.REST
* @property {String} zoomOut.GROUP
* @property {String} zoomOut.HOVER
* @property {String} zoomOut.DOWN
*
* @property {Object} home - Images for the home button.
* @property {String} home.REST
* @property {String} home.GROUP
* @property {String} home.HOVER
* @property {String} home.DOWN
*
* @property {Object} fullpage - Images for the full-page button.
* @property {String} fullpage.REST
* @property {String} fullpage.GROUP
* @property {String} fullpage.HOVER
* @property {String} fullpage.DOWN
*
* @property {Object} rotateleft - Images for the rotate left button.
* @property {String} rotateleft.REST
* @property {String} rotateleft.GROUP
* @property {String} rotateleft.HOVER
* @property {String} rotateleft.DOWN
*
* @property {Object} rotateright - Images for the rotate right button.
* @property {String} rotateright.REST
* @property {String} rotateright.GROUP
* @property {String} rotateright.HOVER
* @property {String} rotateright.DOWN
*
* @property {Object} flip - Images for the flip button.
* @property {String} flip.REST
* @property {String} flip.GROUP
* @property {String} flip.HOVER
* @property {String} flip.DOWN
*
* @property {Object} previous - Images for the previous button.
* @property {String} previous.REST
* @property {String} previous.GROUP
* @property {String} previous.HOVER
* @property {String} previous.DOWN
*
* @property {Object} next - Images for the next button.
* @property {String} next.REST
* @property {String} next.GROUP
* @property {String} next.HOVER
* @property {String} next.DOWN
*
*/
/* eslint-disable no-redeclare */
function OpenSeadragon( options ){
return new OpenSeadragon.Viewer( options );
}
(function( $ ){
/**
* The OpenSeadragon version.
*
* @member {Object} OpenSeadragon.version
* @property {String} versionStr - The version number as a string ('major.minor.revision').
* @property {Number} major - The major version number.
* @property {Number} minor - The minor version number.
* @property {Number} revision - The revision number.
* @since 1.0.0
*/
$.version = {
versionStr: '5.0.0',
major: parseInt('5', 10),
minor: parseInt('0', 10),
revision: parseInt('0', 10)
};
/**
* Taken from jquery 1.6.1
* [[Class]] -> type pairs
* @private
*/
var class2type = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object AsyncFunction]': 'function',
'[object Promise]': 'promise',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object'
},
// Save a reference to some core methods
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty;
/**
* Taken from jQuery 1.6.1
* @function isFunction
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.isFunction = function( obj ) {
return $.type(obj) === "function";
};
/**
* Taken from jQuery 1.6.1
* @function isArray
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.isArray = Array.isArray || function( obj ) {
return $.type(obj) === "array";
};
/**
* A crude way of determining if an object is a window.
* Taken from jQuery 1.6.1
* @function isWindow
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.isWindow = function( obj ) {
return obj && typeof obj === "object" && "setInterval" in obj;
};
/**
* Taken from jQuery 1.6.1
* @function type
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.type = function( obj ) {
return ( obj === null ) || ( obj === undefined ) ?
String( obj ) :
class2type[ toString.call(obj) ] || "object";
};
/**
* Taken from jQuery 1.6.1
* @function isPlainObject
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.isPlainObject = function( obj ) {
// Must be an Object.
// Because of IE, we also have to check the presence of the constructor property.
// Make sure that DOM nodes and window objects don't pass through, as well
if ( !obj || OpenSeadragon.type(obj) !== "object" || obj.nodeType || $.isWindow( obj ) ) {
return false;
}
// Not own constructor property must be Object
if ( obj.constructor &&
!hasOwn.call(obj, "constructor") &&
!hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var lastKey;
for (var key in obj ) {
lastKey = key;
}
return lastKey === undefined || hasOwn.call( obj, lastKey );
};
/**
* Taken from jQuery 1.6.1
* @function isEmptyObject
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.isEmptyObject = function( obj ) {
for ( var name in obj ) {
return false;
}
return true;
};
/**
* Shim around Object.freeze. Does nothing if Object.freeze is not supported.
* @param {Object} obj The object to freeze.
* @returns {Object} obj The frozen object.
*/
$.freezeObject = function(obj) {
if (Object.freeze) {
$.freezeObject = Object.freeze;
} else {
$.freezeObject = function(obj) {
return obj;
};
}
return $.freezeObject(obj);
};
/**
* True if the browser supports the HTML5 canvas element
* @member {Boolean} supportsCanvas
* @memberof OpenSeadragon
*/
$.supportsCanvas = (function () {
var canvasElement = document.createElement( 'canvas' );
return !!( $.isFunction( canvasElement.getContext ) &&
canvasElement.getContext( '2d' ) );
}());
/**
* Test whether the submitted canvas is tainted or not.
* @argument {Canvas} canvas The canvas to test.
* @returns {Boolean} True if the canvas is tainted.
*/
$.isCanvasTainted = function(canvas) {
var isTainted = false;
try {
// We test if the canvas is tainted by retrieving data from it.
// An exception will be raised if the canvas is tainted.
canvas.getContext('2d').getImageData(0, 0, 1, 1);
} catch (e) {
isTainted = true;
}
return isTainted;
};
/**
* True if the browser supports the EventTarget.addEventListener() method
* @member {Boolean} supportsAddEventListener
* @memberof OpenSeadragon
*/
$.supportsAddEventListener = (function () {
return !!(document.documentElement.addEventListener && document.addEventListener);
}());
/**
* True if the browser supports the EventTarget.removeEventListener() method
* @member {Boolean} supportsRemoveEventListener
* @memberof OpenSeadragon
*/
$.supportsRemoveEventListener = (function () {
return !!(document.documentElement.removeEventListener && document.removeEventListener);
}());
/**
* True if the browser supports the newer EventTarget.addEventListener options argument
* @member {Boolean} supportsEventListenerOptions
* @memberof OpenSeadragon
*/
$.supportsEventListenerOptions = (function () {
var supported = 0;
if ( $.supportsAddEventListener ) {
try {
var options = {
get capture() {
supported++;
return false;
},
get once() {
supported++;
return false;
},
get passive() {
supported++;
return false;
}
};
window.addEventListener("test", null, options);
window.removeEventListener("test", null, options);
} catch ( e ) {
supported = 0;
}
}
return supported >= 3;
}());
/**
* A ratio comparing the device screen's pixel density to the canvas's backing store pixel density,
* clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser.
* @function getCurrentPixelDensityRatio
* @memberof OpenSeadragon
* @returns {Number}
*/
$.getCurrentPixelDensityRatio = function() {
if ( $.supportsCanvas ) {
var context = document.createElement('canvas').getContext('2d');
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return Math.max(devicePixelRatio, 1) / backingStoreRatio;
} else {
return 1;
}
};
/**
* A ratio comparing the device screen's pixel density to the canvas's backing store pixel density,
* clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser.
* @member {Number} pixelDensityRatio
* @memberof OpenSeadragon
*/
$.pixelDensityRatio = $.getCurrentPixelDensityRatio();
}( OpenSeadragon ));
/**
* This closure defines all static methods available to the OpenSeadragon
* namespace. Many, if not most, are taken directly from jQuery for use
* to simplify and reduce common programming patterns. More static methods
* from jQuery may eventually make their way into this though we are
* attempting to avoid an explicit dependency on jQuery only because
* OpenSeadragon is a broadly useful code base and would be made less broad
* by requiring jQuery fully.
*
* Some static methods have also been refactored from the original OpenSeadragon
* project.
*/
(function( $ ){
/**
* Taken from jQuery 1.6.1
* @function extend
* @memberof OpenSeadragon
* @see {@link http://www.jquery.com/ jQuery}
*/
$.extend = function() {
var options,
name,
src,
copy,
copyIsArray,
clone,
target = arguments[ 0 ] || {},
length = arguments.length,
deep = false,
i = 1;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
target = arguments[ 1 ] || {};
// skip the boolean and the target
i = 2;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !OpenSeadragon.isFunction( target ) ) {
target = {};
}
// extend jQuery itself if only one argument is passed
if ( length === i ) {
target = this;
--i;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
options = arguments[ i ];
if ( options !== null || options !== undefined ) {
// Extend the base object
for ( name in options ) {
var descriptor = Object.getOwnPropertyDescriptor(options, name);
if (descriptor !== undefined) {
if (descriptor.get || descriptor.set) {
Object.defineProperty(target, name, descriptor);
continue;
}
copy = descriptor.value;
} else {
$.console.warn('Could not copy inherited property "' + name + '".');
continue;
}
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( OpenSeadragon.isPlainObject( copy ) || ( copyIsArray = OpenSeadragon.isArray( copy ) ) ) ) {
src = target[ name ];
if ( copyIsArray ) {
copyIsArray = false;
clone = src && OpenSeadragon.isArray( src ) ? src : [];
} else {
clone = src && OpenSeadragon.isPlainObject( src ) ? src : {};
}
// Never move original objects, clone them
target[ name ] = OpenSeadragon.extend( deep, clone, copy );
// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
};
var isIOSDevice = function () {
if (typeof navigator !== 'object') {
return false;
}
var userAgent = navigator.userAgent;
if (typeof userAgent !== 'string') {
return false;
}
return userAgent.indexOf('iPhone') !== -1 ||
userAgent.indexOf('iPad') !== -1 ||
userAgent.indexOf('iPod') !== -1;
};
$.extend( $, /** @lends OpenSeadragon */{
/**
* The default values for the optional settings documented at {@link OpenSeadragon.Options}.
* @static
* @type {Object}
*/
DEFAULT_SETTINGS: {
//DATA SOURCE DETAILS
xmlPath: null,
tileSources: null,
tileHost: null,
initialPage: 0,
crossOriginPolicy: false,
ajaxWithCredentials: false,
loadTilesWithAjax: false,
ajaxHeaders: {},
splitHashDataForPost: false,
//PAN AND ZOOM SETTINGS AND CONSTRAINTS
panHorizontal: true,
panVertical: true,
constrainDuringPan: false,
wrapHorizontal: false,
wrapVertical: false,
visibilityRatio: 0.5, //-> how much of the viewer can be negative space
minPixelRatio: 0.5, //->closer to 0 draws tiles meant for a higher zoom at this zoom
defaultZoomLevel: 0,
minZoomLevel: null,
maxZoomLevel: null,
homeFillsViewer: false,
//UI RESPONSIVENESS AND FEEL
clickTimeThreshold: 300,
clickDistThreshold: 5,
dblClickTimeThreshold: 300,
dblClickDistThreshold: 20,
springStiffness: 6.5,
animationTime: 1.2,
gestureSettingsMouse: {
dragToPan: true,
scrollToZoom: true,
clickToZoom: true,
dblClickToZoom: false,
dblClickDragToZoom: false,
pinchToZoom: false,
zoomToRefPoint: true,
flickEnabled: false,
flickMinSpeed: 120,
flickMomentum: 0.25,
pinchRotate: false
},
gestureSettingsTouch: {
dragToPan: true,
scrollToZoom: false,
clickToZoom: false,
dblClickToZoom: true,
dblClickDragToZoom: true,
pinchToZoom: true,
zoomToRefPoint: true,
flickEnabled: true,
flickMinSpeed: 120,
flickMomentum: 0.25,
pinchRotate: false
},
gestureSettingsPen: {
dragToPan: true,
scrollToZoom: false,
clickToZoom: true,
dblClickToZoom: false,
dblClickDragToZoom: false,
pinchToZoom: false,
zoomToRefPoint: true,
flickEnabled: false,
flickMinSpeed: 120,
flickMomentum: 0.25,
pinchRotate: false
},
gestureSettingsUnknown: {
dragToPan: true,
scrollToZoom: false,
clickToZoom: false,
dblClickToZoom: true,
dblClickDragToZoom: false,
pinchToZoom: true,
zoomToRefPoint: true,
flickEnabled: true,
flickMinSpeed: 120,
flickMomentum: 0.25,
pinchRotate: false
},
zoomPerClick: 2,
zoomPerScroll: 1.2,
zoomPerDblClickDrag: 1.2,
zoomPerSecond: 1.0,
blendTime: 0,
alwaysBlend: false,
autoHideControls: true,
immediateRender: false,
minZoomImageRatio: 0.9, //-> closer to 0 allows zoom out to infinity
maxZoomPixelRatio: 1.1, //-> higher allows 'over zoom' into pixels
smoothTileEdgesMinZoom: 1.1, //-> higher than maxZoomPixelRatio disables it
iOSDevice: isIOSDevice(),
pixelsPerWheelLine: 40,
pixelsPerArrowPress: 40,
autoResize: true,
preserveImageSizeOnResize: false, // requires autoResize=true
minScrollDeltaTime: 50,
rotationIncrement: 90,
maxTilesPerFrame: 1,
//DEFAULT CONTROL SETTINGS
showSequenceControl: true, //SEQUENCE
sequenceControlAnchor: null, //SEQUENCE
preserveViewport: false, //SEQUENCE
preserveOverlays: false, //SEQUENCE
navPrevNextWrap: false, //SEQUENCE
showNavigationControl: true, //ZOOM/HOME/FULL/ROTATION
navigationControlAnchor: null, //ZOOM/HOME/FULL/ROTATION
showZoomControl: true, //ZOOM
showHomeControl: true, //HOME
showFullPageControl: true, //FULL
showRotationControl: false, //ROTATION
showFlipControl: false, //FLIP
controlsFadeDelay: 2000, //ZOOM/HOME/FULL/SEQUENCE
controlsFadeLength: 1500, //ZOOM/HOME/FULL/SEQUENCE
mouseNavEnabled: true, //GENERAL MOUSE INTERACTIVITY
//VIEWPORT NAVIGATOR SETTINGS
showNavigator: false,
navigatorElement: null,
navigatorId: null,
navigatorPosition: null,
navigatorSizeRatio: 0.2,
navigatorMaintainSizeRatio: false,
navigatorTop: null,
navigatorLeft: null,
navigatorHeight: null,
navigatorWidth: null,
navigatorAutoResize: true,
navigatorAutoFade: true,
navigatorRotate: true,
navigatorBackground: '#000',
navigatorOpacity: 0.8,
navigatorBorderColor: '#555',
navigatorDisplayRegionColor: '#900',
// INITIAL ROTATION
degrees: 0,
// INITIAL FLIP STATE
flipped: false,
overlayPreserveContentDirection: true,
// APPEARANCE
opacity: 1, // to be passed into each TiledImage
compositeOperation: null, // to be passed into each TiledImage
// DRAWER SETTINGS
drawer: ['webgl', 'canvas', 'html'], // prefer using webgl, then canvas (i.e. context2d), then fallback to html
drawerOptions: {
webgl: {
},
canvas: {
},
html: {
},
custom: {
}
},
// TILED IMAGE SETTINGS
preload: false, // to be passed into each TiledImage
imageSmoothingEnabled: true, // to be passed into each TiledImage
placeholderFillStyle: null, // to be passed into each TiledImage
subPixelRoundingForTransparency: null, // to be passed into each TiledImage
//REFERENCE STRIP SETTINGS
showReferenceStrip: false,
referenceStripScroll: 'horizontal',
referenceStripElement: null,
referenceStripHeight: null,
referenceStripWidth: null,
referenceStripPosition: 'BOTTOM_LEFT',
referenceStripSizeRatio: 0.2,
//COLLECTION VISUALIZATION SETTINGS
collectionRows: 3, //or columns depending on layout
collectionColumns: 0, //columns in horizontal layout, rows in vertical layout
collectionLayout: 'horizontal', //vertical
collectionMode: false,
collectionTileSize: 800,
collectionTileMargin: 80,
//PERFORMANCE SETTINGS
imageLoaderLimit: 0,
maxImageCacheCount: 200,
timeout: 30000,
tileRetryMax: 0,
tileRetryDelay: 2500,
//INTERFACE RESOURCE SETTINGS
prefixUrl: "/images/",
navImages: {
zoomIn: {
REST: 'zoomin_rest.png',
GROUP: 'zoomin_grouphover.png',
HOVER: 'zoomin_hover.png',
DOWN: 'zoomin_pressed.png'
},
zoomOut: {
REST: 'zoomout_rest.png',
GROUP: 'zoomout_grouphover.png',
HOVER: 'zoomout_hover.png',
DOWN: 'zoomout_pressed.png'
},
home: {
REST: 'home_rest.png',
GROUP: 'home_grouphover.png',
HOVER: 'home_hover.png',
DOWN: 'home_pressed.png'
},
fullpage: {
REST: 'fullpage_rest.png',
GROUP: 'fullpage_grouphover.png',
HOVER: 'fullpage_hover.png',
DOWN: 'fullpage_pressed.png'
},
rotateleft: {
REST: 'rotateleft_rest.png',
GROUP: 'rotateleft_grouphover.png',
HOVER: 'rotateleft_hover.png',
DOWN: 'rotateleft_pressed.png'
},
rotateright: {
REST: 'rotateright_rest.png',
GROUP: 'rotateright_grouphover.png',
HOVER: 'rotateright_hover.png',
DOWN: 'rotateright_pressed.png'
},
flip: { // Flip icon designed by Yaroslav Samoylov from the Noun Project and modified by Nelson Campos ncampos@criteriamarathon.com, https://thenounproject.com/term/flip/136289/
REST: 'flip_rest.png',
GROUP: 'flip_grouphover.png',
HOVER: 'flip_hover.png',
DOWN: 'flip_pressed.png'
},
previous: {
REST: 'previous_rest.png',
GROUP: 'previous_grouphover.png',
HOVER: 'previous_hover.png',
DOWN: 'previous_pressed.png'
},
next: {
REST: 'next_rest.png',
GROUP: 'next_grouphover.png',
HOVER: 'next_hover.png',
DOWN: 'next_pressed.png'
}
},
//DEVELOPER SETTINGS
debugMode: false,
debugGridColor: ['#437AB2', '#1B9E77', '#D95F02', '#7570B3', '#E7298A', '#66A61E', '#E6AB02', '#A6761D', '#666666'],
silenceMultiImageWarnings: false
},
/**
* Returns a function which invokes the method as if it were a method belonging to the object.
* @function
* @param {Object} object
* @param {Function} method
* @returns {Function}
*/
delegate: function( object, method ) {
return function(){
var args = arguments;
if ( args === undefined ){
args = [];
}
return method.apply( object, args );
};
},
/**
* An enumeration of Browser vendors.
* @static
* @type {Object}
* @property {Number} UNKNOWN
* @property {Number} IE
* @property {Number} FIREFOX
* @property {Number} SAFARI
* @property {Number} CHROME
* @property {Number} OPERA
* @property {Number} EDGE
* @property {Number} CHROMEEDGE
*/
BROWSERS: {
UNKNOWN: 0,
IE: 1,
FIREFOX: 2,
SAFARI: 3,
CHROME: 4,
OPERA: 5,
EDGE: 6,
CHROMEEDGE: 7
},
/**
* An enumeration of when subpixel rounding should occur.
* @static
* @type {Object}
* @property {Number} NEVER Never apply subpixel rounding for transparency.
* @property {Number} ONLY_AT_REST Do not apply subpixel rounding for transparency during animation (panning, zoom, rotation) and apply it once animation is over.
* @property {Number} ALWAYS Apply subpixel rounding for transparency during animation and when animation is over.
*/
SUBPIXEL_ROUNDING_OCCURRENCES: {
NEVER: 0,
ONLY_AT_REST: 1,
ALWAYS: 2
},
/**
* Keep track of which {@link Viewer}s have been created.
* - Key: {@link Element} to which a Viewer is attached.
* - Value: {@link Viewer} of the element defined by the key.
* @private
* @static
* @type {Object}
*/
_viewers: new Map(),
/**
* Returns the {@link Viewer} attached to a given DOM element. If there is
* no viewer attached to the provided element, undefined is returned.
* @function
* @param {String|Element} element Accepts an id or element.
* @returns {Viewer} The viewer attached to the given element, or undefined.
*/
getViewer: function(element) {
return $._viewers.get(this.getElement(element));
},
/**
* Returns a DOM Element for the given id or element.
* @function
* @param {String|Element} element Accepts an id or element.
* @returns {Element} The element with the given id, null, or the element itself.
*/
getElement: function( element ) {
if ( typeof ( element ) === "string" ) {
element = document.getElementById( element );
}
return element;
},
/**
* Determines the position of the upper-left corner of the element.
* @function
* @param {Element|String} element - the element we want the position for.
* @returns {OpenSeadragon.Point} - the position of the upper left corner of the element.
*/
getElementPosition: function( element ) {
var result = new $.Point(),
isFixed,
offsetParent;
element = $.getElement( element );
isFixed = $.getElementStyle( element ).position === "fixed";
offsetParent = getOffsetParent( element, isFixed );
while ( offsetParent ) {
result.x += element.offsetLeft;
result.y += element.offsetTop;
if ( isFixed ) {
result = result.plus( $.getPageScroll() );
}
element = offsetParent;
isFixed = $.getElementStyle( element ).position === "fixed";
offsetParent = getOffsetParent( element, isFixed );
}
return result;
},
/**
* Determines the position of the upper-left corner of the element adjusted for current page and/or element scroll.
* @function
* @param {Element|String} element - the element we want the position for.
* @returns {OpenSeadragon.Point} - the position of the upper left corner of the element adjusted for current page and/or element scroll.
*/
getElementOffset: function( element ) {
element = $.getElement( element );
var doc = element && element.ownerDocument,
docElement,
win,
boundingRect = { top: 0, left: 0 };
if ( !doc ) {
return new $.Point();
}
docElement = doc.documentElement;
if ( typeof element.getBoundingClientRect !== typeof undefined ) {
boundingRect = element.getBoundingClientRect();
}
win = ( doc === doc.window ) ?
doc :
( doc.nodeType === 9 ) ?
doc.defaultView || doc.parentWindow :
false;
return new $.Point(
boundingRect.left + ( win.pageXOffset || docElement.scrollLeft ) - ( docElement.clientLeft || 0 ),
boundingRect.top + ( win.pageYOffset || docElement.scrollTop ) - ( docElement.clientTop || 0 )
);
},
/**
* Determines the height and width of the given element.
* @function
* @param {Element|String} element
* @returns {OpenSeadragon.Point}
*/
getElementSize: function( element ) {
element = $.getElement( element );
return new $.Point(
element.clientWidth,
element.clientHeight
);
},
/**
* Returns the CSSStyle object for the given element.
* @function
* @param {Element|String} element
* @returns {CSSStyle}
*/
getElementStyle:
document.documentElement.currentStyle ?
function( element ) {
element = $.getElement( element );
return element.currentStyle;
} :
function( element ) {
element = $.getElement( element );
return window.getComputedStyle( element, "" );
},
/**
* Returns the property with the correct vendor prefix appended.
* @param {String} property the property name
* @returns {String} the property with the correct prefix or null if not
* supported.
*/
getCssPropertyWithVendorPrefix: function(property) {
var memo = {};
$.getCssPropertyWithVendorPrefix = function(property) {
if (memo[property] !== undefined) {
return memo[property];
}
var style = document.createElement('div').style;
var result = null;
if (style[property] !== undefined) {
result = property;
} else {
var prefixes = ['Webkit', 'Moz', 'MS', 'O',
'webkit', 'moz', 'ms', 'o'];
var suffix = $.capitalizeFirstLetter(property);
for (var i = 0; i < prefixes.length; i++) {
var prop = prefixes[i] + suffix;
if (style[prop] !== undefined) {
result = prop;
break;
}
}
}
memo[property] = result;
return result;
};
return $.getCssPropertyWithVendorPrefix(property);
},
/**
* Capitalizes the first letter of a string
* @param {String} string
* @returns {String} The string with the first letter capitalized
*/
capitalizeFirstLetter: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
* Compute the modulo of a number but makes sure to always return
* a positive value (also known as Euclidean modulo).
* @param {Number} number the number to compute the modulo of
* @param {Number} modulo the modulo
* @returns {Number} the result of the modulo of number
*/
positiveModulo: function(number, modulo) {
var result = number % modulo;
if (result < 0) {
result += modulo;
}
return result;
},
/**
* Determines if a point is within the bounding rectangle of the given element (hit-test).
* @function
* @param {Element|String} element
* @param {OpenSeadragon.Point} point
* @returns {Boolean}
*/
pointInElement: function( element, point ) {
element = $.getElement( element );
var offset = $.getElementOffset( element ),
size = $.getElementSize( element );
return point.x >= offset.x && point.x < offset.x + size.x && point.y < offset.y + size.y && point.y >= offset.y;
},
/**
* Gets the position of the mouse on the screen for a given event.
* @function
* @param {Event} [event]
* @returns {OpenSeadragon.Point}
*/
getMousePosition: function( event ) {
if ( typeof ( event.pageX ) === "number" ) {
$.getMousePosition = function( event ){
var result = new $.Point();
result.x = event.pageX;
result.y = event.pageY;
return result;
};
} else if ( typeof ( event.clientX ) === "number" ) {
$.getMousePosition = function( event ){
var result = new $.Point();
result.x =
event.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
result.y =
event.clientY +
document.body.scrollTop +
document.documentElement.scrollTop;
return result;
};
} else {
throw new Error(
"Unknown event mouse position, no known technique."
);
}
return $.getMousePosition( event );
},
/**
* Determines the page's current scroll position.
* @function
* @returns {OpenSeadragon.Point}
*/
getPageScroll: function() {
var docElement = document.documentElement || {},
body = document.body || {};
if ( typeof ( window.pageXOffset ) === "number" ) {
$.getPageScroll = function(){
return new $.Point(
window.pageXOffset,
window.pageYOffset
);
};
} else if ( body.scrollLeft || body.scrollTop ) {
$.getPageScroll = function(){
return new $.Point(
document.body.scrollLeft,
document.body.scrollTop
);
};
} else if ( docElement.scrollLeft || docElement.scrollTop ) {
$.getPageScroll = function(){
return new $.Point(
document.documentElement.scrollLeft,
document.documentElement.scrollTop
);
};
} else {
// We can't reassign the function yet, as there was no scroll.
return new $.Point(0, 0);
}
return $.getPageScroll();
},
/**
* Set the page scroll position.
* @function
* @returns {OpenSeadragon.Point}
*/
setPageScroll: function( scroll ) {
if ( typeof ( window.scrollTo ) !== "undefined" ) {
$.setPageScroll = function( scroll ) {
window.scrollTo( scroll.x, scroll.y );
};
} else {
var originalScroll = $.getPageScroll();
if ( originalScroll.x === scroll.x &&
originalScroll.y === scroll.y ) {
// We are already correctly positioned and there
// is no way to detect the correct method.
return;
}
document.body.scrollLeft = scroll.x;
document.body.scrollTop = scroll.y;
var currentScroll = $.getPageScroll();
if ( currentScroll.x !== originalScroll.x &&
currentScroll.y !== originalScroll.y ) {
$.setPageScroll = function( scroll ) {
document.body.scrollLeft = scroll.x;
document.body.scrollTop = scroll.y;
};
return;
}
document.documentElement.scrollLeft = scroll.x;
document.documentElement.scrollTop = scroll.y;
currentScroll = $.getPageScroll();
if ( currentScroll.x !== originalScroll.x &&
currentScroll.y !== originalScroll.y ) {
$.setPageScroll = function( scroll ) {
document.documentElement.scrollLeft = scroll.x;
document.documentElement.scrollTop = scroll.y;
};
return;
}
// We can't find anything working, so we do nothing.
$.setPageScroll = function( scroll ) {
};
}
$.setPageScroll( scroll );
},
/**
* Determines the size of the browsers window.
* @function
* @returns {OpenSeadragon.Point}
*/
getWindowSize: function() {
var docElement = document.documentElement || {},
body = document.body || {};
if ( typeof ( window.innerWidth ) === 'number' ) {
$.getWindowSize = function(){
return new $.Point(
window.innerWidth,
window.innerHeight
);
};
} else if ( docElement.clientWidth || docElement.clientHeight ) {
$.getWindowSize = function(){
return new $.Point(
document.documentElement.clientWidth,
document.documentElement.clientHeight
);
};
} else if ( body.clientWidth || body.clientHeight ) {
$.getWindowSize = function(){
return new $.Point(
document.body.clientWidth,
document.body.clientHeight
);
};
} else {
throw new Error("Unknown window size, no known technique.");
}
return $.getWindowSize();
},
/**
* Wraps the given element in a nest of divs so that the element can
* be easily centered using CSS tables
* @function
* @param {Element|String} element
* @returns {Element} outermost wrapper element
*/
makeCenteredNode: function( element ) {
// Convert a possible ID to an actual HTMLElement
element = $.getElement( element );
/*
CSS tables require you to have a display:table/row/cell hierarchy so we need to create
three nested wrapper divs:
*/
var wrappers = [
$.makeNeutralElement( 'div' ),
$.makeNeutralElement( 'div' ),
$.makeNeutralElement( 'div' )
];
// It feels like we should be able to pass style dicts to makeNeutralElement:
$.extend(wrappers[0].style, {
display: "table",
height: "100%",
width: "100%"
});
$.extend(wrappers[1].style, {
display: "table-row"
});
$.extend(wrappers[2].style, {
display: "table-cell",
verticalAlign: "middle",
textAlign: "center"
});
wrappers[0].appendChild(wrappers[1]);
wrappers[1].appendChild(wrappers[2]);
wrappers[2].appendChild(element);
return wrappers[0];
},
/**
* Creates an easily positionable element of the given type that therefor
* serves as an excellent container element.
* @function
* @param {String} tagName
* @returns {Element}
*/
makeNeutralElement: function( tagName ) {
var element = document.createElement( tagName ),
style = element.style;
style.background = "transparent none";
style.border = "none";
style.margin = "0px";
style.padding = "0px";
style.position = "static";
return element;
},
/**
* Returns the current milliseconds, using Date.now() if available
* @function
*/
now: function( ) {
if (Date.now) {
$.now = Date.now;
} else {
$.now = function() {
return new Date().getTime();
};
}
return $.now();
},
/**
* Ensures an image is loaded correctly to support alpha transparency.
* @function
* @param {String} src
* @returns {Element}
*/
makeTransparentImage: function( src ) {
var img = $.makeNeutralElement( "img" );
img.src = src;
return img;
},
/**
* Sets the opacity of the specified element.
* @function
* @param {Element|String} element
* @param {Number} opacity
* @param {Boolean} [usesAlpha]
*/
setElementOpacity: function( element, opacity, usesAlpha ) {
var ieOpacity,
ieFilter;
element = $.getElement( element );
if ( usesAlpha && !$.Browser.alpha ) {
opacity = Math.round( opacity );
}
if ( $.Browser.opacity ) {
element.style.opacity = opacity < 1 ? opacity : "";
} else {
if ( opacity < 1 ) {
ieOpacity = Math.round( 100 * opacity );
ieFilter = "alpha(opacity=" + ieOpacity + ")";
element.style.filter = ieFilter;
} else {
element.style.filter = "";
}
}
},
/**
* Sets the specified element's touch-action style attribute to 'none'.
* @function
* @param {Element|String} element
*/
setElementTouchActionNone: function( element ) {
element = $.getElement( element );
if ( typeof element.style.touchAction !== 'undefined' ) {
element.style.touchAction = 'none';
} else if ( typeof element.style.msTouchAction !== 'undefined' ) {
element.style.msTouchAction = 'none';
}
},
/**
* Sets the specified element's pointer-events style attribute to the passed value.
* @function
* @param {Element|String} element
* @param {String} value
*/
setElementPointerEvents: function( element, value ) {
element = $.getElement( element );
if (typeof element.style !== 'undefined' && typeof element.style.pointerEvents !== 'undefined' ) {
element.style.pointerEvents = value;
}
},
/**
* Sets the specified element's pointer-events style attribute to 'none'.
* @function
* @param {Element|String} element
*/
setElementPointerEventsNone: function( element ) {
$.setElementPointerEvents( element, 'none' );
},
/**
* Add the specified CSS class to the element if not present.
* @function
* @param {Element|String} element
* @param {String} className
*/
addClass: function( element, className ) {
element = $.getElement( element );
if (!element.className) {
element.className = className;
} else if ( ( ' ' + element.className + ' ' ).
indexOf( ' ' + className + ' ' ) === -1 ) {
element.className += ' ' + className;
}
},
/**
* Find the first index at which an element is found in an array or -1
* if not present.
*
* Code taken and adapted from
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Compatibility
*
* @function
* @param {Array} array The array from which to find the element
* @param {Object} searchElement The element to find
* @param {Number} [fromIndex=0] Index to start research.
* @returns {Number} The index of the element in the array.
*/
indexOf: function( array, searchElement, fromIndex ) {
if ( Array.prototype.indexOf ) {
this.indexOf = function( array, searchElement, fromIndex ) {
return array.indexOf( searchElement, fromIndex );
};
} else {
this.indexOf = function( array, searchElement, fromIndex ) {
var i,
pivot = ( fromIndex ) ? fromIndex : 0,
length;
if ( !array ) {
throw new TypeError( );
}
length = array.length;
if ( length === 0 || pivot >= length ) {
return -1;
}
if ( pivot < 0 ) {
pivot = length - Math.abs( pivot );
}
for ( i = pivot; i < length; i++ ) {
if ( array[i] === searchElement ) {
return i;
}
}
return -1;
};
}
return this.indexOf( array, searchElement, fromIndex );
},
/**
* Remove the specified CSS class from the element.
* @function
* @param {Element|String} element
* @param {String} className
*/
removeClass: function( element, className ) {
var oldClasses,
newClasses = [],
i;
element = $.getElement( element );
oldClasses = element.className.split( /\s+/ );
for ( i = 0; i < oldClasses.length; i++ ) {
if ( oldClasses[ i ] && oldClasses[ i ] !== className ) {
newClasses.push( oldClasses[ i ] );
}
}
element.className = newClasses.join(' ');
},
/**
* Convert passed addEventListener() options to boolean or options object,
* depending on browser support.
* @function
* @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
* @param {Boolean} [options.capture]
* @param {Boolean} [options.passive]
* @param {Boolean} [options.once]
* @returns {String} The protocol (http:, https:, file:, ftp: ...)
*/
normalizeEventListenerOptions: function (options) {
var opts;
if ( typeof options !== 'undefined' ) {
if ( typeof options === 'boolean' ) {
// Legacy Boolean useCapture
opts = $.supportsEventListenerOptions ? { capture: options } : options;
} else {
// Options object
opts = $.supportsEventListenerOptions ? options :
( ( typeof options.capture !== 'undefined' ) ? options.capture : false );
}
} else {
// No options specified - Legacy optional useCapture argument
// (for IE, first supported on version 9, so we'll pass a Boolean)
opts = $.supportsEventListenerOptions ? { capture: false } : false;
}
return opts;
},
/**
* Adds an event listener for the given element, eventName and handler.
* @function
* @param {Element|String} element
* @param {String} eventName
* @param {Function} handler
* @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
* @param {Boolean} [options.capture]
* @param {Boolean} [options.passive]
* @param {Boolean} [options.once]
*/
addEvent: (function () {
if ( $.supportsAddEventListener ) {
return function ( element, eventName, handler, options ) {
options = $.normalizeEventListenerOptions(options);
element = $.getElement( element );
element.addEventListener( eventName, handler, options );
};
} else if ( document.documentElement.attachEvent && document.attachEvent ) {
return function ( element, eventName, handler ) {
element = $.getElement( element );
element.attachEvent( 'on' + eventName, handler );
};
} else {
throw new Error( "No known event model." );
}
}()),
/**
* Remove a given event listener for the given element, event type and
* handler.
* @function
* @param {Element|String} element
* @param {String} eventName
* @param {Function} handler
* @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object
* @param {Boolean} [options.capture]
*/
removeEvent: (function () {
if ( $.supportsRemoveEventListener ) {
return function ( element, eventName, handler, options ) {
options = $.normalizeEventListenerOptions(options);
element = $.getElement( element );
element.removeEventListener( eventName, handler, options );
};
} else if ( document.documentElement.detachEvent && document.detachEvent ) {
return function( element, eventName, handler ) {
element = $.getElement( element );
element.detachEvent( 'on' + eventName, handler );
};
} else {
throw new Error( "No known event model." );
}
}()),
/**
* Cancels the default browser behavior had the event propagated all
* the way up the DOM to the window object.
* @function
* @param {Event} [event]
*/
cancelEvent: function( event ) {
event.preventDefault();
},
/**
* Returns true if {@link OpenSeadragon.cancelEvent|cancelEvent} has been called on
* the event, otherwise returns false.
* @function
* @param {Event} [event]
*/
eventIsCanceled: function( event ) {
return event.defaultPrevented;
},
/**
* Stops the propagation of the event through the DOM in the capturing and bubbling phases.
* @function
* @param {Event} [event]
*/
stopEvent: function( event ) {
event.stopPropagation();
},
// Deprecated
createCallback: function( object, method ) {
//TODO: This pattern is painful to use and debug. It's much cleaner
// to use pinning plus anonymous functions. Get rid of this
// pattern!
console.error('The createCallback function is deprecated and will be removed in future versions. Please use alternativeFunction instead.');
var initialArgs = [],
i;
for ( i = 2; i < arguments.length; i++ ) {
initialArgs.push( arguments[ i ] );
}
return function() {
var args = initialArgs.concat( [] ),
i;
for ( i = 0; i < arguments.length; i++ ) {
args.push( arguments[ i ] );
}
return method.apply( object, args );
};
},
/**
* Retrieves the value of a url parameter from the window.location string.
* @function
* @param {String} key
* @returns {String} The value of the url parameter or null if no param matches.
*/
getUrlParameter: function( key ) {
// eslint-disable-next-line no-use-before-define
var value = URLPARAMS[ key ];
return value ? value : null;
},
/**
* Retrieves the protocol used by the url. The url can either be absolute
* or relative.
* @function
* @private
* @param {String} url The url to retrieve the protocol from.
* @returns {String} The protocol (http:, https:, file:, ftp: ...)
*/
getUrlProtocol: function( url ) {
var match = url.match(/^([a-z]+:)\/\//i);
if ( match === null ) {
// Relative URL, retrive the protocol from window.location
return window.location.protocol;
}
return match[1].toLowerCase();
},
/**
* Create an XHR object
* @private
* @param {type} [local] Deprecated. Ignored (IE/ActiveXObject file protocol no longer supported).
* @returns {XMLHttpRequest}
*/
createAjaxRequest: function() {
if ( window.XMLHttpRequest ) {
$.createAjaxRequest = function() {
return new XMLHttpRequest();
};
return new XMLHttpRequest();
} else {
throw new Error( "Browser doesn't support XMLHttpRequest." );
}
},
/**
* Makes an AJAX request.
* @param {Object} options
* @param {String} options.url - the url to request
* @param {Function} options.success - a function to call on a successful response
* @param {Function} options.error - a function to call on when an error occurs
* @param {Object} options.headers - headers to add to the AJAX request
* @param {String} options.responseType - the response type of the AJAX request
* @param {String} options.postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData), GET method used if null
* @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials
* @throws {Error}
* @returns {XMLHttpRequest}
*/
makeAjaxRequest: function( url, onSuccess, onError ) {
var withCredentials;
var headers;
var responseType;
var postData;
// Note that our preferred API is that you pass in a single object; the named
// arguments are for legacy support.
if( $.isPlainObject( url ) ){
onSuccess = url.success;
onError = url.error;
withCredentials = url.withCredentials;
headers = url.headers;
responseType = url.responseType || null;
postData = url.postData || null;
url = url.url;
}
var protocol = $.getUrlProtocol( url );
var request = $.createAjaxRequest();
if ( !$.isFunction( onSuccess ) ) {
throw new Error( "makeAjaxRequest requires a success callback" );
}
request.onreadystatechange = function() {
// 4 = DONE (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Properties)
if ( request.readyState === 4 ) {
request.onreadystatechange = function(){};
// With protocols other than http/https, a successful request status is in
// the 200's on Firefox and 0 on other browsers
if ( (request.status >= 200 && request.status < 300) ||
( request.status === 0 &&
protocol !== "http:" &&
protocol !== "https:" )) {
onSuccess( request );
} else {
if ( $.isFunction( onError ) ) {
onError( request );
} else {
$.console.error( "AJAX request returned %d: %s", request.status, url );
}
}
}
};
var method = postData ? "POST" : "GET";
try {
request.open( method, url, true );
if (responseType) {
request.responseType = responseType;
}
if (headers) {
for (var headerName in headers) {
if (Object.prototype.hasOwnProperty.call(headers, headerName) && headers[headerName]) {
request.setRequestHeader(headerName, headers[headerName]);
}
}
}
if (withCredentials) {
request.withCredentials = true;
}
request.send(postData);
} catch (e) {
$.console.error( "%s while making AJAX request: %s", e.name, e.message );
request.onreadystatechange = function(){};
if ( $.isFunction( onError ) ) {
onError( request, e );
}
}
return request;
},
/**
* Taken from jQuery 1.6.1
* @function
* @param {Object} options
* @param {String} options.url
* @param {Function} options.callback
* @param {String} [options.param='callback'] The name of the url parameter
* to request the jsonp provider with.
* @param {String} [options.callbackName=] The name of the callback to
* request the jsonp provider with.
*/
jsonp: function( options ){
var script,
url = options.url,
head = document.head ||
document.getElementsByTagName( "head" )[ 0 ] ||
document.documentElement,
jsonpCallback = options.callbackName || 'openseadragon' + $.now(),
previous = window[ jsonpCallback ],
replace = "$1" + jsonpCallback + "$2",
callbackParam = options.param || 'callback',
callback = options.callback;
url = url.replace( /(=)\?(&|$)|\?\?/i, replace );
// Add callback manually
url += (/\?/.test( url ) ? "&" : "?") + callbackParam + "=" + jsonpCallback;
// Install callback
window[ jsonpCallback ] = function( response ) {
if ( !previous ){
try{
delete window[ jsonpCallback ];
}catch(e){
//swallow
}
} else {
window[ jsonpCallback ] = previous;
}
if( callback && $.isFunction( callback ) ){
callback( response );
}
};
script = document.createElement( "script" );
//TODO: having an issue with async info requests
if( undefined !== options.async || false !== options.async ){
script.async = "async";
}
if ( options.scriptCharset ) {
script.charset = options.scriptCharset;
}
script.src = url;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function( _, isAbort ) {
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
// Remove the script
if ( head && script.parentNode ) {
head.removeChild( script );
}
// Dereference the script
script = undefined;
}
};
// Use insertBefore instead of appendChild to circumvent an IE6 bug.
// This arises when a base node is used (#2709 and #4378).
head.insertBefore( script, head.firstChild );
},
/**
* Fully deprecated. Will throw an error.
* @function
* @deprecated use {@link OpenSeadragon.Viewer#open}
*/
createFromDZI: function() {
throw "OpenSeadragon.createFromDZI is deprecated, use Viewer.open.";
},
/**
* Parses an XML string into a DOM Document.
* @function
* @param {String} string
* @returns {Document}
*/
parseXml: function( string ) {
if ( window.DOMParser ) {
$.parseXml = function( string ) {
var xmlDoc = null,
parser;
parser = new DOMParser();
xmlDoc = parser.parseFromString( string, "text/xml" );
return xmlDoc;
};
} else {
throw new Error( "Browser doesn't support XML DOM." );
}
return $.parseXml( string );
},
/**
* Parses a JSON string into a Javascript object.
* @function
* @param {String} string
* @returns {Object}
*/
parseJSON: function(string) {
$.parseJSON = window.JSON.parse;
return $.parseJSON(string);
},
/**
* Reports whether the image format is supported for tiling in this
* version.
* @function
* @param {String} [extension]
* @returns {Boolean}
*/
imageFormatSupported: function( extension ) {
extension = extension ? extension : "";
// eslint-disable-next-line no-use-before-define
return !!FILEFORMATS[ extension.toLowerCase() ];
},
/**
* Updates supported image formats with user-specified values.
* Preexisting formats that are not being updated are left unchanged.
* By default, the defined formats are
* <pre><code>{
* avif: true,
* bmp: false,
* jpeg: true,
* jpg: true,
* png: true,
* tif: false,
* wdp: false,
* webp: true
* }
* </code></pre>
* @function
* @example
* // sets bmp as supported and png as unsupported
* setImageFormatsSupported({bmp: true, png: false});
* @param {Object} formats An object containing format extensions as
* keys and booleans as values.
*/
setImageFormatsSupported: function(formats) {
// eslint-disable-next-line no-use-before-define
$.extend(FILEFORMATS, formats);
}
});
//TODO: $.console is often used inside a try/catch block which generally
// prevents allowings errors to occur with detection until a debugger
// is attached. Although I've been guilty of the same anti-pattern
// I eventually was convinced that errors should naturally propagate in
// all but the most special cases.
/**
* A convenient alias for console when available, and a simple null
* function when console is unavailable.
* @static
* @private
*/
var nullfunction = function( msg ){
//document.location.hash = msg;
};
$.console = window.console || {
log: nullfunction,
debug: nullfunction,
info: nullfunction,
warn: nullfunction,
error: nullfunction,
assert: nullfunction
};
/**
* The current browser vendor, version, and related information regarding detected features.
* @member {Object} Browser
* @memberof OpenSeadragon
* @static
* @type {Object}
* @property {OpenSeadragon.BROWSERS} vendor - One of the {@link OpenSeadragon.BROWSERS} enumeration values.
* @property {Number} version
* @property {Boolean} alpha - Does the browser support image alpha transparency.
*/
$.Browser = {
vendor: $.BROWSERS.UNKNOWN,
version: 0,
alpha: true
};
var FILEFORMATS = {
avif: true,
bmp: false,
jpeg: true,
jpg: true,
png: true,
tif: false,
wdp: false,
webp: true
},
URLPARAMS = {};
(function() {
//A small auto-executing routine to determine the browser vendor,
//version and supporting feature sets.
var ver = navigator.appVersion,
ua = navigator.userAgent,
regex;
//console.error( 'appName: ' + navigator.appName );
//console.error( 'appVersion: ' + navigator.appVersion );
//console.error( 'userAgent: ' + navigator.userAgent );
//TODO navigator.appName is deprecated. Should be 'Netscape' for all browsers
// but could be dropped at any time
// See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appName
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
switch( navigator.appName ){
case "Microsoft Internet Explorer":
if( !!window.attachEvent &&
!!window.ActiveXObject ) {
$.Browser.vendor = $.BROWSERS.IE;
$.Browser.version = parseFloat(
ua.substring(
ua.indexOf( "MSIE" ) + 5,
ua.indexOf( ";", ua.indexOf( "MSIE" ) ) )
);
}
break;
case "Netscape":
if (window.addEventListener) {
if ( ua.indexOf( "Edge" ) >= 0 ) {
$.Browser.vendor = $.BROWSERS.EDGE;
$.Browser.version = parseFloat(
ua.substring( ua.indexOf( "Edge" ) + 5 )
);
} else if ( ua.indexOf( "Edg" ) >= 0 ) {
$.Browser.vendor = $.BROWSERS.CHROMEEDGE;
$.Browser.version = parseFloat(
ua.substring( ua.indexOf( "Edg" ) + 4 )
);
} else if ( ua.indexOf( "Firefox" ) >= 0 ) {
$.Browser.vendor = $.BROWSERS.FIREFOX;
$.Browser.version = parseFloat(
ua.substring( ua.indexOf( "Firefox" ) + 8 )
);
} else if ( ua.indexOf( "Safari" ) >= 0 ) {
$.Browser.vendor = ua.indexOf( "Chrome" ) >= 0 ?
$.BROWSERS.CHROME :
$.BROWSERS.SAFARI;
$.Browser.version = parseFloat(
ua.substring(
ua.substring( 0, ua.indexOf( "Safari" ) ).lastIndexOf( "/" ) + 1,
ua.indexOf( "Safari" )
)
);
} else {
regex = new RegExp( "Trident/.*rv:([0-9]{1,}[.0-9]{0,})");
if ( regex.exec( ua ) !== null ) {
$.Browser.vendor = $.BROWSERS.IE;
$.Browser.version = parseFloat( RegExp.$1 );
}
}
}
break;
case "Opera":
$.Browser.vendor = $.BROWSERS.OPERA;
$.Browser.version = parseFloat( ver );
break;
}
// ignore '?' portion of query string
var query = window.location.search.substring( 1 ),
parts = query.split('&'),
part,
sep,
i;
for ( i = 0; i < parts.length; i++ ) {
part = parts[ i ];
sep = part.indexOf( '=' );
if ( sep > 0 ) {
var key = part.substring( 0, sep ),
value = part.substring( sep + 1 );
try {
URLPARAMS[ key ] = decodeURIComponent( value );
} catch (e) {
$.console.error( "Ignoring malformed URL parameter: %s=%s", key, value );
}
}
}
//determine if this browser supports image alpha transparency
$.Browser.alpha = !(
$.Browser.vendor === $.BROWSERS.CHROME && $.Browser.version < 2
);
//determine if this browser supports element.style.opacity
$.Browser.opacity = true;
if ( $.Browser.vendor === $.BROWSERS.IE ) {
$.console.error('Internet Explorer is not supported by OpenSeadragon');
}
})();
// Adding support for HTML5's requestAnimationFrame as suggested by acdha.
// Implementation taken from matt synder's post here:
// http://mattsnider.com/cross-browser-and-legacy-supported-requestframeanimation/
(function( w ) {
// most browsers have an implementation
var requestAnimationFrame = w.requestAnimationFrame ||
w.mozRequestAnimationFrame ||
w.webkitRequestAnimationFrame ||
w.msRequestAnimationFrame;
var cancelAnimationFrame = w.cancelAnimationFrame ||
w.mozCancelAnimationFrame ||
w.webkitCancelAnimationFrame ||
w.msCancelAnimationFrame;
// polyfill, when necessary
if ( requestAnimationFrame && cancelAnimationFrame ) {
// We can't assign these window methods directly to $ because they
// expect their "this" to be "window", so we call them in wrappers.
$.requestAnimationFrame = function(){
return requestAnimationFrame.apply( w, arguments );
};
$.cancelAnimationFrame = function(){
return cancelAnimationFrame.apply( w, arguments );
};
} else {
var aAnimQueue = [],
processing = [],
iRequestId = 0,
iIntervalId;
// create a mock requestAnimationFrame function
$.requestAnimationFrame = function( callback ) {
aAnimQueue.push( [ ++iRequestId, callback ] );
if ( !iIntervalId ) {
iIntervalId = setInterval( function() {
if ( aAnimQueue.length ) {
var time = $.now();
// Process all of the currently outstanding frame
// requests, but none that get added during the
// processing.
// Swap the arrays so we don't have to create a new
// array every frame.
var temp = processing;
processing = aAnimQueue;
aAnimQueue = temp;
while ( processing.length ) {
processing.shift()[ 1 ]( time );
}
} else {
// don't continue the interval, if unnecessary
clearInterval( iIntervalId );
iIntervalId = undefined;
}
}, 1000 / 50); // estimating support for 50 frames per second
}
return iRequestId;
};
// create a mock cancelAnimationFrame function
$.cancelAnimationFrame = function( requestId ) {
// find the request ID and remove it
var i, j;
for ( i = 0, j = aAnimQueue.length; i < j; i += 1 ) {
if ( aAnimQueue[ i ][ 0 ] === requestId ) {
aAnimQueue.splice( i, 1 );
return;
}
}
// If it's not in the queue, it may be in the set we're currently
// processing (if cancelAnimationFrame is called from within a
// requestAnimationFrame callback).
for ( i = 0, j = processing.length; i < j; i += 1 ) {
if ( processing[ i ][ 0 ] === requestId ) {
processing.splice( i, 1 );
return;
}
}
};
}
})( window );
/**
* @private
* @inner
* @function
* @param {Element} element
* @param {Boolean} [isFixed]
* @returns {Element}
*/
function getOffsetParent( element, isFixed ) {
if ( isFixed && element !== document.body ) {
return document.body;
} else {
return element.offsetParent;
}
}
}(OpenSeadragon));
// Universal Module Definition, supports CommonJS, AMD and simple script tag
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// expose as amd module
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// expose as commonjs module
module.exports = factory();
} else {
// expose as window.OpenSeadragon
root.OpenSeadragon = factory();
}
}(this, function () {
return OpenSeadragon;
}));
/*
* OpenSeadragon - Mat3
*
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
/*
* Portions of this source file are taken from WegGL Fundamentals:
*
* Copyright 2012, Gregg Tavares.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Gregg Tavares. nor the names of his
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
(function( $ ){
// Modified from https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
/**
*
*
* @class Mat3
* @classdesc A left-to-right matrix representation, useful for affine transforms for
* positioning tiles for drawing
*
* @memberof OpenSeadragon
*
* @param {Array} [values] - Initial values for the matrix
*
**/
class Mat3{
constructor(values){
if(!values) {
values = [
0, 0, 0,
0, 0, 0,
0, 0, 0
];
}
this.values = values;
}
/**
* @function makeIdentity
* @memberof OpenSeadragon.Mat3
* @static
* @returns {OpenSeadragon.Mat3} an identity matrix
*/
static makeIdentity(){
return new Mat3([
1, 0, 0,
0, 1, 0,
0, 0, 1
]);
}
/**
* @function makeTranslation
* @memberof OpenSeadragon.Mat3
* @static
* @param {Number} tx The x value of the translation
* @param {Number} ty The y value of the translation
* @returns {OpenSeadragon.Mat3} A translation matrix
*/
static makeTranslation(tx, ty) {
return new Mat3([
1, 0, 0,
0, 1, 0,
tx, ty, 1,
]);
}
/**
* @function makeRotation
* @memberof OpenSeadragon.Mat3
* @static
* @param {Number} angleInRadians The desired rotation angle, in radians
* @returns {OpenSeadragon.Mat3} A rotation matrix
*/
static makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return new Mat3([
c, -s, 0,
s, c, 0,
0, 0, 1,
]);
}
/**
* @function makeScaling
* @memberof OpenSeadragon.Mat3
* @static
* @param {Number} sx The x value of the scaling
* @param {Number} sy The y value of the scaling
* @returns {OpenSeadragon.Mat3} A scaling matrix
*/
static makeScaling(sx, sy) {
return new Mat3([
sx, 0, 0,
0, sy, 0,
0, 0, 1,
]);
}
/**
* @alias multiply
* @memberof! OpenSeadragon.Mat3
* @param {OpenSeadragon.Mat3} other the matrix to multiply with
* @returns {OpenSeadragon.Mat3} The result of matrix multiplication
*/
multiply(other) {
let a = this.values;
let b = other.values;
var a00 = a[0 * 3 + 0];
var a01 = a[0 * 3 + 1];
var a02 = a[0 * 3 + 2];
var a10 = a[1 * 3 + 0];
var a11 = a[1 * 3 + 1];
var a12 = a[1 * 3 + 2];
var a20 = a[2 * 3 + 0];
var a21 = a[2 * 3 + 1];
var a22 = a[2 * 3 + 2];
var b00 = b[0 * 3 + 0];
var b01 = b[0 * 3 + 1];
var b02 = b[0 * 3 + 2];
var b10 = b[1 * 3 + 0];
var b11 = b[1 * 3 + 1];
var b12 = b[1 * 3 + 2];
var b20 = b[2 * 3 + 0];
var b21 = b[2 * 3 + 1];
var b22 = b[2 * 3 + 2];
return new Mat3([
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22,
]);
}
}
$.Mat3 = Mat3;
}( OpenSeadragon ));
/*
* OpenSeadragon - full-screen support functions
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ) {
/**
* Determine native full screen support we can get from the browser.
* @member fullScreenApi
* @memberof OpenSeadragon
* @type {object}
* @property {Boolean} supportsFullScreen Return true if full screen API is supported.
* @property {Function} isFullScreen Return true if currently in full screen mode.
* @property {Function} getFullScreenElement Return the element currently in full screen mode.
* @property {Function} requestFullScreen Make a request to go in full screen mode.
* @property {Function} exitFullScreen Make a request to exit full screen mode.
* @property {Function} cancelFullScreen Deprecated, use exitFullScreen instead.
* @property {String} fullScreenEventName Event fired when the full screen mode change.
* @property {String} fullScreenErrorEventName Event fired when a request to go
* in full screen mode failed.
*/
var fullScreenApi = {
supportsFullScreen: false,
isFullScreen: function() { return false; },
getFullScreenElement: function() { return null; },
requestFullScreen: function() {},
exitFullScreen: function() {},
cancelFullScreen: function() {},
fullScreenEventName: '',
fullScreenErrorEventName: ''
};
// check for native support
if ( document.exitFullscreen ) {
// W3C standard
fullScreenApi.supportsFullScreen = true;
fullScreenApi.getFullScreenElement = function() {
return document.fullscreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
return element.requestFullscreen().catch(function (msg) {
$.console.error('Fullscreen request failed: ', msg);
});
};
fullScreenApi.exitFullScreen = function() {
document.exitFullscreen().catch(function (msg) {
$.console.error('Error while exiting fullscreen: ', msg);
});
};
fullScreenApi.fullScreenEventName = "fullscreenchange";
fullScreenApi.fullScreenErrorEventName = "fullscreenerror";
} else if ( document.msExitFullscreen ) {
// IE 11
fullScreenApi.supportsFullScreen = true;
fullScreenApi.getFullScreenElement = function() {
return document.msFullscreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
return element.msRequestFullscreen();
};
fullScreenApi.exitFullScreen = function() {
document.msExitFullscreen();
};
fullScreenApi.fullScreenEventName = "MSFullscreenChange";
fullScreenApi.fullScreenErrorEventName = "MSFullscreenError";
} else if ( document.webkitExitFullscreen ) {
// Recent webkit
fullScreenApi.supportsFullScreen = true;
fullScreenApi.getFullScreenElement = function() {
return document.webkitFullscreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
return element.webkitRequestFullscreen();
};
fullScreenApi.exitFullScreen = function() {
document.webkitExitFullscreen();
};
fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
} else if ( document.webkitCancelFullScreen ) {
// Old webkit
fullScreenApi.supportsFullScreen = true;
fullScreenApi.getFullScreenElement = function() {
return document.webkitCurrentFullScreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
return element.webkitRequestFullScreen();
};
fullScreenApi.exitFullScreen = function() {
document.webkitCancelFullScreen();
};
fullScreenApi.fullScreenEventName = "webkitfullscreenchange";
fullScreenApi.fullScreenErrorEventName = "webkitfullscreenerror";
} else if ( document.mozCancelFullScreen ) {
// Firefox
fullScreenApi.supportsFullScreen = true;
fullScreenApi.getFullScreenElement = function() {
return document.mozFullScreenElement;
};
fullScreenApi.requestFullScreen = function( element ) {
return element.mozRequestFullScreen();
};
fullScreenApi.exitFullScreen = function() {
document.mozCancelFullScreen();
};
fullScreenApi.fullScreenEventName = "mozfullscreenchange";
fullScreenApi.fullScreenErrorEventName = "mozfullscreenerror";
}
fullScreenApi.isFullScreen = function() {
return fullScreenApi.getFullScreenElement() !== null;
};
fullScreenApi.cancelFullScreen = function() {
$.console.error("cancelFullScreen is deprecated. Use exitFullScreen instead.");
fullScreenApi.exitFullScreen();
};
// export api
$.extend( $, fullScreenApi );
})( OpenSeadragon );
/*
* OpenSeadragon - EventSource
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function($){
/**
* Event handler method signature used by all OpenSeadragon events.
*
* @callback EventHandler
* @memberof OpenSeadragon
* @param {Object} event - See individual events for event-specific properties.
*/
/**
* @class EventSource
* @classdesc For use by classes which want to support custom, non-browser events.
*
* @memberof OpenSeadragon
*/
$.EventSource = function() {
this.events = {};
this._rejectedEventList = {};
};
/** @lends OpenSeadragon.EventSource.prototype */
$.EventSource.prototype = {
/**
* Add an event handler to be triggered only once (or a given number of times)
* for a given event. It is not removable with removeHandler().
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event
* is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged
* to the handler.
* @param {Number} [times=1] - The number of times to handle the event
* before removing it.
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
* @returns {Boolean} - True if the handler was added, false if it was rejected
*/
addOnceHandler: function(eventName, handler, userData, times, priority) {
var self = this;
times = times || 1;
var count = 0;
var onceHandler = function(event) {
count++;
if (count === times) {
self.removeHandler(eventName, onceHandler);
}
return handler(event);
};
return this.addHandler(eventName, onceHandler, userData, priority);
},
/**
* Add an event handler for a given event.
* @function
* @param {String} eventName - Name of event to register.
* @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered.
* @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler.
* @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority.
* @returns {Boolean} - True if the handler was added, false if it was rejected
*/
addHandler: function ( eventName, handler, userData, priority ) {
if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
$.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
return false;
}
var events = this.events[ eventName ];
if ( !events ) {
this.events[ eventName ] = events = [];
}
if ( handler && $.isFunction( handler ) ) {
var index = events.length,
event = { handler: handler, userData: userData || null, priority: priority || 0 };
events[ index ] = event;
while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) {
events[ index ] = events[ index - 1 ];
events[ index - 1 ] = event;
index--;
}
}
return true;
},
/**
* Remove a specific event handler for a given event.
* @function
* @param {String} eventName - Name of event for which the handler is to be removed.
* @param {OpenSeadragon.EventHandler} handler - Function to be removed.
*/
removeHandler: function ( eventName, handler ) {
var events = this.events[ eventName ],
handlers = [],
i;
if ( !events ) {
return;
}
if ( $.isArray( events ) ) {
for ( i = 0; i < events.length; i++ ) {
if ( events[i].handler !== handler ) {
handlers.push( events[ i ] );
}
}
this.events[ eventName ] = handlers;
}
},
/**
* Get the amount of handlers registered for a given event.
* @param {String} eventName - Name of event to inspect.
* @returns {number} amount of events
*/
numberOfHandlers: function (eventName) {
var events = this.events[ eventName ];
if ( !events ) {
return 0;
}
return events.length;
},
/**
* Remove all event handlers for a given event type. If no type is given all
* event handlers for every event type are removed.
* @function
* @param {String} eventName - Name of event for which all handlers are to be removed.
*/
removeAllHandlers: function( eventName ) {
if ( eventName ){
this.events[ eventName ] = [];
} else{
for ( var eventType in this.events ) {
this.events[ eventType ] = [];
}
}
},
/**
* Get a function which iterates the list of all handlers registered for a given event, calling the handler for each.
* @function
* @param {String} eventName - Name of event to get handlers for.
*/
getHandler: function ( eventName) {
var events = this.events[ eventName ];
if ( !events || !events.length ) {
return null;
}
events = events.length === 1 ?
[ events[ 0 ] ] :
Array.apply( null, events );
return function ( source, args ) {
var i,
length = events.length;
for ( i = 0; i < length; i++ ) {
if ( events[ i ] ) {
args.eventSource = source;
args.userData = events[ i ].userData;
events[ i ].handler( args );
}
}
};
},
/**
* Trigger an event, optionally passing additional information.
* @function
* @param {String} eventName - Name of event to register.
* @param {Object} eventArgs - Event-specific data.
* @returns {Boolean} True if the event was fired, false if it was rejected because of rejectEventHandler(eventName)
*/
raiseEvent: function( eventName, eventArgs ) {
//uncomment if you want to get a log of all events
//$.console.log( eventName );
if(Object.prototype.hasOwnProperty.call(this._rejectedEventList, eventName)){
$.console.error(`Error adding handler for ${eventName}. ${this._rejectedEventList[eventName]}`);
return false;
}
var handler = this.getHandler( eventName );
if ( handler ) {
handler( this, eventArgs || {} );
}
return true;
},
/**
* Set an event name as being disabled, and provide an optional error message
* to be printed to the console
* @param {String} eventName - Name of the event
* @param {String} [errorMessage] - Optional string to print to the console
* @private
*/
rejectEventHandler(eventName, errorMessage = ''){
this._rejectedEventList[eventName] = errorMessage;
},
/**
* Explicitly allow an event handler to be added for this event type, undoing
* the effects of rejectEventHandler
* @param {String} eventName - Name of the event
* @private
*/
allowEventHandler(eventName){
delete this._rejectedEventList[eventName];
}
};
}( OpenSeadragon ));
/*
* OpenSeadragon - MouseTracker
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function ( $ ) {
// All MouseTracker instances
var MOUSETRACKERS = [];
// dictionary from hash to private properties
var THIS = {};
/**
* @class MouseTracker
* @classdesc Provides simplified handling of common pointer device (mouse, touch, pen, etc.) gestures
* and keyboard events on a specified element.
* @memberof OpenSeadragon
* @param {Object} options
* Allows configurable properties to be entirely specified by passing
* an options object to the constructor. The constructor also supports
* the original positional arguments 'element', 'clickTimeThreshold',
* and 'clickDistThreshold' in that order.
* @param {Element|String} options.element
* A reference to an element or an element id for which the pointer/key
* events will be monitored.
* @param {Boolean} [options.startDisabled=false]
* If true, event tracking on the element will not start until
* {@link OpenSeadragon.MouseTracker.setTracking|setTracking} is called.
* @param {Number} [options.clickTimeThreshold=300]
* The number of milliseconds within which a pointer down-up event combination
* will be treated as a click gesture.
* @param {Number} [options.clickDistThreshold=5]
* The maximum distance allowed between a pointer down event and a pointer up event
* to be treated as a click gesture.
* @param {Number} [options.dblClickTimeThreshold=300]
* The number of milliseconds within which two pointer down-up event combinations
* will be treated as a double-click gesture.
* @param {Number} [options.dblClickDistThreshold=20]
* The maximum distance allowed between two pointer click events
* to be treated as a click gesture.
* @param {Number} [options.stopDelay=50]
* The number of milliseconds without pointer move before the stop
* event is fired.
* @param {OpenSeadragon.EventHandler} [options.preProcessEventHandler=null]
* An optional handler for controlling DOM event propagation and processing.
* @param {OpenSeadragon.EventHandler} [options.contextMenuHandler=null]
* An optional handler for contextmenu.
* @param {OpenSeadragon.EventHandler} [options.enterHandler=null]
* An optional handler for pointer enter.
* @param {OpenSeadragon.EventHandler} [options.leaveHandler=null]
* An optional handler for pointer leave.
* @param {OpenSeadragon.EventHandler} [options.exitHandler=null]
* An optional handler for pointer leave. <span style="color:red;">Deprecated. Use leaveHandler instead.</span>
* @param {OpenSeadragon.EventHandler} [options.overHandler=null]
* An optional handler for pointer over.
* @param {OpenSeadragon.EventHandler} [options.outHandler=null]
* An optional handler for pointer out.
* @param {OpenSeadragon.EventHandler} [options.pressHandler=null]
* An optional handler for pointer press.
* @param {OpenSeadragon.EventHandler} [options.nonPrimaryPressHandler=null]
* An optional handler for pointer non-primary button press.
* @param {OpenSeadragon.EventHandler} [options.releaseHandler=null]
* An optional handler for pointer release.
* @param {OpenSeadragon.EventHandler} [options.nonPrimaryReleaseHandler=null]
* An optional handler for pointer non-primary button release.
* @param {OpenSeadragon.EventHandler} [options.moveHandler=null]
* An optional handler for pointer move.
* @param {OpenSeadragon.EventHandler} [options.scrollHandler=null]
* An optional handler for mouse wheel scroll.
* @param {OpenSeadragon.EventHandler} [options.clickHandler=null]
* An optional handler for pointer click.
* @param {OpenSeadragon.EventHandler} [options.dblClickHandler=null]
* An optional handler for pointer double-click.
* @param {OpenSeadragon.EventHandler} [options.dragHandler=null]
* An optional handler for the drag gesture.
* @param {OpenSeadragon.EventHandler} [options.dragEndHandler=null]
* An optional handler for after a drag gesture.
* @param {OpenSeadragon.EventHandler} [options.pinchHandler=null]
* An optional handler for the pinch gesture.
* @param {OpenSeadragon.EventHandler} [options.keyDownHandler=null]
* An optional handler for keydown.
* @param {OpenSeadragon.EventHandler} [options.keyUpHandler=null]
* An optional handler for keyup.
* @param {OpenSeadragon.EventHandler} [options.keyHandler=null]
* An optional handler for keypress.
* @param {OpenSeadragon.EventHandler} [options.focusHandler=null]
* An optional handler for focus.
* @param {OpenSeadragon.EventHandler} [options.blurHandler=null]
* An optional handler for blur.
* @param {Object} [options.userData=null]
* Arbitrary object to be passed unchanged to any attached handler methods.
*/
$.MouseTracker = function ( options ) {
MOUSETRACKERS.push( this );
var args = arguments;
if ( !$.isPlainObject( options ) ) {
options = {
element: args[ 0 ],
clickTimeThreshold: args[ 1 ],
clickDistThreshold: args[ 2 ]
};
}
this.hash = Math.random(); // An unique hash for this tracker.
/**
* The element for which pointer events are being monitored.
* @member {Element} element
* @memberof OpenSeadragon.MouseTracker#
*/
this.element = $.getElement( options.element );
/**
* The number of milliseconds within which a pointer down-up event combination
* will be treated as a click gesture.
* @member {Number} clickTimeThreshold
* @memberof OpenSeadragon.MouseTracker#
*/
this.clickTimeThreshold = options.clickTimeThreshold || $.DEFAULT_SETTINGS.clickTimeThreshold;
/**
* The maximum distance allowed between a pointer down event and a pointer up event
* to be treated as a click gesture.
* @member {Number} clickDistThreshold
* @memberof OpenSeadragon.MouseTracker#
*/
this.clickDistThreshold = options.clickDistThreshold || $.DEFAULT_SETTINGS.clickDistThreshold;
/**
* The number of milliseconds within which two pointer down-up event combinations
* will be treated as a double-click gesture.
* @member {Number} dblClickTimeThreshold
* @memberof OpenSeadragon.MouseTracker#
*/
this.dblClickTimeThreshold = options.dblClickTimeThreshold || $.DEFAULT_SETTINGS.dblClickTimeThreshold;
/**
* The maximum distance allowed between two pointer click events
* to be treated as a double-click gesture.
* @member {Number} dblClickDistThreshold
* @memberof OpenSeadragon.MouseTracker#
*/
this.dblClickDistThreshold = options.dblClickDistThreshold || $.DEFAULT_SETTINGS.dblClickDistThreshold;
/*eslint-disable no-multi-spaces*/
this.userData = options.userData || null;
this.stopDelay = options.stopDelay || 50;
this.preProcessEventHandler = options.preProcessEventHandler || null;
this.contextMenuHandler = options.contextMenuHandler || null;
this.enterHandler = options.enterHandler || null;
this.leaveHandler = options.leaveHandler || null;
this.exitHandler = options.exitHandler || null; // Deprecated v2.5.0
this.overHandler = options.overHandler || null;
this.outHandler = options.outHandler || null;
this.pressHandler = options.pressHandler || null;
this.nonPrimaryPressHandler = options.nonPrimaryPressHandler || null;
this.releaseHandler = options.releaseHandler || null;
this.nonPrimaryReleaseHandler = options.nonPrimaryReleaseHandler || null;
this.moveHandler = options.moveHandler || null;
this.scrollHandler = options.scrollHandler || null;
this.clickHandler = options.clickHandler || null;
this.dblClickHandler = options.dblClickHandler || null;
this.dragHandler = options.dragHandler || null;
this.dragEndHandler = options.dragEndHandler || null;
this.pinchHandler = options.pinchHandler || null;
this.stopHandler = options.stopHandler || null;
this.keyDownHandler = options.keyDownHandler || null;
this.keyUpHandler = options.keyUpHandler || null;
this.keyHandler = options.keyHandler || null;
this.focusHandler = options.focusHandler || null;
this.blurHandler = options.blurHandler || null;
/*eslint-enable no-multi-spaces*/
//Store private properties in a scope sealed hash map
var _this = this;
/**
* @private
* @property {Boolean} tracking
* Are we currently tracking pointer events for this element.
*/
THIS[ this.hash ] = {
click: function ( event ) { onClick( _this, event ); },
dblclick: function ( event ) { onDblClick( _this, event ); },
keydown: function ( event ) { onKeyDown( _this, event ); },
keyup: function ( event ) { onKeyUp( _this, event ); },
keypress: function ( event ) { onKeyPress( _this, event ); },
focus: function ( event ) { onFocus( _this, event ); },
blur: function ( event ) { onBlur( _this, event ); },
contextmenu: function ( event ) { onContextMenu( _this, event ); },
wheel: function ( event ) { onWheel( _this, event ); },
mousewheel: function ( event ) { onMouseWheel( _this, event ); },
DOMMouseScroll: function ( event ) { onMouseWheel( _this, event ); },
MozMousePixelScroll: function ( event ) { onMouseWheel( _this, event ); },
losecapture: function ( event ) { onLoseCapture( _this, event ); },
mouseenter: function ( event ) { onPointerEnter( _this, event ); },
mouseleave: function ( event ) { onPointerLeave( _this, event ); },
mouseover: function ( event ) { onPointerOver( _this, event ); },
mouseout: function ( event ) { onPointerOut( _this, event ); },
mousedown: function ( event ) { onPointerDown( _this, event ); },
mouseup: function ( event ) { onPointerUp( _this, event ); },
mousemove: function ( event ) { onPointerMove( _this, event ); },
touchstart: function ( event ) { onTouchStart( _this, event ); },
touchend: function ( event ) { onTouchEnd( _this, event ); },
touchmove: function ( event ) { onTouchMove( _this, event ); },
touchcancel: function ( event ) { onTouchCancel( _this, event ); },
gesturestart: function ( event ) { onGestureStart( _this, event ); }, // Safari/Safari iOS
gesturechange: function ( event ) { onGestureChange( _this, event ); }, // Safari/Safari iOS
gotpointercapture: function ( event ) { onGotPointerCapture( _this, event ); },
lostpointercapture: function ( event ) { onLostPointerCapture( _this, event ); },
pointerenter: function ( event ) { onPointerEnter( _this, event ); },
pointerleave: function ( event ) { onPointerLeave( _this, event ); },
pointerover: function ( event ) { onPointerOver( _this, event ); },
pointerout: function ( event ) { onPointerOut( _this, event ); },
pointerdown: function ( event ) { onPointerDown( _this, event ); },
pointerup: function ( event ) { onPointerUp( _this, event ); },
pointermove: function ( event ) { onPointerMove( _this, event ); },
pointercancel: function ( event ) { onPointerCancel( _this, event ); },
pointerupcaptured: function ( event ) { onPointerUpCaptured( _this, event ); },
pointermovecaptured: function ( event ) { onPointerMoveCaptured( _this, event ); },
tracking: false,
// Active pointers lists. Array of GesturePointList objects, one for each pointer device type.
// GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()).
// Active pointers are any pointer being tracked for this element which are in the hit-test area
// of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
activePointersLists: [],
// Tracking for double-click gesture
lastClickPos: null,
dblClickTimeOut: null,
// Tracking for pinch gesture
pinchGPoints: [],
lastPinchDist: 0,
currentPinchDist: 0,
lastPinchCenter: null,
currentPinchCenter: null,
// Tracking for drag
sentDragEvent: false
};
this.hasGestureHandlers = !!( this.pressHandler || this.nonPrimaryPressHandler ||
this.releaseHandler || this.nonPrimaryReleaseHandler ||
this.clickHandler || this.dblClickHandler ||
this.dragHandler || this.dragEndHandler ||
this.pinchHandler );
this.hasScrollHandler = !!this.scrollHandler;
if ( $.MouseTracker.havePointerEvents ) {
$.setElementPointerEvents( this.element, 'auto' );
}
if (this.exitHandler) {
$.console.error("MouseTracker.exitHandler is deprecated. Use MouseTracker.leaveHandler instead.");
}
if ( !options.startDisabled ) {
this.setTracking( true );
}
};
/** @lends OpenSeadragon.MouseTracker.prototype */
$.MouseTracker.prototype = {
/**
* Clean up any events or objects created by the tracker.
* @function
*/
destroy: function () {
var i;
stopTracking( this );
this.element = null;
for ( i = 0; i < MOUSETRACKERS.length; i++ ) {
if ( MOUSETRACKERS[ i ] === this ) {
MOUSETRACKERS.splice( i, 1 );
break;
}
}
THIS[ this.hash ] = null;
delete THIS[ this.hash ];
},
/**
* Are we currently tracking events on this element.
* @deprecated Just use this.tracking
* @function
* @returns {Boolean} Are we currently tracking events on this element.
*/
isTracking: function () {
return THIS[ this.hash ].tracking;
},
/**
* Enable or disable whether or not we are tracking events on this element.
* @function
* @param {Boolean} track True to start tracking, false to stop tracking.
* @returns {OpenSeadragon.MouseTracker} Chainable.
*/
setTracking: function ( track ) {
if ( track ) {
startTracking( this );
} else {
stopTracking( this );
}
//chain
return this;
},
/**
* Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for the given pointer device type,
* creating and caching a new {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} if one doesn't already exist for the type.
* @function
* @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
* @returns {OpenSeadragon.MouseTracker.GesturePointList}
*/
getActivePointersListByType: function ( type ) {
var delegate = THIS[ this.hash ],
i,
len = delegate ? delegate.activePointersLists.length : 0,
list;
for ( i = 0; i < len; i++ ) {
if ( delegate.activePointersLists[ i ].type === type ) {
return delegate.activePointersLists[ i ];
}
}
list = new $.MouseTracker.GesturePointList( type );
if(delegate){
delegate.activePointersLists.push( list );
}
return list;
},
/**
* Returns the total number of pointers currently active on the tracked element.
* @function
* @returns {Number}
*/
getActivePointerCount: function () {
var delegate = THIS[ this.hash ],
i,
len = delegate.activePointersLists.length,
count = 0;
for ( i = 0; i < len; i++ ) {
count += delegate.activePointersLists[ i ].getLength();
}
return count;
},
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
*/
preProcessEventHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Object} event.originalEvent
* The original event object.
* @param {Boolean} event.preventDefault
* Set to true to prevent the default user-agent's handling of the contextmenu event.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
contextMenuHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Number} event.pointers
* Number of pointers (all types) active in the tracked element.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.buttonDownAny
* Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
enterHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @since v2.5.0
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Number} event.pointers
* Number of pointers (all types) active in the tracked element.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.buttonDownAny
* Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
leaveHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @deprecated v2.5.0 Use leaveHandler instead
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Number} event.pointers
* Number of pointers (all types) active in the tracked element.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.buttonDownAny
* Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
exitHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @since v2.5.0
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Number} event.pointers
* Number of pointers (all types) active in the tracked element.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.buttonDownAny
* Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
overHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @since v2.5.0
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Number} event.pointers
* Number of pointers (all types) active in the tracked element.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.buttonDownAny
* Was the button down anywhere in the screen during the event. <span style="color:red;">Deprecated. Use buttons instead.</span>
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
outHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
pressHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.button
* Button which caused the event.
* -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
nonPrimaryPressHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.insideElementPressed
* True if the left mouse button is currently being pressed and was
* initiated inside the tracked element, otherwise false.
* @param {Boolean} event.insideElementReleased
* True if the cursor inside the tracked element when the button was released.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
releaseHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.button
* Button which caused the event.
* -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
nonPrimaryReleaseHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
moveHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.scroll
* The scroll delta for the event.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead. Touch devices no longer generate scroll event.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Boolean} event.preventDefault
* Set to true to prevent the default user-agent's handling of the wheel event.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
scrollHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Boolean} event.quick
* True only if the clickDistThreshold and clickTimeThreshold are both passed. Useful for ignoring drag events.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Element} event.originalTarget
* The DOM element clicked on.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
clickHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
dblClickHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {OpenSeadragon.Point} event.delta
* The x,y components of the difference between the current position and the last drag event position. Useful for ignoring or weighting the events.
* @param {Number} event.speed
* Current computed speed, in pixels per second.
* @param {Number} event.direction
* Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
dragHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.speed
* Speed at the end of a drag gesture, in pixels per second.
* @param {Number} event.direction
* Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
dragEndHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {Array.<OpenSeadragon.MouseTracker.GesturePoint>} event.gesturePoints
* Gesture points associated with the gesture. Velocity data can be found here.
* @param {OpenSeadragon.Point} event.lastCenter
* The previous center point of the two pinch contact points relative to the tracked element.
* @param {OpenSeadragon.Point} event.center
* The center point of the two pinch contact points relative to the tracked element.
* @param {Number} event.lastDistance
* The previous distance between the two pinch contact points in CSS pixels.
* @param {Number} event.distance
* The distance between the two pinch contact points in CSS pixels.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
pinchHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {String} event.pointerType
* "mouse", "touch", "pen", etc.
* @param {OpenSeadragon.Point} event.position
* The position of the event relative to the tracked element.
* @param {Number} event.buttons
* Current buttons pressed.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @param {Boolean} event.isTouchEvent
* True if the original event is a touch event, otherwise false. <span style="color:red;">Deprecated. Use pointerType and/or originalEvent instead.</span>
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
stopHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {Number} event.keyCode
* The key code that was pressed.
* @param {Boolean} event.ctrl
* True if the ctrl key was pressed during this event.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.alt
* True if the alt key was pressed during this event.
* @param {Boolean} event.meta
* True if the meta key was pressed during this event.
* @param {Object} event.originalEvent
* The original event object.
* @param {Boolean} event.preventDefault
* Set to true to prevent the default user-agent's handling of the keydown event.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
keyDownHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {Number} event.keyCode
* The key code that was pressed.
* @param {Boolean} event.ctrl
* True if the ctrl key was pressed during this event.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.alt
* True if the alt key was pressed during this event.
* @param {Boolean} event.meta
* True if the meta key was pressed during this event.
* @param {Object} event.originalEvent
* The original event object.
* @param {Boolean} event.preventDefault
* Set to true to prevent the default user-agent's handling of the keyup event.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
keyUpHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {Number} event.keyCode
* The key code that was pressed.
* @param {Boolean} event.ctrl
* True if the ctrl key was pressed during this event.
* @param {Boolean} event.shift
* True if the shift key was pressed during this event.
* @param {Boolean} event.alt
* True if the alt key was pressed during this event.
* @param {Boolean} event.meta
* True if the meta key was pressed during this event.
* @param {Object} event.originalEvent
* The original event object.
* @param {Boolean} event.preventDefault
* Set to true to prevent the default user-agent's handling of the keypress event.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
keyHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
focusHandler: function () { },
/**
* Implement or assign implementation to these handlers during or after
* calling the constructor.
* @function
* @param {Object} event
* @param {OpenSeadragon.MouseTracker} event.eventSource
* A reference to the tracker instance.
* @param {Object} event.originalEvent
* The original event object.
* @param {Object} event.userData
* Arbitrary user-defined object.
*/
blurHandler: function () { }
};
// https://github.com/openseadragon/openseadragon/pull/790
/**
* True if inside an iframe, otherwise false.
* @member {Boolean} isInIframe
* @private
* @inner
*/
var isInIframe = (function() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
})();
// https://github.com/openseadragon/openseadragon/pull/790
/**
* @function
* @private
* @inner
* @returns {Boolean} True if the target supports DOM Level 2 event subscription methods, otherwise false.
*/
function canAccessEvents (target) {
try {
return target.addEventListener && target.removeEventListener;
} catch (e) {
return false;
}
}
/**
* Provides continuous computation of velocity (speed and direction) of active pointers.
* This is a singleton, used by all MouseTracker instances, as it is unlikely there will ever be more than
* two active gesture pointers at a time.
*
* @private
* @member gesturePointVelocityTracker
* @memberof OpenSeadragon.MouseTracker
*/
$.MouseTracker.gesturePointVelocityTracker = (function () {
var trackerPoints = [],
intervalId = 0,
lastTime = 0;
// Generates a unique identifier for a tracked gesture point
var _generateGuid = function ( tracker, gPoint ) {
return tracker.hash.toString() + gPoint.type + gPoint.id.toString();
};
// Interval timer callback. Computes velocity for all tracked gesture points.
var _doTracking = function () {
var i,
len = trackerPoints.length,
trackPoint,
gPoint,
now = $.now(),
elapsedTime,
distance,
speed;
elapsedTime = now - lastTime;
lastTime = now;
for ( i = 0; i < len; i++ ) {
trackPoint = trackerPoints[ i ];
gPoint = trackPoint.gPoint;
// Math.atan2 gives us just what we need for a velocity vector, as we can simply
// use cos()/sin() to extract the x/y velocity components.
gPoint.direction = Math.atan2( gPoint.currentPos.y - trackPoint.lastPos.y, gPoint.currentPos.x - trackPoint.lastPos.x );
// speed = distance / elapsed time
distance = trackPoint.lastPos.distanceTo( gPoint.currentPos );
trackPoint.lastPos = gPoint.currentPos;
speed = 1000 * distance / ( elapsedTime + 1 );
// Simple biased average, favors the most recent speed computation. Smooths out erratic gestures a bit.
gPoint.speed = 0.75 * speed + 0.25 * gPoint.speed;
}
};
// Public. Add a gesture point to be tracked
var addPoint = function ( tracker, gPoint ) {
var guid = _generateGuid( tracker, gPoint );
trackerPoints.push(
{
guid: guid,
gPoint: gPoint,
lastPos: gPoint.currentPos
} );
// Only fire up the interval timer when there's gesture pointers to track
if ( trackerPoints.length === 1 ) {
lastTime = $.now();
intervalId = window.setInterval( _doTracking, 50 );
}
};
// Public. Stop tracking a gesture point
var removePoint = function ( tracker, gPoint ) {
var guid = _generateGuid( tracker, gPoint ),
i,
len = trackerPoints.length;
for ( i = 0; i < len; i++ ) {
if ( trackerPoints[ i ].guid === guid ) {
trackerPoints.splice( i, 1 );
// Only run the interval timer if theres gesture pointers to track
len--;
if ( len === 0 ) {
window.clearInterval( intervalId );
}
break;
}
}
};
return {
addPoint: addPoint,
removePoint: removePoint
};
} )();
///////////////////////////////////////////////////////////////////////////////
// Pointer event model and feature detection
///////////////////////////////////////////////////////////////////////////////
$.MouseTracker.captureElement = document;
/**
* Detect available mouse wheel event name.
*/
$.MouseTracker.wheelEventName = ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel'
document.onmousewheel !== undefined ? 'mousewheel' : // Webkit (and unsupported IE) support at least 'mousewheel'
'DOMMouseScroll'; // Assume old Firefox (deprecated)
/**
* Detect browser pointer device event model(s) and build appropriate list of events to subscribe to.
*/
$.MouseTracker.subscribeEvents = [ "click", "dblclick", "keydown", "keyup", "keypress", "focus", "blur", "contextmenu", $.MouseTracker.wheelEventName ];
if( $.MouseTracker.wheelEventName === "DOMMouseScroll" ) {
// Older Firefox
$.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" );
}
if ( window.PointerEvent ) {
// W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents)
$.MouseTracker.havePointerEvents = true;
$.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" );
// Pointer events capture support
$.MouseTracker.havePointerCapture = (function () {
var divElement = document.createElement( 'div' );
return $.isFunction( divElement.setPointerCapture ) && $.isFunction( divElement.releasePointerCapture );
}());
if ( $.MouseTracker.havePointerCapture ) {
$.MouseTracker.subscribeEvents.push( "gotpointercapture", "lostpointercapture" );
}
} else {
// Legacy W3C mouse events
$.MouseTracker.havePointerEvents = false;
$.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" );
$.MouseTracker.mousePointerId = "legacy-mouse";
// Legacy mouse events capture support (IE/Firefox only?)
$.MouseTracker.havePointerCapture = (function () {
var divElement = document.createElement( 'div' );
return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture );
}());
if ( $.MouseTracker.havePointerCapture ) {
$.MouseTracker.subscribeEvents.push( "losecapture" );
}
// Legacy touch events
if ( 'ontouchstart' in window ) {
// iOS, Android, and other W3c Touch Event implementations
// (see http://www.w3.org/TR/touch-events/)
// (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
// (see https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
$.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" );
}
if ( 'ongesturestart' in window ) {
// iOS (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
// Subscribe to these to prevent default gesture handling
$.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" );
}
}
///////////////////////////////////////////////////////////////////////////////
// Classes and typedefs
///////////////////////////////////////////////////////////////////////////////
/**
* Used for the processing/disposition of DOM events (propagation, default handling, capture, etc.)
*
* @typedef {Object} EventProcessInfo
* @memberof OpenSeadragon.MouseTracker
* @since v2.5.0
*
* @property {OpenSeadragon.MouseTracker} eventSource
* A reference to the tracker instance.
* @property {Object} originalEvent
* The original DOM event object.
* @property {Number} eventPhase
* 0 == NONE, 1 == CAPTURING_PHASE, 2 == AT_TARGET, 3 == BUBBLING_PHASE.
* @property {String} eventType
* "keydown", "keyup", "keypress", "focus", "blur", "contextmenu", "gotpointercapture", "lostpointercapture", "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel", "wheel", "click", "dblclick".
* @property {String} pointerType
* "mouse", "touch", "pen", etc.
* @property {Boolean} isEmulated
* True if this is an emulated event. If true, originalEvent is either the event that caused
* the emulated event, a synthetic event object created with values from the actual DOM event,
* or null if no DOM event applies. Emulated events can occur on eventType "wheel" on legacy mouse-scroll
* event emitting user agents.
* @property {Boolean} isStoppable
* True if propagation of the event (e.g. bubbling) can be stopped with stopPropagation/stopImmediatePropagation.
* @property {Boolean} isCancelable
* True if the event's default handling by the browser can be prevented with preventDefault.
* @property {Boolean} defaultPrevented
* True if the event's default handling has already been prevented by a descendent element.
* @property {Boolean} preventDefault
* Set to true to prevent the event's default handling by the browser.
* @property {Boolean} preventGesture
* Set to true to prevent this MouseTracker from generating a gesture from the event.
* Valid on eventType "pointerdown".
* @property {Boolean} stopPropagation
* Set to true prevent the event from propagating to ancestor/descendent elements on capture/bubble phase.
* @property {Boolean} shouldCapture
* (Internal Use) Set to true if the pointer should be captured (events (re)targeted to tracker element).
* @property {Boolean} shouldReleaseCapture
* (Internal Use) Set to true if the captured pointer should be released.
* @property {Object} userData
* Arbitrary user-defined object.
*/
/**
* Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointer device.
*
* @typedef {Object} GesturePoint
* @memberof OpenSeadragon.MouseTracker
*
* @property {Number} id
* Identifier unique from all other active GesturePoints for a given pointer device.
* @property {String} type
* The pointer device type: "mouse", "touch", "pen", etc.
* @property {Boolean} captured
* True if events for the gesture point are captured to the tracked element.
* @property {Boolean} isPrimary
* True if the gesture point is a master pointer amongst the set of active pointers for each pointer type. True for mouse and primary (first) touch/pen pointers.
* @property {Boolean} insideElementPressed
* True if button pressed or contact point initiated inside the screen area of the tracked element.
* @property {Boolean} insideElement
* True if pointer or contact point is currently inside the bounds of the tracked element.
* @property {Number} speed
* Current computed speed, in pixels per second.
* @property {Number} direction
* Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0.
* @property {OpenSeadragon.Point} contactPos
* The initial pointer contact position, relative to the page including any scrolling. Only valid if the pointer has contact (pressed, touch contact, pen contact).
* @property {Number} contactTime
* The initial pointer contact time, in milliseconds. Only valid if the pointer has contact (pressed, touch contact, pen contact).
* @property {OpenSeadragon.Point} lastPos
* The last pointer position, relative to the page including any scrolling.
* @property {Number} lastTime
* The last pointer contact time, in milliseconds.
* @property {OpenSeadragon.Point} currentPos
* The current pointer position, relative to the page including any scrolling.
* @property {Number} currentTime
* The current pointer contact time, in milliseconds.
*/
/**
* @class GesturePointList
* @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type.
* Active pointers are any pointer being tracked for this element which are in the hit-test area
* of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
* @memberof OpenSeadragon.MouseTracker
* @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
*/
$.MouseTracker.GesturePointList = function ( type ) {
this._gPoints = [];
/**
* The pointer device type: "mouse", "touch", "pen", etc.
* @member {String} type
* @memberof OpenSeadragon.MouseTracker.GesturePointList#
*/
this.type = type;
/**
* Current buttons pressed for the device.
* Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser.
* @member {Number} buttons
* @memberof OpenSeadragon.MouseTracker.GesturePointList#
*/
this.buttons = 0;
/**
* Current number of contact points (touch points, mouse down, etc.) for the device.
* @member {Number} contacts
* @memberof OpenSeadragon.MouseTracker.GesturePointList#
*/
this.contacts = 0;
/**
* Current number of clicks for the device. Used for multiple click gesture tracking.
* @member {Number} clicks
* @memberof OpenSeadragon.MouseTracker.GesturePointList#
*/
this.clicks = 0;
/**
* Current number of captured pointers for the device.
* @member {Number} captureCount
* @memberof OpenSeadragon.MouseTracker.GesturePointList#
*/
this.captureCount = 0;
};
/** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */
$.MouseTracker.GesturePointList.prototype = {
/**
* @function
* @returns {Number} Number of gesture points in the list.
*/
getLength: function () {
return this._gPoints.length;
},
/**
* @function
* @returns {Array.<OpenSeadragon.MouseTracker.GesturePoint>} The list of gesture points in the list as an array (read-only).
*/
asArray: function () {
return this._gPoints;
},
/**
* @function
* @param {OpenSeadragon.MouseTracker.GesturePoint} gesturePoint - A gesture point to add to the list.
* @returns {Number} Number of gesture points in the list.
*/
add: function ( gp ) {
return this._gPoints.push( gp );
},
/**
* @function
* @param {Number} id - The id of the gesture point to remove from the list.
* @returns {Number} Number of gesture points in the list.
*/
removeById: function ( id ) {
var i,
len = this._gPoints.length;
for ( i = 0; i < len; i++ ) {
if ( this._gPoints[ i ].id === id ) {
this._gPoints.splice( i, 1 );
break;
}
}
return this._gPoints.length;
},
/**
* @function
* @param {Number} index - The index of the gesture point to retrieve from the list.
* @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point at the given index, or null if not found.
*/
getByIndex: function ( index ) {
if ( index < this._gPoints.length) {
return this._gPoints[ index ];
}
return null;
},
/**
* @function
* @param {Number} id - The id of the gesture point to retrieve from the list.
* @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point with the given id, or null if not found.
*/
getById: function ( id ) {
var i,
len = this._gPoints.length;
for ( i = 0; i < len; i++ ) {
if ( this._gPoints[ i ].id === id ) {
return this._gPoints[ i ];
}
}
return null;
},
/**
* @function
* @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The primary gesture point in the list, or null if not found.
*/
getPrimary: function ( id ) {
var i,
len = this._gPoints.length;
for ( i = 0; i < len; i++ ) {
if ( this._gPoints[ i ].isPrimary ) {
return this._gPoints[ i ];
}
}
return null;
},
/**
* Increment this pointer list's contact count.
* It will evaluate whether this pointer type is allowed to have multiple contacts.
* @function
*/
addContact: function() {
++this.contacts;
if (this.contacts > 1 && (this.type === "mouse" || this.type === "pen")) {
$.console.warn('GesturePointList.addContact() Implausible contacts value');
this.contacts = 1;
}
},
/**
* Decrement this pointer list's contact count.
* It will make sure the count does not go below 0.
* @function
*/
removeContact: function() {
--this.contacts;
if (this.contacts < 0) {
this.contacts = 0;
}
}
};
///////////////////////////////////////////////////////////////////////////////
// Utility functions
///////////////////////////////////////////////////////////////////////////////
/**
* Removes all tracked pointers.
* @private
* @inner
*/
function clearTrackedPointers( tracker ) {
var delegate = THIS[ tracker.hash ],
i, j,
pointsList,
gPoints,
gPointsToRemove,
pointerListCount = delegate.activePointersLists.length;
for ( i = 0; i < pointerListCount; i++ ) {
pointsList = delegate.activePointersLists[ i ];
if ( pointsList.getLength() > 0 ) {
// Make an array containing references to the gPoints in the pointer list
// (because calls to stopTrackingPointer() are going to modify the pointer list)
gPointsToRemove = [];
gPoints = pointsList.asArray();
for ( j = 0; j < gPoints.length; j++ ) {
gPointsToRemove.push( gPoints[ j ] );
}
// Release and remove all gPoints from the pointer list
for ( j = 0; j < gPointsToRemove.length; j++ ) {
stopTrackingPointer( tracker, pointsList, gPointsToRemove[ j ] );
}
}
}
for ( i = 0; i < pointerListCount; i++ ) {
delegate.activePointersLists.pop();
}
delegate.sentDragEvent = false;
}
/**
* Starts tracking pointer events on the tracked element.
* @private
* @inner
*/
function startTracking( tracker ) {
var delegate = THIS[ tracker.hash ],
event,
i;
if ( !delegate.tracking ) {
for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
event = $.MouseTracker.subscribeEvents[ i ];
$.addEvent(
tracker.element,
event,
delegate[ event ],
event === $.MouseTracker.wheelEventName ? { passive: false, capture: false } : false
);
}
clearTrackedPointers( tracker );
delegate.tracking = true;
}
}
/**
* Stops tracking pointer events on the tracked element.
* @private
* @inner
*/
function stopTracking( tracker ) {
var delegate = THIS[ tracker.hash ],
event,
i;
if ( delegate.tracking ) {
for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) {
event = $.MouseTracker.subscribeEvents[ i ];
$.removeEvent(
tracker.element,
event,
delegate[ event ],
false
);
}
clearTrackedPointers( tracker );
delegate.tracking = false;
}
}
/**
* @private
* @inner
*/
function getCaptureEventParams( tracker, pointerType ) {
var delegate = THIS[ tracker.hash ];
if ( pointerType === 'pointerevent' ) {
return {
upName: 'pointerup',
upHandler: delegate.pointerupcaptured,
moveName: 'pointermove',
moveHandler: delegate.pointermovecaptured
};
} else if ( pointerType === 'mouse' ) {
return {
upName: 'pointerup',
upHandler: delegate.pointerupcaptured,
moveName: 'pointermove',
moveHandler: delegate.pointermovecaptured
};
} else if ( pointerType === 'touch' ) {
return {
upName: 'touchend',
upHandler: delegate.touchendcaptured,
moveName: 'touchmove',
moveHandler: delegate.touchmovecaptured
};
} else {
throw new Error( "MouseTracker.getCaptureEventParams: Unknown pointer type." );
}
}
/**
* Begin capturing pointer events to the tracked element.
* @private
* @inner
*/
function capturePointer( tracker, gPoint ) {
var eventParams;
if ( $.MouseTracker.havePointerCapture ) {
if ( $.MouseTracker.havePointerEvents ) {
// Can throw NotFoundError (InvalidPointerId Firefox < 82)
// (should never happen so we'll log a warning)
try {
tracker.element.setPointerCapture( gPoint.id );
//$.console.log('element.setPointerCapture() called');
} catch ( e ) {
$.console.warn('setPointerCapture() called on invalid pointer ID');
return;
}
} else {
tracker.element.setCapture( true );
//$.console.log('element.setCapture() called');
}
} else {
// Emulate mouse capture by hanging listeners on the document object.
// (Note we listen on the capture phase so the captured handlers will get called first)
// eslint-disable-next-line no-use-before-define
//$.console.log('Emulated mouse capture set');
eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type );
// https://github.com/openseadragon/openseadragon/pull/790
if (isInIframe && canAccessEvents(window.top)) {
$.addEvent(
window.top,
eventParams.upName,
eventParams.upHandler,
true
);
}
$.addEvent(
$.MouseTracker.captureElement,
eventParams.upName,
eventParams.upHandler,
true
);
$.addEvent(
$.MouseTracker.captureElement,
eventParams.moveName,
eventParams.moveHandler,
true
);
}
updatePointerCaptured( tracker, gPoint, true );
}
/**
* Stop capturing pointer events to the tracked element.
* @private
* @inner
*/
function releasePointer( tracker, gPoint ) {
var eventParams;
var pointsList;
var cachedGPoint;
if ( $.MouseTracker.havePointerCapture ) {
if ( $.MouseTracker.havePointerEvents ) {
pointsList = tracker.getActivePointersListByType( gPoint.type );
cachedGPoint = pointsList.getById( gPoint.id );
if ( !cachedGPoint || !cachedGPoint.captured ) {
return;
}
// Can throw NotFoundError (InvalidPointerId Firefox < 82)
// (should never happen, but it does on Firefox 79 touch so we won't log a warning)
try {
tracker.element.releasePointerCapture( gPoint.id );
//$.console.log('element.releasePointerCapture() called');
} catch ( e ) {
//$.console.warn('releasePointerCapture() called on invalid pointer ID');
}
} else {
tracker.element.releaseCapture();
//$.console.log('element.releaseCapture() called');
}
} else {
// Emulate mouse capture by hanging listeners on the document object.
// (Note we listen on the capture phase so the captured handlers will get called first)
//$.console.log('Emulated mouse capture release');
eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type );
// https://github.com/openseadragon/openseadragon/pull/790
if (isInIframe && canAccessEvents(window.top)) {
$.removeEvent(
window.top,
eventParams.upName,
eventParams.upHandler,
true
);
}
$.removeEvent(
$.MouseTracker.captureElement,
eventParams.moveName,
eventParams.moveHandler,
true
);
$.removeEvent(
$.MouseTracker.captureElement,
eventParams.upName,
eventParams.upHandler,
true
);
}
updatePointerCaptured( tracker, gPoint, false );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
* @private
* @inner
*/
function getPointerId( event ) {
return ( $.MouseTracker.havePointerEvents ) ? event.pointerId : $.MouseTracker.mousePointerId;
}
/**
* Gets a W3C Pointer Events model compatible pointer type string from a DOM pointer event.
*
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
* @private
* @inner
*/
function getPointerType( event ) {
return $.MouseTracker.havePointerEvents && event.pointerType ? event.pointerType : 'mouse';
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
* @private
* @inner
*/
function getIsPrimary( event ) {
return ( $.MouseTracker.havePointerEvents ) ? event.isPrimary : true;
}
/**
* @private
* @inner
*/
function getMouseAbsolute( event ) {
return $.getMousePosition( event );
}
/**
* @private
* @inner
*/
function getMouseRelative( event, element ) {
return getPointRelativeToAbsolute( getMouseAbsolute( event ), element );
}
/**
* @private
* @inner
*/
function getPointRelativeToAbsolute( point, element ) {
var offset = $.getElementOffset( element );
return point.minus( offset );
}
/**
* @private
* @inner
*/
function getCenterPoint( point1, point2 ) {
return new $.Point( ( point1.x + point2.x ) / 2, ( point1.y + point2.y ) / 2 );
}
///////////////////////////////////////////////////////////////////////////////
// Device-specific DOM event handlers
///////////////////////////////////////////////////////////////////////////////
/**
* @private
* @inner
*/
function onClick( tracker, event ) {
//$.console.log('click ' + (tracker.userData ? tracker.userData.toString() : ''));
var eventInfo = {
originalEvent: event,
eventType: 'click',
pointerType: 'mouse',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onDblClick( tracker, event ) {
//$.console.log('dblclick ' + (tracker.userData ? tracker.userData.toString() : ''));
var eventInfo = {
originalEvent: event,
eventType: 'dblclick',
pointerType: 'mouse',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onKeyDown( tracker, event ) {
//$.console.log( "keydown %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
var eventArgs = null;
var eventInfo = {
originalEvent: event,
eventType: 'keydown',
pointerType: '',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( tracker.keyDownHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventArgs = {
eventSource: tracker,
keyCode: event.keyCode ? event.keyCode : event.charCode,
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey,
meta: event.metaKey,
originalEvent: event,
preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
userData: tracker.userData
};
tracker.keyDownHandler( eventArgs );
}
if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onKeyUp( tracker, event ) {
//$.console.log( "keyup %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
var eventArgs = null;
var eventInfo = {
originalEvent: event,
eventType: 'keyup',
pointerType: '',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( tracker.keyUpHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventArgs = {
eventSource: tracker,
keyCode: event.keyCode ? event.keyCode : event.charCode,
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey,
meta: event.metaKey,
originalEvent: event,
preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
userData: tracker.userData
};
tracker.keyUpHandler( eventArgs );
}
if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onKeyPress( tracker, event ) {
//$.console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey );
var eventArgs = null;
var eventInfo = {
originalEvent: event,
eventType: 'keypress',
pointerType: '',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( tracker.keyHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventArgs = {
eventSource: tracker,
keyCode: event.keyCode ? event.keyCode : event.charCode,
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey,
meta: event.metaKey,
originalEvent: event,
preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
userData: tracker.userData
};
tracker.keyHandler( eventArgs );
}
if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onFocus( tracker, event ) {
//$.console.log('focus ' + (tracker.userData ? tracker.userData.toString() : ''));
// focus doesn't bubble and is not cancelable, but we call
// preProcessEvent() so it's dispatched to preProcessEventHandler
// if necessary
var eventInfo = {
originalEvent: event,
eventType: 'focus',
pointerType: '',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( tracker.focusHandler && !eventInfo.preventGesture ) {
tracker.focusHandler(
{
eventSource: tracker,
originalEvent: event,
userData: tracker.userData
}
);
}
}
/**
* @private
* @inner
*/
function onBlur( tracker, event ) {
//$.console.log('blur ' + (tracker.userData ? tracker.userData.toString() : ''));
// blur doesn't bubble and is not cancelable, but we call
// preProcessEvent() so it's dispatched to preProcessEventHandler
// if necessary
var eventInfo = {
originalEvent: event,
eventType: 'blur',
pointerType: '',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( tracker.blurHandler && !eventInfo.preventGesture ) {
tracker.blurHandler(
{
eventSource: tracker,
originalEvent: event,
userData: tracker.userData
}
);
}
}
/**
* @private
* @inner
*/
function onContextMenu( tracker, event ) {
//$.console.log('contextmenu ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var eventArgs = null;
var eventInfo = {
originalEvent: event,
eventType: 'contextmenu',
pointerType: 'mouse',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
// ContextMenu
if ( tracker.contextMenuHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventArgs = {
eventSource: tracker,
position: getPointRelativeToAbsolute( getMouseAbsolute( event ), tracker.element ),
originalEvent: eventInfo.originalEvent,
preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
userData: tracker.userData
};
tracker.contextMenuHandler( eventArgs );
}
if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* Handler for 'wheel' events
*
* @private
* @inner
*/
function onWheel( tracker, event ) {
handleWheelEvent( tracker, event, event );
}
/**
* Handler for 'mousewheel', 'DOMMouseScroll', and 'MozMousePixelScroll' events
*
* @private
* @inner
*/
function onMouseWheel( tracker, event ) {
// Simulate a 'wheel' event
var simulatedEvent = {
target: event.target || event.srcElement,
type: "wheel",
shiftKey: event.shiftKey || false,
clientX: event.clientX,
clientY: event.clientY,
pageX: event.pageX ? event.pageX : event.clientX,
pageY: event.pageY ? event.pageY : event.clientY,
deltaMode: event.type === "MozMousePixelScroll" ? 0 : 1, // 0=pixel, 1=line, 2=page
deltaX: 0,
deltaZ: 0
};
// Calculate deltaY
if ( $.MouseTracker.wheelEventName === "mousewheel" ) {
simulatedEvent.deltaY = -event.wheelDelta / $.DEFAULT_SETTINGS.pixelsPerWheelLine;
} else {
simulatedEvent.deltaY = event.detail;
}
handleWheelEvent( tracker, simulatedEvent, event );
}
/**
* Handles 'wheel' events.
* The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()).
*
* @private
* @inner
*/
function handleWheelEvent( tracker, event, originalEvent ) {
var nDelta = 0,
eventInfo;
var eventArgs = null;
// The nDelta variable is gated to provide smooth z-index scrolling
// since the mouse wheel allows for substantial deltas meant for rapid
// y-index scrolling.
// event.deltaMode: 0=pixel, 1=line, 2=page
// TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached
nDelta = event.deltaY ? (event.deltaY < 0 ? 1 : -1) : 0;
eventInfo = {
originalEvent: event,
eventType: 'wheel',
pointerType: 'mouse',
isEmulated: event !== originalEvent
};
preProcessEvent( tracker, eventInfo );
if ( tracker.scrollHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventArgs = {
eventSource: tracker,
pointerType: 'mouse',
position: getMouseRelative( event, tracker.element ),
scroll: nDelta,
shift: event.shiftKey,
isTouchEvent: false,
originalEvent: originalEvent,
preventDefault: eventInfo.preventDefault || eventInfo.defaultPrevented,
userData: tracker.userData
};
tracker.scrollHandler( eventArgs );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( originalEvent );
}
if ( ( eventArgs && eventArgs.preventDefault ) || ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) ) {
$.cancelEvent( originalEvent );
}
}
/**
* TODO Never actually seen this event fired, and documentation is tough to find
* @private
* @inner
*/
function onLoseCapture( tracker, event ) {
//$.console.log('losecapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var gPoint = {
id: $.MouseTracker.mousePointerId,
type: 'mouse'
};
var eventInfo = {
originalEvent: event,
eventType: 'lostpointercapture',
pointerType: 'mouse',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( event.target === tracker.element ) {
updatePointerCaptured( tracker, gPoint, false );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onTouchStart( tracker, event ) {
var time,
i,
touchCount = event.changedTouches.length,
gPoint,
pointsList = tracker.getActivePointersListByType( 'touch' );
time = $.now();
//$.console.log('touchstart ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
if ( pointsList.getLength() > event.touches.length - touchCount ) {
$.console.warn('Tracked touch contact count doesn\'t match event.touches.length');
}
var eventInfo = {
originalEvent: event,
eventType: 'pointerdown',
pointerType: 'touch',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
for ( i = 0; i < touchCount; i++ ) {
gPoint = {
id: event.changedTouches[ i ].identifier,
type: 'touch',
// Simulate isPrimary
isPrimary: pointsList.getLength() === 0,
currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
currentTime: time
};
// simulate touchenter on our tracked element
updatePointerEnter( tracker, eventInfo, gPoint );
updatePointerDown( tracker, eventInfo, gPoint, 0 );
updatePointerCaptured( tracker, gPoint, true );
}
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onTouchEnd( tracker, event ) {
var time,
i,
touchCount = event.changedTouches.length,
gPoint;
time = $.now();
//$.console.log('touchend ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var eventInfo = {
originalEvent: event,
eventType: 'pointerup',
pointerType: 'touch',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
for ( i = 0; i < touchCount; i++ ) {
gPoint = {
id: event.changedTouches[ i ].identifier,
type: 'touch',
currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
currentTime: time
};
updatePointerUp( tracker, eventInfo, gPoint, 0 );
updatePointerCaptured( tracker, gPoint, false );
// simulate touchleave on our tracked element
updatePointerLeave( tracker, eventInfo, gPoint );
}
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onTouchMove( tracker, event ) {
var time,
i,
touchCount = event.changedTouches.length,
gPoint;
time = $.now();
var eventInfo = {
originalEvent: event,
eventType: 'pointermove',
pointerType: 'touch',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
for ( i = 0; i < touchCount; i++ ) {
gPoint = {
id: event.changedTouches[ i ].identifier,
type: 'touch',
currentPos: getMouseAbsolute( event.changedTouches[ i ] ),
currentTime: time
};
updatePointerMove( tracker, eventInfo, gPoint );
}
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onTouchCancel( tracker, event ) {
var touchCount = event.changedTouches.length,
i,
gPoint;
//$.console.log('touchcancel ' + (tracker.userData ? tracker.userData.toString() : ''));
var eventInfo = {
originalEvent: event,
eventType: 'pointercancel',
pointerType: 'touch',
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
for ( i = 0; i < touchCount; i++ ) {
gPoint = {
id: event.changedTouches[ i ].identifier,
type: 'touch'
};
//TODO need to only do this if our element is target?
updatePointerCancel( tracker, eventInfo, gPoint );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onGestureStart( tracker, event ) {
if ( !$.eventIsCanceled( event ) ) {
event.preventDefault();
}
return false;
}
/**
* @private
* @inner
*/
function onGestureChange( tracker, event ) {
if ( !$.eventIsCanceled( event ) ) {
event.preventDefault();
}
return false;
}
/**
* @private
* @inner
*/
function onGotPointerCapture( tracker, event ) {
//$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var eventInfo = {
originalEvent: event,
eventType: 'gotpointercapture',
pointerType: getPointerType( event ),
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( event.target === tracker.element ) {
//$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : ''));
updatePointerCaptured( tracker, {
id: event.pointerId,
type: getPointerType( event )
}, true );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onLostPointerCapture( tracker, event ) {
//$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var eventInfo = {
originalEvent: event,
eventType: 'lostpointercapture',
pointerType: getPointerType( event ),
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
if ( event.target === tracker.element ) {
//$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : ''));
updatePointerCaptured( tracker, {
id: event.pointerId,
type: getPointerType( event )
}, false );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerEnter( tracker, event ) {
//$.console.log('pointerenter ' + (tracker.userData ? tracker.userData.toString() : ''));
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
// pointerenter doesn't bubble and is not cancelable, but we call
// preProcessEvent() so it's dispatched to preProcessEventHandler
// if necessary
var eventInfo = {
originalEvent: event,
eventType: 'pointerenter',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerEnter( tracker, eventInfo, gPoint );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerLeave( tracker, event ) {
//$.console.log('pointerleave ' + (tracker.userData ? tracker.userData.toString() : ''));
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
// pointerleave doesn't bubble and is not cancelable, but we call
// preProcessEvent() so it's dispatched to preProcessEventHandler
// if necessary
var eventInfo = {
originalEvent: event,
eventType: 'pointerleave',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerLeave( tracker, eventInfo, gPoint );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerOver( tracker, event ) {
//$.console.log('pointerover ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
var eventInfo = {
originalEvent: event,
eventType: 'pointerover',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerOver( tracker, eventInfo, gPoint );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerOut( tracker, event ) {
//$.console.log('pointerout ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
var eventInfo = {
originalEvent: event,
eventType: 'pointerout',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerOut( tracker, eventInfo, gPoint );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerDown( tracker, event ) {
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
// Most browsers implicitly capture touch pointer events
// Note no IE versions (unsupported) have element.hasPointerCapture() so
// no implicit pointer capture possible
// var implicitlyCaptured = ($.MouseTracker.havePointerEvents &&
// event.target.hasPointerCapture &&
// $.Browser.vendor !== $.BROWSERS.IE) ?
// event.target.hasPointerCapture(event.pointerId) : false;
var implicitlyCaptured = $.MouseTracker.havePointerEvents &&
gPoint.type === 'touch';
//$.console.log('pointerdown ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var eventInfo = {
originalEvent: event,
eventType: 'pointerdown',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerDown( tracker, eventInfo, gPoint, event.button );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
if ( eventInfo.shouldCapture ) {
if ( implicitlyCaptured ) {
updatePointerCaptured( tracker, gPoint, true );
} else {
capturePointer( tracker, gPoint );
}
}
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerUp( tracker, event ) {
handlePointerUp( tracker, event );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* This handler is attached to the window object (on the capture phase) to emulate mouse capture.
* onPointerUp is still attached to the tracked element, so stop propagation to avoid processing twice.
*
* @private
* @inner
*/
function onPointerUpCaptured( tracker, event ) {
var pointsList = tracker.getActivePointersListByType( getPointerType( event ) );
if ( pointsList.getById( event.pointerId ) ) {
handlePointerUp( tracker, event );
}
$.stopEvent( event );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function handlePointerUp( tracker, event ) {
var gPoint;
//$.console.log('pointerup ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
var eventInfo = {
originalEvent: event,
eventType: 'pointerup',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerUp( tracker, eventInfo, gPoint, event.button );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
// Per spec, pointerup events are supposed to release capture. Not all browser
// versions have adhered to the spec, and there's no harm in releasing
// explicitly
if ( eventInfo.shouldReleaseCapture ) {
if ( event.target === tracker.element ) {
releasePointer( tracker, gPoint );
} else {
updatePointerCaptured( tracker, gPoint, false );
}
}
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function onPointerMove( tracker, event ) {
handlePointerMove( tracker, event );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* This handler is attached to the window object (on the capture phase) to emulate mouse capture.
* onPointerMove is still attached to the tracked element, so stop propagation to avoid processing twice.
*
* @private
* @inner
*/
function onPointerMoveCaptured( tracker, event ) {
var pointsList = tracker.getActivePointersListByType( getPointerType( event ) );
if ( pointsList.getById( event.pointerId ) ) {
handlePointerMove( tracker, event );
}
$.stopEvent( event );
}
/**
* Note: Called for both pointer events and legacy mouse events
* ($.MouseTracker.havePointerEvents determines which)
*
* @private
* @inner
*/
function handlePointerMove( tracker, event ) {
// Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
var gPoint = {
id: getPointerId( event ),
type: getPointerType( event ),
isPrimary: getIsPrimary( event ),
currentPos: getMouseAbsolute( event ),
currentTime: $.now()
};
var eventInfo = {
originalEvent: event,
eventType: 'pointermove',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
updatePointerMove( tracker, eventInfo, gPoint );
if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) {
$.cancelEvent( event );
}
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
/**
* @private
* @inner
*/
function onPointerCancel( tracker, event ) {
//$.console.log('pointercancel ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : ''));
var gPoint = {
id: event.pointerId,
type: getPointerType( event )
};
var eventInfo = {
originalEvent: event,
eventType: 'pointercancel',
pointerType: gPoint.type,
isEmulated: false
};
preProcessEvent( tracker, eventInfo );
//TODO need to only do this if our element is target?
updatePointerCancel( tracker, eventInfo, gPoint );
if ( eventInfo.stopPropagation ) {
$.stopEvent( event );
}
}
///////////////////////////////////////////////////////////////////////////////
// Device-agnostic DOM event handlers
///////////////////////////////////////////////////////////////////////////////
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
* The GesturePointList to track the pointer in.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point to track.
* @returns {Number} Number of gesture points in pointsList.
*/
function startTrackingPointer( pointsList, gPoint ) {
//$.console.log('startTrackingPointer *** ' + pointsList.type + ' ' + gPoint.id.toString());
gPoint.speed = 0;
gPoint.direction = 0;
gPoint.contactPos = gPoint.currentPos;
gPoint.contactTime = gPoint.currentTime;
gPoint.lastPos = gPoint.currentPos;
gPoint.lastTime = gPoint.currentTime;
return pointsList.add( gPoint );
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList
* The GesturePointList to stop tracking the pointer on.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point to stop tracking.
* @returns {Number} Number of gesture points in pointsList.
*/
function stopTrackingPointer( tracker, pointsList, gPoint ) {
//$.console.log('stopTrackingPointer *** ' + pointsList.type + ' ' + gPoint.id.toString());
var listLength;
var trackedGPoint = pointsList.getById( gPoint.id );
if ( trackedGPoint ) {
if ( trackedGPoint.captured ) {
$.console.warn('stopTrackingPointer() called on captured pointer');
releasePointer( tracker, trackedGPoint );
}
// If child element relinquishes capture to a parent we may get here
// from a pointerleave event while a pointerup event will never be received.
// In that case, we'll clean up the contact count
pointsList.removeContact();
listLength = pointsList.removeById( gPoint.id );
} else {
listLength = pointsList.getLength();
}
return listLength;
}
/**
* @function
* @private
* @inner
*/
function getEventProcessDefaults( tracker, eventInfo ) {
switch ( eventInfo.eventType ) {
case 'pointermove':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = false;
eventInfo.preventGesture = !tracker.hasGestureHandlers;
eventInfo.stopPropagation = false;
break;
case 'pointerover':
case 'pointerout':
case 'contextmenu':
case 'keydown':
case 'keyup':
case 'keypress':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = false; // onContextMenu(), onKeyDown(), onKeyUp(), onKeyPress() may set true
eventInfo.preventGesture = false;
eventInfo.stopPropagation = false;
break;
case 'pointerdown':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = false; // updatePointerDown() may set true (tracker.hasGestureHandlers)
eventInfo.preventGesture = !tracker.hasGestureHandlers;
eventInfo.stopPropagation = false;
break;
case 'pointerup':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = false;
eventInfo.preventGesture = !tracker.hasGestureHandlers;
eventInfo.stopPropagation = false;
break;
case 'wheel':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = false; // handleWheelEvent() may set true
eventInfo.preventGesture = !tracker.hasScrollHandler;
eventInfo.stopPropagation = false;
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
eventInfo.isStoppable = true;
eventInfo.isCancelable = false;
eventInfo.preventDefault = false;
eventInfo.preventGesture = false;
eventInfo.stopPropagation = false;
break;
case 'click':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = !!tracker.clickHandler;
eventInfo.preventGesture = false;
eventInfo.stopPropagation = false;
break;
case 'dblclick':
eventInfo.isStoppable = true;
eventInfo.isCancelable = true;
eventInfo.preventDefault = !!tracker.dblClickHandler;
eventInfo.preventGesture = false;
eventInfo.stopPropagation = false;
break;
case 'focus':
case 'blur':
case 'pointerenter':
case 'pointerleave':
default:
eventInfo.isStoppable = false;
eventInfo.isCancelable = false;
eventInfo.preventDefault = false;
eventInfo.preventGesture = false;
eventInfo.stopPropagation = false;
break;
}
}
/**
* Sets up for and calls preProcessEventHandler. Call with the following parameters -
* this function will fill in the rest of the preProcessEventHandler event object
* properties
*
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* @param {Object} eventInfo.originalEvent
* @param {String} eventInfo.eventType
* @param {String} eventInfo.pointerType
* @param {Boolean} eventInfo.isEmulated
*/
function preProcessEvent( tracker, eventInfo ) {
eventInfo.eventSource = tracker;
eventInfo.eventPhase = eventInfo.originalEvent ?
((typeof eventInfo.originalEvent.eventPhase !== 'undefined') ?
eventInfo.originalEvent.eventPhase : 0) : 0;
eventInfo.defaultPrevented = $.eventIsCanceled( eventInfo.originalEvent );
eventInfo.shouldCapture = false;
eventInfo.shouldReleaseCapture = false;
eventInfo.userData = tracker.userData;
getEventProcessDefaults( tracker, eventInfo );
if ( tracker.preProcessEventHandler ) {
tracker.preProcessEventHandler( eventInfo );
}
}
/**
* Sets or resets the captured property on the tracked pointer matching the passed gPoint's id/type
*
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {Object} gPoint
* An object with id and type properties describing the pointer to update.
* @param {Boolean} isCaptured
* Value to set the captured property to.
*/
function updatePointerCaptured( tracker, gPoint, isCaptured ) {
var pointsList = tracker.getActivePointersListByType( gPoint.type );
var updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
if ( isCaptured && !updateGPoint.captured ) {
updateGPoint.captured = true;
pointsList.captureCount++;
} else if ( !isCaptured && updateGPoint.captured ) {
updateGPoint.captured = false;
pointsList.captureCount--;
if ( pointsList.captureCount < 0 ) {
pointsList.captureCount = 0;
$.console.warn('updatePointerCaptured() - pointsList.captureCount went negative');
}
}
} else {
$.console.warn('updatePointerCaptured() called on untracked pointer');
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point associated with the event.
*/
function updatePointerEnter( tracker, eventInfo, gPoint ) {
var pointsList = tracker.getActivePointersListByType( gPoint.type ),
updateGPoint;
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
// Already tracking the pointer...update it
updateGPoint.insideElement = true;
updateGPoint.lastPos = updateGPoint.currentPos;
updateGPoint.lastTime = updateGPoint.currentTime;
updateGPoint.currentPos = gPoint.currentPos;
updateGPoint.currentTime = gPoint.currentTime;
gPoint = updateGPoint;
} else {
// Initialize for tracking and add to the tracking list
gPoint.captured = false; // Handled by updatePointerCaptured()
gPoint.insideElementPressed = false;
gPoint.insideElement = true;
startTrackingPointer( pointsList, gPoint );
}
// Enter (doesn't bubble and not cancelable)
if ( tracker.enterHandler ) {
tracker.enterHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
pointers: tracker.getActivePointerCount(),
insideElementPressed: gPoint.insideElementPressed,
buttonDownAny: pointsList.buttons !== 0,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point associated with the event.
*/
function updatePointerLeave( tracker, eventInfo, gPoint ) {
var pointsList = tracker.getActivePointersListByType(gPoint.type),
updateGPoint,
dispatchEventObj;
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
// Already tracking the pointer. If captured then update it, else stop tracking it
if ( updateGPoint.captured ) {
updateGPoint.insideElement = false;
updateGPoint.lastPos = updateGPoint.currentPos;
updateGPoint.lastTime = updateGPoint.currentTime;
updateGPoint.currentPos = gPoint.currentPos;
updateGPoint.currentTime = gPoint.currentTime;
} else {
stopTrackingPointer( tracker, pointsList, updateGPoint );
}
gPoint = updateGPoint;
} else {
gPoint.captured = false; // Handled by updatePointerCaptured()
gPoint.insideElementPressed = false;
}
// Leave (doesn't bubble and not cancelable)
// Note: exitHandler is deprecated (v2.5.0), replaced by leaveHandler
if ( tracker.leaveHandler || tracker.exitHandler ) {
dispatchEventObj = {
eventSource: tracker,
pointerType: gPoint.type,
// GitHub PR: https://github.com/openseadragon/openseadragon/pull/1754 (gPoint.currentPos && )
position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
pointers: tracker.getActivePointerCount(),
insideElementPressed: gPoint.insideElementPressed,
buttonDownAny: pointsList.buttons !== 0,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
};
if ( tracker.leaveHandler ) {
tracker.leaveHandler( dispatchEventObj );
}
// Deprecated
if ( tracker.exitHandler ) {
tracker.exitHandler( dispatchEventObj );
}
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point associated with the event.
*/
function updatePointerOver( tracker, eventInfo, gPoint ) {
var pointsList,
updateGPoint;
pointsList = tracker.getActivePointersListByType( gPoint.type );
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
gPoint = updateGPoint;
} else {
gPoint.captured = false;
gPoint.insideElementPressed = false;
//gPoint.insideElement = true; // Tracked by updatePointerEnter
}
if ( tracker.overHandler ) {
// Over
tracker.overHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
pointers: tracker.getActivePointerCount(),
insideElementPressed: gPoint.insideElementPressed,
buttonDownAny: pointsList.buttons !== 0,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point associated with the event.
*/
function updatePointerOut( tracker, eventInfo, gPoint ) {
var pointsList,
updateGPoint;
pointsList = tracker.getActivePointersListByType(gPoint.type);
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
gPoint = updateGPoint;
} else {
gPoint.captured = false;
gPoint.insideElementPressed = false;
//gPoint.insideElement = true; // Tracked by updatePointerEnter
}
if ( tracker.outHandler ) {
// Out
tracker.outHandler( {
eventSource: tracker,
pointerType: gPoint.type,
position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
pointers: tracker.getActivePointerCount(),
insideElementPressed: gPoint.insideElementPressed,
buttonDownAny: pointsList.buttons !== 0,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
} );
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture point associated with the event.
* @param {Number} buttonChanged
* The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
*/
function updatePointerDown( tracker, eventInfo, gPoint, buttonChanged ) {
var delegate = THIS[ tracker.hash ],
pointsList = tracker.getActivePointersListByType( gPoint.type ),
updateGPoint;
if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
pointsList.buttons = eventInfo.originalEvent.buttons;
} else {
if ( buttonChanged === 0 ) {
// Primary
pointsList.buttons |= 1;
} else if ( buttonChanged === 1 ) {
// Aux
pointsList.buttons |= 4;
} else if ( buttonChanged === 2 ) {
// Secondary
pointsList.buttons |= 2;
} else if ( buttonChanged === 3 ) {
// X1 (Back)
pointsList.buttons |= 8;
} else if ( buttonChanged === 4 ) {
// X2 (Forward)
pointsList.buttons |= 16;
} else if ( buttonChanged === 5 ) {
// Pen Eraser
pointsList.buttons |= 32;
}
}
// Only capture and track primary button, pen, and touch contacts
if ( buttonChanged !== 0 ) {
eventInfo.shouldCapture = false;
eventInfo.shouldReleaseCapture = false;
// Aux Press
if ( tracker.nonPrimaryPressHandler &&
!eventInfo.preventGesture &&
!eventInfo.defaultPrevented ) {
eventInfo.preventDefault = true;
tracker.nonPrimaryPressHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
button: buttonChanged,
buttons: pointsList.buttons,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
return;
}
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
// Already tracking the pointer...update it
//updateGPoint.captured = true; // Handled by updatePointerCaptured()
updateGPoint.insideElementPressed = true;
updateGPoint.insideElement = true;
updateGPoint.originalTarget = eventInfo.originalEvent.target;
updateGPoint.contactPos = gPoint.currentPos;
updateGPoint.contactTime = gPoint.currentTime;
updateGPoint.lastPos = updateGPoint.currentPos;
updateGPoint.lastTime = updateGPoint.currentTime;
updateGPoint.currentPos = gPoint.currentPos;
updateGPoint.currentTime = gPoint.currentTime;
gPoint = updateGPoint;
} else {
// Initialize for tracking and add to the tracking list (no pointerenter event occurred before this)
// NOTE: pointerdown event on untracked pointer
gPoint.captured = false; // Handled by updatePointerCaptured()
gPoint.insideElementPressed = true;
gPoint.insideElement = true;
gPoint.originalTarget = eventInfo.originalEvent.target;
startTrackingPointer( pointsList, gPoint );
}
pointsList.addContact();
//$.console.log('contacts++ ', pointsList.contacts);
if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
eventInfo.shouldCapture = true;
eventInfo.shouldReleaseCapture = false;
eventInfo.preventDefault = true;
if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
$.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, gPoint );
}
if ( pointsList.contacts === 1 ) {
// Press
if ( tracker.pressHandler && !eventInfo.preventGesture ) {
tracker.pressHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute( gPoint.contactPos, tracker.element ),
buttons: pointsList.buttons,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
} else if ( pointsList.contacts === 2 ) {
if ( tracker.pinchHandler && gPoint.type === 'touch' ) {
// Initialize for pinch
delegate.pinchGPoints = pointsList.asArray();
delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
}
}
} else {
eventInfo.shouldCapture = false;
eventInfo.shouldReleaseCapture = false;
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture points associated with the event.
* @param {Number} buttonChanged
* The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
*/
function updatePointerUp( tracker, eventInfo, gPoint, buttonChanged ) {
var delegate = THIS[ tracker.hash ],
pointsList = tracker.getActivePointersListByType( gPoint.type ),
releasePoint,
releaseTime,
updateGPoint,
wasCaptured = false,
quick;
if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
pointsList.buttons = eventInfo.originalEvent.buttons;
} else {
if ( buttonChanged === 0 ) {
// Primary
pointsList.buttons ^= ~1;
} else if ( buttonChanged === 1 ) {
// Aux
pointsList.buttons ^= ~4;
} else if ( buttonChanged === 2 ) {
// Secondary
pointsList.buttons ^= ~2;
} else if ( buttonChanged === 3 ) {
// X1 (Back)
pointsList.buttons ^= ~8;
} else if ( buttonChanged === 4 ) {
// X2 (Forward)
pointsList.buttons ^= ~16;
} else if ( buttonChanged === 5 ) {
// Pen Eraser
pointsList.buttons ^= ~32;
}
}
eventInfo.shouldCapture = false;
// Only capture and track primary button, pen, and touch contacts
if ( buttonChanged !== 0 ) {
eventInfo.shouldReleaseCapture = false;
// Aux Release
if ( tracker.nonPrimaryReleaseHandler &&
!eventInfo.preventGesture &&
!eventInfo.defaultPrevented ) {
eventInfo.preventDefault = true;
tracker.nonPrimaryReleaseHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute(gPoint.currentPos, tracker.element),
button: buttonChanged,
buttons: pointsList.buttons,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
return;
}
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
pointsList.removeContact();
//$.console.log('contacts-- ', pointsList.contacts);
// Update the pointer, stop tracking it if not still in this element
if ( updateGPoint.captured ) {
//updateGPoint.captured = false; // Handled by updatePointerCaptured()
wasCaptured = true;
}
updateGPoint.lastPos = updateGPoint.currentPos;
updateGPoint.lastTime = updateGPoint.currentTime;
updateGPoint.currentPos = gPoint.currentPos;
updateGPoint.currentTime = gPoint.currentTime;
if ( !updateGPoint.insideElement ) {
stopTrackingPointer( tracker, pointsList, updateGPoint );
}
releasePoint = updateGPoint.currentPos;
releaseTime = updateGPoint.currentTime;
} else {
// NOTE: updatePointerUp(): pointerup on untracked gPoint
// ...we'll start to track pointer again
gPoint.captured = false; // Handled by updatePointerCaptured()
gPoint.insideElementPressed = false;
gPoint.insideElement = true;
startTrackingPointer( pointsList, gPoint );
updateGPoint = gPoint;
}
if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
if ( wasCaptured ) {
// Pointer was activated in our element but could have been removed in any element since events are captured to our element
eventInfo.shouldReleaseCapture = true;
eventInfo.preventDefault = true;
if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) {
$.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, updateGPoint );
}
if ( pointsList.contacts === 0 ) {
// Release (pressed in our element)
if ( tracker.releaseHandler && releasePoint ) {
tracker.releaseHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( releasePoint, tracker.element ),
buttons: pointsList.buttons,
insideElementPressed: updateGPoint.insideElementPressed,
insideElementReleased: updateGPoint.insideElement,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
// Drag End
if ( tracker.dragEndHandler && delegate.sentDragEvent ) {
tracker.dragEndHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
speed: updateGPoint.speed,
direction: updateGPoint.direction,
shift: eventInfo.originalEvent.shiftKey,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
// We want to clear this flag regardless of whether we fired the dragEndHandler
delegate.sentDragEvent = false;
// Click / Double-Click
if ( ( tracker.clickHandler || tracker.dblClickHandler ) && updateGPoint.insideElement ) {
quick = releaseTime - updateGPoint.contactTime <= tracker.clickTimeThreshold &&
updateGPoint.contactPos.distanceTo( releasePoint ) <= tracker.clickDistThreshold;
// Click
if ( tracker.clickHandler ) {
tracker.clickHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
quick: quick,
shift: eventInfo.originalEvent.shiftKey,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
originalTarget: updateGPoint.originalTarget,
userData: tracker.userData
}
);
}
// Double-Click
if ( tracker.dblClickHandler && quick ) {
pointsList.clicks++;
if ( pointsList.clicks === 1 ) {
delegate.lastClickPos = releasePoint;
/*jshint loopfunc:true*/
delegate.dblClickTimeOut = setTimeout( function() {
pointsList.clicks = 0;
}, tracker.dblClickTimeThreshold );
/*jshint loopfunc:false*/
} else if ( pointsList.clicks === 2 ) {
clearTimeout( delegate.dblClickTimeOut );
pointsList.clicks = 0;
if ( delegate.lastClickPos.distanceTo( releasePoint ) <= tracker.dblClickDistThreshold ) {
tracker.dblClickHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
shift: eventInfo.originalEvent.shiftKey,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
delegate.lastClickPos = null;
}
}
}
} else if ( pointsList.contacts === 2 ) {
if ( tracker.pinchHandler && updateGPoint.type === 'touch' ) {
// Reset for pinch
delegate.pinchGPoints = pointsList.asArray();
delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
}
}
} else {
// Pointer was activated in another element but removed in our element
eventInfo.shouldReleaseCapture = false;
// Release (pressed in another element)
if ( tracker.releaseHandler && releasePoint ) {
tracker.releaseHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( releasePoint, tracker.element ),
buttons: pointsList.buttons,
insideElementPressed: updateGPoint.insideElementPressed,
insideElementReleased: updateGPoint.insideElement,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
eventInfo.preventDefault = true;
}
}
}
}
/**
* Call when pointer(s) change coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height)
*
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture points associated with the event.
*/
function updatePointerMove( tracker, eventInfo, gPoint ) {
var delegate = THIS[ tracker.hash ],
pointsList = tracker.getActivePointersListByType( gPoint.type ),
updateGPoint,
gPointArray,
delta;
if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) {
pointsList.buttons = eventInfo.originalEvent.buttons;
}
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
// Already tracking the pointer...update it
updateGPoint.lastPos = updateGPoint.currentPos;
updateGPoint.lastTime = updateGPoint.currentTime;
updateGPoint.currentPos = gPoint.currentPos;
updateGPoint.currentTime = gPoint.currentTime;
} else {
// Should never get here, but due to user agent bugs (e.g. legacy touch) it sometimes happens
return;
}
eventInfo.shouldCapture = false;
eventInfo.shouldReleaseCapture = false;
// Stop (mouse only)
if ( tracker.stopHandler && gPoint.type === 'mouse' ) {
clearTimeout( tracker.stopTimeOut );
tracker.stopTimeOut = setTimeout( function() {
handlePointerStop( tracker, eventInfo.originalEvent, gPoint.type );
}, tracker.stopDelay );
}
if ( pointsList.contacts === 0 ) {
// Move (no contacts: hovering mouse or other hover-capable device)
if ( tracker.moveHandler ) {
tracker.moveHandler(
{
eventSource: tracker,
pointerType: gPoint.type,
position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
isTouchEvent: gPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
} else if ( pointsList.contacts === 1 ) {
// Move (1 contact)
if ( tracker.moveHandler ) {
updateGPoint = pointsList.asArray()[ 0 ];
tracker.moveHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
// Drag
if ( tracker.dragHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
updateGPoint = pointsList.asArray()[ 0 ];
delta = updateGPoint.currentPos.minus( updateGPoint.lastPos );
tracker.dragHandler(
{
eventSource: tracker,
pointerType: updateGPoint.type,
position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ),
buttons: pointsList.buttons,
delta: delta,
speed: updateGPoint.speed,
direction: updateGPoint.direction,
shift: eventInfo.originalEvent.shiftKey,
isTouchEvent: updateGPoint.type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
eventInfo.preventDefault = true;
delegate.sentDragEvent = true;
}
} else if ( pointsList.contacts === 2 ) {
// Move (2 contacts, use center)
if ( tracker.moveHandler ) {
gPointArray = pointsList.asArray();
tracker.moveHandler(
{
eventSource: tracker,
pointerType: gPointArray[ 0 ].type,
position: getPointRelativeToAbsolute( getCenterPoint( gPointArray[ 0 ].currentPos, gPointArray[ 1 ].currentPos ), tracker.element ),
buttons: pointsList.buttons,
isTouchEvent: gPointArray[ 0 ].type === 'touch',
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
}
// Pinch
if ( tracker.pinchHandler && gPoint.type === 'touch' &&
!eventInfo.preventGesture && !eventInfo.defaultPrevented ) {
delta = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos );
if ( delta !== delegate.currentPinchDist ) {
delegate.lastPinchDist = delegate.currentPinchDist;
delegate.currentPinchDist = delta;
delegate.lastPinchCenter = delegate.currentPinchCenter;
delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos );
tracker.pinchHandler(
{
eventSource: tracker,
pointerType: 'touch',
gesturePoints: delegate.pinchGPoints,
lastCenter: getPointRelativeToAbsolute( delegate.lastPinchCenter, tracker.element ),
center: getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ),
lastDistance: delegate.lastPinchDist,
distance: delegate.currentPinchDist,
shift: eventInfo.originalEvent.shiftKey,
originalEvent: eventInfo.originalEvent,
userData: tracker.userData
}
);
eventInfo.preventDefault = true;
}
}
}
}
/**
* @function
* @private
* @inner
* @param {OpenSeadragon.MouseTracker} tracker
* A reference to the MouseTracker instance.
* @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo
* Processing info for originating DOM event.
* @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint
* Gesture points associated with the event.
*/
function updatePointerCancel( tracker, eventInfo, gPoint ) {
var pointsList = tracker.getActivePointersListByType( gPoint.type ),
updateGPoint;
updateGPoint = pointsList.getById( gPoint.id );
if ( updateGPoint ) {
stopTrackingPointer( tracker, pointsList, updateGPoint );
}
}
/**
* @private
* @inner
*/
function handlePointerStop( tracker, originalMoveEvent, pointerType ) {
if ( tracker.stopHandler ) {
tracker.stopHandler( {
eventSource: tracker,
pointerType: pointerType,
position: getMouseRelative( originalMoveEvent, tracker.element ),
buttons: tracker.getActivePointersListByType( pointerType ).buttons,
isTouchEvent: pointerType === 'touch',
originalEvent: originalMoveEvent,
userData: tracker.userData
} );
}
}
}(OpenSeadragon));
/*
* OpenSeadragon - Control
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
* An enumeration of supported locations where controls can be anchored.
* The anchoring is always relative to the container.
* @member ControlAnchor
* @memberof OpenSeadragon
* @static
* @type {Object}
* @property {Number} NONE
* @property {Number} TOP_LEFT
* @property {Number} TOP_RIGHT
* @property {Number} BOTTOM_LEFT
* @property {Number} BOTTOM_RIGHT
* @property {Number} ABSOLUTE
*/
$.ControlAnchor = {
NONE: 0,
TOP_LEFT: 1,
TOP_RIGHT: 2,
BOTTOM_RIGHT: 3,
BOTTOM_LEFT: 4,
ABSOLUTE: 5
};
/**
* @class Control
* @classdesc A Control represents any interface element which is meant to allow the user
* to interact with the zoomable interface. Any control can be anchored to any
* element.
*
* @memberof OpenSeadragon
* @param {Element} element - the control element to be anchored in the container.
* @param {Object } options - All required and optional settings for configuring a control element.
* @param {OpenSeadragon.ControlAnchor} [options.anchor=OpenSeadragon.ControlAnchor.NONE] - the position of the control
* relative to the container.
* @param {Boolean} [options.attachToViewer=true] - Whether the control should be added directly to the viewer, or
* directly to the container
* @param {Boolean} [options.autoFade=true] - Whether the control should have the autofade behavior
* @param {Element} container - the element to control will be anchored too.
*/
$.Control = function ( element, options, container ) {
var parent = element.parentNode;
if (typeof options === 'number')
{
$.console.error("Passing an anchor directly into the OpenSeadragon.Control constructor is deprecated; " +
"please use an options object instead. " +
"Support for this deprecated variant is scheduled for removal in December 2013");
options = {anchor: options};
}
options.attachToViewer = (typeof options.attachToViewer === 'undefined') ? true : options.attachToViewer;
/**
* True if the control should have autofade behavior.
* @member {Boolean} autoFade
* @memberof OpenSeadragon.Control#
*/
this.autoFade = (typeof options.autoFade === 'undefined') ? true : options.autoFade;
/**
* The element providing the user interface with some type of control (e.g. a zoom-in button).
* @member {Element} element
* @memberof OpenSeadragon.Control#
*/
this.element = element;
/**
* The position of the Control relative to its container.
* @member {OpenSeadragon.ControlAnchor} anchor
* @memberof OpenSeadragon.Control#
*/
this.anchor = options.anchor;
/**
* The Control's containing element.
* @member {Element} container
* @memberof OpenSeadragon.Control#
*/
this.container = container;
/**
* A neutral element surrounding the control element.
* @member {Element} wrapper
* @memberof OpenSeadragon.Control#
*/
if ( this.anchor === $.ControlAnchor.ABSOLUTE ) {
this.wrapper = $.makeNeutralElement( "div" );
this.wrapper.style.position = "absolute";
this.wrapper.style.top = typeof (options.top) === "number" ? (options.top + 'px') : options.top;
this.wrapper.style.left = typeof (options.left) === "number" ? (options.left + 'px') : options.left;
this.wrapper.style.height = typeof (options.height) === "number" ? (options.height + 'px') : options.height;
this.wrapper.style.width = typeof (options.width) === "number" ? (options.width + 'px') : options.width;
this.wrapper.style.margin = "0px";
this.wrapper.style.padding = "0px";
this.element.style.position = "relative";
this.element.style.top = "0px";
this.element.style.left = "0px";
this.element.style.height = "100%";
this.element.style.width = "100%";
} else {
this.wrapper = $.makeNeutralElement( "div" );
this.wrapper.style.display = "inline-block";
if ( this.anchor === $.ControlAnchor.NONE ) {
// IE6 fix
this.wrapper.style.width = this.wrapper.style.height = "100%";
}
}
this.wrapper.appendChild( this.element );
if (options.attachToViewer ) {
if ( this.anchor === $.ControlAnchor.TOP_RIGHT ||
this.anchor === $.ControlAnchor.BOTTOM_RIGHT ) {
this.container.insertBefore(
this.wrapper,
this.container.firstChild
);
} else {
this.container.appendChild( this.wrapper );
}
} else {
parent.appendChild( this.wrapper );
}
};
/** @lends OpenSeadragon.Control.prototype */
$.Control.prototype = {
/**
* Removes the control from the container.
* @function
*/
destroy: function() {
this.wrapper.removeChild( this.element );
if (this.anchor !== $.ControlAnchor.NONE) {
this.container.removeChild(this.wrapper);
}
},
/**
* Determines if the control is currently visible.
* @function
* @returns {Boolean} true if currently visible, false otherwise.
*/
isVisible: function() {
return this.wrapper.style.display !== "none";
},
/**
* Toggles the visibility of the control.
* @function
* @param {Boolean} visible - true to make visible, false to hide.
*/
setVisible: function( visible ) {
this.wrapper.style.display = visible ?
( this.anchor === $.ControlAnchor.ABSOLUTE ? 'block' : 'inline-block' ) :
"none";
},
/**
* Sets the opacity level for the control.
* @function
* @param {Number} opactiy - a value between 1 and 0 inclusively.
*/
setOpacity: function( opacity ) {
$.setElementOpacity( this.wrapper, opacity, true );
}
};
}( OpenSeadragon ));
/*
* OpenSeadragon - ControlDock
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
* @class ControlDock
* @classdesc Provides a container element (a <form> element) with support for the layout of control elements.
*
* @memberof OpenSeadragon
*/
$.ControlDock = function( options ){
var layouts = [ 'topleft', 'topright', 'bottomright', 'bottomleft'],
layout,
i;
$.extend( true, this, {
id: 'controldock-' + $.now() + '-' + Math.floor(Math.random() * 1000000),
container: $.makeNeutralElement( 'div' ),
controls: []
}, options );
// Disable the form's submit; otherwise button clicks and return keys
// can trigger it.
this.container.onsubmit = function() {
return false;
};
if( this.element ){
this.element = $.getElement( this.element );
this.element.appendChild( this.container );
if( $.getElementStyle(this.element).position === 'static' ){
this.element.style.position = 'relative';
}
this.container.style.width = '100%';
this.container.style.height = '100%';
}
for( i = 0; i < layouts.length; i++ ){
layout = layouts[ i ];
this.controls[ layout ] = $.makeNeutralElement( "div" );
this.controls[ layout ].style.position = 'absolute';
if ( layout.match( 'left' ) ){
this.controls[ layout ].style.left = '0px';
}
if ( layout.match( 'right' ) ){
this.controls[ layout ].style.right = '0px';
}
if ( layout.match( 'top' ) ){
this.controls[ layout ].style.top = '0px';
}
if ( layout.match( 'bottom' ) ){
this.controls[ layout ].style.bottom = '0px';
}
}
this.container.appendChild( this.controls.topleft );
this.container.appendChild( this.controls.topright );
this.container.appendChild( this.controls.bottomright );
this.container.appendChild( this.controls.bottomleft );
};
/** @lends OpenSeadragon.ControlDock.prototype */
$.ControlDock.prototype = {
/**
* @function
*/
addControl: function ( element, controlOptions ) {
element = $.getElement( element );
var div = null;
if ( getControlIndex( this, element ) >= 0 ) {
return; // they're trying to add a duplicate control
}
switch ( controlOptions.anchor ) {
case $.ControlAnchor.TOP_RIGHT:
div = this.controls.topright;
element.style.position = "relative";
element.style.paddingRight = "0px";
element.style.paddingTop = "0px";
break;
case $.ControlAnchor.BOTTOM_RIGHT:
div = this.controls.bottomright;
element.style.position = "relative";
element.style.paddingRight = "0px";
element.style.paddingBottom = "0px";
break;
case $.ControlAnchor.BOTTOM_LEFT:
div = this.controls.bottomleft;
element.style.position = "relative";
element.style.paddingLeft = "0px";
element.style.paddingBottom = "0px";
break;
case $.ControlAnchor.TOP_LEFT:
div = this.controls.topleft;
element.style.position = "relative";
element.style.paddingLeft = "0px";
element.style.paddingTop = "0px";
break;
case $.ControlAnchor.ABSOLUTE:
div = this.container;
element.style.margin = "0px";
element.style.padding = "0px";
break;
default:
case $.ControlAnchor.NONE:
div = this.container;
element.style.margin = "0px";
element.style.padding = "0px";
break;
}
this.controls.push(
new $.Control( element, controlOptions, div )
);
element.style.display = "inline-block";
},
/**
* @function
* @returns {OpenSeadragon.ControlDock} Chainable.
*/
removeControl: function ( element ) {
element = $.getElement( element );
var i = getControlIndex( this, element );
if ( i >= 0 ) {
this.controls[ i ].destroy();
this.controls.splice( i, 1 );
}
return this;
},
/**
* @function
* @returns {OpenSeadragon.ControlDock} Chainable.
*/
clearControls: function () {
while ( this.controls.length > 0 ) {
this.controls.pop().destroy();
}
return this;
},
/**
* @function
* @returns {Boolean}
*/
areControlsEnabled: function () {
var i;
for ( i = this.controls.length - 1; i >= 0; i-- ) {
if ( this.controls[ i ].isVisible() ) {
return true;
}
}
return false;
},
/**
* @function
* @returns {OpenSeadragon.ControlDock} Chainable.
*/
setControlsEnabled: function( enabled ) {
var i;
for ( i = this.controls.length - 1; i >= 0; i-- ) {
this.controls[ i ].setVisible( enabled );
}
return this;
}
};
///////////////////////////////////////////////////////////////////////////////
// Utility methods
///////////////////////////////////////////////////////////////////////////////
function getControlIndex( dock, element ) {
var controls = dock.controls,
i;
for ( i = controls.length - 1; i >= 0; i-- ) {
if ( controls[ i ].element === element ) {
return i;
}
}
return -1;
}
}( OpenSeadragon ));
/*
* OpenSeadragon - Placement
*
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function($) {
/**
* An enumeration of positions to anchor an element.
* @member Placement
* @memberOf OpenSeadragon
* @static
* @readonly
* @property {OpenSeadragon.Placement} CENTER
* @property {OpenSeadragon.Placement} TOP_LEFT
* @property {OpenSeadragon.Placement} TOP
* @property {OpenSeadragon.Placement} TOP_RIGHT
* @property {OpenSeadragon.Placement} RIGHT
* @property {OpenSeadragon.Placement} BOTTOM_RIGHT
* @property {OpenSeadragon.Placement} BOTTOM
* @property {OpenSeadragon.Placement} BOTTOM_LEFT
* @property {OpenSeadragon.Placement} LEFT
*/
$.Placement = $.freezeObject({
CENTER: 0,
TOP_LEFT: 1,
TOP: 2,
TOP_RIGHT: 3,
RIGHT: 4,
BOTTOM_RIGHT: 5,
BOTTOM: 6,
BOTTOM_LEFT: 7,
LEFT: 8,
properties: {
0: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: false,
isVerticallyCentered: true,
isBottom: false
},
1: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
2: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
3: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: true,
isVerticallyCentered: false,
isBottom: false
},
4: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: false,
isVerticallyCentered: true,
isBottom: false
},
5: {
isLeft: false,
isHorizontallyCentered: false,
isRight: true,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
6: {
isLeft: false,
isHorizontallyCentered: true,
isRight: false,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
7: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: false,
isVerticallyCentered: false,
isBottom: true
},
8: {
isLeft: true,
isHorizontallyCentered: false,
isRight: false,
isTop: false,
isVerticallyCentered: true,
isBottom: false
}
}
});
}(OpenSeadragon));
/*
* OpenSeadragon - Viewer
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 OpenSeadragon contributors
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
// dictionary from hash to private properties
var THIS = {};
var nextHash = 1;
/**
*
* The main point of entry into creating a zoomable image on the page.<br>
* <br>
* We have provided an idiomatic javascript constructor which takes
* a single object, but still support the legacy positional arguments.<br>
* <br>
* The options below are given in order that they appeared in the constructor
* as arguments and we translate a positional call into an idiomatic call.<br>
* <br>
* To create a viewer, you can use either of this methods:<br>
* <ul>
* <li><code>var viewer = new OpenSeadragon.Viewer(options);</code></li>
* <li><code>var viewer = OpenSeadragon(options);</code></li>
* </ul>
* @class Viewer
* @classdesc The main OpenSeadragon viewer class.
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.EventSource
* @extends OpenSeadragon.ControlDock
* @param {OpenSeadragon.Options} options - Viewer options.
*
**/
$.Viewer = function( options ) {
var args = arguments,
_this = this,
i;
//backward compatibility for positional args while preferring more
//idiomatic javascript options object as the only argument
if( !$.isPlainObject( options ) ){
options = {
id: args[ 0 ],
xmlPath: args.length > 1 ? args[ 1 ] : undefined,
prefixUrl: args.length > 2 ? args[ 2 ] : undefined,
controls: args.length > 3 ? args[ 3 ] : undefined,
overlays: args.length > 4 ? args[ 4 ] : undefined
};
}
//options.config and the general config argument are deprecated
//in favor of the more direct specification of optional settings
//being pass directly on the options object
if ( options.config ){
$.extend( true, options, options.config );
delete options.config;
}
// Move deprecated drawer options from the base options object into a sub-object
// This is an array to make it easy to add additional properties to convert to
// drawer options later if it makes sense to set at the drawer level rather than
// per tiled image (for example, subPixelRoundingForTransparency).
let drawerOptionList = [
'useCanvas', // deprecated
];
options.drawerOptions = Object.assign({},
drawerOptionList.reduce((drawerOptions, option) => {
drawerOptions[option] = options[option];
delete options[option];
return drawerOptions;
}, {}),
options.drawerOptions);
//Public properties
//Allow the options object to override global defaults
$.extend( true, this, {
//internal state and dom identifiers
id: options.id,
hash: options.hash || nextHash++,
/**
* Index for page to be shown first next time open() is called (only used in sequenceMode).
* @member {Number} initialPage
* @memberof OpenSeadragon.Viewer#
*/
initialPage: 0,
//dom nodes
/**
* The parent element of this Viewer instance, passed in when the Viewer was created.
* @member {Element} element
* @memberof OpenSeadragon.Viewer#
*/
element: null,
/**
* A <div> element (provided by {@link OpenSeadragon.ControlDock}), the base element of this Viewer instance.<br><br>
* Child element of {@link OpenSeadragon.Viewer#element}.
* @member {Element} container
* @memberof OpenSeadragon.Viewer#
*/
container: null,
/**
* A <div> element, the element where user-input events are handled for panning and zooming.<br><br>
* Child element of {@link OpenSeadragon.Viewer#container},
* positioned on top of {@link OpenSeadragon.Viewer#keyboardCommandArea}.<br><br>
* The parent of {@link OpenSeadragon.Drawer#canvas} instances.
* @member {Element} canvas
* @memberof OpenSeadragon.Viewer#
*/
canvas: null,
// Overlays list. An overlay allows to add html on top of the viewer.
overlays: [],
// Container inside the canvas where overlays are drawn.
overlaysContainer: null,
//private state properties
previousBody: [],
//This was originally initialized in the constructor and so could never
//have anything in it. now it can because we allow it to be specified
//in the options and is only empty by default if not specified. Also
//this array was returned from get_controls which I find confusing
//since this object has a controls property which is treated in other
//functions like clearControls. I'm removing the accessors.
customControls: [],
//These are originally not part options but declared as members
//in initialize. It's still considered idiomatic to put them here
//source is here for backwards compatibility. It is not an official
//part of the API and should not be relied upon.
source: null,
/**
* Handles rendering of tiles in the viewer. Created for each TileSource opened.
* @member {OpenSeadragon.Drawer} drawer
* @memberof OpenSeadragon.Viewer#
*/
drawer: null,
/**
* Keeps track of all of the tiled images in the scene.
* @member {OpenSeadragon.World} world
* @memberof OpenSeadragon.Viewer#
*/
world: null,
/**
* Handles coordinate-related functionality - zoom, pan, rotation, etc. Created for each TileSource opened.
* @member {OpenSeadragon.Viewport} viewport
* @memberof OpenSeadragon.Viewer#
*/
viewport: null,
/**
* @member {OpenSeadragon.Navigator} navigator
* @memberof OpenSeadragon.Viewer#
*/
navigator: null,
//A collection viewport is a separate viewport used to provide
//simultaneous rendering of sets of tiles
collectionViewport: null,
collectionDrawer: null,
//UI image resources
//TODO: rename navImages to uiImages
navImages: null,
//interface button controls
buttonGroup: null,
//TODO: this is defunct so safely remove it
profiler: null
}, $.DEFAULT_SETTINGS, options );
if ( typeof ( this.hash) === "undefined" ) {
throw new Error("A hash must be defined, either by specifying options.id or options.hash.");
}
if ( typeof ( THIS[ this.hash ] ) !== "undefined" ) {
// We don't want to throw an error here, as the user might have discarded
// the previous viewer with the same hash and now want to recreate it.
$.console.warn("Hash " + this.hash + " has already been used.");
}
//Private state properties
THIS[ this.hash ] = {
fsBoundsDelta: new $.Point( 1, 1 ),
prevContainerSize: null,
animating: false,
forceRedraw: false,
needsResize: false,
forceResize: false,
mouseInside: false,
group: null,
// whether we should be continuously zooming
zooming: false,
// how much we should be continuously zooming by
zoomFactor: null,
lastZoomTime: null,
fullPage: false,
onfullscreenchange: null,
lastClickTime: null,
draggingToZoom: false,
};
this._sequenceIndex = 0;
this._firstOpen = true;
this._updateRequestId = null;
this._loadQueue = [];
this.currentOverlays = [];
this._updatePixelDensityRatioBind = null;
this._lastScrollTime = $.now(); // variable used to help normalize the scroll event speed of different devices
//Inherit some behaviors and properties
$.EventSource.call( this );
this.addHandler( 'open-failed', function ( event ) {
var msg = $.getString( "Errors.OpenFailed", event.eventSource, event.message);
_this._showMessage( msg );
});
$.ControlDock.call( this, options );
//Deal with tile sources
if (this.xmlPath) {
//Deprecated option. Now it is preferred to use the tileSources option
this.tileSources = [ this.xmlPath ];
}
this.element = this.element || document.getElementById( this.id );
this.canvas = $.makeNeutralElement( "div" );
this.canvas.className = "openseadragon-canvas";
(function( style ){
style.width = "100%";
style.height = "100%";
style.overflow = "hidden";
style.position = "absolute";
style.top = "0px";
style.left = "0px";
}(this.canvas.style));
$.setElementTouchActionNone( this.canvas );
if (options.tabIndex !== "") {
this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex);
}
//the container is created through applying the ControlDock constructor above
this.container.className = "openseadragon-container";
(function( style ){
style.width = "100%";
style.height = "100%";
style.position = "relative";
style.overflow = "hidden";
style.left = "0px";
style.top = "0px";
style.textAlign = "left"; // needed to protect against
}( this.container.style ));
$.setElementTouchActionNone( this.container );
this.container.insertBefore( this.canvas, this.container.firstChild );
this.element.appendChild( this.container );
//Used for toggling between fullscreen and default container size
//TODO: these can be closure private and shared across Viewer
// instances.
this.bodyWidth = document.body.style.width;
this.bodyHeight = document.body.style.height;
this.bodyOverflow = document.body.style.overflow;
this.docOverflow = document.documentElement.style.overflow;
this.innerTracker = new $.MouseTracker({
userData: 'Viewer.innerTracker',
element: this.canvas,
startDisabled: !this.mouseNavEnabled,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
dblClickTimeThreshold: this.dblClickTimeThreshold,
dblClickDistThreshold: this.dblClickDistThreshold,
contextMenuHandler: $.delegate( this, onCanvasContextMenu ),
keyDownHandler: $.delegate( this, onCanvasKeyDown ),
keyHandler: $.delegate( this, onCanvasKeyPress ),
clickHandler: $.delegate( this, onCanvasClick ),
dblClickHandler: $.delegate( this, onCanvasDblClick ),
dragHandler: $.delegate( this, onCanvasDrag ),
dragEndHandler: $.delegate( this, onCanvasDragEnd ),
enterHandler: $.delegate( this, onCanvasEnter ),
leaveHandler: $.delegate( this, onCanvasLeave ),
pressHandler: $.delegate( this, onCanvasPress ),
releaseHandler: $.delegate( this, onCanvasRelease ),
nonPrimaryPressHandler: $.delegate( this, onCanvasNonPrimaryPress ),
nonPrimaryReleaseHandler: $.delegate( this, onCanvasNonPrimaryRelease ),
scrollHandler: $.delegate( this, onCanvasScroll ),
pinchHandler: $.delegate( this, onCanvasPinch ),
focusHandler: $.delegate( this, onCanvasFocus ),
blurHandler: $.delegate( this, onCanvasBlur ),
});
this.outerTracker = new $.MouseTracker({
userData: 'Viewer.outerTracker',
element: this.container,
startDisabled: !this.mouseNavEnabled,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
dblClickTimeThreshold: this.dblClickTimeThreshold,
dblClickDistThreshold: this.dblClickDistThreshold,
enterHandler: $.delegate( this, onContainerEnter ),
leaveHandler: $.delegate( this, onContainerLeave )
});
if( this.toolbar ){
this.toolbar = new $.ControlDock({ element: this.toolbar });
}
this.bindStandardControls();
THIS[ this.hash ].prevContainerSize = _getSafeElemSize( this.container );
if(window.ResizeObserver){
this._autoResizePolling = false;
this._resizeObserver = new ResizeObserver(function(){
THIS[_this.hash].needsResize = true;
});
this._resizeObserver.observe(this.container, {});
} else {
this._autoResizePolling = true;
}
// Create the world
this.world = new $.World({
viewer: this
});
this.world.addHandler('add-item', function(event) {
// For backwards compatibility, we maintain the source property
_this.source = _this.world.getItemAt(0).source;
THIS[ _this.hash ].forceRedraw = true;
if (!_this._updateRequestId) {
_this._updateRequestId = scheduleUpdate( _this, updateMulti );
}
});
this.world.addHandler('remove-item', function(event) {
// For backwards compatibility, we maintain the source property
if (_this.world.getItemCount()) {
_this.source = _this.world.getItemAt(0).source;
} else {
_this.source = null;
}
THIS[ _this.hash ].forceRedraw = true;
});
this.world.addHandler('metrics-change', function(event) {
if (_this.viewport) {
_this.viewport._setContentBounds(_this.world.getHomeBounds(), _this.world.getContentFactor());
}
});
this.world.addHandler('item-index-change', function(event) {
// For backwards compatibility, we maintain the source property
_this.source = _this.world.getItemAt(0).source;
});
// Create the viewport
this.viewport = new $.Viewport({
containerSize: THIS[ this.hash ].prevContainerSize,
springStiffness: this.springStiffness,
animationTime: this.animationTime,
minZoomImageRatio: this.minZoomImageRatio,
maxZoomPixelRatio: this.maxZoomPixelRatio,
visibilityRatio: this.visibilityRatio,
wrapHorizontal: this.wrapHorizontal,
wrapVertical: this.wrapVertical,
defaultZoomLevel: this.defaultZoomLevel,
minZoomLevel: this.minZoomLevel,
maxZoomLevel: this.maxZoomLevel,
viewer: this,
degrees: this.degrees,
flipped: this.flipped,
overlayPreserveContentDirection: this.overlayPreserveContentDirection,
navigatorRotate: this.navigatorRotate,
homeFillsViewer: this.homeFillsViewer,
margins: this.viewportMargins,
silenceMultiImageWarnings: this.silenceMultiImageWarnings
});
this.viewport._setContentBounds(this.world.getHomeBounds(), this.world.getContentFactor());
// Create the image loader
this.imageLoader = new $.ImageLoader({
jobLimit: this.imageLoaderLimit,
timeout: options.timeout,
tileRetryMax: this.tileRetryMax,
tileRetryDelay: this.tileRetryDelay
});
// Create the tile cache
this.tileCache = new $.TileCache({
maxImageCacheCount: this.maxImageCacheCount
});
//Create the drawer based on selected options
if (Object.prototype.hasOwnProperty.call(this.drawerOptions, 'useCanvas') ){
$.console.error('useCanvas is deprecated, use the "drawer" option to indicate preferred drawer(s)');
// for backwards compatibility, use HTMLDrawer if useCanvas is defined and is falsey
if (!this.drawerOptions.useCanvas){
this.drawer = $.HTMLDrawer;
}
delete this.drawerOptions.useCanvas;
}
let drawerCandidates = Array.isArray(this.drawer) ? this.drawer : [this.drawer];
if (drawerCandidates.length === 0){
// if an empty array was passed in, throw a warning and use the defaults
// note: if the drawer option is not specified, the defaults will already be set so this won't apply
drawerCandidates = [$.DEFAULT_SETTINGS.drawer].flat(); // ensure it is a list
$.console.warn('No valid drawers were selected. Using the default value.');
}
this.drawer = null;
for (const drawerCandidate of drawerCandidates){
let success = this.requestDrawer(drawerCandidate, {mainDrawer: true, redrawImmediately: false});
if(success){
break;
}
}
if (!this.drawer){
$.console.error('No drawer could be created!');
throw('Error with creating the selected drawer(s)');
}
// Pass the imageSmoothingEnabled option along to the drawer
this.drawer.setImageSmoothingEnabled(this.imageSmoothingEnabled);
// Overlay container
this.overlaysContainer = $.makeNeutralElement( "div" );
this.canvas.appendChild( this.overlaysContainer );
// Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons
if (!this.drawer.canRotate()) {
// Disable/remove the rotate left/right buttons since they aren't supported
if (this.rotateLeft) {
i = this.buttonGroup.buttons.indexOf(this.rotateLeft);
this.buttonGroup.buttons.splice(i, 1);
this.buttonGroup.element.removeChild(this.rotateLeft.element);
}
if (this.rotateRight) {
i = this.buttonGroup.buttons.indexOf(this.rotateRight);
this.buttonGroup.buttons.splice(i, 1);
this.buttonGroup.element.removeChild(this.rotateRight.element);
}
}
this._addUpdatePixelDensityRatioEvent();
//Instantiate a navigator if configured
if ( this.showNavigator){
this.navigator = new $.Navigator({
element: this.navigatorElement,
id: this.navigatorId,
position: this.navigatorPosition,
sizeRatio: this.navigatorSizeRatio,
maintainSizeRatio: this.navigatorMaintainSizeRatio,
top: this.navigatorTop,
left: this.navigatorLeft,
width: this.navigatorWidth,
height: this.navigatorHeight,
autoResize: this.navigatorAutoResize,
autoFade: this.navigatorAutoFade,
prefixUrl: this.prefixUrl,
viewer: this,
navigatorRotate: this.navigatorRotate,
background: this.navigatorBackground,
opacity: this.navigatorOpacity,
borderColor: this.navigatorBorderColor,
displayRegionColor: this.navigatorDisplayRegionColor,
crossOriginPolicy: this.crossOriginPolicy,
animationTime: this.animationTime,
drawer: this.drawer.getType(),
loadTilesWithAjax: this.loadTilesWithAjax,
ajaxHeaders: this.ajaxHeaders,
ajaxWithCredentials: this.ajaxWithCredentials,
});
}
// Sequence mode
if (this.sequenceMode) {
this.bindSequenceControls();
}
// Open initial tilesources
if (this.tileSources) {
this.open( this.tileSources );
}
// Add custom controls
for ( i = 0; i < this.customControls.length; i++ ) {
this.addControl(
this.customControls[ i ].id,
{anchor: this.customControls[ i ].anchor}
);
}
// Initial fade out
$.requestAnimationFrame( function(){
beginControlsAutoHide( _this );
} );
// Register the viewer
$._viewers.set(this.element, this);
};
$.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** @lends OpenSeadragon.Viewer.prototype */{
/**
* @function
* @returns {Boolean}
*/
isOpen: function () {
return !!this.world.getItemCount();
},
// deprecated
openDzi: function ( dzi ) {
$.console.error( "[Viewer.openDzi] this function is deprecated; use Viewer.open() instead." );
return this.open( dzi );
},
// deprecated
openTileSource: function ( tileSource ) {
$.console.error( "[Viewer.openTileSource] this function is deprecated; use Viewer.open() instead." );
return this.open( tileSource );
},
//deprecated
get buttons () {
$.console.warn('Viewer.buttons is deprecated; Please use Viewer.buttonGroup');
return this.buttonGroup;
},
/**
* Open tiled images into the viewer, closing any others.
* To get the TiledImage instance created by open, add an event listener for
* {@link OpenSeadragon.Viewer.html#.event:open}, which when fired can be used to get access
* to the instance, i.e., viewer.world.getItemAt(0).
* @function
* @param {Array|String|Object|Function} tileSources - This can be a TiledImage
* specifier, a TileSource specifier, or an array of either. A TiledImage specifier
* is the same as the options parameter for {@link OpenSeadragon.Viewer#addTiledImage},
* except for the index property; images are added in sequence.
* A TileSource specifier is anything you could pass as the tileSource property
* of the options parameter for {@link OpenSeadragon.Viewer#addTiledImage}.
* @param {Number} initialPage - If sequenceMode is true, display this page initially
* for the given tileSources. If specified, will overwrite the Viewer's existing initialPage property.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:open
* @fires OpenSeadragon.Viewer.event:open-failed
*/
open: function (tileSources, initialPage) {
var _this = this;
this.close();
if (!tileSources) {
return this;
}
if (this.sequenceMode && $.isArray(tileSources)) {
if (this.referenceStrip) {
this.referenceStrip.destroy();
this.referenceStrip = null;
}
if (typeof initialPage !== 'undefined' && !isNaN(initialPage)) {
this.initialPage = initialPage;
}
this.tileSources = tileSources;
this._sequenceIndex = Math.max(0, Math.min(this.tileSources.length - 1, this.initialPage));
if (this.tileSources.length) {
this.open(this.tileSources[this._sequenceIndex]);
if ( this.showReferenceStrip ){
this.addReferenceStrip();
}
}
this._updateSequenceButtons( this._sequenceIndex );
return this;
}
if (!$.isArray(tileSources)) {
tileSources = [tileSources];
}
if (!tileSources.length) {
return this;
}
this._opening = true;
var expected = tileSources.length;
var successes = 0;
var failures = 0;
var failEvent;
var checkCompletion = function() {
if (successes + failures === expected) {
if (successes) {
if (_this._firstOpen || !_this.preserveViewport) {
_this.viewport.goHome( true );
_this.viewport.update();
}
_this._firstOpen = false;
var source = tileSources[0];
if (source.tileSource) {
source = source.tileSource;
}
// Global overlays
if( _this.overlays && !_this.preserveOverlays ){
for ( var i = 0; i < _this.overlays.length; i++ ) {
_this.currentOverlays[ i ] = getOverlayObject( _this, _this.overlays[ i ] );
}
}
_this._drawOverlays();
_this._opening = false;
/**
* Raised when the viewer has opened and loaded one or more TileSources.
*
* @event open
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TileSource} source - The tile source that was opened.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
// TODO: what if there are multiple sources?
_this.raiseEvent( 'open', { source: source } );
} else {
_this._opening = false;
/**
* Raised when an error occurs loading a TileSource.
*
* @event open-failed
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {String} message - Information about what failed.
* @property {String} source - The tile source that failed.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'open-failed', failEvent );
}
}
};
var doOne = function(options) {
if (!$.isPlainObject(options) || !options.tileSource) {
options = {
tileSource: options
};
}
if (options.index !== undefined) {
$.console.error('[Viewer.open] setting indexes here is not supported; use addTiledImage instead');
delete options.index;
}
if (options.collectionImmediately === undefined) {
options.collectionImmediately = true;
}
var originalSuccess = options.success;
options.success = function(event) {
successes++;
// TODO: now that options has other things besides tileSource, the overlays
// should probably be at the options level, not the tileSource level.
if (options.tileSource.overlays) {
for (var i = 0; i < options.tileSource.overlays.length; i++) {
_this.addOverlay(options.tileSource.overlays[i]);
}
}
if (originalSuccess) {
originalSuccess(event);
}
checkCompletion();
};
var originalError = options.error;
options.error = function(event) {
failures++;
if (!failEvent) {
failEvent = event;
}
if (originalError) {
originalError(event);
}
checkCompletion();
};
_this.addTiledImage(options);
};
// TileSources
for (var i = 0; i < tileSources.length; i++) {
doOne(tileSources[i]);
}
return this;
},
/**
* @function
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:close
*/
close: function ( ) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return this;
}
this._opening = false;
if ( this.navigator ) {
this.navigator.close();
}
if (!this.preserveOverlays) {
this.clearOverlays();
this.overlaysContainer.innerHTML = "";
}
THIS[ this.hash ].animating = false;
this.world.removeAll();
this.imageLoader.clear();
/**
* Raised when the viewer is closed (see {@link OpenSeadragon.Viewer#close}).
*
* @event close
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'close' );
return this;
},
/**
* Function to destroy the viewer and clean up everything created by OpenSeadragon.
*
* Example:
* var viewer = OpenSeadragon({
* [...]
* });
*
* //when you are done with the viewer:
* viewer.destroy();
* viewer = null; //important
*
* @function
* @fires OpenSeadragon.Viewer.event:before-destroy
* @fires OpenSeadragon.Viewer.event:destroy
*/
destroy: function( ) {
if ( !THIS[ this.hash ] ) {
//this viewer has already been destroyed: returning immediately
return;
}
/**
* Raised when the viewer is about to be destroyed (see {@link OpenSeadragon.Viewer#before-destroy}).
*
* @event before-destroy
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'before-destroy' );
this._removeUpdatePixelDensityRatioEvent();
this.close();
this.clearOverlays();
this.overlaysContainer.innerHTML = "";
//TODO: implement this...
//this.unbindSequenceControls()
//this.unbindStandardControls()
if (this._resizeObserver){
this._resizeObserver.disconnect();
}
if (this.referenceStrip) {
this.referenceStrip.destroy();
this.referenceStrip = null;
}
if ( this._updateRequestId !== null ) {
$.cancelAnimationFrame( this._updateRequestId );
this._updateRequestId = null;
}
if ( this.drawer ) {
this.drawer.destroy();
}
if ( this.navigator ) {
this.navigator.destroy();
THIS[ this.navigator.hash ] = null;
delete THIS[ this.navigator.hash ];
this.navigator = null;
}
if (this.buttonGroup) {
this.buttonGroup.destroy();
} else if (this.customButtons) {
while (this.customButtons.length) {
this.customButtons.pop().destroy();
}
}
if (this.paging) {
this.paging.destroy();
}
// Go through top element (passed to us) and remove all children
// Use removeChild to make sure it handles SVG or any non-html
// also it performs better - http://jsperf.com/innerhtml-vs-removechild/15
if (this.element){
while (this.element.firstChild) {
this.element.removeChild(this.element.firstChild);
}
}
this.container.onsubmit = null;
this.clearControls();
// destroy the mouse trackers
if (this.innerTracker){
this.innerTracker.destroy();
}
if (this.outerTracker){
this.outerTracker.destroy();
}
THIS[ this.hash ] = null;
delete THIS[ this.hash ];
// clear all our references to dom objects
this.canvas = null;
this.container = null;
// Unregister the viewer
$._viewers.delete(this.element);
// clear our reference to the main element - they will need to pass it in again, creating a new viewer
this.element = null;
/**
* Raised when the viewer is destroyed (see {@link OpenSeadragon.Viewer#destroy}).
*
* @event destroy
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'destroy' );
this.removeAllHandlers();
},
/**
* Request a drawer for this viewer, as a supported string or drawer constructor.
* @param {String | OpenSeadragon.DrawerBase} drawerCandidate The type of drawer to try to construct.
* @param { Object } options
* @param { Boolean } [options.mainDrawer] Whether to use this as the viewer's main drawer. Default = true.
* @param { Boolean } [options.redrawImmediately] Whether to immediately draw a new frame. Only used if options.mainDrawer = true. Default = true.
* @param { Object } [options.drawerOptions] Options for this drawer. Defaults to viewer.drawerOptions.
* for this viewer type. See {@link OpenSeadragon.Options}.
* @returns {Object | Boolean} The drawer that was created, or false if the requested drawer is not supported
*/
requestDrawer(drawerCandidate, options){
const defaultOpts = {
mainDrawer: true,
redrawImmediately: true,
drawerOptions: null
};
options = $.extend(true, defaultOpts, options);
const mainDrawer = options.mainDrawer;
const redrawImmediately = options.redrawImmediately;
const drawerOptions = options.drawerOptions;
const oldDrawer = this.drawer;
let Drawer = null;
//if the candidate inherits from a drawer base, use it
if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
Drawer = drawerCandidate;
drawerCandidate = 'custom';
} else if (typeof drawerCandidate === "string") {
Drawer = $.determineDrawer(drawerCandidate);
}
if(!Drawer){
$.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
}
// if the drawer is supported, create it and return true
if (Drawer && Drawer.isSupported()) {
// first destroy the previous drawer
if(oldDrawer && mainDrawer){
oldDrawer.destroy();
}
// create the new drawer
const newDrawer = new Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor,
options: drawerOptions || this.drawerOptions[drawerCandidate],
});
if(mainDrawer){
this.drawer = newDrawer;
if(redrawImmediately){
this.forceRedraw();
}
}
return newDrawer;
}
return false;
},
/**
* @function
* @returns {Boolean}
*/
isMouseNavEnabled: function () {
return this.innerTracker.isTracking();
},
/**
* @function
* @param {Boolean} enabled - true to enable, false to disable
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:mouse-enabled
*/
setMouseNavEnabled: function( enabled ){
this.innerTracker.setTracking( enabled );
this.outerTracker.setTracking( enabled );
/**
* Raised when mouse/touch navigation is enabled or disabled (see {@link OpenSeadragon.Viewer#setMouseNavEnabled}).
*
* @event mouse-enabled
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} enabled
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'mouse-enabled', { enabled: enabled } );
return this;
},
/**
* @function
* @returns {Boolean}
*/
areControlsEnabled: function () {
var enabled = this.controls.length,
i;
for( i = 0; i < this.controls.length; i++ ){
enabled = enabled && this.controls[ i ].isVisible();
}
return enabled;
},
/**
* Shows or hides the controls (e.g. the default navigation buttons).
*
* @function
* @param {Boolean} true to show, false to hide.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:controls-enabled
*/
setControlsEnabled: function( enabled ) {
if( enabled ){
abortControlsAutoHide( this );
} else {
beginControlsAutoHide( this );
}
/**
* Raised when the navigation controls are shown or hidden (see {@link OpenSeadragon.Viewer#setControlsEnabled}).
*
* @event controls-enabled
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} enabled
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'controls-enabled', { enabled: enabled } );
return this;
},
/**
* Turns debugging mode on or off for this viewer.
*
* @function
* @param {Boolean} debugMode true to turn debug on, false to turn debug off.
*/
setDebugMode: function(debugMode){
for (var i = 0; i < this.world.getItemCount(); i++) {
this.world.getItemAt(i).debugMode = debugMode;
}
this.debugMode = debugMode;
this.forceRedraw();
},
/**
* Update headers to include when making AJAX requests.
*
* Unless `propagate` is set to false (which is likely only useful in rare circumstances),
* the updated headers are propagated to all tiled images, each of which will subsequently
* propagate the changed headers to all their tiles.
* If applicable, the headers of the viewer's navigator and reference strip will also be updated.
*
* Note that the rules for merging headers still apply, i.e. headers returned by
* {@link OpenSeadragon.TileSource#getTileAjaxHeaders} take precedence over
* `TiledImage.ajaxHeaders`, which take precedence over the headers here in the viewer.
*
* @function
* @param {Object} ajaxHeaders Updated AJAX headers.
* @param {Boolean} [propagate=true] Whether to propagate updated headers to tiled images, etc.
*/
setAjaxHeaders: function(ajaxHeaders, propagate) {
if (ajaxHeaders === null) {
ajaxHeaders = {};
}
if (!$.isPlainObject(ajaxHeaders)) {
console.error('[Viewer.setAjaxHeaders] Ignoring invalid headers, must be a plain object');
return;
}
if (propagate === undefined) {
propagate = true;
}
this.ajaxHeaders = ajaxHeaders;
if (propagate) {
for (var i = 0; i < this.world.getItemCount(); i++) {
this.world.getItemAt(i)._updateAjaxHeaders(true);
}
if (this.navigator) {
this.navigator.setAjaxHeaders(this.ajaxHeaders, true);
}
if (this.referenceStrip && this.referenceStrip.miniViewers) {
for (var key in this.referenceStrip.miniViewers) {
this.referenceStrip.miniViewers[key].setAjaxHeaders(this.ajaxHeaders, true);
}
}
}
},
/**
* Adds the given button to this viewer.
*
* @function
* @param {OpenSeadragon.Button} button
*/
addButton: function( button ){
this.buttonGroup.addButton(button);
},
/**
* @function
* @returns {Boolean}
*/
isFullPage: function () {
return THIS[this.hash] && THIS[ this.hash ].fullPage;
},
/**
* Toggle full page mode.
* @function
* @param {Boolean} fullPage
* If true, enter full page mode. If false, exit full page mode.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:pre-full-page
* @fires OpenSeadragon.Viewer.event:full-page
*/
setFullPage: function( fullPage ) {
var body = document.body,
bodyStyle = body.style,
docStyle = document.documentElement.style,
_this = this,
nodes,
i;
//don't bother modifying the DOM if we are already in full page mode.
if ( fullPage === this.isFullPage() ) {
return this;
}
var fullPageEventArgs = {
fullPage: fullPage,
preventDefaultAction: false
};
/**
* Raised when the viewer is about to change to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
*
* @event pre-full-page
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullPage - True if entering full-page mode, false if exiting full-page mode.
* @property {Boolean} preventDefaultAction - Set to true to prevent full-page mode change. Default: false.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'pre-full-page', fullPageEventArgs );
if ( fullPageEventArgs.preventDefaultAction ) {
return this;
}
if ( fullPage && this.element ) {
this.elementSize = $.getElementSize( this.element );
this.pageScroll = $.getPageScroll();
this.elementMargin = this.element.style.margin;
this.element.style.margin = "0";
this.elementPadding = this.element.style.padding;
this.element.style.padding = "0";
this.bodyMargin = bodyStyle.margin;
this.docMargin = docStyle.margin;
bodyStyle.margin = "0";
docStyle.margin = "0";
this.bodyPadding = bodyStyle.padding;
this.docPadding = docStyle.padding;
bodyStyle.padding = "0";
docStyle.padding = "0";
this.bodyWidth = bodyStyle.width;
this.docWidth = docStyle.width;
bodyStyle.width = "100%";
docStyle.width = "100%";
this.bodyHeight = bodyStyle.height;
this.docHeight = docStyle.height;
bodyStyle.height = "100%";
docStyle.height = "100%";
this.bodyDisplay = bodyStyle.display;
bodyStyle.display = "block";
//when entering full screen on the ipad it wasn't sufficient to leave
//the body intact as only only the top half of the screen would
//respond to touch events on the canvas, while the bottom half treated
//them as touch events on the document body. Thus we remove and store
//the bodies elements and replace them when we leave full screen.
this.previousBody = [];
THIS[ this.hash ].prevElementParent = this.element.parentNode;
THIS[ this.hash ].prevNextSibling = this.element.nextSibling;
THIS[ this.hash ].prevElementWidth = this.element.style.width;
THIS[ this.hash ].prevElementHeight = this.element.style.height;
nodes = body.childNodes.length;
for ( i = 0; i < nodes; i++ ) {
this.previousBody.push( body.childNodes[ 0 ] );
body.removeChild( body.childNodes[ 0 ] );
}
//If we've got a toolbar, we need to enable the user to use css to
//preserve it in fullpage mode
if ( this.toolbar && this.toolbar.element ) {
//save a reference to the parent so we can put it back
//in the long run we need a better strategy
this.toolbar.parentNode = this.toolbar.element.parentNode;
this.toolbar.nextSibling = this.toolbar.element.nextSibling;
body.appendChild( this.toolbar.element );
//Make sure the user has some ability to style the toolbar based
//on the mode
$.addClass( this.toolbar.element, 'fullpage' );
}
$.addClass( this.element, 'fullpage' );
body.appendChild( this.element );
this.element.style.height = '100vh';
this.element.style.width = '100vw';
if ( this.toolbar && this.toolbar.element ) {
this.element.style.height = (
$.getElementSize( this.element ).y - $.getElementSize( this.toolbar.element ).y
) + 'px';
}
THIS[ this.hash ].fullPage = true;
// mouse will be inside container now
$.delegate( this, onContainerEnter )( {} );
} else {
this.element.style.margin = this.elementMargin;
this.element.style.padding = this.elementPadding;
bodyStyle.margin = this.bodyMargin;
docStyle.margin = this.docMargin;
bodyStyle.padding = this.bodyPadding;
docStyle.padding = this.docPadding;
bodyStyle.width = this.bodyWidth;
docStyle.width = this.docWidth;
bodyStyle.height = this.bodyHeight;
docStyle.height = this.docHeight;
bodyStyle.display = this.bodyDisplay;
body.removeChild( this.element );
nodes = this.previousBody.length;
for ( i = 0; i < nodes; i++ ) {
body.appendChild( this.previousBody.shift() );
}
$.removeClass( this.element, 'fullpage' );
THIS[ this.hash ].prevElementParent.insertBefore(
this.element,
THIS[ this.hash ].prevNextSibling
);
//If we've got a toolbar, we need to enable the user to use css to
//reset it to its original state
if ( this.toolbar && this.toolbar.element ) {
body.removeChild( this.toolbar.element );
//Make sure the user has some ability to style the toolbar based
//on the mode
$.removeClass( this.toolbar.element, 'fullpage' );
this.toolbar.parentNode.insertBefore(
this.toolbar.element,
this.toolbar.nextSibling
);
delete this.toolbar.parentNode;
delete this.toolbar.nextSibling;
}
this.element.style.width = THIS[ this.hash ].prevElementWidth;
this.element.style.height = THIS[ this.hash ].prevElementHeight;
// After exiting fullPage or fullScreen, it can take some time
// before the browser can actually set the scroll.
var restoreScrollCounter = 0;
var restoreScroll = function() {
$.setPageScroll( _this.pageScroll );
var pageScroll = $.getPageScroll();
restoreScrollCounter++;
if (restoreScrollCounter < 10 &&
(pageScroll.x !== _this.pageScroll.x ||
pageScroll.y !== _this.pageScroll.y)) {
$.requestAnimationFrame( restoreScroll );
}
};
$.requestAnimationFrame( restoreScroll );
THIS[ this.hash ].fullPage = false;
// mouse will likely be outside now
$.delegate( this, onContainerLeave )( { } );
}
if ( this.navigator && this.viewport ) {
this.navigator.update( this.viewport );
}
/**
* Raised when the viewer has changed to/from full-page mode (see {@link OpenSeadragon.Viewer#setFullPage}).
*
* @event full-page
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullPage - True if changed to full-page mode, false if exited full-page mode.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'full-page', { fullPage: fullPage } );
return this;
},
/**
* Toggle full screen mode if supported. Toggle full page mode otherwise.
* @function
* @param {Boolean} fullScreen
* If true, enter full screen mode. If false, exit full screen mode.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:pre-full-screen
* @fires OpenSeadragon.Viewer.event:full-screen
*/
setFullScreen: function( fullScreen ) {
var _this = this;
if ( !$.supportsFullScreen ) {
return this.setFullPage( fullScreen );
}
if ( $.isFullScreen() === fullScreen ) {
return this;
}
var fullScreeEventArgs = {
fullScreen: fullScreen,
preventDefaultAction: false
};
/**
* Raised when the viewer is about to change to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
* Note: the pre-full-screen event is not raised when the user is exiting
* full-screen mode by pressing the Esc key. In that case, consider using
* the full-screen, pre-full-page or full-page events.
*
* @event pre-full-screen
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullScreen - True if entering full-screen mode, false if exiting full-screen mode.
* @property {Boolean} preventDefaultAction - Set to true to prevent full-screen mode change. Default: false.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'pre-full-screen', fullScreeEventArgs );
if ( fullScreeEventArgs.preventDefaultAction ) {
return this;
}
if ( fullScreen ) {
this.setFullPage( true );
// If the full page mode is not actually entered, we need to prevent
// the full screen mode.
if ( !this.isFullPage() ) {
return this;
}
this.fullPageStyleWidth = this.element.style.width;
this.fullPageStyleHeight = this.element.style.height;
this.element.style.width = '100%';
this.element.style.height = '100%';
var onFullScreenChange = function() {
var isFullScreen = $.isFullScreen();
if ( !isFullScreen ) {
$.removeEvent( document, $.fullScreenEventName, onFullScreenChange );
$.removeEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
_this.setFullPage( false );
if ( _this.isFullPage() ) {
_this.element.style.width = _this.fullPageStyleWidth;
_this.element.style.height = _this.fullPageStyleHeight;
}
}
if ( _this.navigator && _this.viewport ) {
//09/08/2018 - Fabroh : Fix issue #1504 : Ensure to get the navigator updated on fullscreen out with custom location with a timeout
setTimeout(function(){
_this.navigator.update( _this.viewport );
});
}
/**
* Raised when the viewer has changed to/from full-screen mode (see {@link OpenSeadragon.Viewer#setFullScreen}).
*
* @event full-screen
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} fullScreen - True if changed to full-screen mode, false if exited full-screen mode.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'full-screen', { fullScreen: isFullScreen } );
};
$.addEvent( document, $.fullScreenEventName, onFullScreenChange );
$.addEvent( document, $.fullScreenErrorEventName, onFullScreenChange );
$.requestFullScreen( document.body );
} else {
$.exitFullScreen();
}
return this;
},
/**
* @function
* @returns {Boolean}
*/
isVisible: function () {
return this.container.style.visibility !== "hidden";
},
//
/**
* @function
* @returns {Boolean} returns true if the viewer is in fullscreen
*/
isFullScreen: function () {
return $.isFullScreen() && this.isFullPage();
},
/**
* @function
* @param {Boolean} visible
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:visible
*/
setVisible: function( visible ){
this.container.style.visibility = visible ? "" : "hidden";
/**
* Raised when the viewer is shown or hidden (see {@link OpenSeadragon.Viewer#setVisible}).
*
* @event visible
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Boolean} visible
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'visible', { visible: visible } );
return this;
},
/**
* Add a tiled image to the viewer.
* options.tileSource can be anything that {@link OpenSeadragon.Viewer#open}
* supports except arrays of images.
* Note that you can specify options.width or options.height, but not both.
* The other dimension will be calculated according to the item's aspect ratio.
* If collectionMode is on (see {@link OpenSeadragon.Options}), the new image is
* automatically arranged with the others.
* @function
* @param {Object} options
* @param {String|Object|Function} options.tileSource - The TileSource specifier.
* A String implies a url used to determine the tileSource implementation
* based on the file extension of url. JSONP is implied by *.js,
* otherwise the url is retrieved as text and the resulting text is
* introspected to determine if its json, xml, or text and parsed.
* An Object implies an inline configuration which has a single
* property sufficient for being able to determine tileSource
* implementation. If the object has a property which is a function
* named 'getTileUrl', it is treated as a custom TileSource.
* @param {Number} [options.index] The index of the item. Added on top of
* all other items if not specified.
* @param {Boolean} [options.replace=false] If true, the item at options.index will be
* removed and the new item is added in its place. options.tileSource will be
* interpreted and fetched if necessary before the old item is removed to avoid leaving
* a gap in the world.
* @param {Number} [options.x=0] The X position for the image in viewport coordinates.
* @param {Number} [options.y=0] The Y position for the image in viewport coordinates.
* @param {Number} [options.width=1] The width for the image in viewport coordinates.
* @param {Number} [options.height] The height for the image in viewport coordinates.
* @param {OpenSeadragon.Rect} [options.fitBounds] The bounds in viewport coordinates
* to fit the image into. If specified, x, y, width and height get ignored.
* @param {OpenSeadragon.Placement} [options.fitBoundsPlacement=OpenSeadragon.Placement.CENTER]
* How to anchor the image in the bounds if options.fitBounds is set.
* @param {OpenSeadragon.Rect} [options.clip] - An area, in image pixels, to clip to
* (portions of the image outside of this area will not be visible). Only works on
* browsers that support the HTML5 canvas.
* @param {Number} [options.opacity=1] Proportional opacity of the tiled images (1=opaque, 0=hidden)
* @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks)
* @param {Number} [options.degrees=0] Initial rotation of the tiled image around
* its top left corner in degrees.
* @param {Boolean} [options.flipped=false] Whether to horizontally flip the image.
* @param {String} [options.compositeOperation] How the image is composited onto other images.
* @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image,
* overriding viewer.crossOriginPolicy.
* @param {Boolean} [options.ajaxWithCredentials] Whether to set withCredentials on tile AJAX
* @param {Boolean} [options.loadTilesWithAjax]
* Whether to load tile data using AJAX requests.
* Defaults to the setting in {@link OpenSeadragon.Options}.
* @param {Object} [options.ajaxHeaders]
* A set of headers to include when making tile AJAX requests.
* Note that these headers will be merged over any headers specified in {@link OpenSeadragon.Options}.
* Specifying a falsy value for a header will clear its existing value set at the Viewer level (if any).
* @param {Function} [options.success] A function that gets called when the image is
* successfully added. It's passed the event object which contains a single property:
* "item", which is the resulting instance of TiledImage.
* @param {Function} [options.error] A function that gets called if the image is
* unable to be added. It's passed the error event object, which contains "message"
* and "source" properties.
* @param {Boolean} [options.collectionImmediately=false] If collectionMode is on,
* specifies whether to snap to the new arrangement immediately or to animate to it.
* @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}.
* @fires OpenSeadragon.World.event:add-item
* @fires OpenSeadragon.Viewer.event:add-item-failed
*/
addTiledImage: function( options ) {
$.console.assert(options, "[Viewer.addTiledImage] options is required");
$.console.assert(options.tileSource, "[Viewer.addTiledImage] options.tileSource is required");
$.console.assert(!options.replace || (options.index > -1 && options.index < this.world.getItemCount()),
"[Viewer.addTiledImage] if options.replace is used, options.index must be a valid index in Viewer.world");
var _this = this;
if (options.replace) {
options.replaceItem = _this.world.getItemAt(options.index);
}
this._hideMessage();
if (options.placeholderFillStyle === undefined) {
options.placeholderFillStyle = this.placeholderFillStyle;
}
if (options.opacity === undefined) {
options.opacity = this.opacity;
}
if (options.preload === undefined) {
options.preload = this.preload;
}
if (options.compositeOperation === undefined) {
options.compositeOperation = this.compositeOperation;
}
if (options.crossOriginPolicy === undefined) {
options.crossOriginPolicy = options.tileSource.crossOriginPolicy !== undefined ? options.tileSource.crossOriginPolicy : this.crossOriginPolicy;
}
if (options.ajaxWithCredentials === undefined) {
options.ajaxWithCredentials = this.ajaxWithCredentials;
}
if (options.loadTilesWithAjax === undefined) {
options.loadTilesWithAjax = this.loadTilesWithAjax;
}
if (!$.isPlainObject(options.ajaxHeaders)) {
options.ajaxHeaders = {};
}
var myQueueItem = {
options: options
};
function raiseAddItemFailed( event ) {
for (var i = 0; i < _this._loadQueue.length; i++) {
if (_this._loadQueue[i] === myQueueItem) {
_this._loadQueue.splice(i, 1);
break;
}
}
if (_this._loadQueue.length === 0) {
refreshWorld(myQueueItem);
}
/**
* Raised when an error occurs while adding a item.
* @event add-item-failed
* @memberOf OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {String} message
* @property {String} source
* @property {Object} options The options passed to the addTiledImage method.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
_this.raiseEvent( 'add-item-failed', event );
if (options.error) {
options.error(event);
}
}
function refreshWorld(theItem) {
if (_this.collectionMode) {
_this.world.arrange({
immediately: theItem.options.collectionImmediately,
rows: _this.collectionRows,
columns: _this.collectionColumns,
layout: _this.collectionLayout,
tileSize: _this.collectionTileSize,
tileMargin: _this.collectionTileMargin
});
_this.world.setAutoRefigureSizes(true);
}
}
if ($.isArray(options.tileSource)) {
setTimeout(function() {
raiseAddItemFailed({
message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.",
source: options.tileSource,
options: options
});
});
return;
}
this._loadQueue.push(myQueueItem);
function processReadyItems() {
var queueItem, tiledImage, optionsClone;
while (_this._loadQueue.length) {
queueItem = _this._loadQueue[0];
if (!queueItem.tileSource) {
break;
}
_this._loadQueue.splice(0, 1);
if (queueItem.options.replace) {
var newIndex = _this.world.getIndexOfItem(queueItem.options.replaceItem);
if (newIndex !== -1) {
queueItem.options.index = newIndex;
}
_this.world.removeItem(queueItem.options.replaceItem);
}
tiledImage = new $.TiledImage({
viewer: _this,
source: queueItem.tileSource,
viewport: _this.viewport,
drawer: _this.drawer,
tileCache: _this.tileCache,
imageLoader: _this.imageLoader,
x: queueItem.options.x,
y: queueItem.options.y,
width: queueItem.options.width,
height: queueItem.options.height,
fitBounds: queueItem.options.fitBounds,
fitBoundsPlacement: queueItem.options.fitBoundsPlacement,
clip: queueItem.options.clip,
placeholderFillStyle: queueItem.options.placeholderFillStyle,
opacity: queueItem.options.opacity,
preload: queueItem.options.preload,
degrees: queueItem.options.degrees,
flipped: queueItem.options.flipped,
compositeOperation: queueItem.options.compositeOperation,
springStiffness: _this.springStiffness,
animationTime: _this.animationTime,
minZoomImageRatio: _this.minZoomImageRatio,
wrapHorizontal: _this.wrapHorizontal,
wrapVertical: _this.wrapVertical,
maxTilesPerFrame: _this.maxTilesPerFrame,
immediateRender: _this.immediateRender,
blendTime: _this.blendTime,
alwaysBlend: _this.alwaysBlend,
minPixelRatio: _this.minPixelRatio,
smoothTileEdgesMinZoom: _this.smoothTileEdgesMinZoom,
iOSDevice: _this.iOSDevice,
crossOriginPolicy: queueItem.options.crossOriginPolicy,
ajaxWithCredentials: queueItem.options.ajaxWithCredentials,
loadTilesWithAjax: queueItem.options.loadTilesWithAjax,
ajaxHeaders: queueItem.options.ajaxHeaders,
debugMode: _this.debugMode,
subPixelRoundingForTransparency: _this.subPixelRoundingForTransparency
});
if (_this.collectionMode) {
_this.world.setAutoRefigureSizes(false);
}
if (_this.navigator) {
optionsClone = $.extend({}, queueItem.options, {
replace: false, // navigator already removed the layer, nothing to replace
originalTiledImage: tiledImage,
tileSource: queueItem.tileSource
});
_this.navigator.addTiledImage(optionsClone);
}
_this.world.addItem( tiledImage, {
index: queueItem.options.index
});
if (_this._loadQueue.length === 0) {
//this restores the autoRefigureSizes flag to true.
refreshWorld(queueItem);
}
if (_this.world.getItemCount() === 1 && !_this.preserveViewport) {
_this.viewport.goHome(true);
}
if (queueItem.options.success) {
queueItem.options.success({
item: tiledImage
});
}
}
}
getTileSourceImplementation( this, options.tileSource, options, function( tileSource ) {
myQueueItem.tileSource = tileSource;
// add everybody at the front of the queue that's ready to go
processReadyItems();
}, function( event ) {
event.options = options;
raiseAddItemFailed(event);
// add everybody at the front of the queue that's ready to go
processReadyItems();
} );
},
/**
* Add a simple image to the viewer.
* The options are the same as the ones in {@link OpenSeadragon.Viewer#addTiledImage}
* except for options.tileSource which is replaced by options.url.
* @function
* @param {Object} options - See {@link OpenSeadragon.Viewer#addTiledImage}
* for all the options
* @param {String} options.url - The URL of the image to add.
* @fires OpenSeadragon.World.event:add-item
* @fires OpenSeadragon.Viewer.event:add-item-failed
*/
addSimpleImage: function(options) {
$.console.assert(options, "[Viewer.addSimpleImage] options is required");
$.console.assert(options.url, "[Viewer.addSimpleImage] options.url is required");
var opts = $.extend({}, options, {
tileSource: {
type: 'image',
url: options.url
}
});
delete opts.url;
this.addTiledImage(opts);
},
// deprecated
addLayer: function( options ) {
var _this = this;
$.console.error( "[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead." );
var optionsClone = $.extend({}, options, {
success: function(event) {
_this.raiseEvent("add-layer", {
options: options,
drawer: event.item
});
},
error: function(event) {
_this.raiseEvent("add-layer-failed", event);
}
});
this.addTiledImage(optionsClone);
return this;
},
// deprecated
getLayerAtLevel: function( level ) {
$.console.error( "[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead." );
return this.world.getItemAt(level);
},
// deprecated
getLevelOfLayer: function( drawer ) {
$.console.error( "[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead." );
return this.world.getIndexOfItem(drawer);
},
// deprecated
getLayersCount: function() {
$.console.error( "[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead." );
return this.world.getItemCount();
},
// deprecated
setLayerLevel: function( drawer, level ) {
$.console.error( "[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead." );
return this.world.setItemIndex(drawer, level);
},
// deprecated
removeLayer: function( drawer ) {
$.console.error( "[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead." );
return this.world.removeItem(drawer);
},
/**
* Force the viewer to redraw its contents.
* @returns {OpenSeadragon.Viewer} Chainable.
*/
forceRedraw: function() {
THIS[ this.hash ].forceRedraw = true;
return this;
},
/**
* Force the viewer to reset its size to match its container.
*/
forceResize: function() {
THIS[this.hash].needsResize = true;
THIS[this.hash].forceResize = true;
},
/**
* @function
* @returns {OpenSeadragon.Viewer} Chainable.
*/
bindSequenceControls: function(){
//////////////////////////////////////////////////////////////////////////
// Image Sequence Controls
//////////////////////////////////////////////////////////////////////////
var onFocusHandler = $.delegate( this, onFocus ),
onBlurHandler = $.delegate( this, onBlur ),
onNextHandler = $.delegate( this, this.goToNextPage ),
onPreviousHandler = $.delegate( this, this.goToPreviousPage ),
navImages = this.navImages,
useGroup = true;
if( this.showSequenceControl ){
if( this.previousButton || this.nextButton ){
//if we are binding to custom buttons then layout and
//grouping is the responsibility of the page author
useGroup = false;
}
this.previousButton = new $.Button({
element: this.previousButton ? $.getElement( this.previousButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.PreviousPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.previous.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.previous.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.previous.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.previous.DOWN ),
onRelease: onPreviousHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
});
this.nextButton = new $.Button({
element: this.nextButton ? $.getElement( this.nextButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.NextPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.next.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.next.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.next.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.next.DOWN ),
onRelease: onNextHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
});
if( !this.navPrevNextWrap ){
this.previousButton.disable();
}
if (!this.tileSources || !this.tileSources.length) {
this.nextButton.disable();
}
if( useGroup ){
this.paging = new $.ButtonGroup({
buttons: [
this.previousButton,
this.nextButton
],
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold
});
this.pagingControl = this.paging.element;
if( this.toolbar ){
this.toolbar.addControl(
this.pagingControl,
{anchor: $.ControlAnchor.BOTTOM_RIGHT}
);
}else{
this.addControl(
this.pagingControl,
{anchor: this.sequenceControlAnchor || $.ControlAnchor.TOP_LEFT}
);
}
}
}
return this;
},
/**
* @function
* @returns {OpenSeadragon.Viewer} Chainable.
*/
bindStandardControls: function(){
//////////////////////////////////////////////////////////////////////////
// Navigation Controls
//////////////////////////////////////////////////////////////////////////
var beginZoomingInHandler = $.delegate( this, beginZoomingIn ),
endZoomingHandler = $.delegate( this, endZooming ),
doSingleZoomInHandler = $.delegate( this, doSingleZoomIn ),
beginZoomingOutHandler = $.delegate( this, beginZoomingOut ),
doSingleZoomOutHandler = $.delegate( this, doSingleZoomOut ),
onHomeHandler = $.delegate( this, onHome ),
onFullScreenHandler = $.delegate( this, onFullScreen ),
onRotateLeftHandler = $.delegate( this, onRotateLeft ),
onRotateRightHandler = $.delegate( this, onRotateRight ),
onFlipHandler = $.delegate( this, onFlip),
onFocusHandler = $.delegate( this, onFocus ),
onBlurHandler = $.delegate( this, onBlur ),
navImages = this.navImages,
buttons = [],
useGroup = true;
if ( this.showNavigationControl ) {
if( this.zoomInButton || this.zoomOutButton ||
this.homeButton || this.fullPageButton ||
this.rotateLeftButton || this.rotateRightButton ||
this.flipButton ) {
//if we are binding to custom buttons then layout and
//grouping is the responsibility of the page author
useGroup = false;
}
if ( this.showZoomControl ) {
buttons.push( this.zoomInButton = new $.Button({
element: this.zoomInButton ? $.getElement( this.zoomInButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.ZoomIn" ),
srcRest: resolveUrl( this.prefixUrl, navImages.zoomIn.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.zoomIn.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.zoomIn.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.zoomIn.DOWN ),
onPress: beginZoomingInHandler,
onRelease: endZoomingHandler,
onClick: doSingleZoomInHandler,
onEnter: beginZoomingInHandler,
onExit: endZoomingHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
buttons.push( this.zoomOutButton = new $.Button({
element: this.zoomOutButton ? $.getElement( this.zoomOutButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.ZoomOut" ),
srcRest: resolveUrl( this.prefixUrl, navImages.zoomOut.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.zoomOut.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.zoomOut.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.zoomOut.DOWN ),
onPress: beginZoomingOutHandler,
onRelease: endZoomingHandler,
onClick: doSingleZoomOutHandler,
onEnter: beginZoomingOutHandler,
onExit: endZoomingHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showHomeControl ) {
buttons.push( this.homeButton = new $.Button({
element: this.homeButton ? $.getElement( this.homeButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.Home" ),
srcRest: resolveUrl( this.prefixUrl, navImages.home.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.home.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.home.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.home.DOWN ),
onRelease: onHomeHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showFullPageControl ) {
buttons.push( this.fullPageButton = new $.Button({
element: this.fullPageButton ? $.getElement( this.fullPageButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.FullPage" ),
srcRest: resolveUrl( this.prefixUrl, navImages.fullpage.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.fullpage.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.fullpage.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.fullpage.DOWN ),
onRelease: onFullScreenHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showRotationControl ) {
buttons.push( this.rotateLeftButton = new $.Button({
element: this.rotateLeftButton ? $.getElement( this.rotateLeftButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.RotateLeft" ),
srcRest: resolveUrl( this.prefixUrl, navImages.rotateleft.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.rotateleft.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.rotateleft.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.rotateleft.DOWN ),
onRelease: onRotateLeftHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
buttons.push( this.rotateRightButton = new $.Button({
element: this.rotateRightButton ? $.getElement( this.rotateRightButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.RotateRight" ),
srcRest: resolveUrl( this.prefixUrl, navImages.rotateright.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.rotateright.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.rotateright.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.rotateright.DOWN ),
onRelease: onRotateRightHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( this.showFlipControl ) {
buttons.push( this.flipButton = new $.Button({
element: this.flipButton ? $.getElement( this.flipButton ) : null,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold,
tooltip: $.getString( "Tooltips.Flip" ),
srcRest: resolveUrl( this.prefixUrl, navImages.flip.REST ),
srcGroup: resolveUrl( this.prefixUrl, navImages.flip.GROUP ),
srcHover: resolveUrl( this.prefixUrl, navImages.flip.HOVER ),
srcDown: resolveUrl( this.prefixUrl, navImages.flip.DOWN ),
onRelease: onFlipHandler,
onFocus: onFocusHandler,
onBlur: onBlurHandler
}));
}
if ( useGroup ) {
this.buttonGroup = new $.ButtonGroup({
buttons: buttons,
clickTimeThreshold: this.clickTimeThreshold,
clickDistThreshold: this.clickDistThreshold
});
this.navControl = this.buttonGroup.element;
this.addHandler( 'open', $.delegate( this, lightUp ) );
if( this.toolbar ){
this.toolbar.addControl(
this.navControl,
{anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT}
);
} else {
this.addControl(
this.navControl,
{anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT}
);
}
} else {
this.customButtons = buttons;
}
}
return this;
},
/**
* Gets the active page of a sequence
* @function
* @returns {Number}
*/
currentPage: function() {
return this._sequenceIndex;
},
/**
* @function
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:page
*/
goToPage: function( page ){
if( this.tileSources && page >= 0 && page < this.tileSources.length ){
this._sequenceIndex = page;
this._updateSequenceButtons( page );
this.open( this.tileSources[ page ] );
if( this.referenceStrip ){
this.referenceStrip.setFocus( page );
}
/**
* Raised when the page is changed on a viewer configured with multiple image sources (see {@link OpenSeadragon.Viewer#goToPage}).
*
* @event page
* @memberof OpenSeadragon.Viewer
* @type {Object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Number} page - The page index.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'page', { page: page } );
}
return this;
},
/**
* Adds an html element as an overlay to the current viewport. Useful for
* highlighting words or areas of interest on an image or other zoomable
* interface. The overlays added via this method are removed when the viewport
* is closed which include when changing page.
* @method
* @param {Element|String|Object} element - A reference to an element or an id for
* the element which will be overlaid. Or an Object specifying the configuration for the overlay.
* If using an object, see {@link OpenSeadragon.Overlay} for a list of
* all available options.
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlaid. This is a viewport relative location.
* @param {OpenSeadragon.Placement} [placement=OpenSeadragon.Placement.TOP_LEFT] - The position of the
* viewport which the location coordinates will be treated as relative
* to.
* @param {function} [onDraw] - If supplied the callback is called when the overlay
* needs to be drawn. It is the responsibility of the callback to do any drawing/positioning.
* It is passed position, size and element.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:add-overlay
*/
addOverlay: function( element, location, placement, onDraw ) {
var options;
if( $.isPlainObject( element ) ){
options = element;
} else {
options = {
element: element,
location: location,
placement: placement,
onDraw: onDraw
};
}
element = $.getElement( options.element );
if ( getOverlayIndex( this.currentOverlays, element ) >= 0 ) {
// they're trying to add a duplicate overlay
return this;
}
var overlay = getOverlayObject( this, options);
this.currentOverlays.push(overlay);
overlay.drawHTML( this.overlaysContainer, this.viewport );
/**
* Raised when an overlay is added to the viewer (see {@link OpenSeadragon.Viewer#addOverlay}).
*
* @event add-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {Element} element - The overlay element.
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.Placement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'add-overlay', {
element: element,
location: options.location,
placement: options.placement
});
return this;
},
/**
* Updates the overlay represented by the reference to the element or
* element id moving it to the new location, relative to the new placement.
* @method
* @param {Element|String} element - A reference to an element or an id for
* the element which is overlaid.
* @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
* rectangle which will be overlaid. This is a viewport relative location.
* @param {OpenSeadragon.Placement} [placement=OpenSeadragon.Placement.TOP_LEFT] - The position of the
* viewport which the location coordinates will be treated as relative
* to.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:update-overlay
*/
updateOverlay: function( element, location, placement ) {
var i;
element = $.getElement( element );
i = getOverlayIndex( this.currentOverlays, element );
if ( i >= 0 ) {
this.currentOverlays[ i ].update( location, placement );
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when an overlay's location or placement changes
* (see {@link OpenSeadragon.Viewer#updateOverlay}).
*
* @event update-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the
* Viewer which raised the event.
* @property {Element} element
* @property {OpenSeadragon.Point|OpenSeadragon.Rect} location
* @property {OpenSeadragon.Placement} placement
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'update-overlay', {
element: element,
location: location,
placement: placement
});
}
return this;
},
/**
* Removes an overlay identified by the reference element or element id
* and schedules an update.
* @method
* @param {Element|String} element - A reference to the element or an
* element id which represent the ovelay content to be removed.
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:remove-overlay
*/
removeOverlay: function( element ) {
var i;
element = $.getElement( element );
i = getOverlayIndex( this.currentOverlays, element );
if ( i >= 0 ) {
this.currentOverlays[ i ].destroy();
this.currentOverlays.splice( i, 1 );
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when an overlay is removed from the viewer
* (see {@link OpenSeadragon.Viewer#removeOverlay}).
*
* @event remove-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the
* Viewer which raised the event.
* @property {Element} element - The overlay element.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'remove-overlay', {
element: element
});
}
return this;
},
/**
* Removes all currently configured Overlays from this Viewer and schedules
* an update.
* @method
* @returns {OpenSeadragon.Viewer} Chainable.
* @fires OpenSeadragon.Viewer.event:clear-overlay
*/
clearOverlays: function() {
while ( this.currentOverlays.length > 0 ) {
this.currentOverlays.pop().destroy();
}
THIS[ this.hash ].forceRedraw = true;
/**
* Raised when all overlays are removed from the viewer (see {@link OpenSeadragon.Drawer#clearOverlays}).
*
* @event clear-overlay
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'clear-overlay', {} );
return this;
},
/**
* Finds an overlay identified by the reference element or element id
* and returns it as an object, return null if not found.
* @method
* @param {Element|String} element - A reference to the element or an
* element id which represents the overlay content.
* @returns {OpenSeadragon.Overlay} the matching overlay or null if none found.
*/
getOverlayById: function( element ) {
var i;
element = $.getElement( element );
i = getOverlayIndex( this.currentOverlays, element );
if (i >= 0) {
return this.currentOverlays[i];
} else {
return null;
}
},
/**
* Updates the sequence buttons.
* @function OpenSeadragon.Viewer.prototype._updateSequenceButtons
* @private
* @param {Number} Sequence Value
*/
_updateSequenceButtons: function( page ) {
if ( this.nextButton ) {
if(!this.tileSources || this.tileSources.length - 1 === page) {
//Disable next button
if ( !this.navPrevNextWrap ) {
this.nextButton.disable();
}
} else {
this.nextButton.enable();
}
}
if ( this.previousButton ) {
if ( page > 0 ) {
//Enable previous button
this.previousButton.enable();
} else {
if ( !this.navPrevNextWrap ) {
this.previousButton.disable();
}
}
}
},
/**
* Display a message in the viewport
* @function OpenSeadragon.Viewer.prototype._showMessage
* @private
* @param {String} text message
*/
_showMessage: function ( message ) {
this._hideMessage();
var div = $.makeNeutralElement( "div" );
div.appendChild( document.createTextNode( message ) );
this.messageDiv = $.makeCenteredNode( div );
$.addClass(this.messageDiv, "openseadragon-message");
this.container.appendChild( this.messageDiv );
},
/**
* Hide any currently displayed viewport message
* @function OpenSeadragon.Viewer.prototype._hideMessage
* @private
*/
_hideMessage: function () {
var div = this.messageDiv;
if (div) {
div.parentNode.removeChild(div);
delete this.messageDiv;
}
},
/**
* Gets this viewer's gesture settings for the given pointer device type.
* @method
* @param {String} type - The pointer device type to get the gesture settings for ("mouse", "touch", "pen", etc.).
* @returns {OpenSeadragon.GestureSettings}
*/
gestureSettingsByDeviceType: function ( type ) {
switch ( type ) {
case 'mouse':
return this.gestureSettingsMouse;
case 'touch':
return this.gestureSettingsTouch;
case 'pen':
return this.gestureSettingsPen;
default:
return this.gestureSettingsUnknown;
}
},
// private
_drawOverlays: function() {
var i,
length = this.currentOverlays.length;
for ( i = 0; i < length; i++ ) {
this.currentOverlays[ i ].drawHTML( this.overlaysContainer, this.viewport );
}
},
/**
* Cancel the "in flight" images.
*/
_cancelPendingImages: function() {
this._loadQueue = [];
},
/**
* Removes the reference strip and disables displaying it.
* @function
*/
removeReferenceStrip: function() {
this.showReferenceStrip = false;
if (this.referenceStrip) {
this.referenceStrip.destroy();
this.referenceStrip = null;
}
},
/**
* Enables and displays the reference strip based on the currently set tileSources.
* Works only when the Viewer has sequenceMode set to true.
* @function
*/
addReferenceStrip: function() {
this.showReferenceStrip = true;
if (this.sequenceMode) {
if (this.referenceStrip) {
return;
}
if (this.tileSources.length && this.tileSources.length > 1) {
this.referenceStrip = new $.ReferenceStrip({
id: this.referenceStripElement,
position: this.referenceStripPosition,
sizeRatio: this.referenceStripSizeRatio,
scroll: this.referenceStripScroll,
height: this.referenceStripHeight,
width: this.referenceStripWidth,
tileSources: this.tileSources,
prefixUrl: this.prefixUrl,
viewer: this
});
this.referenceStrip.setFocus( this._sequenceIndex );
}
} else {
$.console.warn('Attempting to display a reference strip while "sequenceMode" is off.');
}
},
/**
* Adds _updatePixelDensityRatio to the window resize event.
* @private
*/
_addUpdatePixelDensityRatioEvent: function() {
this._updatePixelDensityRatioBind = this._updatePixelDensityRatio.bind(this);
$.addEvent( window, 'resize', this._updatePixelDensityRatioBind );
},
/**
* Removes _updatePixelDensityRatio from the window resize event.
* @private
*/
_removeUpdatePixelDensityRatioEvent: function() {
$.removeEvent( window, 'resize', this._updatePixelDensityRatioBind );
},
/**
* Update pixel density ratio, clears all tiles and triggers updates for
* all items if the ratio has changed.
* @private
*/
_updatePixelDensityRatio: function() {
var previusPixelDensityRatio = $.pixelDensityRatio;
var currentPixelDensityRatio = $.getCurrentPixelDensityRatio();
if (previusPixelDensityRatio !== currentPixelDensityRatio) {
$.pixelDensityRatio = currentPixelDensityRatio;
this.world.resetItems();
this.forceRedraw();
}
},
/**
* Sets the image source to the source with index equal to
* currentIndex - 1. Changes current image in sequence mode.
* If specified, wraps around (see navPrevNextWrap in
* {@link OpenSeadragon.Options})
*
* @method
*/
goToPreviousPage: function () {
var previous = this._sequenceIndex - 1;
if(this.navPrevNextWrap && previous < 0){
previous += this.tileSources.length;
}
this.goToPage( previous );
},
/**
* Sets the image source to the source with index equal to
* currentIndex + 1. Changes current image in sequence mode.
* If specified, wraps around (see navPrevNextWrap in
* {@link OpenSeadragon.Options})
*
* @method
*/
goToNextPage: function () {
var next = this._sequenceIndex + 1;
if(this.navPrevNextWrap && next >= this.tileSources.length){
next = 0;
}
this.goToPage( next );
},
isAnimating: function () {
return THIS[ this.hash ].animating;
},
});
/**
* _getSafeElemSize is like getElementSize(), but refuses to return 0 for x or y,
* which was causing some calling operations to return NaN.
* @returns {Point}
* @private
*/
function _getSafeElemSize (oElement) {
oElement = $.getElement( oElement );
return new $.Point(
(oElement.clientWidth === 0 ? 1 : oElement.clientWidth),
(oElement.clientHeight === 0 ? 1 : oElement.clientHeight)
);
}
/**
* @function
* @private
*/
function getTileSourceImplementation( viewer, tileSource, imgOptions, successCallback,
failCallback ) {
var _this = viewer;
//allow plain xml strings or json strings to be parsed here
if ( $.type( tileSource ) === 'string' ) {
//xml should start with "<" and end with ">"
if ( tileSource.match( /^\s*<.*>\s*$/ ) ) {
tileSource = $.parseXml( tileSource );
//json should start with "{" or "[" and end with "}" or "]"
} else if ( tileSource.match(/^\s*[{[].*[}\]]\s*$/ ) ) {
try {
var tileSourceJ = $.parseJSON(tileSource);
tileSource = tileSourceJ;
} catch (e) {
//tileSource = tileSource;
}
}
}
function waitUntilReady(tileSource, originalTileSource) {
if (tileSource.ready) {
successCallback(tileSource);
} else {
tileSource.addHandler('ready', function () {
successCallback(tileSource);
});
tileSource.addHandler('open-failed', function (event) {
failCallback({
message: event.message,
source: originalTileSource
});
});
}
}
setTimeout( function() {
if ( $.type( tileSource ) === 'string' ) {
//If its still a string it means it must be a url at this point
tileSource = new $.TileSource({
url: tileSource,
crossOriginPolicy: imgOptions.crossOriginPolicy !== undefined ?
imgOptions.crossOriginPolicy : viewer.crossOriginPolicy,
ajaxWithCredentials: viewer.ajaxWithCredentials,
ajaxHeaders: imgOptions.ajaxHeaders ?
imgOptions.ajaxHeaders : viewer.ajaxHeaders,
splitHashDataForPost: viewer.splitHashDataForPost,
success: function( event ) {
successCallback( event.tileSource );
}
});
tileSource.addHandler( 'open-failed', function( event ) {
failCallback( event );
} );
} else if ($.isPlainObject(tileSource) || tileSource.nodeType) {
if (tileSource.crossOriginPolicy === undefined &&
(imgOptions.crossOriginPolicy !== undefined || viewer.crossOriginPolicy !== undefined)) {
tileSource.crossOriginPolicy = imgOptions.crossOriginPolicy !== undefined ?
imgOptions.crossOriginPolicy : viewer.crossOriginPolicy;
}
if (tileSource.ajaxWithCredentials === undefined) {
tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials;
}
if ( $.isFunction( tileSource.getTileUrl ) ) {
//Custom tile source
var customTileSource = new $.TileSource( tileSource );
customTileSource.getTileUrl = tileSource.getTileUrl;
successCallback( customTileSource );
} else {
//inline configuration
var $TileSource = $.TileSource.determineType( _this, tileSource );
if ( !$TileSource ) {
failCallback( {
message: "Unable to load TileSource",
source: tileSource
});
return;
}
var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] );
waitUntilReady(new $TileSource(options), tileSource);
}
} else {
//can assume it's already a tile source implementation
waitUntilReady(tileSource, tileSource);
}
});
}
function getOverlayObject( viewer, overlay ) {
if ( overlay instanceof $.Overlay ) {
return overlay;
}
var element = null;
if ( overlay.element ) {
element = $.getElement( overlay.element );
} else {
var id = overlay.id ?
overlay.id :
"openseadragon-overlay-" + Math.floor( Math.random() * 10000000 );
element = $.getElement( overlay.id );
if ( !element ) {
element = document.createElement( "a" );
element.href = "#/overlay/" + id;
}
element.id = id;
$.addClass( element, overlay.className ?
overlay.className :
"openseadragon-overlay"
);
}
var location = overlay.location;
var width = overlay.width;
var height = overlay.height;
if (!location) {
var x = overlay.x;
var y = overlay.y;
if (overlay.px !== undefined) {
var rect = viewer.viewport.imageToViewportRectangle(new $.Rect(
overlay.px,
overlay.py,
width || 0,
height || 0));
x = rect.x;
y = rect.y;
width = width !== undefined ? rect.width : undefined;
height = height !== undefined ? rect.height : undefined;
}
location = new $.Point(x, y);
}
var placement = overlay.placement;
if (placement && $.type(placement) === "string") {
placement = $.Placement[overlay.placement.toUpperCase()];
}
return new $.Overlay({
element: element,
location: location,
placement: placement,
onDraw: overlay.onDraw,
checkResize: overlay.checkResize,
width: width,
height: height,
rotationMode: overlay.rotationMode
});
}
/**
* @private
* @inner
* Determines the index of the given overlay in the given overlays array.
*/
function getOverlayIndex( overlays, element ) {
var i;
for ( i = overlays.length - 1; i >= 0; i-- ) {
if ( overlays[ i ].element === element ) {
return i;
}
}
return -1;
}
///////////////////////////////////////////////////////////////////////////////
// Schedulers provide the general engine for animation
///////////////////////////////////////////////////////////////////////////////
function scheduleUpdate( viewer, updateFunc ){
return $.requestAnimationFrame( function(){
updateFunc( viewer );
} );
}
//provides a sequence in the fade animation
function scheduleControlsFade( viewer ) {
$.requestAnimationFrame( function(){
updateControlsFade( viewer );
});
}
//initiates an animation to hide the controls
function beginControlsAutoHide( viewer ) {
if ( !viewer.autoHideControls ) {
return;
}
viewer.controlsShouldFade = true;
viewer.controlsFadeBeginTime =
$.now() +
viewer.controlsFadeDelay;
window.setTimeout( function(){
scheduleControlsFade( viewer );
}, viewer.controlsFadeDelay );
}
//determines if fade animation is done or continues the animation
function updateControlsFade( viewer ) {
var currentTime,
deltaTime,
opacity,
i;
if ( viewer.controlsShouldFade ) {
currentTime = $.now();
deltaTime = currentTime - viewer.controlsFadeBeginTime;
opacity = 1.0 - deltaTime / viewer.controlsFadeLength;
opacity = Math.min( 1.0, opacity );
opacity = Math.max( 0.0, opacity );
for ( i = viewer.controls.length - 1; i >= 0; i--) {
if (viewer.controls[ i ].autoFade) {
viewer.controls[ i ].setOpacity( opacity );
}
}
if ( opacity > 0 ) {
// fade again
scheduleControlsFade( viewer );
}
}
}
//stop the fade animation on the controls and show them
function abortControlsAutoHide( viewer ) {
var i;
viewer.controlsShouldFade = false;
for ( i = viewer.controls.length - 1; i >= 0; i-- ) {
viewer.controls[ i ].setOpacity( 1.0 );
}
}
///////////////////////////////////////////////////////////////////////////////
// Default view event handlers.
///////////////////////////////////////////////////////////////////////////////
function onFocus(){
abortControlsAutoHide( this );
}
function onBlur(){
beginControlsAutoHide( this );
}
function onCanvasContextMenu( event ) {
var eventArgs = {
tracker: event.eventSource,
position: event.position,
originalEvent: event.originalEvent,
preventDefault: event.preventDefault
};
/**
* Raised when a contextmenu event occurs in the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-contextmenu
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event.
* @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element.
* @property {Object} originalEvent - The original DOM event.
* @property {Boolean} preventDefault - Set to true to prevent the default user-agent's handling of the contextmenu event.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent( 'canvas-contextmenu', eventArgs );
event.preventDefault = eventArgs.preventDefault;
}
function onCanvasKeyDown( event ) {
var canvasKeyDownEventArgs = {
originalEvent: event.originalEvent,
preventDefaultAction: false,
preventVerticalPan: event.preventVerticalPan || !this.panVertical,
preventHorizontalPan: event.preventHorizontalPan || !this.panHorizontal
};
/**
* Raised when a keyboard key is pressed and the focus is on the {@link OpenSeadragon.Viewer#canvas} element.
*
* @event canvas-key
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event.
* @property {Object} originalEvent - The original DOM event.
* @property {Boolean} preventDefaultAction - Set to true to prevent default keyboard behaviour. Default: false.
* @property {Boolean} preventVerticalPan - Set to true to prevent keyboard vertical panning. Default: false.
* @property {Boolean} preventHorizontalPan - Set to true to prevent keyboard horizontal panning. Default: false.
* @property {?Object} userData - Arbitrary subscriber-defined object.
*/
this.raiseEvent('canvas-key', canvasKeyDownEventArgs);