UNPKG

viewer

Version:

A viewer for documents converted with the Box View API

896 lines 128 kB
/*! 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("&","&#38;"); // 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