viewer
Version:
A viewer for documents converted with the Box View API
896 lines • 128 kB
JavaScript
/*! Crocodoc Viewer - v0.10.11 | (c) 2016 Box */
!function(a){/*global jQuery*/
/*jshint unused:false, undef:false*/
"use strict";a.Crocodoc=function(a){
// nodejs / browserify - export a function that accepts a jquery impl
return"object"!=typeof exports?a(jQuery):void(module.exports=a)}(function(b){/**
* Creates a global method for loading svg text into the proxy svg object
* @NOTE: this function should never be called directly in this context;
* it's converted to a string and encoded into the proxy svg data:url
* @returns {void}
* @private
*/
function c(){a.loadSVG=function(b){var c=new a.DOMParser,d=c.parseFromString(b,"image/svg+xml"),e=document.importNode(d.documentElement,!0);
// make sure the svg width/height are explicity set to 100%
e.setAttribute("width","100%"),e.setAttribute("height","100%"),document.body?document.body.appendChild(e):document.documentElement.appendChild(e)}}var d="crocodoc-",e="data-svg-version",f=d+"viewer",g=d+"doc",h=d+"viewport",i=d+"viewer-logo",j=d+"draggable",k=d+"dragging",l=d+"text-selected",m=d+"text-disabled",n=d+"links-disabled",o=d+"mobile",p=d+"ielt9",q=d+"supports-svg",r=d+"window-as-viewport",s=d+"layout-",t=d+"current-page",u=d+"preceding-page",v=d+"page",w=v+"-inner",x=v+"-content",y=v+"-svg",z=v+"-text",A=v+"-link",B=v+"-links",C=v+"-autoscale",D=v+"-loading",E=v+"-error",F=v+"-visible",C=v+"-autoscale",G=v+"-prev",H=v+"-next",I=v+"-before",J=v+"-after",K=v+"-before-buffer",L=v+"-after-buffer",M=[H,J,G,I,K,L].join(" "),N='<div tabindex="-1" class="'+h+'"><div class="'+g+'"></div></div><div class="'+i+'"></div>',O='<div class="'+v+" "+D+'" style="width:{{w}}px; height:{{h}}px;" data-width="{{w}}" data-height="{{h}}"><div class="'+w+'"><div class="'+x+'"><div class="'+y+'"></div><div class="'+C+'"><div class="'+z+'"></div><div class="'+B+'"></div></div></div></div></div>',P=1024,Q="fitwidth",R="fitheight",S="auto",T="in",U="out",V="previous",W="next",X="vertical",Y="vertical-single-column",Z="horizontal",$="presentation",_="presentation-two-page",aa="text",ba="converting",ca="not loaded",da="loading",ea="loaded",fa="error",ga="padding-",ha=ga+"top",ia=ga+"right",ja=ga+"left",ka=ga+"bottom",
// threshold for removing similar zoom levels (closer to 1 is more similar)
la=.95,
// threshold for removing similar zoom presets (e.g., auto, fit-width, etc)
ma=.99,na=100,//ms between initiating page loads
oa=32,pa=8,
// the delay in ms to wait before triggering preloading after `ready`
qa=1e3,ra="image/svg+xml",sa="<style>html,body{width:100%;height:100%;margin:0;overflow:hidden;}</style>",ta='<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><script><![CDATA[('+c+")()]]></script></svg>",
// 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 [?]
ua=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
va=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?
wa=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
xa=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
ya=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
za=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 embed 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
Aa=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
Ba=8;/*jshint unused:false*/
if("undefined"==typeof b)throw new Error("jQuery is required");/**
* The one global object for Crocodoc JavaScript.
* @namespace
*/
var Ca=function(){/**
* 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 a(c,d,e){var f;for(e=e||c,f=0;f<d.length;++f){if(c===d[f])throw new Error("Circular dependency detected: "+e+"->"+d[f]);b[d[f]]&&a(c,b[d[f]].mixins,e+"->"+d[f])}}var b={},c={};return{
// Zoom, scroll, page status, layout constants
ZOOM_FIT_WIDTH:Q,ZOOM_FIT_HEIGHT:R,ZOOM_AUTO:S,ZOOM_IN:T,ZOOM_OUT:U,SCROLL_PREVIOUS:V,SCROLL_NEXT:W,LAYOUT_VERTICAL:X,LAYOUT_VERTICAL_SINGLE_COLUMN:Y,LAYOUT_HORIZONTAL:Z,LAYOUT_PRESENTATION:$,LAYOUT_PRESENTATION_TWO_PAGE:_,LAYOUT_TEXT:aa,
// The number of times to retry loading an asset before giving up
ASSET_REQUEST_RETRIES:1,
// templates exposed to allow more customization
viewerTemplate:N,pageTemplate:O,
// exposed for testing purposes only
// should not be accessed directly otherwise
components:b,utilities:c,/**
* 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(a,b){return new Ca.Viewer(a,b)},/**
* Get a viewer instance by id
* @param {number} id The id
* @returns {Object} The viewer instance
*/
getViewer:function(a){return Ca.Viewer.get(a)},/**
* 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(c,d,e){d instanceof Function&&(e=d,d=[]),
// make sure this component won't cause a circular mixin dependency
a(c,d),b[c]={mixins:d,creator:e}},/**
* 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(a,c){var d=b[a];if(d){for(var e=[],f=0;f<d.mixins.length;++f)e.push(this.createComponent(d.mixins[f],c));return e.unshift(c),d.creator.apply(d.creator,e)}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(a,b){this.addComponent("plugin-"+a,b)},/**
* Register a new Crocodoc data provider
* @param {string} modelName The model name this data provider provides
* @param {Function} creator Factory function used to create an instance of the data provider.
*/
addDataProvider:function(a,b){this.addComponent("data-provider-"+a,b)},/**
* 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(a,b){c[a]={creator:b,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(a){var b=c[a];return b?(b.instance||(b.instance=b.creator(this)),b.instance):null}}}();/**
* The Crocodoc.Viewer namespace
* @namespace
*/
/**
* Common utility functions used throughout Crocodoc JS
*/
/*global window, document*/
/**
* URL utility
*/
/**
* Dragger component definition
*/
/**
* Base layout component for controlling viewer layout and viewport
*/
/**
* The horizontal layout
*/
/**
* Base layout component for controlling viewer layout and viewport
*/
/**
* The presentation-two-page layout
*/
/**
*The presentation layout
*/
/**
* Layout for text-based files
*/
/**
* The vertical-single-column layout
*/
/**
* The vertical layout
*/
/*global setTimeout, clearTimeout*/
/**
* lazy-loader component for controlling when pages should be loaded and unloaded
*/
/**
* page-img component used to display raster image instead of SVG content for
* browsers that do not support SVG
*/
/**
* page-links component definition
*/
/**
* page-svg component
*/
/**
* page-text component
*/
/**
* Page component
*/
/**
* resizer component definition
*/
/*global setTimeout, clearTimeout */
return function(){/**
* Scope class used for component scoping (creating, destroying, broadcasting messages)
* @constructor
*/
Ca.Scope=function(a){/**
* 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}
* @private
*/
function c(a,b){var c,d,e,h;for(c=0,d=g.length;d>c;++c)e=g[c],e&&(h=e.messages||[],-1!==f.inArray(a,h)&&f.isFn(e.onmessage)&&e.onmessage.call(e,a,b))}/**
* Broadcasts any (pageavailable) messages that were queued up
* before the viewer was ready
* @returns {void}
* @private
*/
function d(){for(var a;h.length;)a=h.shift(),c(a.name,a.data);h=null}/**
* Call the destroy method on a component instance if it exists and the
* instance has not already been destroyed
* @param {Object} instance The component instance
* @returns {void}
*/
function e(a){f.isFn(a.destroy)&&!a._destroyed&&(a.destroy(),a._destroyed=!0)}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
var f=Ca.getUtility("common"),g=[],h=[],i={},j=!1;
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
a.dataProviders=a.dataProviders||{},/**
* 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(a){var b=Ca.createComponent(a,this);return b&&(b.componentName=a,g.push(b)),b},/**
* Remove and call the destroy method on a component instance
* @param {Object} instance The component instance to remove
* @returns {void}
*/
this.destroyComponent=function(a){var b,c;for(b=0,c=g.length;c>b;++b)if(a===g[b]){e(a),g.splice(b,1);break}},/**
* Remove and call the destroy method on all instances in this scope
* @returns {void}
*/
this.destroy=function(){var a,b,c,d=g.slice();for(a=0,b=d.length;b>a;++a)c=d[a],e(c);g=[],i={}},/**
* 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}
*/
this.broadcast=function(a,b){j?c(a,b):h.push({name:a,data:b})},/**
* 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(a){return Ca.getUtility(a)},/**
* Get the config object associated with this scope
* @returns {Object} The config object
*/
this.getConfig=function(){return a},/**
* Tell the scope that the viewer is ready and broadcast queued messages
* @returns {void}
*/
this.ready=function(){j||(j=!0,d())},/**
* Get a model object from a data provider. If the objectType is listed
* in config.dataProviders, this will get the value from the data
* provider that is specified in that map instead.
* @param {string} objectType The type of object to retrieve ('page-svg', 'page-text', etc)
* @param {string} objectKey The key of the object to retrieve
* @returns {$.Promise}
*/
this.get=function(c,d){var e=a.dataProviders[c]||c,f=this.getDataProvider(e);return f?f.get(c,d):b.Deferred().reject("data-provider not found").promise()},/**
* Get an instance of a data provider. Ignores config.dataProviders
* overrides.
* @param {string} objectType The type of object to retrieve a data provider for ('page-svg', 'page-text', etc)
* @returns {Object} The data provider
*/
this.getDataProvider=function(a){var b;return i[a]?b=i[a]:(b=this.createComponent("data-provider-"+a),i[a]=b),b}}}(),function(){/**
* Build an event object for the given type and data
* @param {string} type The event type
* @param {Object} data The event data
* @returns {Object} The event object
*/
function a(a,b){var c=!1;return{type:a,data:b,/**
* Prevent the default action for this event
* @returns {void}
*/
preventDefault:function(){c=!0},/**
* Return true if preventDefault() has been called on this event
* @returns {Boolean}
*/
isDefaultPrevented:function(){return c}}}/**
* An object that is capable of generating custom events and also
* executing handlers for events when they occur.
* @constructor
*/
Ca.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={}},Ca.EventTarget.prototype={
// restore constructor
constructor:Ca.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(a,b){"undefined"==typeof this._handlers[a]&&(this._handlers[a]=[]),this._handlers[a].push(b)},/**
* 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 {Object} The event object
*/
fire:function(b,c){var d,e,f,g=a(b,c);if(d=this._handlers[g.type],d instanceof Array)for(d=d.concat(),e=0,f=d.length;f>e;e++)d[e]&&d[e].call(this,g);if(d=this._handlers.all,d instanceof Array)for(d=d.concat(),e=0,f=d.length;f>e;e++)d[e]&&d[e].call(this,g);return g},/**
* 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(a,b){var c,d,e=this._handlers[a];if(e instanceof Array){if(!b)return void(e.length=0);for(c=0,d=e.length;d>c;c++)if(e[c]===b||e[c].handler===b){e.splice(c,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(a,b){var c=this,d=function(e){c.off(a,d),b.call(c,e)};d.handler=b,this.on(a,d)}}}(),function(){var a=0,c={};/**
* Crocodoc.Viewer constructor
* @param {jQuery|string|Element} el The element to wrap
* @param {Object} options Configuration options
* @constructor
*/
Ca.Viewer=function(d,e){function f(){l.init()}
// call the EventTarget constructor to init handlers
Ca.EventTarget.call(this);var g,h=Ca.getUtility("common"),i=b(d),j=h.extend(!0,{},Ca.Viewer.defaults,e),k=new Ca.Scope(j),l=k.createComponent("viewer-base");
//Container exists?
if(0===i.length)throw new Error("Invalid container element");this.id=j.id=++a,j.api=this,j.$el=i,
// register this instance
c[this.id]=this,
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Destroy the viewer instance
* @returns {void}
*/
this.destroy=function(){
// unregister this instance
delete c[j.id],
// broadcast a destroy message
k.broadcast("destroy"),
// destroy all components and plugins in this scope
k.destroy()},/**
* Intiate loading of document assets
* @returns {void}
*/
this.load=function(){l.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(a){g=null,g=l.setLayout(a)},/**
* 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(a){
// adjust for page scale if passed value is a number
var b=parseFloat(a);g&&(b&&(a=b/(j.pageScale||1)),g.setZoom(a))},/**
* Scroll to the given page
* @TODO: rename to scrollToPage when possible (and remove this for non-
* page-based viewers)
* @param {int|string} page Page number or one of:
* Crocodoc.SCROLL_PREVIOUS
* Crocodoc.SCROLL_NEXT
* @returns {void}
*/
this.scrollTo=function(a){g&&h.isFn(g.scrollTo)&&g.scrollTo(a)},/**
* 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(a,b){g&&g.scrollBy(a,b)},/**
* Focuses the viewport so it can be natively scrolled with the keyboard
* @returns {void}
*/
this.focus=function(){g&&g.focus()},/**
* Enable text selection, loading text assets per page if necessary
* @returns {void}
*/
this.enableTextSelection=function(){i.toggleClass(m,!1),j.enableTextSelection||(j.enableTextSelection=!0,k.broadcast("textenabledchange",{enabled:!0}))},/**
* 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(){i.toggleClass(m,!0),j.enableTextSelection&&(j.enableTextSelection=!1,k.broadcast("textenabledchange",{enabled:!1}))},/**
* Enable links
* @returns {void}
*/
this.enableLinks=function(){j.enableLinks||(i.removeClass(n),j.enableLinks=!0)},/**
* Disable links
* @returns {void}
*/
this.disableLinks=function(){j.enableLinks&&(i.addClass(n),j.enableLinks=!1)},/**
* Force layout update
* @returns {void}
*/
this.updateLayout=function(){g&&g.update()},f()},Ca.Viewer.prototype=new Ca.EventTarget,Ca.Viewer.prototype.constructor=Ca.Viewer,/**
* Get a viewer instance by id
* @param {number} id The id
* @returns {Object} The viewer instance
*/
Ca.Viewer.get=function(a){return c[a]},
// Global defaults
Ca.Viewer.defaults={
// the url to load the assets from (required)
url:null,
// document viewer layout
layout:X,
// initial zoom level
zoom:S,
// page to start on
page:1,
// enable/disable text layer
enableTextSelection:!0,
// enable/disable links layer
enableLinks:!0,
// enable/disable click-and-drag
enableDragging:!1,
// query string parameters to append to all asset requests
queryParams:null,
// plugin configs
plugins:{},
// 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:!1,
//--------------------------------------------------------------------------
// 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 or not the conversion is finished (eg., pages are ready to be loaded)
conversionIsComplete:!0,
// 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"},
// default data-providers
dataProviders:{metadata:"metadata",stylesheet:"stylesheet","page-svg":"page-svg","page-text":"page-text","page-img":"page-img"},
// page to start/end on (pages outside this range will not be shown)
pageStart:null,pageEnd:null,
// whether or not to automatically load page one assets immediately (even
// if conversion is not yet complete)
autoloadFirstPage:!0,
// zoom levels are relative to the viewport size,
// and the dynamic zoom levels (auto, fitwidth, etc) will be added into the mix
zoomLevels:[.25,.5,.75,1,1.25,1.5,2,3]}}(),Ca.addDataProvider("metadata",function(a){/**
* Process metadata json and return the result
* @param {string} json The original JSON text
* @returns {string} The processed JSON text
* @private
*/
function b(a){return d.parseJSON(a)}var c=a.getUtility("ajax"),d=a.getUtility("common"),e=a.getConfig();
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Retrieve the info.json asset from the server
* @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request.
*/
get:function(){var a=this.getURL(),d=c.fetch(a,Ca.ASSET_REQUEST_RETRIES);
// @NOTE: promise.then() creates a new promise, which does not copy
// custom properties, so we need to create a futher promise and add
// an object with the abort method as the new target
return d.then(b).promise({abort:d.abort})},/**
* Build and return the URL to the metadata JSON
* @returns {string} The URL
*/
getURL:function(){var a=e.template.json;return e.url+a+e.queryString}}}),Ca.addDataProvider("page-img",function(a){var c=a.getUtility("common"),d=a.getConfig();
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Retrieve the page image asset from the server
* @param {string} objectType The type of data being requested
* @param {number} pageNum The page number for which to request the page image
* @returns {$.Promise} A promise with an additional abort() method that will abort the img request.
*/
get:function(a,c){function d(){f.setAttribute("src",i)}function e(){f&&f.removeAttribute("src")}var f=this.getImage(),g=Ca.ASSET_REQUEST_RETRIES,h=!1,i=this.getURL(c),j=b.Deferred();
// add load and error handlers
// load the image
return f.onload=function(){h=!0,j.resolve(f)},f.onerror=function(){g>0?(g--,e(),d()):(f=null,h=!1,j.reject({error:"image failed to load",resource:i}))},d(),j.promise({abort:function(){h||(e(),j.reject())}})},/**
* Build and return the URL to the PNG asset for the specified page
* @param {number} pageNum The page number
* @returns {string} The URL
*/
getURL:function(a){var b=c.template(d.template.img,{page:a});return d.url+b+d.queryString},/**
* Create and return a new image element (used for testing purporses)
* @returns {Image}
*/
getImage:function(){return new Image}}}),Ca.addDataProvider("page-svg",function(a){/**
* Interpolate CSS text into the SVG text
* @param {string} text The SVG text
* @param {string} cssText The CSS text
* @returns {string} The full SVG text
*/
function b(a,b){
// CSS text
var c="<style>"+b+"</style>";
// If using Firefox with no subpx support, add "text-rendering" CSS.
// @NOTE(plai): We are not adding this to Chrome because Chrome supports "textLength"
// on tspans and because the "text-rendering" property slows Chrome down significantly.
// In Firefox, we're waiting on this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=890692
// @TODO: Use feature detection instead (textLength)
// inline the CSS!
return g.firefox&&!h.isSubpxSupported()&&(c+="<style>text { text-rendering: geometricPrecision; }</style>"),a=a.replace(l,c)}/**
* Process SVG text and return the embeddable result
* @param {string} text The original SVG text
* @returns {string} The processed SVG text
* @private
*/
function c(c){if(!j){var f,g=i.queryString.replace("&","&");
// remove data:urls from the SVG content if the number exceeds MAX_DATA_URLS
// remove all data:url images that are smaller than 5KB
// @TODO: remove this, because we no longer use any external assets in this way
// modify external asset urls for absolute path
return f=e.countInStr(c,'xlink:href="data:image'),f>d&&(c=c.replace(/<image[\s\w-_="]*xlink:href="data:image\/[^"]{0,5120}"[^>]*>/gi,"")),c=c.replace(/href="([^"#:]*)"/g,function(a,b){return'href="'+i.url+b+g+'"'}),a.get("stylesheet").then(function(a){return b(c,a)})}}var d=1e3,e=a.getUtility("common"),f=a.getUtility("ajax"),g=a.getUtility("browser"),h=a.getUtility("subpx"),i=a.getConfig(),j=!1,k={},
// NOTE: there are cases where the stylesheet link tag will be self-
// closing, so check for both cases
l=/<xhtml:link[^>]*>(\s*<\/xhtml:link>)?/i;
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Retrieve a SVG asset from the server
* @param {string} objectType The type of data being requested
* @param {number} pageNum The page number for which to request the SVG
* @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request.
*/
get:function(a,b){var d,e=this.getURL(b);
// @NOTE: promise.then() creates a new promise, which does not copy
// custom properties, so we need to create a futher promise and add
// an object with the abort method as the new target
return k[b]?k[b]:(d=f.fetch(e,Ca.ASSET_REQUEST_RETRIES),k[b]=d.then(c).promise({abort:function(){d.abort(),k&&delete k[b]}}),k[b])},/**
* Build and return the URL to the SVG asset for the specified page
* @param {number} pageNum The page number
* @returns {string} The URL
*/
getURL:function(a){var b=e.template(i.template.svg,{page:a});return i.url+b+i.queryString},/**
* Cleanup the data-provider
* @returns {void}
*/
destroy:function(){j=!0,e=f=h=g=i=k=null}}}),Ca.addDataProvider("page-text",function(a){/**
* Process HTML text and return the embeddable result
* @param {string} text The original HTML text
* @returns {string} The processed HTML text
* @private
*/
function b(a){if(!g){
// in the text layer, divs are only used for text boxes, so
// they should provide an accurate count
var b=d.countInStr(a,"<div");
// too many textboxes... don't load this page for performance reasons
// too many textboxes... don't load this page for performance reasons
// remove reference to the styles
return b>c?"":a=a.replace(/<link rel="stylesheet".*/,"")}}var c=256,d=a.getUtility("common"),e=a.getUtility("ajax"),f=a.getConfig(),g=!1,h={};
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Retrieve a text asset from the server
* @param {string} objectType The type of data being requested
* @param {number} pageNum The page number for which to request the text HTML
* @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request.
*/
get:function(a,c){var d,f=this.getURL(c);
// @NOTE: promise.then() creates a new promise, which does not copy
// custom properties, so we need to create a futher promise and add
// an object with the abort method as the new target
return h[c]?h[c]:(d=e.fetch(f,Ca.ASSET_REQUEST_RETRIES),h[c]=d.then(b).promise({abort:function(){d.abort(),h&&delete h[c]}}),h[c])},/**
* Build and return the URL to the HTML asset for the specified page
* @param {number} pageNum The page number
* @returns {string} The URL
*/
getURL:function(a){var b=d.template(f.template.html,{page:a});return f.url+b+f.queryString},/**
* Cleanup the data-provider
* @returns {void}
*/
destroy:function(){g=!0,d=e=f=h=null}}}),Ca.addDataProvider("stylesheet",function(a){/**
* Process stylesheet text and return the embeddable result
* @param {string} text The original CSS text
* @returns {string} The processed CSS text
* @private
*/
function b(a){
// @NOTE: There is a bug in IE that causes the text layer to
// not render the font when loaded for a second time (i.e.,
// destroy and recreate a viewer for the same document), so
// namespace the font-family so there is no collision
return e.ie&&(a=a.replace(/font-family:[\s\"\']*([\w-]+)\b/g,"$0-"+f.id)),a}var c,d=a.getUtility("ajax"),e=a.getUtility("browser"),f=a.getConfig();
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Retrieve the stylesheet.css asset from the server
* @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request.
*/
get:function(){if(c)return c;var a=d.fetch(this.getURL(),Ca.ASSET_REQUEST_RETRIES);
// @NOTE: promise.then() creates a new promise, which does not copy
// custom properties, so we need to create a futher promise and add
// an object with the abort method as the new target
return c=a.then(b).promise({abort:function(){a.abort(),c=null}})},/**
* Build and return the URL to the stylesheet CSS
* @returns {string} The URL
*/
getURL:function(){var a=f.template.css;return f.url+a+f.queryString},/**
* Cleanup the data-provider
* @returns {void}
*/
destroy:function(){d=e=f=null,c=null}}}),Ca.addUtility("ajax",function(a){/**
* 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 c(a){var b,c,d;try{b=a.status,c=a.statusText,d=a.responseText}catch(e){b=0,c="",d=null}return{status:b,statusText:c,responseText:d,rawRequest:a}}/**
* Returns true if the url is referencing a local file
* @param {string} url The URL
* @param {Boolean}
*/
function d(a){return"file:"===l.parse(a).protocol}/**
* Return true if the given status code looks successful
* @param {number} status The http status code
* @returns {Boolean}
*/
function e(a){return a>=200&&300>a||304===a}/**
* Parse AJAX options
* @param {Object} options The options
* @returns {Object} The parsed options
*/
function f(a){return a=j.extend(!0,{},a||{}),a.method=a.method||"GET",a.headers=a.headers||[],a.data=a.data||"","string"!=typeof a.data&&(a.data=b.param(a.data),"GET"!==a.method&&(a.data=a.data,a.headers.push(["Content-Type","application/x-www-form-urlencoded"]))),a}/**
* Set XHR headers
* @param {XMLHttpRequest} req The request object
* @param {Array} headers Array of headers to set
*/
function g(a,b){var c;for(c=0;c<b.length;++c)a.setRequestHeader(b[c][0],b[c][1])}/**
* Make an XHR request
* @param {string} url request URL
* @param {string} method request method
* @param {*} data request data to send
* @param {Array} headers request headers
* @param {Function} success success callback function
* @param {Function} fail fail callback function
* @returns {XMLHttpRequest} Request object
* @private
*/
function h(a,b,c,f,h,i){var j=k.getXHR();return j.open(b,a,!0),j.onreadystatechange=function(){var b;if(4===j.readyState){// 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
j.onreadystatechange=function(){};try{b=j.status}catch(c){
// NOTE: IE (9?) throws an error when the request is aborted
return void i(j)}
// status is 0 for successful local file requests, so assume 200
0===b&&d(a)&&(b=200),e(b)?h(j):i(j)}},g(j,f),j.send(c),j}/**
* Make an XDR request
* @param {string} url request URL
* @param {string} method request method
* @param {*} data request data to send
* @param {Function} success success callback function
* @param {Function} fail fail callback function
* @returns {XDomainRequest} Request object
* @private
*/
function i(a,b,c,d,e){var f=k.getXDR();try{f.open(b,a),f.onload=function(){d(f)},
// NOTE: IE (8/9) requires onerror, ontimeout, and onprogress
// to be defined when making XDR to https servers
f.onerror=function(){e(f)},f.ontimeout=function(){e(f)},f.onprogress=function(){},f.send(c)}catch(g){return e({status:0,statusText:g.message})}return f}var j=a.getUtility("common"),k=a.getUtility("support"),l=a.getUtility("url");return{/**
* Make a raw 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 {Array} [options.headers] request headers (defaults to [])
* @param {*} [options.data] request data to send (defaults to null)
* @param {Function} [options.success] success callback function
* @param {Function} [options.fail] fail callback function
* @returns {XMLHttpRequest|XDomainRequest} Request object
*/
request:function(a,b){/**
* Function to call on successful AJAX request
* @returns {void}
* @private
*/
function d(a){return j.isFn(g.success)&&g.success.call(c(a)),a}/**
* Function to call on failed AJAX request
* @returns {void}
* @private
*/
function e(a){return j.isFn(g.fail)&&g.fail.call(c(a)),a}var g=f(b),m=g.method,n=g.data,o=g.headers;
// is XHR supported at all?
// is XHR supported at all?
// cross-domain request? check if CORS is supported...
return"GET"===m&&n&&(a=l.appendQueryParams(a,n),n=""),k.isXHRSupported()?l.isCrossDomain(a)&&!k.isCORSSupported()?i(a,m,n,d,e):h(a,m,n,o,d,e):g.fail({status:0,statusText:"AJAX not supported"})},/**
* Fetch an asset, retrying if necessary
* @param {string} url A url for the desired asset
* @param {number} retries The number of times to retry if the request fails
* @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request.
*/
fetch:function(a,c){/**
* If there are retries remaining, make another attempt, otherwise
* give up and reject the deferred
* @param {Object} error The error object
* @returns {void}
* @private
*/
function d(a){c>0?(
// if we have retries remaining, make another request
c--,f=e()):
// finally give up
i.reject(a)}/**
* Make an AJAX request for the asset
* @returns {XMLHttpRequest|XDomainRequest} Request object
* @private
*/
function e(){return h.request(a,{success:function(){var b,c;if(!g){
// check status code for 202
if(c=this.rawRequest,202===this.status&&j.isFn(c.getResponseHeader)&&(b=parseInt(c.getResponseHeader("retry-after")),b>0))return void setTimeout(e,1e3*b);this.responseText?i.resolve(this.responseText):
// the response was empty, so consider this a
// failed request
d({error:"empty response",status:this.status,resource:a})}},fail:function(){g||d({error:this.statusText,status:this.status,resource:a})}})}var f,g=!1,h=this,i=b.Deferred();return f=e(),i.promise({abort:function(){g=!0,f.abort()}})}}}),Ca.addUtility("browser",function(){var a,b=navigator.userAgent,c={},d=/ip(hone|od|ad)/i.test(b),e=/android/i.test(b),f=/blackberry/i.test(b),g=/webos/i.test(b),h=/silk|kindle/i.test(b),i=/MSIE|Trident/i.test(b);return i&&(c.ie=!0,a=/MSIE/i.test(b)?/MSIE\s+(\d+\.\d+)/i.exec(b):/Trident.*rv[ :](\d+\.\d+)/.exec(b),c.version=a&&parseFloat(a[1]),c.ielt9=c.version<9,c.ielt10=c.version<10,c.ielt11=c.version<11),d&&(c.ios=!0,a=navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),c.version=a&&parseFloat(a[1]+"."+a[2])),c.mobile=/mobile/i.test(b)||d||e||f||g||h,c.firefox=/firefox/i.test(b),/safari/i.test(b)&&(c.chrome=/chrome/i.test(b),c.safari=!c.chrome),c.safari&&(a=navigator.appVersion.match(/Version\/(\d+(\.\d+)?)/),c.version=a&&parseFloat(a[1])),c}),Ca.addUtility("common",function(){var c=1.33333,d={};// IE 8+
return d.extend=b.extend,d.each=b.each,d.map=b.map,d.param=b.param,d.parseJSON=b.parseJSON,d.stringifyJSON="undefined"!=typeof a.JSON?a.JSON.stringify:function(){throw new Error("JSON.stringify not supported")},b.extend(d,{/**
* 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(a,b,c){for(var d,e,f=0,g=a.length;g>f;)e=Math.floor((f+g)/2),d=c?a[e][c]:a[e],b>d?f=e+1:g=e;return f},/**
* 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(a,b,c){for(var d,e,f=0,g=a.length;g>f;)e=Math.floor((f+g)/2),d=c?a[e][c]:a[e],d>b?g=e:f=e+1;return f},/**
* 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(a,b,c){return b>a?b:a>c?c:a},/**
* 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(a){var b=parseInt(a,10);return b?0>b?-1:1:b},/**
* 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(a){return"function"==typeof a},/**
* 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(a,c){return d.isFn(c.indexOf)?c.indexOf(a):b.inArray(a,c)},/**
* 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(a,b,c){var e=b-a;return 0>e?{min:-1,max:-1}:(a=d.clamp(a,0,c),b=d.clamp(a+e,0,c),e>b-a&&(a=d.clamp(b-e,0,c)),{min:a,max:b})},/**
* 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(a,b){function c(){i=d.now(),g=null,h=b.apply(e,f)}var e,f,g,h,i=0;return function(){var j=d.now(),k=a-(j-i);return e=this,f=arguments,0>=k?(clearTimeout(g),g=null,i=j,h=b.apply(e,f)):g||(g=setTimeout(c,k)),h}},/**
* 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(a,b){function c(){var j=d.now()-h;a>j?g=setTimeout(c,a-j):(g=null,i=b.apply(e,f),e=f=null)}var e,f,g,h,i;return function(){return e=this,f=arguments,h=d.now(),g||(g=setTimeout(c,a)),i}},/**
* Insert the given CSS string into the DOM and return the resulting DOMElement
* @param {string} css The CSS string to insert
* @returns {Element} The <style> element that was created and inserted
*/
insertCSS:function(a){var b=document.createElement("style"),c=document.createTextNode(a);try{b.setAttribute("type","text/css"),b.appendChild(c)}catch(d){}return document.getElementsByTagName("head")[0].appendChild(b),b},/**
* Append a CSS rule to the given stylesheet
* @param {CSSStyleSheet} sheet The stylesheet object
* @param {string} selector The selector
* @param {string} rule The rule
* @returns {int} The index of the new rule
*/
appendCSSRule:function(a,b,c){var d;return a.insertRule?a.insertRule(b+"{"+c+"}",a.cssRules.length):(d=a.addRule(b,c,a.rules.length),0>d&&(d=a.rules.length-1),d)},/**
* Delete a CSS rule at the given index from the given stylesheet
* @param {CSSStyleSheet} sheet The stylesheet object
* @param {int} index The index of the rule to delete
* @returns {void}
*/
deleteCSSRule:function(a,b){a.deleteRule?a.deleteRule(b):a.removeRule(b)},/**
* Get the parent element of the (first) text node that is currently selected
* @returns {Element} The selected element
* @TODO: return all selected elements
*/
getSelectedNode:function(){var b,c,d;return a.getSelection?(c=a.getSelection(),c.rangeCount&&(d=c.getRangeAt(0),d.collapsed||(b=c.anchorNode.parentNode))):document.selection&&(b=document.selection.createRange().parentElement()),b},/**
* Cross-browser getComputedStyle, which is faster than jQuery.css
* @param {HTMLElement} el The element
* @returns {CSSStyleDeclaration} The computed styles
*/
getComputedStyle:function(b){return"getComputedStyle"in a?a.getComputedStyle(b):b.currentStyle},/**
* Calculates the size of 1pt in pixels
* @returns {number} The pixel value
*/
calculatePtSize:function(){var a,b,e=1e4,f=document.createElement("div");return f.style.display="block",f.style.position="absolute",f.style.width=e+"pt",document.body.appendChild(f),a=d.getComputedStyle(f),b=a&&a.width?parseFloat(a.width)/e:c,document.body.removeChild(f),b},/**
* Count and return the number of occurrences of token in str
* @param {string} str The string to search
* @param {string} token The string to search for
* @returns {int} The number of occurrences
*/
countInStr:function(a,b){for(var c,d=0;c=a.indexOf(b,c)+1;)d++;return d},/**
* Apply the given data to a template
* @param {string} template The template
* @param {Object} data The data to apply to the template
* @returns {string} The filled template
*/
template:function(a,b){var c;for(c in b)b.hasOwnProperty(c)&&(a=a.replace(new RegExp("\\{\\{"+c+"\\}\\}","g"),b[c]));return a}})}),Ca.addUtility("subpx",function(c){/**
* Return true if subpixel rendering is supported
* @returns {Boolean}
* @private
*/
function d(){
// Test if subpixel rendering is supported
// @NOTE: jQuery.support.leadingWhitespace is apparently false if browser is IE6-8
if(!b.support.leadingWhitespace)return!1;
//span #1 - desired font-size: 12.5px
var c=b(g.template(f,{size:12.5})).appendTo(document.documentElement).get(0),d=b(c).css("font-size"),e=b(c).width();b(c).remove(),
//span #2 - desired font-size: 12.6px
c=b(g.template(f,{size:12.6})).appendTo(document.documentElement).get(0);var h=b(c).css("font-size"),i=b(c).width();
// is not mobile device?
// @NOTE(plai): Mobile WebKit supports subpixel rendering even though the browser fails the following tests.
// @NOTE(plai): When modifying these tests, make sure that these tests will work even when the browser zoom is changed.
// @TODO(plai): Find a better way of testing for mobile Safari.
if(b(c).remove(),!("ontouchstart"in a)){
//font sizes are the same? (Chrome and Safari will fail this)
if(d===h)return!1;
//widths are the same? (Firefox on Windows without GPU will fail this)
if(e===i)return!1}return!0}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
var e="crocodoc-subpx-fix",f='<span style="font:{{size}}px serif; color:transparent; white-space:nowrap;">'+new Array(100).join("A")+"</span>",g=c.getUtility("common"),h=d();
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return{/**
* Apply the subpixel rendering fix to the given element if necessary.
* @NOTE: Fix is only applied if the "zoom" CSS property exists
* (ie., this fix is never applied in Firefox)
* @param {Element} el The element
* @returns {Element} The element
*/
fix:function(a){if(!h&&void 0!==document.body.style.zoom){var c=b("<div>").addClass(e);b(a).wrap(c)}return a},/**
* Is sub-pixel text rendering supported?
* @param {void}
* @returns {boolean} true if sub-pixel tex rendering is supported
*/
isSubpxSupported:function(){return h}}}),Ca.addUtility("support",function(){/**
* Helper function to get the proper vendor property name
* (`transition` => `WebkitTransition`)
* @param {string} prop The property name to test for
* @returns {string|boolean} The vendor-prefixed property name or false if the property is not supported
*/
function b(a){var b,e,f,g=document.createElement("div");
// Handle unprefixed versions (FF16+, for example)
if(a in g.style)return a;if(b=a.charAt(0).toUpperCase()+a.substr(1),a in g.style)return a;for(e=0;e<d.length;++e)if(f=d[e]+b,f in g.style)return 0===f.indexOf("ms")&&(f="-"+f),c(f);return!1}/**
* Converts a camelcase stri