/*! Crocodoc Viewer - v0.4.3 | (c) 2014 Box */
var Crocodoc = (function ($) {
/*global Crocodoc:true*/
/**
* The one global object for Crocodoc JavaScript.
* @namespace
*/
var Crocodoc = (function () {
'use strict';
var components = {},
utilities = {};
/**
* Find circular dependencies in component mixins
* @param {string} componentName The component name that is being added
* @param {Array} dependencies Array of component mixin dependencies
* @param {void} path String used to keep track of depencency graph
* @returns {void}
*/
function findCircularDependencies(componentName, dependencies, path) {
var i;
path = path || componentName;
for (i = 0; i < dependencies.length; ++i) {
if (componentName === dependencies[i]) {
throw new Error('Circular dependency detected: ' + path + '->' + dependencies[i]);
} else if (components[dependencies[i]]) {
findCircularDependencies(componentName, components[dependencies[i]].mixins, path + '->' + dependencies[i]);
}
}
}
return {
// Zoom, scroll, page status, layout constants
ZOOM_FIT_WIDTH: 'fitwidth',
ZOOM_FIT_HEIGHT: 'fitheight',
ZOOM_AUTO: 'auto',
ZOOM_IN: 'in',
ZOOM_OUT: 'out',
SCROLL_PREVIOUS: 'previous',
SCROLL_NEXT: 'next',
LAYOUT_VERTICAL: 'vertical',
LAYOUT_VERTICAL_SINGLE_COLUMN: 'vertical-single-column',
LAYOUT_HORIZONTAL: 'horizontal',
LAYOUT_PRESENTATION: 'presentation',
LAYOUT_PRESENTATION_TWO_PAGE: 'presentation-two-page',
PAGE_STATUS_CONVERTING: 'converting',
PAGE_STATUS_NOT_LOADED: 'not loaded',
PAGE_STATUS_LOADING: 'loading',
PAGE_STATUS_LOADED: 'loaded',
PAGE_STATUS_ERROR: 'error',
// exposed for testing purposes only
// should not be accessed directly otherwise
components: components,
utilities: utilities,
/**
* Create and return a viewer instance initialized with the given parameters
* @param {string|Element|jQuery} el The element to bind the viewer to
* @param {Object} config The viewer configuration parameters
* @returns {Object} The viewer instance
*/
createViewer: function (el, config) {
return new Crocodoc.Viewer(el, config);
},
/**
* Register a new component
* @param {string} name The (unique) name of the component
* @param {Array} mixins Array of component names to instantiate and pass as mixinable objects to the creator method
* @param {Function} creator Factory function used to create an instance of the component
* @returns {void}
*/
addComponent: function (name, mixins, creator) {
if (mixins instanceof Function) {
creator = mixins;
mixins = [];
}
// make sure this component won't cause a circular mixin dependency
findCircularDependencies(name, mixins);
components[name] = {
mixins: mixins,
creator: creator
};
},
/**
* Create and return an instance of the named component
* @param {string} name The name of the component to create
* @param {Crocodoc.Scope} scope The scope object to create the component on
* @returns {?Object} The component instance or null if the component doesn't exist
*/
createComponent: function (name, scope) {
var component = components[name];
if (component) {
var args = [];
for (var i = 0; i < component.mixins.length; ++i) {
args.push(this.createComponent(component.mixins[i], scope));
}
args.unshift(scope);
return component.creator.apply(component.creator, args);
}
return null;
},
/**
* Register a new Crocodoc plugin
* @param {string} name The (unique) name of the plugin
* @param {Function} creator Factory function used to create an instance of the plugin
* @returns {void}
*/
addPlugin: function (name, creator) {
this.addComponent('plugin-' + name, creator);
},
/**
* Register a new utility
* @param {string} name The (unique) name of the utility
* @param {Function} creator Factory function used to create an instance of the utility
* @returns {void}
*/
addUtility: function (name, creator) {
utilities[name] = {
creator: creator,
instance: null
};
},
/**
* Retrieve the named utility
* @param {string} name The name of the utility to retrieve
* @returns {?Object} The utility or null if the utility doesn't exist
*/
getUtility: function (name) {
var utility = utilities[name];
if (utility) {
if (!utility.instance) {
utility.instance = utility.creator(this);
}
return utility.instance;
}
return null;
}
};
})();
(function () {
'use strict';
/**
* Scope class used for component scoping (creating, destroying, broadcasting messages)
* @constructor
*/
Crocodoc.Scope = function Scope(config) {
var util = Crocodoc.getUtility('common');
var instances = [];
/**
* Create and return an instance of the named component,
* and add it to the list of instances in this scope
* @param {string} componentName The name of the component to create
* @returns {?Object} The component instance or null if the component doesn't exist
*/
this.createComponent = function (componentName) {
var instance = Crocodoc.createComponent(componentName, this);
if (instance) {
instance.componentName = componentName;
instances.push(instance);
}
return instance;
};
/**
* Remove and call the destroy method on a component instance
* @param {Object} instance The component instance to remove
* @returns {void}
*/
this.destroyComponent = function (instance) {
var i, len;
for (i = 0, len = instances.length; i < len; ++i) {
if (instance === instances[i]) {
if (typeof instance.destroy === 'function') {
instance.destroy();
}
instances.splice(i, 1);
break;
}
}
};
/**
* Remove and call the destroy method on all instances in this scope
* @returns {void}
*/
this.destroy = function () {
var i, len, instance;
for (i = 0, len = instances.length; i < len; ++i) {
instance = instances[i];
if (typeof instance.destroy === 'function') {
instance.destroy();
}
}
instances = [];
};
/**
* Broadcast a message to all components in this scope that have registered
* a listener for the named message type
* @param {string} messageName The message name
* @param {any} data The message data
* @returns {void}
*/
this.broadcast = function (messageName, data) {
var i, len, instance, messages;
for (i = 0, len = instances.length; i < len; ++i) {
instance = instances[i];
if (!instance) {
continue;
}
messages = instance.messages || [];
if (util.inArray(messageName, messages) !== -1) {
if (typeof instance.onmessage === 'function') {
instance.onmessage.call(instance, messageName, data);
}
}
}
};
/**
* Passthrough method to the framework that retrieves utilities.
* @param {string} name The name of the utility to retrieve
* @returns {?Object} An object if the utility is found or null if not
*/
this.getUtility = function (name) {
return Crocodoc.getUtility(name);
};
/**
* Get the config object associated with this scope
* @returns {Object} The config object
*/
this.getConfig = function () {
return config;
};
};
})();
(function () {
'use strict';
/**
* An object that is capable of generating custom events and also
* executing handlers for events when they occur.
* @constructor
*/
Crocodoc.EventTarget = function() {
/**
* Map of events to handlers. The keys in the object are the event names.
* The values in the object are arrays of event handler functions.
* @type {Object}
* @private
*/
this._handlers = {};
};
Crocodoc.EventTarget.prototype = {
// restore constructor
constructor: Crocodoc.EventTarget,
/**
* Adds a new event handler for a particular type of event.
* @param {string} type The name of the event to listen for.
* @param {Function} handler The function to call when the event occurs.
* @returns {void}
*/
on: function(type, handler) {
if (typeof this._handlers[type] === 'undefined') {
this._handlers[type] = [];
}
this._handlers[type].push(handler);
},
/**
* Fires an event with the given name and data.
* @param {string} type The type of event to fire.
* @param {Object} data An object with properties that should end up on
* the event object for the given event.
* @returns {void}
*/
fire: function(type, data) {
var handlers,
i,
len,
event = {
type: type,
data: data
};
// if there are handlers for the event, call them in order
handlers = this._handlers[event.type];
if (handlers instanceof Array) {
// @NOTE: do a concat() here to create a copy of the handlers array,
// so that if another handler is removed of the same type, it doesn't
// interfere with the handlers array
handlers = handlers.concat();
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]) {
handlers[i].call(this, event);
}
}
}
// call handlers for `all` event type
handlers = this._handlers.all;
if (handlers instanceof Array) {
// @NOTE: do a concat() here to create a copy of the handlers array,
// so that if another handler is removed of the same type, it doesn't
// interfere with the handlers array
handlers = handlers.concat();
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]) {
handlers[i].call(this, event);
}
}
}
},
/**
* Removes an event handler from a given event.
* If the handler is not provided, remove all handlers of the given type.
* @param {string} type The name of the event to remove from.
* @param {Function} handler The function to remove as a handler.
* @returns {void}
*/
off: function(type, handler) {
var handlers = this._handlers[type],
i,
len;
if (handlers instanceof Array) {
if (!handler) {
handlers.length = 0;
return;
}
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i] === handler || handlers[i].handler === handler) {
handlers.splice(i, 1);
break;
}
}
}
},
/**
* Adds a new event handler that should be removed after it's been triggered once.
* @param {string} type The name of the event to listen for.
* @param {Function} handler The function to call when the event occurs.
* @returns {void}
*/
one: function(type, handler) {
var self = this,
proxy = function (event) {
self.off(type, proxy);
handler.call(self, event);
};
proxy.handler = handler;
this.on(type, proxy);
}
};
})();
/**
* The Crocodoc.Viewer namespace
* @namespace
*/
(function () {
'use strict';
var CSS_CLASS_TEXT_DISABLED = 'crocodoc-text-disabled',
CSS_CLASS_LINKS_DISABLED = 'crocodoc-links-disabled';
var viewerInstanceCount = 0;
/**
* Crocodoc.Viewer constructor
* @param {jQuery|string|Element} el The element to wrap
* @param {Object} options Configuration options
* @constructor
*/
Crocodoc.Viewer = function (el, options) {
// call the EventTarget constructor to init handlers
Crocodoc.EventTarget.call(this);
var util = Crocodoc.getUtility('common');
var layout,
$el = $(el),
ready = false,
messageQueue = [],
config = util.extend(true, {}, Crocodoc.Viewer.defaults, options),
scope = new Crocodoc.Scope(config),
viewerBase = scope.createComponent('viewer-base');
//Container exists?
if ($el.length === 0) {
throw new Error('Invalid container element');
}
config.id = ++viewerInstanceCount;
config.api = this;
config.$el = $el;
viewerBase.init();
/**
* Broadcast a message or queue it until the viewer is ready
* @param {string} name The name of the message
* @param {*} data The message data
* @returns {void}
*/
function broadcastMessageWhenReady(name, data) {
if (ready) {
scope.broadcast(name, data);
} else {
messageQueue.push({ name: name, data: data });
}
}
/**
* Broadcasts any (pageavailable) messages that were queued up
* before the viewer was ready
* @returns {void}
*/
function broadcastQueuedMessages() {
var message;
while (messageQueue.length) {
message = messageQueue.shift();
scope.broadcast(message.name, message.data);
}
}
/**
* Handle ready message from the viewer
* @returns {void}
*/
function handleReadyMessage() {
ready = true;
broadcastQueuedMessages();
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Destroy the viewer instance
* @returns {void}
*/
this.destroy = function () {
// broadcast a destroy message
scope.broadcast('destroy');
// destroy all components and plugins in this scope
scope.destroy();
};
/**
* Intiate loading of document assets
* @returns {void}
*/
this.load = function () {
// add a / to the end of the base url if necessary
if (config.url) {
if (!/\/$/.test(config.url)) {
config.url += '/';
}
} else {
scope.broadcast('fail', { error: 'no URL given for assets' });
return;
}
viewerBase.loadAssets();
};
/**
* Set the layout to the given mode, destroying and cleaning up the current
* layout if there is one
* @param {string} mode The layout mode
* @returns {void}
*/
this.setLayout = function (mode) {
layout = viewerBase.setLayout(mode);
};
/**
* Zoom to the given value
* @param {float|string} val Numeric zoom level to zoom to or one of:
* Crocodoc.ZOOM_IN
* Crocodoc.ZOOM_OUT
* Crocodoc.ZOOM_AUTO
* Crocodoc.ZOOM_FIT_WIDTH
* Crocodoc.ZOOM_FIT_HEIGHT
* @returns {void}
*/
this.zoom = function (val) {
// adjust for page scale if passed value is a number
var valFloat = parseFloat(val);
if (layout) {
if (valFloat) {
val = valFloat / (config.pageScale || 1);
}
layout.setZoom(val);
}
};
/**
* Scroll to the given page
* @param {int|string} page Page number or one of:
* Crocodoc.SCROLL_PREVIOUS
* Crocodoc.SCROLL_NEXT
* @returns {void}
*/
this.scrollTo = function (page) {
if (layout) {
layout.scrollTo(page);
}
};
/**
* Scrolls by the given pixel amount from the current location
* @param {int} left Left offset to scroll to
* @param {int} top Top offset to scroll to
* @returns {void}
*/
this.scrollBy = function (left, top) {
if (layout) {
layout.scrollBy(left, top);
}
};
/**
* Focuses the viewport so it can be natively scrolled with the keyboard
* @returns {void}
*/
this.focus = function () {
if (layout) {
layout.focus();
}
};
/**
* Enable text selection, loading text assets per page if necessary
* @returns {void}
*/
this.enableTextSelection = function () {
if (!config.enableTextSelection) {
$el.removeClass(CSS_CLASS_TEXT_DISABLED);
config.enableTextSelection = true;
scope.broadcast('textenabledchange', { enabled: true });
}
};
/**
* Disable text selection, hiding text layer on pages if it's already there
* and disabling the loading of new text assets
* @returns {void}
*/
this.disableTextSelection = function () {
if (config.enableTextSelection) {
$el.addClass(CSS_CLASS_TEXT_DISABLED);
config.enableTextSelection = false;
scope.broadcast('textenabledchange', { enabled: false });
}
};
/**
* Enable links
* @returns {void}
*/
this.enableLinks = function () {
if (!config.enableLinks) {
$el.removeClass(CSS_CLASS_LINKS_DISABLED);
config.enableLinks = true;
}
};
/**
* Disable links
* @returns {void}
*/
this.disableLinks = function () {
if (config.enableLinks) {
$el.addClass(CSS_CLASS_LINKS_DISABLED);
config.enableLinks = false;
}
};
/**
* Force layout update
* @returns {void}
*/
this.updateLayout = function () {
if (layout) {
// force update layout (incl. calculating page paddings)
layout.updatePageStates(true);
layout.setZoom();
}
};
/**
* Notify the viewer that a page is available (ie., it's finished converting)
* @param {int} page The page that's available
* @returns {void}
* @TODO(clakenen): maybe come up with a better name for this?
* @TODO(clakenen): if this is called before the viewer has recieved document metadata
* it will be ignored; perhaps we should cache these messages in that condition?
*/
this.setPageAvailable = function (page) {
broadcastMessageWhenReady('pageavailable', { page: page });
};
/**
* Notify the viewer that all pages up to a given page are available
* @param {int} page The page that is (and all pages up to are) available
* @returns {void}
* @TODO(clakenen): see TODOs on setPageAvailable
*/
this.setPagesAvailableUpTo = function (page) {
broadcastMessageWhenReady('pageavailable', { upto: page });
};
/**
* Notify the viewer that all pages are available
* @returns {void}
*/
this.setAllPagesAvailable = function () {
if (!ready) {
config.conversionIsComplete = true;
} else {
scope.broadcast('pageavailable', { upto: config.numPages });
}
};
this.one('ready', handleReadyMessage);
};
Crocodoc.Viewer.prototype = new Crocodoc.EventTarget();
Crocodoc.Viewer.prototype.constructor = Crocodoc.Viewer;
// Global defaults
Crocodoc.Viewer.defaults = {
// the url to load the assets from (required)
url: null,
// document viewer layout
layout: Crocodoc.LAYOUT_VERTICAL,
// initial zoom level
zoom: Crocodoc.ZOOM_AUTO,
// page to start on
page: 1,
// enable/disable text layer
enableTextSelection: true,
// enable/disable links layer
enableLinks: true,
// enable/disable click-and-drag
enableDragging: false,
// query string parameters to append to all asset requests
queryParams: null,
// plugin configs
plugins: {},
//--------------------------------------------------------------------------
// The following are undocumented, internal, or experimental options,
// which are very subject to change and likely to be broken.
// --
// USE AT YOUR OWN RISK!
//--------------------------------------------------------------------------
// whether to use the browser window as the viewport into the document (this
// is useful when the document should take up the entire browser window, e.g.,
// on mobile devices)
useWindowAsViewport: false,
// whether or not the conversion is finished (eg., pages are ready to be loaded)
conversionIsComplete: true,
// template for loading assets... this should rarely (if ever) change
template: {
svg: 'page-{{page}}.svg',
img: 'page-{{page}}.png',
html: 'text-{{page}}.html',
css: 'stylesheet.css',
json: 'info.json'
},
// page to start/end on (pages outside this range will not be shown)
pageStart: null,
pageEnd: null,
// zoom levels are relative to the viewport size,
// and the dynamic zoom levels (auto, fitwidth, etc) will be added into the mix
zoomLevels: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0]
};
})();
Crocodoc.addUtility('ajax', function (framework) {
'use strict';
var util = framework.getUtility('common');
/**
* Creates a request object to call the success/fail handlers on
* @param {XMLHttpRequest} req The request object to wrap
* @returns {Object} The request object
* @private
*/
function createRequestWrapper(req) {
var status,
statusText,
responseText;
try {
status = req.status;
statusText = req.statusText;
responseText = req.responseText;
} catch (e) {
status = 0;
statusText = '';
responseText = null;
}
return {
status: status,
statusText: statusText,
responseText: responseText
};
}
/**
* Get a XHR object
* @returns {XMLHttpRequest} An XHR object
* @private
*/
function getXMLHttpRequest() {
if (window.XMLHttpRequest) {
return new window.XMLHttpRequest();
} else {
try {
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
}
catch(ex) {
return null;
}
}
}
return {
/**
* Basic AJAX request
* @param {string} url request URL
* @param {Object} [options] AJAX request options
* @param {string} [options.method] request method, eg. 'GET', 'POST' (defaults to 'GET')
* @param {Function} [options.success] success callback function
* @param {Function} [options.fail] fail callback function
* @returns {XMLHttpRequest|XDomainRequest} Request object
*/
request: function (url, options) {
options = options || {};
var method = options.method || 'GET',
req = getXMLHttpRequest();
/**
* Function to call on successful AJAX request
* @returns {void}
* @private
*/
function ajaxSuccess() {
if (util.isFn(options.success)) {
options.success.call(createRequestWrapper(req));
}
}
/**
* Function to call on failed AJAX request
* @returns {void}
* @private
*/
function ajaxFail() {
if (util.isFn(options.fail)) {
options.fail.call(createRequestWrapper(req));
}
}
if (util.isCrossDomain(url) && !('withCredentials' in req)) {
if ('XDomainRequest' in window) {
req = new window.XDomainRequest();
try {
req.open(method, url);
req.onload = ajaxSuccess;
// NOTE: IE (8/9) requires onerror, ontimeout, and onprogress
// to be defined when making XDR to https servers
req.onerror = ajaxFail;
req.ontimeout = ajaxFail;
req.onprogress = function () {};
req.send();
} catch (e) {
req = {
status: 0,
statusText: e.message
};
ajaxFail();
}
} else {
// CORS is not supported!
req = {
status: 0,
statusText: 'CORS not supported'
};
ajaxFail();
}
} else if (req) {
req.open(method, url, true);
req.onreadystatechange = function () {
if (req.readyState === 4) { // DONE
// remove the onreadystatechange handler,
// because it could be called again
// @NOTE: we replace it with a noop function, because
// IE8 will throw an error if the value is not of type
// 'function' when using ActiveXObject
req.onreadystatechange = function () {};
try {
if (req.status === 200) {
ajaxSuccess();
} else {
ajaxFail();
}
} catch (e) {
// NOTE: IE (9?) throws an error when the request is aborted
ajaxFail();
}
}
};
req.send();
} else {
req = {
status: 0,
statusText: 'AJAX not supported'
};
ajaxFail();
}
return req;
}
};
});
Crocodoc.addUtility('browser', function () {
'use strict';
var ua = navigator.userAgent,
browser = {},
ios, android, blackberry,
webos, silk, ie;
ios = /iphone|ipod|ipad/i.test(ua);
android = /android/i.test(ua);
webos = /webos/i.test(ua);
blackberry = /blackberry/i.test(ua);
silk = /blackberry/i.test(ua);
ie = /MSIE/i.test(ua);
if (ie) {
browser.ie = true;
browser.version = parseFloat(/MSIE\s+(\d+\.\d+)/i.exec(ua)[1]);
browser.ielt9 = browser.version < 9;
browser.ielt10 = browser.version < 10;
}
if (ios) {
browser.ios = true;
}
browser.mobile = /mobile/i.test(ua) || ios || android || blackberry || webos || silk;
browser.firefox = /firefox/i.test(ua);
if (/safari/i.test(ua)) {
browser.chrome = /chrome/i.test(ua);
browser.safari = !browser.chrome;
}
return browser;
});
/**
* Common utility functions used throughout Crocodoc JS
*/
Crocodoc.addUtility('common', function () {
'use strict';
var util = {};
util.extend = $.extend;
util.each = $.each;
util.map = $.map;
util.parseJSON = $.parseJSON;
return $.extend(util, {
/**
* Left bistect of list, optionally of property of objects in list
* @param {Array} list List of items to bisect
* @param {number} x The number to bisect against
* @param {string} [prop] Optional property to check on list items instead of using the item itself
* @returns {int} The index of the bisection
*/
bisectLeft: function (list, x, prop) {
var val, mid, low = 0, high = list.length;
while (low < high) {
mid = Math.floor((low + high) / 2);
val = prop ? list[mid][prop] : list[mid];
if (val < x) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
},
/**
* Right bistect of list, optionally of property of objects in list
* @param {Array} list List of items to bisect
* @param {number} x The number to bisect against
* @param {string} [prop] Optional property to check on list items instead of using the item itself
* @returns {int} The index of the bisection
*/
bisectRight: function (list, x, prop) {
var val, mid, low = 0, high = list.length;
while (low < high) {
mid = Math.floor((low + high) / 2);
val = prop ? list[mid][prop] : list[mid];
if (x < val) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
},
/**
* Clamp x to range [a,b]
* @param {number} x The value to clamp
* @param {number} a Low value
* @param {number} b High value
* @returns {number} The clamped value
*/
clamp: function (x, a, b) {
if (x < a) {
return a;
} else if (x > b) {
return b;
}
return x;
},
/**
* Returns the sign of the given number
* @param {number} value The number
* @returns {number} The sign (-1 or 1), or 0 if value === 0
*/
sign: function(value) {
var number = parseInt(value, 10);
if (!number) {
return number;
}
return number < 0 ? -1 : 1;
},
/**
* Returns true if the given value is a function
* @param {*} val Any value
* @returns {Boolean} true if val is a function, false otherwise
*/
isFn: function (val) {
return typeof val === 'function';
},
/**
* Search for a specified value within an array, and return its index (or -1 if not found)
* @param {*} value The value to search for
* @param {Array} array The array to search
* @returns {int} The index of value in array or -1 if not found
*/
inArray: function (value, array) {
if (util.isFn(array.indexOf)) {
return array.indexOf(value);
} else {
return $.inArray(value, array);
}
},
/**
* Constrains the range [low,high] to the range [0,max]
* @param {number} low The low value
* @param {number} high The high value
* @param {number} max The max value (0 is implicit min)
* @returns {Object} The range object containing min and max values
*/
constrainRange: function (low, high, max) {
var length = high - low;
low = util.clamp(low, 0, max);
high = util.clamp(low + length, 0, max);
if (high - low < length) {
low = util.clamp(high - length, 0, max);
}
return {
min: low,
max: high
};
},
/**
* Make the given path absolute
* - if path doesn't contain protocol and domain, prepend the current protocol and domain
* - if the path is relative (eg. doesn't begin with /), also fill in the current path
* @param {string} path The path to make absolute
* @returns {string} The absolute path
*/
makeAbsolute: function (path) {
var location = window.location,
pathname = location.pathname;
if (/^http|^\/\//i.test(path)) {
return path;
}
if (path.charAt(0) !== '/') {
if (pathname.lastIndexOf('/') !== pathname.length - 1) {
pathname = pathname.substring(0, pathname.lastIndexOf('/') + 1);
}
path = pathname + path;
}
return location.protocol + '//' + location.host + path;
},
/**
* Return the current time since epoch in ms
* @returns {int} The current time
*/
now: function () {
return (new Date()).getTime();
},
/**
* Creates and returns a new, throttled version of the passed function,
* that, when invoked repeatedly, will only actually call the original
* function at most once per every wait milliseconds
* @param {int} wait Time to wait between calls in ms
* @param {Function} fn The function to throttle
* @returns {Function} The throttled function
*/
throttle: function (wait, fn) {
var context,
args,
timeout,
result,
previous = 0;
function later () {
previous = util.now();
timeout = null;
result = fn.apply(context, args);
}
return function throttled() {
var now = util.now(),
remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = fn.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
/**
* Creates and returns a new debounced version of the passed function
* which will postpone its execution until after wait milliseconds
* have elapsed since the last time it was invoked.
* @param {int} wait Time to wait between calls in ms
* @param {Function} fn The function to debounced
* @returns {Function} The debounced function
*/
debounce: function (wait, fn) {
var context,
args,
timeout,
timestamp,
result;
function later() {
var last = util.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
result = fn.apply(context, args);
context = args = null;
}
}
return function debounced() {
context = this;
args = arguments;
timestamp = util.now();
if (!timeout) {
timeout = setTimeout(later, wait);
}
return result;
};
},
/**
* Insert the given CSS string into the DOM and return the resulting DOMElement
* @param {string} css The CSS string to insert
* @returns {Element} The ',
SVG_CONTAINER_TEMPLATE = '',
// Embed the svg in an iframe (initialized to about:blank), and inject
// the SVG directly to the iframe window using document.write()
// @NOTE: this breaks images in Safari because [?]
EMBED_STRATEGY_IFRAME_INNERHTML = 1,
// Embed the svg with a data-url
// @NOTE: ff allows direct script access to objects embedded with a data url,
// and this method prevents a throbbing spinner because document.write
// causes a spinner in ff
// @NOTE: NOT CURRENTLY USED - this breaks images in firefox because:
// https://bugzilla.mozilla.org/show_bug.cgi?id=922433
EMBED_STRATEGY_DATA_URL = 2,
// Embed the svg directly in html via inline svg.
// @NOTE: NOT CURRENTLY USED - seems to be slow everywhere, but I'm keeping
// this here because it's very little extra code, and inline SVG might
// be better some day?
EMBED_STRATEGY_INLINE_SVG = 3,
// Embed the svg directly with an object tag; don't replace linked resources
// @NOTE: NOT CURRENTLY USED - this is only here for testing purposes, because
// it works in every browser; it doesn't support query string params
// and causes a spinner
EMBED_STRATEGY_BASIC_OBJECT = 4,
// Embed the svg directly with an img tag; don't replace linked resources
// @NOTE: NOT CURRENTLY USED - this is only here for testing purposes
EMBED_STRATEGY_BASIC_IMG = 5,
// Embed a proxy svg script as an object tag via data:url, which exposes a
// loadSVG method on its contentWindow, then call the loadSVG method directly
// with the svg text as the argument
// @NOTE: only works in firefox because of its security policy on data:uri
EMBED_STRATEGY_DATA_URL_PROXY = 6,
// Embed in a way similar to the EMBED_STRATEGY_DATA_URL_PROXY, but in this
// method we use an iframe initialized to about:blank and document.write()
// the proxy script before calling loadSVG on the iframe's contentWindow
// @NOTE: this is a workaround for the image issue with EMBED_STRATEGY_IFRAME_INNERHTML
// in safari; it also works in firefox, but causes a spinner because of
// document.write()
EMBED_STRATEGY_IFRAME_PROXY = 7,
// Embed in an img tag via data:url, downloading stylesheet separately, and
// injecting it into the data:url of SVG text before embedding
// @NOTE: this method seems to be more performant on IE
EMBED_STRATEGY_DATA_URL_IMG = 8;
var util = scope.getUtility('common'),
ajax = scope.getUtility('ajax'),
browser = scope.getUtility('browser'),
subpx = scope.getUtility('subpx'),
DOMParser = window.DOMParser;
var $svg, $svgLayer,
$loadSVGTextPromise,
request,
config,
baseURL,
queryString,
svgSrc,
svgText,
destroyed = false,
unloaded = false,
svgLoaded = false,
viewerConfig = scope.getConfig(),
removeOnUnload = browser.mobile || browser.ielt10,
embedStrategy = browser.ie ? EMBED_STRATEGY_DATA_URL_IMG :
browser.firefox ? EMBED_STRATEGY_DATA_URL_IMG :
browser.safari ? EMBED_STRATEGY_IFRAME_PROXY :
EMBED_STRATEGY_IFRAME_INNERHTML;
/**
* Create and return a jQuery object for the SVG element
* @returns {Object} The SVG $element
* @private
*/
function createSVGEl() {
switch (embedStrategy) {
case EMBED_STRATEGY_IFRAME_INNERHTML:
case EMBED_STRATEGY_IFRAME_PROXY:
return $('