├── git-hooks └── pre-commit ├── .travis.yml ├── src ├── js │ ├── .jshintrc │ ├── wrap.js │ ├── components │ │ ├── layout-vertical-single-column.js │ │ ├── controller-text.js │ │ ├── layout-text.js │ │ ├── dragger.js │ │ ├── layout-presentation-two-page.js │ │ ├── page-img.js │ │ ├── page-links.js │ │ ├── layout-horizontal.js │ │ ├── scroller.js │ │ ├── page-text.js │ │ └── layout-vertical.js │ ├── utilities │ │ ├── browser.js │ │ ├── url.js │ │ ├── subpx.js │ │ └── support.js │ └── data-providers │ │ ├── metadata.js │ │ ├── stylesheet.js │ │ ├── page-img.js │ │ └── page-text.js ├── images │ ├── logo.png │ ├── logo@2x.png │ ├── spinner.gif │ ├── logo.svg │ └── spinner.svg └── css │ ├── logo.css │ ├── theme.css │ └── text.css ├── .gitignore ├── jsdoc-conf.json ├── install-githooks.sh ├── test ├── js │ ├── .jshintrc │ ├── components │ │ ├── page-text-test.js │ │ ├── layout-vertical-single-column-test.js │ │ ├── controller-text-test.js │ │ ├── layout-text-test.js │ │ ├── layout-presentation-two-page-test.js │ │ ├── page-links-test.js │ │ ├── resizer-test.js │ │ ├── layout-vertical-test.js │ │ ├── page-svg-test.js │ │ ├── page-img-test.js │ │ ├── layout-horizontal-test.js │ │ ├── scroller-test.js │ │ └── layout-presentation-test.js │ ├── utilities │ │ ├── support-test.js │ │ └── url-test.js │ ├── data-providers │ │ ├── metadata-test.js │ │ ├── stylesheet-test.js │ │ ├── page-text-test.js │ │ ├── page-img-test.js │ │ └── page-svg-test.js │ └── core │ │ ├── event-target-test.js │ │ └── crocodoc-test.js ├── plugins │ ├── .jshintrc │ ├── realtime │ │ └── crocodoc.realtime-test.js │ ├── index.html │ └── fullscreen │ │ └── fullscreen-test.js └── framework │ └── crocodoc-test-utils.js ├── .npmignore ├── .editorconfig ├── plugins ├── fullscreen │ ├── fullscreen.css │ └── README.md ├── download │ ├── README.md │ └── download.js └── realtime │ └── README.md ├── .jshintrc ├── examples ├── presentations │ ├── fade.css │ ├── slide.css │ ├── pop.css │ ├── spin.css │ ├── carousel.css │ ├── pageflip.css │ └── index.html ├── thumbnails │ ├── index.html │ ├── thumbnails.js │ └── thumbnails.css ├── page-content-thumbnails │ ├── page-content.css │ ├── index.html │ ├── page-content.js │ └── example.js ├── basic-viewer │ └── index.html ├── README.md ├── remember-page │ └── index.html ├── page-content-flip │ ├── page-content.js │ └── index.html ├── realtime │ ├── server.js │ └── index.html └── slider │ └── index.html ├── bower.json ├── package.json └── CONTRIBUTING.md /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /src/js/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "unused": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | build 5 | doc 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/box/viewer.js/master/src/images/logo.png -------------------------------------------------------------------------------- /src/images/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/box/viewer.js/master/src/images/logo@2x.png -------------------------------------------------------------------------------- /src/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/box/viewer.js/master/src/images/spinner.gif -------------------------------------------------------------------------------- /jsdoc-conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "plugins": [ 6 | 7 | ] 8 | } -------------------------------------------------------------------------------- /install-githooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -d .git/hooks ] 4 | then 5 | cp git-hooks/* .git/hooks/ 6 | fi 7 | -------------------------------------------------------------------------------- /test/js/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "module": false, 4 | "test": false, 5 | "qunit": false, 6 | "sinon": false 7 | }, 8 | "sub": true 9 | } 10 | -------------------------------------------------------------------------------- /test/plugins/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "module": false, 4 | "test": false, 5 | "qunit": false, 6 | "sinon": false 7 | }, 8 | "sub": true 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .gitignore 3 | .jshintrc 4 | .travis.yml 5 | 6 | Gruntfile.js 7 | jsdoc-conf.json 8 | bower.json 9 | 10 | build 11 | examples 12 | git-hooks 13 | node_modules 14 | test 15 | testing 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/js/components/page-text-test.js: -------------------------------------------------------------------------------- 1 | module('Component - page-text', { 2 | setup: function () { 3 | this.scope = Crocodoc.getScopeForTest(this); 4 | this.component = Crocodoc.getComponentForTest('page-text', this.scope); 5 | } 6 | }); 7 | 8 | /*test('', function () { 9 | 10 | }); 11 | */ 12 | -------------------------------------------------------------------------------- /plugins/fullscreen/fullscreen.css: -------------------------------------------------------------------------------- 1 | .crocodoc-fullscreen { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | .crocodoc-fakefullscreen { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | bottom: 0; 10 | right: 0; 11 | margin: 0; 12 | padding: 0; 13 | width: 100%; 14 | height: 100%; 15 | z-index: 9999999999; 16 | } 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "$": false, 4 | "Crocodoc": false 5 | }, 6 | "asi": false, 7 | "browser": true, 8 | "camelcase": true, 9 | "curly": true, 10 | "eqeqeq": true, 11 | "eqnull": true, 12 | "evil": true, 13 | "laxcomma": false, 14 | "newcap": true, 15 | "quotmark": "single", 16 | "regexdash": true, 17 | "sub": true, 18 | "trailing": true, 19 | "undef": true, 20 | "unused": true, 21 | "wsh": true 22 | } 23 | -------------------------------------------------------------------------------- /src/js/wrap.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | /*global jQuery*/ 3 | /*jshint unused:false, undef:false*/ 4 | 'use strict'; 5 | window.Crocodoc = (function(fn) { 6 | if (typeof exports === 'object') { 7 | // nodejs / browserify - export a function that accepts a jquery impl 8 | module.exports = fn; 9 | } else { 10 | // normal browser environment 11 | return fn(jQuery); 12 | } 13 | }(function($) { 14 | 15 | //__crocodoc_viewer__ 16 | 17 | return Crocodoc; 18 | })); 19 | })(typeof window !== 'undefined' ? window : this); 20 | -------------------------------------------------------------------------------- /examples/presentations/fade.css: -------------------------------------------------------------------------------- 1 | .crocodoc-presentation-fade .crocodoc-page-before, 2 | .crocodoc-presentation-fade .crocodoc-page-after { 3 | opacity: 0; 4 | } 5 | .crocodoc-presentation-fade .crocodoc-preceding-page { 6 | /* Delay visibility to keep the page visible during the transition */ 7 | -webkit-transition: opacity 0.5s, 8 | visibility 0s ease 0.5s; 9 | transition: opacity 0.5s, 10 | visibility 0s ease 0.5s; 11 | 12 | /* Position the preceding page on top of the current page, so no matter which 13 | order the pages are actually in, the transition will look the same */ 14 | z-index: 2 !important; 15 | } 16 | -------------------------------------------------------------------------------- /src/css/logo.css: -------------------------------------------------------------------------------- 1 | /* logo overlay */ 2 | .crocodoc-viewer-logo { 3 | position: absolute; 4 | bottom: 10px; 5 | left: 10px; 6 | width: 29px; 7 | height: 16px; 8 | opacity: 0.5; 9 | filter: alpha(opacity=50); 10 | background: transparent url(../images/logo.png) 0 0 no-repeat; 11 | pointer-events: none; 12 | } 13 | 14 | .crocodoc-window-as-viewport .crocodoc-viewer-logo { 15 | position: fixed; 16 | } 17 | 18 | /* hidpi logo */ 19 | @media only screen and (-webkit-min-device-pixel-ratio: 2) { 20 | .crocodoc-viewer-logo { 21 | background: transparent url(../images/logo@2x.png) 0 0 no-repeat; 22 | background-size: 29px auto; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/js/components/layout-vertical-single-column.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview layout-vertical-single-column component definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * The vertical-single-column layout 8 | */ 9 | Crocodoc.addComponent('layout-' + LAYOUT_VERTICAL_SINGLE_COLUMN, ['layout-' + LAYOUT_VERTICAL], function (scope, vertical) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Public 15 | //-------------------------------------------------------------------------- 16 | 17 | // there is nothing different about this layout aside from the name (and CSS class name) 18 | // so we can just return the vertical layout 19 | return vertical; 20 | }); 21 | -------------------------------------------------------------------------------- /examples/presentations/slide.css: -------------------------------------------------------------------------------- 1 | /*Make all pages visible and apply a transition on opacity and transform*/ 2 | .crocodoc-presentation-slide .crocodoc-page { 3 | -webkit-transition: opacity 0.2s, -webkit-transform 0.4s; 4 | transition: opacity 0.2s, transform 0.4s; 5 | visibility: visible; 6 | } 7 | /*Transform 100% to the left and transparentize*/ 8 | .crocodoc-presentation-slide .crocodoc-page-before { 9 | -webkit-transform: translateX(-100%); 10 | transform: translateX(-100%); 11 | -webkit-transition-delay: 0.2s, 0s; 12 | transition-delay: 0.2s, 0s; 13 | opacity: 0; 14 | } 15 | /*Transform 100% to the right and transparentize*/ 16 | .crocodoc-presentation-slide .crocodoc-page-after { 17 | -webkit-transform: translateX(100%); 18 | transform: translateX(100%); 19 | -webkit-transition-delay: 0.2s, 0s; 20 | transition-delay: 0.2s, 0s; 21 | opacity: 0; 22 | } 23 | -------------------------------------------------------------------------------- /examples/thumbnails/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | ? / ? 18 | 19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/css/theme.css: -------------------------------------------------------------------------------- 1 | /* Default Theme -------------------------------- */ 2 | 3 | /* Background */ 4 | .crocodoc-viewport { 5 | background: transparent; 6 | } 7 | 8 | /* Page borders/backgrounds */ 9 | .crocodoc-page-content { 10 | border: 0; 11 | background: #fff; 12 | } 13 | 14 | /* Page paddings */ 15 | .crocodoc-page { 16 | padding: 10px 15px; 17 | } 18 | 19 | /* Text selection color */ 20 | /* NOTE: these two *-selection rules MUST be separate (cannot be comma-separated) */ 21 | .crocodoc-page-text { 22 | opacity: 0.4; 23 | } 24 | 25 | /* override these rules with !important to change the highlight color */ 26 | /* @NOTE: !important is used here because stylesheet.css would otherwise take precedence in this case */ 27 | .crocodoc-page-text ::-moz-selection { 28 | background: rgba(50, 151, 253, 0.75) !important; 29 | } 30 | .crocodoc-page-text ::selection { 31 | background: rgba(50, 151, 253, 0.75) !important; 32 | } 33 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewer", 3 | "version": "0.11.1", 4 | "homepage": "https://github.com/box/viewer.js", 5 | "authors": [ 6 | "Cameron Lakenen ", 7 | "Nicholas Silva " 8 | ], 9 | "description": "A viewer for documents converted with the Box View API", 10 | "main": ["dist/crocodoc.viewer.js", "dist/crocodoc.viewer.css"], 11 | "keywords": [ 12 | "box", 13 | "box", 14 | "view", 15 | "crocodoc", 16 | "view", 17 | "api", 18 | "viewer", 19 | "viewerjs" 20 | ], 21 | "license": "Apache-2.0", 22 | "ignore": [ 23 | "**/.*", 24 | "build", 25 | "node_modules", 26 | "bower_components", 27 | "examples", 28 | "git-hooks", 29 | "test", 30 | "testing", 31 | ".editorconfig", 32 | ".gitignore", 33 | ".npmignore", 34 | ".jshintrc", 35 | ".travis.yml", 36 | "Gruntfile.js", 37 | "jsdoc-conf.json", 38 | "postinstall.sh" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /examples/presentations/pop.css: -------------------------------------------------------------------------------- 1 | /*Apply a transition to all pages*/ 2 | .crocodoc-presentation-pop .crocodoc-page { 3 | -webkit-transition: visibility 0s, opacity 0.3s, -webkit-transform 0.25s; 4 | transition: visibility 0s, opacity 0.3s, transform 0.25s; 5 | } 6 | /*Transform and transparentize (all) previous pages */ 7 | .crocodoc-presentation-pop .crocodoc-page-before { 8 | -webkit-transform: rotate(-3deg) translate(-12%, -12%); 9 | transform: rotate(-3deg) translate(-12%, -12%); 10 | /*Delay the visibility "transition", because we want to see the opacity/transform transitions*/ 11 | -webkit-transition-delay: 0.3s, 0s, 0s; 12 | transition-delay: 0.3s, 0s, 0s; 13 | opacity: 0; 14 | z-index: 2; 15 | } 16 | /*The current and next page should always be visible and transform-less*/ 17 | .crocodoc-presentation-pop .crocodoc-page-next { 18 | -webkit-transform: none; 19 | transform: none; 20 | visibility: visible; 21 | opacity: 1; 22 | } 23 | -------------------------------------------------------------------------------- /plugins/download/README.md: -------------------------------------------------------------------------------- 1 | # Download Plugin 2 | 3 | A simple plugin that initiates a download for the original document from the Box View API. 4 | 5 | ## Contents 6 | * [Usage](#usage) 7 | * [Options](#options) 8 | * [API Methods](#api-methods) 9 | 10 | ## Usage 11 | 12 | Include `download.js` in your page. 13 | 14 | Example: 15 | ```js 16 | var viewer = Crocodoc.createViewer('.viewer', { 17 | // ... 18 | plugins: { 19 | download: { 20 | url: '' 21 | } 22 | } 23 | }); 24 | ``` 25 | 26 | 27 | ## Options 28 | 29 | The following configuration options are available: 30 | 31 | **url** 32 | 33 | The URL to the Box View download endpoint associated with the viewing session. This would be available in the View API session response under `urls.download` if the session was created with the `is_downloadable` flag. 34 | 35 | 36 | ## API Methods 37 | 38 | The following method is added to the viewer API when using the download plugin: 39 | 40 | **download()** 41 | 42 | Initiate a download for the document. 43 | -------------------------------------------------------------------------------- /test/js/components/layout-vertical-single-column-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-vertical-single-column', { 2 | setup: function () { 3 | this.utilities = { 4 | common: Crocodoc.getUtilityForTest('common') 5 | }; 6 | this.config = { 7 | layout: Crocodoc.LAYOUT_VERTICAL_SINGLE_COLUMN 8 | }; 9 | this.scope = Crocodoc.getScopeForTest(this); 10 | this.mixins = { 11 | 'layout-vertical': { 12 | init: function () {}, 13 | extend: function (obj) { 14 | return Crocodoc.getUtility('common').extend({}, this, obj); 15 | } 16 | } 17 | }; 18 | 19 | this.component = Crocodoc.getComponentForTest('layout-vertical-single-column', this.scope, this.mixins); 20 | } 21 | }); 22 | 23 | test('init() should initialize a vertical layout when called', function () { 24 | var initSpy = this.spy(this.mixins['layout-vertical'], 'init'); 25 | this.component.init(); 26 | ok(initSpy.calledOn(this.component), 'init was called on the proper context'); 27 | }); 28 | -------------------------------------------------------------------------------- /test/js/utilities/support-test.js: -------------------------------------------------------------------------------- 1 | 2 | module('Utility - support', { 3 | setup: function () { 4 | this.fakeXHR = {}; 5 | this.util = Crocodoc.getUtilityForTest('support'); 6 | } 7 | }); 8 | 9 | test('isXHRSupported() should return true if XHR is supported', function() { 10 | this.stub(this.util, 'getXHR').returns(this.fakeXHR); 11 | ok(this.util.isXHRSupported(), 'XHR should be supported'); 12 | }); 13 | 14 | test('isXHRSupported() should return false if XHR is not supported', function() { 15 | this.stub(this.util, 'getXHR').returns(null); 16 | ok(!this.util.isXHRSupported(), 'XHR should be supported'); 17 | }); 18 | 19 | test('isCORSSupported() should return true if CORS is supported', function() { 20 | this.fakeXHR.withCredentials = true; 21 | this.stub(this.util, 'getXHR').returns(this.fakeXHR); 22 | ok(this.util.isCORSSupported(), 'CORS should be supported'); 23 | }); 24 | 25 | test('isCORSSupported() should return false if CORS is not supported', function() { 26 | this.stub(this.util, 'getXHR').returns(this.fakeXHR); 27 | ok(!this.util.isCORSSupported(), 'CORS should not be supported'); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/page-content-thumbnails/page-content.css: -------------------------------------------------------------------------------- 1 | .page-content { 2 | font-family: Calibri,Arial,sans-serif; 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | bottom: 0; 7 | right: 0; 8 | text-align: left; 9 | } 10 | .page-content .page-number { 11 | background: white; 12 | border: 5px solid #aaa; 13 | margin-left: -5%; 14 | margin-top: 2%; 15 | min-width: 15%; 16 | font-size: 100px; 17 | display: inline-block; 18 | text-align: center; 19 | } 20 | .page-content button { 21 | position: absolute; 22 | width: 100%; 23 | bottom: 0; 24 | left: 0; 25 | font-size: 100px; 26 | margin: 0; 27 | display: block; 28 | } 29 | .page-content .remove-page-btn { 30 | display: none; 31 | } 32 | .crocodoc-page.page-content-added .add-page-btn { 33 | display: none; 34 | } 35 | .crocodoc-page.page-content-added .remove-page-btn { 36 | display: block; 37 | } 38 | .thumbnails .crocodoc-page { 39 | padding: 40px 0 40px 100px; 40 | } 41 | /* align the thumbnail pages to the left instead of center */ 42 | .thumbnails .crocodoc-pages { 43 | text-align: left; 44 | } 45 | .thumbnails .crocodoc-viewer-logo { 46 | display: none; 47 | } 48 | -------------------------------------------------------------------------------- /examples/presentations/spin.css: -------------------------------------------------------------------------------- 1 | .crocodoc-presentation-spin .crocodoc-doc { 2 | /*Make it look 3d!*/ 3 | -webkit-transform-style: preserve-3d; 4 | transform-style: preserve-3d; 5 | -webkit-perspective: 1000px; 6 | perspective: 1000px; 7 | } 8 | 9 | /*Apply a transition to all pages*/ 10 | .crocodoc-presentation-spin .crocodoc-page { 11 | -webkit-transition: visibility 0s, -webkit-transform 0.5s; 12 | transition: visibility 0s, transform 0.5s; 13 | 14 | /*We don't want the backs of pages showing*/ 15 | -webkit-backface-visibility: hidden; 16 | backface-visibility: hidden; 17 | 18 | visibility: visible !important; 19 | } 20 | 21 | .crocodoc-presentation-spin .crocodoc-page-before { 22 | -webkit-transform: rotateY(-180deg); 23 | transform: rotateY(-180deg); 24 | /*Delay the visibility "transition", because we want to see the transform transition*/ 25 | -webkit-transition-delay: 0.5s, 0s; 26 | transition-delay: 0.5s, 0s; 27 | } 28 | 29 | .crocodoc-presentation-spin .crocodoc-page-after { 30 | -webkit-transform: rotateY(180deg); 31 | transform: rotateY(180deg); 32 | -webkit-transition-delay: 0.5s, 0s; 33 | transition-delay: 0.5s, 0s; 34 | } 35 | -------------------------------------------------------------------------------- /plugins/download/download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview The download plugin for the View API 3 | * @author lakenen 4 | */ 5 | 6 | Crocodoc.addPlugin('download', function (scope) { 7 | 'use strict'; 8 | 9 | /** 10 | * Initiate a download for the given URL 11 | * @param {string} url The download URL 12 | * @returns {void} 13 | * @private 14 | */ 15 | function download(url) { 16 | var a = document.createElement('a'); 17 | a.href = url; 18 | a.setAttribute('download', 'doc'); 19 | document.body.appendChild(a); 20 | a.click(); 21 | document.body.removeChild(a); 22 | } 23 | 24 | return { 25 | /** 26 | * Initialize the download plugin 27 | * @param {Object} config The config object 28 | * @param {string} config.url The download URL 29 | * @returns {void} 30 | */ 31 | init: function (config) { 32 | var url = config.url, 33 | viewerAPI = scope.getConfig().api; 34 | 35 | if (url) { 36 | viewerAPI.download = function () { 37 | download(url); 38 | }; 39 | } 40 | } 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /examples/basic-viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 21 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Viewer.js Examples # 2 | 3 | In this directory, you'll find several examples of common viewer.js use cases. 4 | 5 | * `basic-viewer` 6 | - The simplest possible viewer.js example. Loads a document with default configurations. 7 | * `page-content-flip` 8 | - Add additional content to each page, with a fancy card-flip transition when interacted with. 9 | * `page-content-thumbnails` 10 | - Similar to `thumbnails`, but with additional interactive content added to each page via a custom plugin. 11 | * `presentations` 12 | - Various demos of presentation transitions and modes. 13 | * `realtime` 14 | - An example using the realtime plugin to stream pages to the viewer as they are finished converting. This example uses a local server to simulate conversion, so you can test it out on already converted documents. 15 | * `remember-page` 16 | - Store current page number in the url hash, then resume from the proper page when linked to or reloading the browser window. 17 | * `slider` 18 | - Two separate viewer instances are created: left and right 19 | - There is a slider in the middle that can be dragged to reveal more of either viewer. A filter is applied to on the left (in browsers that support CSS filters). 20 | * `thumbnails` 21 | - Two separate viewer instances are created: one for the presentation, and the other for thumbnails. When a thumbnail is clicked, the presentation view jumps to the correct page. 22 | -------------------------------------------------------------------------------- /examples/page-content-thumbnails/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/js/components/controller-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview controller-text component 3 | * @author lakenen 4 | */ 5 | 6 | Crocodoc.addComponent('controller-text', function (scope) { 7 | 8 | 'use strict'; 9 | 10 | //-------------------------------------------------------------------------- 11 | // Private 12 | //-------------------------------------------------------------------------- 13 | 14 | var $promise; 15 | 16 | //-------------------------------------------------------------------------- 17 | // Public 18 | //-------------------------------------------------------------------------- 19 | 20 | return { 21 | 22 | /** 23 | * Initialize the controller 24 | * @returns {void} 25 | */ 26 | init: function () { 27 | var config = scope.getConfig(); 28 | config.$textContainer = $(); 29 | 30 | // we can just load the text immediately 31 | $promise = scope.get('page-text', 1).then(function (html) { 32 | // the viewport could be window in useWindowAsViewport, so get 33 | // the real viewport div 34 | var $viewport = config.$doc.parent(); 35 | config.$doc = $(html); 36 | $viewport.html(config.$doc); 37 | }); 38 | }, 39 | 40 | /** 41 | * Destroy the viewer-base component 42 | * @returns {void} 43 | */ 44 | destroy: function () { 45 | $promise.abort(); 46 | } 47 | }; 48 | }); 49 | -------------------------------------------------------------------------------- /test/js/components/controller-text-test.js: -------------------------------------------------------------------------------- 1 | module('Component - controller-text', { 2 | setup: function () { 3 | var self = this; 4 | 5 | this.metadata = { 6 | type: 'text' 7 | }; 8 | 9 | this.components = { 10 | }; 11 | 12 | this.utilities = { 13 | }; 14 | 15 | this.config = $.extend(true, {}, Crocodoc.Viewer.defaults); 16 | this.config.$el = $('
'); 17 | this.config.$viewport = $('
').appendTo(this.config.$el); 18 | this.config.$doc = $('
').appendTo(this.config.$viewport); 19 | 20 | this.scope = Crocodoc.getScopeForTest(this); 21 | this.component = Crocodoc.getComponentForTest('controller-text', this.scope); 22 | } 23 | }); 24 | 25 | test('init() should request the text file and setting it as $doc', function () { 26 | var stub = this.stub(this.scope, 'get'); 27 | stub.withArgs('page-text').returns($.Deferred().resolve('
').promise()); 28 | 29 | this.component.init(); 30 | 31 | ok(stub.called, 'requested page text'); 32 | equal(this.config.$el.find('.crocodoc-text').length, 1, 'inserted page text into viewport'); 33 | }); 34 | 35 | 36 | test('init() should work with useWindowAsViewport', function () { 37 | this.config.$viewport = $(window); 38 | var stub = this.stub(this.scope, 'get'); 39 | stub.withArgs('page-text').returns($.Deferred().resolve('
').promise()); 40 | 41 | this.component.init(); 42 | 43 | ok(stub.called, 'requested page text'); 44 | equal(this.config.$el.find('.crocodoc-text').length, 1, 'inserted page text into viewport'); 45 | }); 46 | -------------------------------------------------------------------------------- /src/js/utilities/browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview browser detection for use when feature detection won't work 3 | */ 4 | 5 | Crocodoc.addUtility('browser', function () { 6 | 7 | 'use strict'; 8 | 9 | var ua = navigator.userAgent, 10 | version, 11 | browser = {}, 12 | ios = /ip(hone|od|ad)/i.test(ua), 13 | android = /android/i.test(ua), 14 | blackberry = /blackberry/i.test(ua), 15 | webos = /webos/i.test(ua), 16 | kindle = /silk|kindle/i.test(ua), 17 | ie = /MSIE|Trident/i.test(ua); 18 | 19 | if (ie) { 20 | browser.ie = true; 21 | if (/MSIE/i.test(ua)) { 22 | version = /MSIE\s+(\d+\.\d+)/i.exec(ua); 23 | } else { 24 | version = /Trident.*rv[ :](\d+\.\d+)/.exec(ua); 25 | } 26 | browser.version = version && parseFloat(version[1]); 27 | browser.ielt9 = browser.version < 9; 28 | browser.ielt10 = browser.version < 10; 29 | browser.ielt11 = browser.version < 11; 30 | } 31 | if (ios) { 32 | browser.ios = true; 33 | version = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/); 34 | browser.version = version && parseFloat(version[1] + '.' + version[2]); 35 | } 36 | browser.mobile = /mobile/i.test(ua) || ios || android || blackberry || webos || kindle; 37 | browser.firefox = /firefox/i.test(ua); 38 | if (/safari/i.test(ua)) { 39 | browser.chrome = /chrome/i.test(ua); 40 | browser.safari = !browser.chrome; 41 | } 42 | if (browser.safari) { 43 | version = (navigator.appVersion).match(/Version\/(\d+(\.\d+)?)/); 44 | browser.version = version && parseFloat(version[1]); 45 | } 46 | 47 | return browser; 48 | }); 49 | -------------------------------------------------------------------------------- /test/js/components/layout-text-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-text', { 2 | setup: function () { 3 | var self = this; 4 | this.utilities = { 5 | common: Crocodoc.getUtilityForTest('common') 6 | }; 7 | this.config = { 8 | $el: $(), 9 | $viewport: $('
'), 10 | $doc: $(''), 11 | minZoom: 0.01, 12 | maxZoom: 5, 13 | zoomLevels: [0.5, 1, 1.5, 5] 14 | }; 15 | this.scope = Crocodoc.getScopeForTest(this); 16 | 17 | this.component = Crocodoc.getComponentForTest('layout-text', this.scope); 18 | } 19 | }); 20 | 21 | QUnit.cases([ 22 | { prevZoom: 0.5, zoom: 1.5, canZoomIn: true, canZoomOut: false }, 23 | { prevZoom: 1.5, zoom: 1.5, canZoomIn: true, canZoomOut: true }, 24 | { prevZoom: 1, zoom: 5, canZoomIn: false, canZoomOut: false } 25 | ]).test('setZoom() should update the zoom state appropriately when called', function (params) { 26 | var calculateNextZoomLevelStub = this.stub(this.component, 'calculateNextZoomLevel'); 27 | calculateNextZoomLevelStub.withArgs(Crocodoc.ZOOM_IN).returns(params.canZoomIn); 28 | calculateNextZoomLevelStub.withArgs(Crocodoc.ZOOM_OUT).returns(params.canZoomOut); 29 | 30 | this.component.init(); 31 | this.component.state.zoomState.zoom = params.prevZoom; 32 | 33 | this.component.setZoom(params.zoom); 34 | 35 | equal(this.component.state.zoomState.zoom, params.zoom, 'zoom was updated'); 36 | equal(this.component.state.zoomState.prevZoom, params.prevZoom, 'prevZoom was updated'); 37 | equal(this.component.state.zoomState.canZoomIn, params.canZoomIn , 'canZoomIn was updated'); 38 | equal(this.component.state.zoomState.canZoomOut, params.canZoomOut, 'canZoomOut was updated'); 39 | }); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewer", 3 | "version": "0.11.1", 4 | "description": "A viewer for documents converted with the Box View API", 5 | "author": "Cameron Lakenen ", 6 | "contributors": [ 7 | "Nicholas Silva " 8 | ], 9 | "main": "dist/crocodoc.viewer.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/box/viewer.js.git" 13 | }, 14 | "scripts": { 15 | "test": "node_modules/.bin/grunt test" 16 | }, 17 | "keywords": [ 18 | "box", 19 | "box view", 20 | "crocodoc", 21 | "view api", 22 | "viewer", 23 | "viewerjs" 24 | ], 25 | "homepage": "https://github.com/box/viewer.js", 26 | "bugs": { 27 | "url": "https://github.com/box/viewer.js/issues" 28 | }, 29 | "licenses": [ 30 | { 31 | "type": "Apache-2.0", 32 | "url": "https://github.com/box/viewer.js/blob/master/LICENSE" 33 | } 34 | ], 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "event-source-polyfill": "0.0.1", 38 | "grunt": "^0.4.5", 39 | "grunt-bump": "^0.3.0", 40 | "grunt-cli": "~0.1.10", 41 | "grunt-connect-rewrite": "^0.2.1", 42 | "grunt-contrib-concat": "^0.5.1", 43 | "grunt-contrib-connect": "^0.9.0", 44 | "grunt-contrib-copy": "^0.8.0", 45 | "grunt-contrib-cssmin": "^0.12.2", 46 | "grunt-contrib-jshint": "^0.11.0", 47 | "grunt-contrib-qunit": "^0.5.2", 48 | "grunt-contrib-uglify": "^0.8.0", 49 | "grunt-editor": "^0.1.0", 50 | "grunt-file-info": "^1.0.8", 51 | "grunt-git": "^0.3.2", 52 | "grunt-image-embed": "^0.3.1", 53 | "grunt-jsdoc": "~0.5.1", 54 | "grunt-parallel": "^0.4.1", 55 | "grunt-text-replace": "^0.4.0", 56 | "jquery": "^1.11.2", 57 | "qunit-parameterize": "^0.4.0", 58 | "qunitjs": "^1.14.0", 59 | "sinon": "^1.10.2", 60 | "sinon-qunit": "^2.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/js/components/layout-presentation-two-page-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-presentation-two-page', { 2 | setup: function () { 3 | this.utilities = { 4 | common: Crocodoc.getUtilityForTest('common') 5 | }; 6 | this.config = { 7 | layout: Crocodoc.LAYOUT_PRESENTATION_TWO_PAGE 8 | }; 9 | this.scope = Crocodoc.getScopeForTest(this); 10 | this.mixins = { 11 | 'layout-presentation': { 12 | init: function () {}, 13 | calculatePreviousPage: function () {}, 14 | calculateNextPage: function () {}, 15 | extend: function (obj) { 16 | return Crocodoc.getUtility('common').extend({}, this, obj); 17 | } 18 | } 19 | }; 20 | 21 | this.component = Crocodoc.getComponentForTest('layout-presentation-two-page', this.scope, this.mixins); 22 | } 23 | }); 24 | 25 | test('init() should enable twoPageMode and initialize a presentation layout when called', function () { 26 | var initSpy = this.spy(this.mixins['layout-presentation'], 'init'); 27 | this.component.init(); 28 | ok(this.component.twoPageMode, 'twoPageMode has been set'); 29 | ok(initSpy.calledOn(this.component), 'init was called with the proper config on the proper context'); 30 | }); 31 | 32 | test('calculatePreviousPage() should return the correct page number when called', function () { 33 | var page = 8; 34 | this.component.state = { currentPage: page }; 35 | equal(this.component.calculatePreviousPage(), page - 2, 'the page was correct'); 36 | }); 37 | 38 | test('calculateNextPage() should return the correct page number when called', function () { 39 | var page = 8; 40 | this.component.state = { currentPage: page }; 41 | equal(this.component.calculateNextPage(), page + 2, 'the page was correct'); 42 | }); 43 | -------------------------------------------------------------------------------- /src/js/components/layout-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview layout-text component definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * Layout for text-based files 8 | */ 9 | Crocodoc.addComponent('layout-' + LAYOUT_TEXT, ['layout-base'], function (scope, base) { 10 | 'use strict'; 11 | 12 | var util = scope.getUtility('common'); 13 | 14 | return base.extend({ 15 | init: function () { 16 | base.init.call(this); 17 | this.zoomLevels = this.config.zoomLevels.slice(); 18 | this.minZoom = this.zoomLevels[0]; 19 | this.maxZoom = this.zoomLevels[this.zoomLevels.length - 1]; 20 | }, 21 | 22 | setZoom: function (val) { 23 | var z, 24 | zoomState = this.state.zoomState, 25 | currentZoom = zoomState.zoom; 26 | 27 | if (typeof val === 'string') { 28 | z = this.calculateNextZoomLevel(val); 29 | if (!z) { 30 | if (val === 'auto' || val === 'fitwidth' || val === 'fitheight') { 31 | z = 1; 32 | } else { 33 | z = currentZoom; 34 | } 35 | } 36 | } else { 37 | z = parseFloat(val) || currentZoom; 38 | } 39 | 40 | z = util.clamp(z, this.minZoom, this.maxZoom); 41 | this.config.$doc.css('font-size', (z * 10) + 'pt'); 42 | 43 | zoomState.prevZoom = currentZoom; 44 | zoomState.zoom = z; 45 | zoomState.canZoomIn = this.calculateNextZoomLevel(Crocodoc.ZOOM_IN) !== false; 46 | zoomState.canZoomOut = this.calculateNextZoomLevel(Crocodoc.ZOOM_OUT) !== false; 47 | 48 | scope.broadcast('zoom', util.extend({ 49 | isDraggable: this.isDraggable() 50 | }, zoomState)); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/js/components/page-links-test.js: -------------------------------------------------------------------------------- 1 | module('Component - page-links', { 2 | setup: function () { 3 | this.links = [ 4 | {bbox: [690.89, 45.87, 716.15, 63.55], uri: 'http://box.com/'} 5 | ]; 6 | this.browser = Crocodoc.getUtilityForTest('browser'); 7 | this.scope = Crocodoc.getScopeForTest(this); 8 | this.utilities = { 9 | browser: this.browser 10 | }; 11 | this.component = Crocodoc.getComponentForTest('page-links', this.scope); 12 | } 13 | }); 14 | 15 | test('init() should create links when called', function () { 16 | var links = []; 17 | this.mock(this.component) 18 | .expects('createLinks') 19 | .withArgs(links); 20 | this.component.init($(), links); 21 | }); 22 | 23 | test('init() should create links with a child span element for IE workaround when called', function () { 24 | var $el = $('
'); 25 | this.browser.ie = true; 26 | this.component.init($el, this.links); 27 | ok($el.find('.crocodoc-page-link span').length > 0, 'span element should exist'); 28 | }); 29 | 30 | test('init() should create links with rel="noreferrer" when called', function () { 31 | var $el = $('
'); 32 | this.component.init($el, this.links); 33 | ok($el.find('.crocodoc-page-link').attr('rel') === 'noreferrer', 'link should have rel=noreferrer'); 34 | }); 35 | 36 | test('module should broadcast `linkclick` event with appropriate data when a link is clicked', function () { 37 | var $el = $('
'), 38 | linkData = this.links[0]; 39 | 40 | this.browser.ie = false; 41 | this.mock(this.scope) 42 | .expects('broadcast') 43 | .withArgs('linkclick', linkData); 44 | this.component.init($el, this.links); 45 | 46 | var link = $el.find('.crocodoc-page-link').get(0); 47 | var ev = $.Event('click'); 48 | ev.target = link; 49 | 50 | $el.trigger(ev); 51 | }); 52 | -------------------------------------------------------------------------------- /src/js/data-providers/metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A standard data provider for metadata 3 | * @author lakenen 4 | */ 5 | Crocodoc.addDataProvider('metadata', function(scope) { 6 | 'use strict'; 7 | 8 | var ajax = scope.getUtility('ajax'), 9 | util = scope.getUtility('common'), 10 | config = scope.getConfig(); 11 | 12 | /** 13 | * Process metadata json and return the result 14 | * @param {string} json The original JSON text 15 | * @returns {string} The processed JSON text 16 | * @private 17 | */ 18 | function processJSONContent(json) { 19 | return util.parseJSON(json); 20 | } 21 | 22 | //-------------------------------------------------------------------------- 23 | // Public 24 | //-------------------------------------------------------------------------- 25 | 26 | return { 27 | /** 28 | * Retrieve the info.json asset from the server 29 | * @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request. 30 | */ 31 | get: function() { 32 | var url = this.getURL(), 33 | $promise = ajax.fetch(url, Crocodoc.ASSET_REQUEST_RETRIES); 34 | 35 | // @NOTE: promise.then() creates a new promise, which does not copy 36 | // custom properties, so we need to create a futher promise and add 37 | // an object with the abort method as the new target 38 | return $promise.then(processJSONContent).promise({ 39 | abort: $promise.abort 40 | }); 41 | }, 42 | 43 | /** 44 | * Build and return the URL to the metadata JSON 45 | * @returns {string} The URL 46 | */ 47 | getURL: function () { 48 | var jsonPath = config.template.json; 49 | return config.url + jsonPath + config.queryString; 50 | } 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /test/plugins/realtime/crocodoc.realtime-test.js: -------------------------------------------------------------------------------- 1 | module('Plugin support - Crocodoc.Realtime', { 2 | setup: function () { 3 | this.eventSourceIntance = { 4 | addEventListener: function () {}, 5 | removeEventListener: function () {}, 6 | close: function () {} 7 | }; 8 | this.Realtime = Crocodoc.Realtime; 9 | this.EventSource = sinon.stub().returns(this.eventSourceIntance); 10 | }, 11 | teardown: function () { 12 | if (this.realtime) { 13 | this.realtime.destroy(); 14 | } 15 | } 16 | }); 17 | 18 | test('constructor should throw an error if EventSource is not available', function () { 19 | var self = this; 20 | throws(function () { 21 | self.realtime = new self.Realtime('', null); 22 | }, 'constructor should throw an error'); 23 | }); 24 | 25 | test('constructor should create an instance of EventSource with the given url when called', function () { 26 | var url = 'test'; 27 | this.realtime = new this.Realtime(url, this.EventSource); 28 | ok(this.EventSource.calledWith(url), 'EventSource should be created with the given url'); 29 | }); 30 | 31 | test('on() should register a new event listener when called', function () { 32 | var name = 'myevent', 33 | handler = function () {}; 34 | this.mock(this.eventSourceIntance) 35 | .expects('addEventListener') 36 | .withArgs(name, handler, false); 37 | 38 | this.realtime = new this.Realtime('', this.EventSource); 39 | this.realtime.on(name, handler); 40 | }); 41 | 42 | test('off() should remove the specified event listener when called', function () { 43 | var name = 'myevent', 44 | handler = function () {}; 45 | this.mock(this.eventSourceIntance) 46 | .expects('removeEventListener') 47 | .withArgs(name, handler); 48 | 49 | this.realtime = new this.Realtime('', this.EventSource); 50 | this.realtime.off(name, handler); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/presentations/carousel.css: -------------------------------------------------------------------------------- 1 | .crocodoc-presentation-carousel .crocodoc-doc { 2 | background: transparent; 3 | overflow: visible; 4 | -webkit-perspective: 500px; 5 | perspective: 500px; 6 | -webkit-perspective-origin: 50% 50%; 7 | perspective-origin: 50% 50%; 8 | -webkit-transform-style: preserve-3d; 9 | transform-style: preserve-3d; 10 | } 11 | 12 | .crocodoc-presentation-carousel .crocodoc-page-inner { 13 | border: 0; 14 | } 15 | 16 | .crocodoc-presentation-carousel .crocodoc-page { 17 | -webkit-transform-style: preserve-3d; 18 | transform-style: preserve-3d; 19 | -webkit-transition: -webkit-transform 0.5s, opacity 0.3s; 20 | transition: transform 0.5s, opacity 0.3s; 21 | } 22 | 23 | .crocodoc-presentation-carousel .crocodoc-page-prev, 24 | .crocodoc-presentation-carousel .crocodoc-page-next, 25 | .crocodoc-presentation-carousel .crocodoc-page-before, 26 | .crocodoc-presentation-carousel .crocodoc-page-after { 27 | visibility: visible !important; 28 | } 29 | .crocodoc-presentation-carousel .crocodoc-current-page { 30 | -webkit-transform: none; 31 | transform: none; 32 | } 33 | .crocodoc-presentation-carousel .crocodoc-page-before { 34 | -webkit-transform: rotateY(-90deg) translateX(-200%); 35 | transform: rotateY(-90deg) translateX(-200%); 36 | opacity: 0; 37 | } 38 | .crocodoc-presentation-carousel .crocodoc-page-after { 39 | -webkit-transform: rotateY(90deg) translateX(200%); 40 | transform: rotateY(90deg) translateX(200%); 41 | opacity: 0; 42 | } 43 | .crocodoc-presentation-carousel .crocodoc-page-prev { 44 | -webkit-transform: rotateY(-30deg) translateX(-100%); 45 | transform: rotateY(-30deg) translateX(-100%); 46 | z-index: 1; 47 | opacity: 1; 48 | } 49 | .crocodoc-presentation-carousel .crocodoc-page-next { 50 | -webkit-transform: rotateY(30deg) translateX(100%); 51 | transform: rotateY(30deg) translateX(100%); 52 | z-index: 1; 53 | opacity: 1; 54 | } 55 | -------------------------------------------------------------------------------- /src/css/text.css: -------------------------------------------------------------------------------- 1 | .crocodoc-text-disabled .crocodoc-text { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | 10 | .crocodoc-doc.crocodoc-text { 11 | margin: 0; 12 | padding: 0; 13 | opacity: 1; 14 | text-align: left; 15 | counter-reset: line-numbering; 16 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 17 | font-size: 10pt; 18 | background: #fff; 19 | min-height: 100%; 20 | width: 100%; 21 | tab-size: 8; 22 | border-collapse: collapse; 23 | border-spacing: 0; 24 | } 25 | .crocodoc-text tr:last-child { 26 | height: 100%; 27 | } 28 | 29 | .crocodoc-text tr:first-child td { 30 | padding-top: 0.3em; 31 | } 32 | .crocodoc-text tr:last-child td { 33 | padding-bottom: 0.3em; 34 | } 35 | 36 | /* line numbers */ 37 | 38 | .crocodoc-text td:first-child { 39 | width: 1%; 40 | padding-left: 0.5em; 41 | padding-right: 0.3em; 42 | vertical-align: top; 43 | text-align: right; 44 | color: #aaa; 45 | background: #f8f8f8; 46 | border-right: 1px solid #eee; 47 | 48 | -webkit-touch-callout: none; 49 | -webkit-user-select: none; 50 | -khtml-user-select: none; 51 | -moz-user-select: none; 52 | -ms-user-select: none; 53 | user-select: none; 54 | -webkit-box-sizing: border-box !important; 55 | -moz-box-sizing: border-box !important; 56 | box-sizing: border-box !important; 57 | } 58 | .crocodoc-text td:first-child:before { 59 | content: counter(line-numbering); 60 | counter-increment: line-numbering; 61 | } 62 | 63 | /* line contents */ 64 | .crocodoc-text td:last-child { 65 | vertical-align: top; 66 | text-align: left; 67 | margin: 0; 68 | overflow: hidden; 69 | position: relative; 70 | padding: 0 10px; 71 | white-space: pre-wrap; 72 | word-wrap: break-word; 73 | word-break: break-all; 74 | color: #333; 75 | vertical-align: top; 76 | } 77 | -------------------------------------------------------------------------------- /test/js/components/resizer-test.js: -------------------------------------------------------------------------------- 1 | module('Component - resizer', { 2 | setup: function () { 3 | var self = this; 4 | this.utilities = { 5 | common: Crocodoc.getUtilityForTest('common') 6 | }; 7 | this.scope = Crocodoc.getScopeForTest(this); 8 | this.component = Crocodoc.getComponentForTest('resizer', this.scope); 9 | }, 10 | teardown: function () { 11 | this.component.destroy(); 12 | } 13 | }); 14 | 15 | test('module should fire "resize" event with the proper data when initialized', function () { 16 | var w = 100, h = 200, 17 | data = { 18 | width: w, 19 | height: h 20 | }, 21 | $el = $('
').css(data).appendTo(document.body); 22 | 23 | this.mock(this.scope) 24 | .expects('broadcast') 25 | .withArgs('resize', sinon.match(data)); 26 | this.component.init($el); 27 | }); 28 | 29 | asyncTest('module should fire "resize" event with the proper data when element is resized', function () { 30 | var w = 100, h = 200, 31 | data = { 32 | width: w, 33 | height: h 34 | }, 35 | module = this.component, 36 | $el = $('
').css({ 37 | position: 'absolute', 38 | width: 0, 39 | height: 0 40 | }).appendTo(document.body); 41 | 42 | module.init($el); 43 | 44 | this.scope.broadcast = function (name, d) { 45 | equal(name, 'resize', 'resize event fired'); 46 | equal(d.width, data.width, 'width is correct'); 47 | equal(d.height, data.height, 'height is correct'); 48 | QUnit.start(); 49 | }; 50 | 51 | $el.css(data); 52 | }); 53 | 54 | test('onmessage() should trigger a resize message when called', function () { 55 | var w = 100, h = 200, 56 | data = { 57 | width: w, 58 | height: h 59 | }, 60 | $el = $('
').css(data).appendTo(document.body); 61 | this.component.init($el); 62 | this.mock(this.scope) 63 | .expects('broadcast') 64 | .withArgs('resize', sinon.match(data)); 65 | this.component.onmessage('layoutchange'); 66 | }); 67 | -------------------------------------------------------------------------------- /plugins/fullscreen/README.md: -------------------------------------------------------------------------------- 1 | # Fullscreen Plugin 2 | 3 | Enables fullscreen functionality in viewer.js. 4 | 5 | ## Contents 6 | * [Usage](#usage) 7 | * [Options](#options) 8 | * [API Methods](#api-methods) 9 | * [Events](#events) 10 | 11 | 12 | ## Usage 13 | 14 | Include `fullscreen.css` and `fullscreen.js` in your page. 15 | 16 | Example: 17 | ```js 18 | var viewer = Crocodoc.createViewer('.viewer', { 19 | // ... 20 | plugins: { 21 | fullscreen: { 22 | element: '.viewer-container', 23 | useFakeFullscreen: true 24 | } 25 | } 26 | }); 27 | ``` 28 | 29 | 30 | ## Options 31 | 32 | The following configuration options are available: 33 | 34 | **element** 35 | 36 | A selector or DOM element to use as the fullscreen element. If using `useWindowAsViewport` option on the viewer, this option is ignored (and `document.documentElement` is used). Default: the viewer element. 37 | 38 | **useFakeFullscreen** 39 | 40 | If true, fallback to "fake fullscreen" mode for browsers that do not support native HTML fullscreen. This adds the class `fakefullscreen` to the viewer element, which forces the element to take up the full window. Default: `true`. 41 | 42 | 43 | ## API Methods 44 | 45 | The following methods are added to the viewer API when using the fullscreen plugin: 46 | 47 | **enterFullscreen()** 48 | 49 | Enter fullcreen mode. 50 | 51 | **exitFullscreen()** 52 | 53 | Exit fullscreen mode. 54 | 55 | **isFullscreen()** 56 | 57 | Returns true if currently in fullscreen mode. 58 | 59 | **isFullscreenSupported()** 60 | 61 | Returns true if native fullscreen is supported. 62 | 63 | Example: 64 | ```js 65 | if (viewer.isFullscreenSupported()) { 66 | viewer.enterFullscreen(); // enter fullscreen mode 67 | } 68 | ``` 69 | 70 | 71 | ## Events 72 | 73 | The following events will be fired on the viewer object: 74 | 75 | * `fullscreenchange` - fired when the fullscreen mode changes 76 | * `fullscreenenter` - fired when entering fullscreen mode 77 | * `fullscreenexit` - fired when exiting fullscreen mode 78 | 79 | Example: 80 | ```js 81 | viewer.on('fullscreenenter', function () { 82 | alert('welcome to fullscreen mode!'); 83 | }); 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /test/plugins/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit 6 | 7 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/js/utilities/url-test.js: -------------------------------------------------------------------------------- 1 | module('Utility - url', { 2 | setup: function () { 3 | this.util = Crocodoc.getUtilityForTest('url'); 4 | } 5 | }); 6 | 7 | test('getCurrentURL() should return the current URL when called', function () { 8 | equal(this.util.getCurrentURL(), window.location.href); 9 | }); 10 | 11 | test('makeAbsolute() should return an absolute href when called', function () { 12 | var path = '/hello/world', 13 | origin = location.protocol + '//' + location.host; 14 | 15 | equal(this.util.makeAbsolute(path), origin + path); 16 | }); 17 | 18 | test('makeAbsolute() should return the same url when called with a url that is already absolute', function () { 19 | var path = 'http://blah.webs/hello/world'; 20 | 21 | equal(this.util.makeAbsolute(path), path); 22 | }); 23 | 24 | test('isCrossDomain() should return true when called with a cross-domain url', function () { 25 | var path = 'http://blah.webs/hello/world'; 26 | ok(this.util.isCrossDomain(path)); 27 | }); 28 | 29 | test('isCrossDomain() should return false when called with a same-domain url', function () { 30 | var path = location.protocol + '//' + location.host + '/hello/world'; 31 | ok(!this.util.isCrossDomain(path)); 32 | path = 'a/relative/path'; 33 | ok(!this.util.isCrossDomain(path)); 34 | }); 35 | 36 | test('parse() should return a parsed version of a url when called', function () { 37 | var port = 4000, 38 | protocol = 'http:', 39 | hostname = 'viewer.technology', 40 | host = hostname + ':' + port, 41 | pathname = '/is/a/technology', 42 | hash = '#for-realz', 43 | search = '?beep=boop&bop=beep', 44 | href = protocol + '//' + host + pathname + search + hash, 45 | parsed = this.util.parse(href); 46 | 47 | equal(parsed.port, port); 48 | equal(parsed.protocol, protocol); 49 | equal(parsed.hostname, hostname); 50 | equal(parsed.host, host); 51 | equal(parsed.pathname, pathname); 52 | equal(parsed.hash, hash); 53 | equal(parsed.search, search); 54 | equal(parsed.href, href); 55 | }); 56 | 57 | test('appendQueryParams() should return the correct value when called', function () { 58 | var url, params; 59 | 60 | url = '/hello'; 61 | params = 'foo=bar&baz=wow'; 62 | equal(this.util.appendQueryParams(url, params), url + '?' + params); 63 | 64 | url = '/hello?already=param'; 65 | params = 'foo=bar&baz=wow'; 66 | equal(this.util.appendQueryParams(url, params), url + '&' + params); 67 | }); 68 | -------------------------------------------------------------------------------- /test/js/data-providers/metadata-test.js: -------------------------------------------------------------------------------- 1 | module('Data Provider: metadata', { 2 | setup: function () { 3 | var me = this; 4 | this.$deferred = $.Deferred(); 5 | this.promise = { 6 | abort: function () {}, 7 | then: function () { return me.$deferred.promise(); }, 8 | promise: function (x) { return me.$deferred.promise(x); } 9 | }; 10 | this.utilities = { 11 | ajax: { 12 | fetch: function () {} 13 | }, 14 | common: { 15 | parseJSON: $.parseJSON 16 | } 17 | }; 18 | this.config = { 19 | url: '', 20 | template: { 21 | json: 'info.json' 22 | }, 23 | queryString: '' 24 | }; 25 | this.scope = Crocodoc.getScopeForTest(this); 26 | this.dataProvider = Crocodoc.getComponentForTest('data-provider-metadata', this.scope); 27 | }, 28 | teardown: function () { 29 | this.scope.destroy(); 30 | } 31 | }); 32 | 33 | test('creator should return an object with a get function', function(){ 34 | equal(typeof this.dataProvider, 'object'); 35 | equal(typeof this.dataProvider.get, 'function'); 36 | }); 37 | 38 | test('get() should return a $.Promise with an abort() function', function() { 39 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 40 | propEqual(this.dataProvider.get(), $.Deferred().promise({abort:function(){}})); 41 | }); 42 | 43 | test('abort() should call abort on the promise returned from ajax.fetch when called on the returned promise', function() { 44 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 45 | this.mock(this.promise).expects('abort').once(); 46 | 47 | var promise = this.dataProvider.get(); 48 | promise.abort(); 49 | }); 50 | 51 | test('getURL() should return the correct URL to the json file when called', function() { 52 | this.config.url = 'http://beep.boop/bop/'; 53 | equal(this.dataProvider.getURL(), this.config.url + this.config.template.json, 'the URL should be correct'); 54 | }); 55 | 56 | test('get() should parse the JSON response when called', function () { 57 | var json = '{ "numpages": 10, "dimensions": { "width": 100, "height": 100 } }'; 58 | 59 | this.stub(this.utilities.ajax, 'fetch').returns(this.$deferred.promise()); 60 | 61 | this.$deferred.resolve(json); 62 | 63 | var promise = this.dataProvider.get(); 64 | promise.done(function (data) { 65 | equal(typeof data, 'object', 'data should be an object'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /examples/page-content-thumbnails/page-content.js: -------------------------------------------------------------------------------- 1 | // Let's add a `page-content` plugin that binds to the viewer and 2 | // inserts content into each page element. 3 | Crocodoc.addPlugin('page-content', function (scope) { 4 | var util = scope.getUtility('common'); 5 | 6 | var config = { 7 | template: '' 8 | }, 9 | $pages; 10 | 11 | // function that gets called when the "Add Page" button is clicked 12 | function addPage(pageNum) { 13 | $pages.eq(pageNum - 1).addClass('page-content-added'); 14 | } 15 | 16 | // function that gets called when the "Remove Page" button is clicked 17 | function removePage(pageNum) { 18 | $pages.eq(pageNum - 1).removeClass('page-content-added'); 19 | } 20 | 21 | // render the template and insert it into the given page 22 | function insertContent(pageNum) { 23 | var $pageOverlay = $pages.eq(pageNum - 1).find('.crocodoc-page-autoscale'); 24 | var $content = $(util.template(config.template, { 25 | pageNum: pageNum 26 | })); 27 | 28 | // listen for click events on the buttons 29 | $content.find('.add-page-btn').on('click', function (event) { 30 | addPage(pageNum); 31 | event.stopPropagation(); 32 | }); 33 | $content.find('.remove-page-btn').on('click', function (event) { 34 | removePage(pageNum); 35 | event.stopPropagation(); 36 | }); 37 | 38 | $pageOverlay.append($content); 39 | } 40 | 41 | // the plugin's public interface 42 | // init, onmessage and destroy are called by the framework when appropriate 43 | return { 44 | // this plugin listens for the 'pageload message' 45 | messages: ['ready', 'pageload'], 46 | 47 | // insert content into each page as it loads 48 | onmessage: function (name, data) { 49 | switch (name) { 50 | case 'ready': 51 | // $pages won't be available until the 'ready' message is broadcas 52 | $pages = scope.getConfig().$pages; 53 | break; 54 | case 'pageload': 55 | insertContent(data.page); 56 | break; 57 | } 58 | }, 59 | 60 | // initialize config and $pages object 61 | init: function (pluginConfig) { 62 | config = util.extend(config, pluginConfig); 63 | }, 64 | 65 | // remove all page content when destroyed 66 | destroy: function () { 67 | $pages.find('.page-content').remove(); 68 | } 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /src/js/components/dragger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Dragger component used to click-to-drag the document when enabled 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * Dragger component definition 8 | */ 9 | Crocodoc.addComponent('dragger', function (scope) { 10 | 11 | 'use strict'; 12 | 13 | var $el, 14 | $window = $(window), 15 | downScrollPosition, 16 | downMousePosition; 17 | 18 | /** 19 | * Handle mousemove events 20 | * @param {Event} event The event object 21 | * @returns {void} 22 | */ 23 | function handleMousemove(event) { 24 | $el.scrollTop(downScrollPosition.top - (event.clientY - downMousePosition.y)); 25 | $el.scrollLeft(downScrollPosition.left - (event.clientX - downMousePosition.x)); 26 | event.preventDefault(); 27 | } 28 | 29 | /** 30 | * Handle mouseup events 31 | * @param {Event} event The event object 32 | * @returns {void} 33 | */ 34 | function handleMouseup(event) { 35 | scope.broadcast('dragend'); 36 | $window.off('mousemove', handleMousemove); 37 | $window.off('mouseup', handleMouseup); 38 | event.preventDefault(); 39 | } 40 | 41 | /** 42 | * Handle mousedown events 43 | * @param {Event} event The event object 44 | * @returns {void} 45 | */ 46 | function handleMousedown(event) { 47 | scope.broadcast('dragstart'); 48 | downScrollPosition = { 49 | top: $el.scrollTop(), 50 | left: $el.scrollLeft() 51 | }; 52 | downMousePosition = { 53 | x: event.clientX, 54 | y: event.clientY 55 | }; 56 | $window.on('mousemove', handleMousemove); 57 | $window.on('mouseup', handleMouseup); 58 | event.preventDefault(); 59 | } 60 | 61 | //-------------------------------------------------------------------------- 62 | // Public 63 | //-------------------------------------------------------------------------- 64 | 65 | return { 66 | /** 67 | * Initialize the scroller component 68 | * @param {Element} el The Element 69 | * @returns {void} 70 | */ 71 | init: function (el) { 72 | $el = $(el); 73 | $el.on('mousedown', handleMousedown); 74 | }, 75 | 76 | /** 77 | * Destroy the scroller component 78 | * @returns {void} 79 | */ 80 | destroy: function () { 81 | $el.off('mousedown', handleMousedown); 82 | $el.off('mousemove', handleMousemove); 83 | $window.off('mouseup', handleMouseup); 84 | } 85 | }; 86 | }); 87 | -------------------------------------------------------------------------------- /test/js/components/layout-vertical-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-vertical', { 2 | setup: function () { 3 | this.utilities = { 4 | common: Crocodoc.getUtilityForTest('common'), 5 | browser: { 6 | mobile: false 7 | } 8 | }; 9 | this.scope = Crocodoc.getScopeForTest(this); 10 | this.mixins = { 11 | 'layout-paged': { 12 | calculateZoomValue: function () {}, 13 | init: function () {}, 14 | handleResize: function () {}, 15 | handleScroll: function () {}, 16 | updateCurrentPage: function () {}, 17 | extend: function (obj) { 18 | return Crocodoc.getUtility('common').extend({}, this, obj); 19 | } 20 | } 21 | }; 22 | this.config = { 23 | 24 | }; 25 | 26 | this.component = Crocodoc.getComponentForTest('layout-vertical', this.scope, this.mixins); 27 | } 28 | }); 29 | 30 | QUnit.cases([ 31 | { fitWidth: 1.1, fitHeight: 2, widestWidth: 100, tallestHeight: 50, mobile: false, value: 1 }, 32 | { fitWidth: 0.8, fitHeight: 2, widestWidth: 100, tallestHeight: 50, mobile: false, value: 0.8 }, 33 | { fitWidth: 1.8, fitHeight: 0.8, widestWidth: 100, tallestHeight: 50, mobile: false, value: 0.8 }, 34 | { fitWidth: 1.8, fitHeight: 1.8, widestWidth: 100, tallestHeight: 50, mobile: false, value: 1 }, 35 | { fitWidth: 0.9, fitHeight: 0.8, widestWidth: 100, tallestHeight: 150, mobile: false, value: 0.9 }, 36 | { fitWidth: 1.9, fitHeight: 0.8, widestWidth: 100, tallestHeight: 150, mobile: true, value: 1.9 }, 37 | ]).test('calculateZoomAutoValue() should return the correct zoom auto value when called', function (params) { 38 | var stub = this.stub(this.component, 'calculateZoomValue'); 39 | stub.withArgs(Crocodoc.ZOOM_FIT_WIDTH).returns(params.fitWidth); 40 | stub.withArgs(Crocodoc.ZOOM_FIT_HEIGHT).returns(params.fitHeight); 41 | 42 | this.utilities.browser.mobile = params.mobile; 43 | this.component.state = { 44 | widestPage: { 45 | actualWidth: params.widestWidth 46 | }, 47 | tallestPage: { 48 | actualHeight: params.tallestHeight 49 | } 50 | }; 51 | equal(this.component.calculateZoomAutoValue(), params.value, 'value is correct'); 52 | }); 53 | 54 | test('handleResize() should update the current page when called', function () { 55 | this.mock(this.component) 56 | .expects('updateCurrentPage'); 57 | this.component.handleResize(); 58 | }); 59 | 60 | test('handleScroll() should update the current page when called', function () { 61 | this.mock(this.component) 62 | .expects('updateCurrentPage'); 63 | this.component.handleScroll(); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/remember-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 |
24 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/js/data-providers/stylesheet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A standard data provider for stylesheet.css 3 | * @author lakenen 4 | */ 5 | Crocodoc.addDataProvider('stylesheet', function(scope) { 6 | 'use strict'; 7 | 8 | var ajax = scope.getUtility('ajax'), 9 | browser = scope.getUtility('browser'), 10 | config = scope.getConfig(), 11 | $cachedPromise; 12 | 13 | /** 14 | * Process stylesheet text and return the embeddable result 15 | * @param {string} text The original CSS text 16 | * @returns {string} The processed CSS text 17 | * @private 18 | */ 19 | function processStylesheetContent(text) { 20 | // @NOTE: There is a bug in IE that causes the text layer to 21 | // not render the font when loaded for a second time (i.e., 22 | // destroy and recreate a viewer for the same document), so 23 | // namespace the font-family so there is no collision 24 | if (browser.ie) { 25 | text = text.replace(/font-family:[\s\"\']*([\w-]+)\b/g, 26 | '$0-' + config.id); 27 | } 28 | 29 | return text; 30 | } 31 | 32 | //-------------------------------------------------------------------------- 33 | // Public 34 | //-------------------------------------------------------------------------- 35 | 36 | return { 37 | /** 38 | * Retrieve the stylesheet.css asset from the server 39 | * @returns {$.Promise} A promise with an additional abort() method that will abort the XHR request. 40 | */ 41 | get: function() { 42 | if ($cachedPromise) { 43 | return $cachedPromise; 44 | } 45 | 46 | var $promise = ajax.fetch(this.getURL(), Crocodoc.ASSET_REQUEST_RETRIES); 47 | 48 | // @NOTE: promise.then() creates a new promise, which does not copy 49 | // custom properties, so we need to create a futher promise and add 50 | // an object with the abort method as the new target 51 | $cachedPromise = $promise.then(processStylesheetContent).promise({ 52 | abort: function () { 53 | $promise.abort(); 54 | $cachedPromise = null; 55 | } 56 | }); 57 | return $cachedPromise; 58 | }, 59 | 60 | /** 61 | * Build and return the URL to the stylesheet CSS 62 | * @returns {string} The URL 63 | */ 64 | getURL: function () { 65 | var cssPath = config.template.css; 66 | return config.url + cssPath + config.queryString; 67 | }, 68 | 69 | /** 70 | * Cleanup the data-provider 71 | * @returns {void} 72 | */ 73 | destroy: function () { 74 | ajax = browser = config = null; 75 | $cachedPromise = null; 76 | } 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /examples/page-content-thumbnails/example.js: -------------------------------------------------------------------------------- 1 | var url = 'https://view-api.box.com/1/sessions/5a34f299b35947b5ac1e0b4d83553392/assets'; 2 | var thumbnails = Crocodoc.createViewer('.thumbnails', { 3 | url: url, 4 | enableTextSelection: false, 5 | enableLinks: false, 6 | plugins: { 7 | 'page-content': { 8 | template: $('#page-content-tmpl').html() 9 | } 10 | }, 11 | minZoom: 0.17, 12 | zoom: 0.17, 13 | layout: Crocodoc.LAYOUT_VERTICAL 14 | }); 15 | var visibleThumbnails = []; 16 | thumbnails.on('ready', function () { 17 | presentation.load(); 18 | }); 19 | thumbnails.on('zoom', function (event) { 20 | visibleThumbnails = event.data.fullyVisiblePages; 21 | }); 22 | thumbnails.on('pagefocus', function (event) { 23 | visibleThumbnails = event.data.fullyVisiblePages; 24 | }); 25 | 26 | thumbnails.load(); 27 | 28 | var presentation = Crocodoc.createViewer('.presentation', { 29 | url: url, 30 | layout: Crocodoc.LAYOUT_PRESENTATION 31 | }); 32 | 33 | 34 | // Bind 'ready' and 'pagefocus' event handlers to update the page controls 35 | presentation.on('ready', function (event) { 36 | updatePageControls(event.data.page, event.data.numPages); 37 | }); 38 | presentation.on('pagefocus', function (event) { 39 | updatePageControls(event.data.page, event.data.numPages); 40 | }); 41 | 42 | // Bind 'zoom' event to update the zoom controls 43 | presentation.on('zoom', function (event) { 44 | $('.zoom-in').prop('disabled', !event.data.canZoomIn); 45 | $('.zoom-out').prop('disabled', !event.data.canZoomOut); 46 | }); 47 | 48 | function updatePageControls(currentPage, numPages) { 49 | $('.page').get(0).textContent = currentPage + ' / ' + numPages; 50 | $('.scroll-previous').prop('disabled', currentPage === 1); 51 | $('.scroll-next').prop('disabled', currentPage === numPages); 52 | 53 | // scroll to the thumbnail if it's not fully visible 54 | if ($.inArray(currentPage, visibleThumbnails) === -1) { 55 | thumbnails.scrollTo(currentPage); 56 | } 57 | $('.thumbnails .crocodoc-page').removeClass('current-thumbnail').eq(currentPage - 1).addClass('current-thumbnail'); 58 | } 59 | 60 | // Bind click events for controlling the viewer 61 | $('.scroll-previous').on('click', function () { 62 | presentation.scrollTo(Crocodoc.SCROLL_PREVIOUS); 63 | }); 64 | $('.scroll-next').on('click', function () { 65 | presentation.scrollTo(Crocodoc.SCROLL_NEXT); 66 | }); 67 | $('.thumbnails').on('click', ' .crocodoc-page', function () { 68 | var pageNum = $(this).index()+1; 69 | presentation.scrollTo(pageNum); 70 | }); 71 | 72 | $(window).on('keydown', function (ev) { 73 | if (ev.keyCode === 37) { 74 | presentation.scrollTo(Crocodoc.SCROLL_PREVIOUS); 75 | } else if (ev.keyCode === 39) { 76 | presentation.scrollTo(Crocodoc.SCROLL_NEXT); 77 | } else { 78 | return; 79 | } 80 | ev.preventDefault(); 81 | }); 82 | -------------------------------------------------------------------------------- /test/js/data-providers/stylesheet-test.js: -------------------------------------------------------------------------------- 1 | module('Data Provider: stylesheet', { 2 | setup: function () { 3 | var me = this; 4 | this.$deferred = $.Deferred(); 5 | this.promise = { 6 | abort: function () {}, 7 | then: function () { return me.$deferred.promise(); }, 8 | promise: function (x) { return me.$deferred.promise(x); } 9 | }; 10 | this.utilities = { 11 | ajax: { 12 | fetch: function () {} 13 | }, 14 | browser: {} 15 | }; 16 | this.config = { 17 | id: 'VIEWER-ID', 18 | url: '', 19 | template: { 20 | css: 'stylesheet.css' 21 | }, 22 | queryString: '' 23 | }; 24 | this.scope = Crocodoc.getScopeForTest(this); 25 | this.dataProvider = Crocodoc.getComponentForTest('data-provider-stylesheet', this.scope); 26 | }, 27 | teardown: function () { 28 | this.scope.destroy(); 29 | this.dataProvider.destroy(); 30 | } 31 | }); 32 | 33 | test('creator should return an object with a get function', function(){ 34 | equal(typeof this.dataProvider, 'object'); 35 | equal(typeof this.dataProvider.get, 'function'); 36 | }); 37 | 38 | test('get() should return a $.Promise with an abort() function', function() { 39 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 40 | propEqual(this.dataProvider.get(), $.Deferred().promise({abort:function(){}})); 41 | }); 42 | 43 | test('get() should return a cached promise when called a second time', function() { 44 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 45 | equal(this.dataProvider.get(), this.dataProvider.get()); 46 | }); 47 | 48 | test('abort() should call abort on the promise returned from ajax.fetch when called on the returned promise', function() { 49 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 50 | this.mock(this.promise).expects('abort').once(); 51 | 52 | var promise = this.dataProvider.get(); 53 | promise.abort(); 54 | }); 55 | 56 | test('getURL() should return the correct URL to the css file when called', function() { 57 | this.config.url = 'http://beep.boop/bop/'; 58 | equal(this.dataProvider.getURL(), this.config.url + this.config.template.css, 'the URL should be correct'); 59 | }); 60 | 61 | test('get() should apply the IE font hack to the css when called in IE', function () { 62 | var css = '.crocodoc { font-family: crocodoc-font-blah; }'; 63 | this.utilities.browser.ie = true; 64 | 65 | this.stub(this.utilities.ajax, 'fetch').returns(this.$deferred.promise()); 66 | this.$deferred.resolve(css); 67 | 68 | var promise = this.dataProvider.get(); 69 | var self = this; 70 | promise.done(function (css) { 71 | ok(css.indexOf(self.config.id) > -1, 'IE font hack should be applied'); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /plugins/realtime/README.md: -------------------------------------------------------------------------------- 1 | # Realtime Plugin 2 | 3 | Enables realtime page-streaming functionality for Box View conversions in viewer.js. This plugin will automatically load pages as necessary when they finish converting. 4 | 5 | The realtime plugin is meant to be used with Box View [webhooks](http://developers.box.com/view-webhooks/) to create viewing sessions before the document has completely finished converting (i.e., when the `document.viewable` notification fires). 6 | 7 | ## Contents 8 | * [Dependencies](#dependencies) 9 | * [Usage](#usage) 10 | * [Options](#options) 11 | * [Events](#events) 12 | 13 | 14 | ## Dependencies 15 | 16 | The realtime plugin depends on the [EventSource polyfill](https://github.com/Yaffle/EventSource). You can also get a copy of the polyfill on npm: 17 | 18 | ``` 19 | npm install event-source-polyfill 20 | ``` 21 | 22 | Just include `eventsource.js` in your app along with `realtime.js`. 23 | 24 | 25 | ## Usage 26 | 27 | Include `realtime.js` in your page, and load the plugin as follows: 28 | 29 | Example: 30 | ```js 31 | var viewer = Crocodoc.createViewer('.viewer', { 32 | // ... 33 | plugins: { 34 | // the Box View realtime URL received when requesting a session 35 | realtime: { 36 | url: '' 37 | } 38 | } 39 | }); 40 | ``` 41 | 42 | 43 | ## Options 44 | 45 | The following configuration options are available: 46 | 47 | **url** 48 | 49 | The URL to the Box View realtime endpoint associated with the viewing session. This would be available in the View API session response under `urls.realtime`. 50 | 51 | For example, with a session response as follows, the URL would be `'https://view-api.box.com/sse/4fba9eda0dd745d491ad0b98e224aa25'`. 52 | 53 | ```js 54 | { 55 | 'type': 'session', 56 | 'id': '4fba9eda0dd745d491ad0b98e224aa25', 57 | 'expires_at': '3915-10-29T01:31:48.677Z', 58 | 'urls': { 59 | 'view': 'https://view-api.box.com/1/sessions/4fba9eda0dd745d491ad0b98e224aa25/view', 60 | 'assets': 'https://view-api.box.com/1/sessions/4fba9eda0dd745d491ad0b98e224aa25/assets/', 61 | 'realtime': 'https://view-api.box.com/sse/4fba9eda0dd745d491ad0b98e224aa25' 62 | } 63 | } 64 | ``` 65 | 66 | ## Events 67 | 68 | The following events will be fired on the viewer object: 69 | 70 | * `realtimeupdate` - fired when a new realtime update arrives. Event properties: 71 | * `page` - the page that has become available 72 | * `realtimeerror` - fired when the an error occurs with realtime. Event properties: 73 | * `error` - the error details 74 | * `realtimecomplete` - fired when the conversion is complete (we have been notified of all pages being available) 75 | 76 | Example: 77 | ```js 78 | viewer.on('realtimeupdate', function (event) { 79 | // some magic function that updates a conversion progress bar 80 | updateConversionProgress(event.data.page); 81 | }); 82 | 83 | viewer.on('realtimecomplete', function () { 84 | alert('the document is finished converting!'); 85 | }); 86 | ``` 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome to this project. 4 | 5 | ## Contributor License Agreement 6 | 7 | Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: 8 | 9 | http://opensource.box.com/cla 10 | 11 | To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). 12 | 13 | ## How to contribute 14 | 15 | * **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature). 16 | * **Send a pull request** - if you want to contribute code. Please be sure to file an issue first. 17 | 18 | ## Pull request best practices 19 | 20 | We want to accept your pull requests. Please follow these steps: 21 | 22 | ### Step 1: File an issue 23 | 24 | Before writing any code, please file an issue stating the problem you want to solve or the feature you want to implement. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue allows us to communicate and figure out if it's worth your time to write a bunch of code for the project. 25 | 26 | ### Step 2: Fork this repository in GitHub 27 | 28 | This will create your own copy of our repository. 29 | 30 | ### Step 3: Add the upstream source 31 | 32 | The upstream source is the project under the Box organization on GitHub. To add an upstream source for this project, type: 33 | 34 | ``` 35 | git remote add upstream git@github.com:box/viewer.js.git 36 | ``` 37 | 38 | This will come in useful later. 39 | 40 | ### Step 4: Create a feature branch 41 | 42 | Create a branch with a descriptive name, such as `add-search`. 43 | 44 | ### Step 5: Push your feature branch to your fork 45 | 46 | As you develop code, continue to push code to your remote feature branch. Please make sure to include the issue number you're addressing in your commit message, such as: 47 | 48 | ``` 49 | git commit -m "Adding search (fixes #123)" 50 | ``` 51 | 52 | This helps us out by allowing us to track which issue your commit relates to. 53 | 54 | Keep a separate feature branch for each issue you want to address. 55 | 56 | ### Step 6: Rebase 57 | 58 | Before sending a pull request, rebase against upstream, such as: 59 | 60 | ``` 61 | git fetch upstream 62 | git rebase upstream/master 63 | ``` 64 | 65 | This will add your changes on top of what's already in upstream, minimizing merge issues. 66 | 67 | ### Step 7: Run the tests 68 | 69 | Make sure that all tests are passing before submitting a pull request. 70 | 71 | ### Step 8: Send the pull request 72 | 73 | Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. 74 | 75 | Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues. 76 | -------------------------------------------------------------------------------- /src/js/data-providers/page-img.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A standard data provider for page-img 3 | * @author lakenen 4 | */ 5 | Crocodoc.addDataProvider('page-img', function(scope) { 6 | 'use strict'; 7 | 8 | var util = scope.getUtility('common'), 9 | config = scope.getConfig(); 10 | 11 | //-------------------------------------------------------------------------- 12 | // Public 13 | //-------------------------------------------------------------------------- 14 | 15 | return { 16 | /** 17 | * Retrieve the page image asset from the server 18 | * @param {string} objectType The type of data being requested 19 | * @param {number} pageNum The page number for which to request the page image 20 | * @returns {$.Promise} A promise with an additional abort() method that will abort the img request. 21 | */ 22 | get: function(objectType, pageNum) { 23 | var img = this.getImage(), 24 | retries = Crocodoc.ASSET_REQUEST_RETRIES, 25 | loaded = false, 26 | url = this.getURL(pageNum), 27 | $deferred = $.Deferred(); 28 | 29 | function loadImage() { 30 | img.setAttribute('src', url); 31 | } 32 | 33 | function abortImage() { 34 | if (img) { 35 | img.removeAttribute('src'); 36 | } 37 | } 38 | 39 | // add load and error handlers 40 | img.onload = function () { 41 | loaded = true; 42 | $deferred.resolve(img); 43 | }; 44 | 45 | img.onerror = function () { 46 | if (retries > 0) { 47 | retries--; 48 | abortImage(); 49 | loadImage(); 50 | } else { 51 | img = null; 52 | loaded = false; 53 | $deferred.reject({ 54 | error: 'image failed to load', 55 | resource: url 56 | }); 57 | } 58 | }; 59 | 60 | // load the image 61 | loadImage(); 62 | 63 | return $deferred.promise({ 64 | abort: function () { 65 | if (!loaded) { 66 | abortImage(); 67 | $deferred.reject(); 68 | } 69 | } 70 | }); 71 | }, 72 | 73 | /** 74 | * Build and return the URL to the PNG asset for the specified page 75 | * @param {number} pageNum The page number 76 | * @returns {string} The URL 77 | */ 78 | getURL: function (pageNum) { 79 | var imgPath = util.template(config.template.img, { page: pageNum }); 80 | return config.url + imgPath + config.queryString; 81 | }, 82 | 83 | /** 84 | * Create and return a new image element (used for testing purporses) 85 | * @returns {Image} 86 | */ 87 | getImage: function () { 88 | return new Image(); 89 | } 90 | }; 91 | }); 92 | -------------------------------------------------------------------------------- /test/js/components/page-svg-test.js: -------------------------------------------------------------------------------- 1 | module('Component - page-svg', { 2 | setup: function () { 3 | var self = this; 4 | this.svgText = ''; 5 | this.utilities = { 6 | common: { 7 | isFn: function () {}, 8 | ajax: function () {} 9 | }, 10 | ajax: { 11 | request: function () {} 12 | } 13 | }; 14 | this.config = { 15 | embedStrategy: 1 //EMBED_STRATEGY_IFRAME_INNERHTML 16 | }; 17 | this.scope = Crocodoc.getScopeForTest(this); 18 | 19 | this.component = Crocodoc.getComponentForTest('page-svg', this.scope); 20 | this.$el = $('
').appendTo(document.documentElement); 21 | }, 22 | teardown: function () { 23 | this.$el.remove(); 24 | } 25 | }); 26 | 27 | test('destroy() should unload the svg and empty the element when called', function () { 28 | this.mock(this.component) 29 | .expects('unload'); 30 | 31 | this.component.init(this.$el, 1); 32 | this.component.destroy(); 33 | ok(this.$el.html() === '', 'the element has been emptied'); 34 | }); 35 | 36 | test('preload() should create and insert the SVG object into the container element and make a data provider request when called', function () { 37 | var pageNum = 3, 38 | initalHTML = this.$el.html(); 39 | 40 | this.mock(this.scope) 41 | .expects('get') 42 | .withArgs('page-svg', pageNum); 43 | 44 | this.component.init(this.$el, pageNum); 45 | this.component.preload(); 46 | ok(this.$el.html() !== initalHTML, 'the element has been inserted'); 47 | }); 48 | 49 | test('load() should embed the SVG when the load succeeds', function () { 50 | var pageNum = 3, 51 | $deferred = $.Deferred().resolve(this.svgText); 52 | 53 | this.mock(this.scope) 54 | .expects('get') 55 | .withArgs('page-svg', pageNum) 56 | .returns($deferred.promise({ abort: function () {} })); 57 | 58 | this.component.init(this.$el, pageNum); 59 | this.component.load(); 60 | ok(this.$el.find('iframe').length > 0, 'the SVG has been embedded'); 61 | }); 62 | 63 | test('load() should broadcast an asseterror when the load fails', function () { 64 | var pageNum = 3, 65 | error = { error: 'fail' }, 66 | $deferred = $.Deferred().reject(error), 67 | mock = this.mock(this.scope); 68 | 69 | mock.expects('get') 70 | .withArgs('page-svg', pageNum) 71 | .returns($deferred.promise({ abort: function () {} })); 72 | 73 | mock.expects('broadcast') 74 | .withArgs('asseterror', error); 75 | 76 | this.component.init(this.$el, pageNum); 77 | this.component.load(); 78 | }); 79 | 80 | test('unload() should abort the request if there is one when called', function () { 81 | var pageNum = 3, 82 | $deferred = $.Deferred(); 83 | 84 | var spy = this.spy(); 85 | 86 | this.stub(this.scope, 'get') 87 | .withArgs('page-svg', pageNum) 88 | .returns($deferred.promise({ abort: spy })); 89 | 90 | this.component.init(this.$el, pageNum); 91 | this.component.load(); 92 | this.component.unload(); 93 | 94 | ok(spy.called, 'request should be aborted'); 95 | }); 96 | -------------------------------------------------------------------------------- /test/js/components/page-img-test.js: -------------------------------------------------------------------------------- 1 | module('Component - page-img', { 2 | setup: function () { 3 | var self = this; 4 | this.utilities = { 5 | browser: {} 6 | }; 7 | this.scope = Crocodoc.getScopeForTest(this); 8 | 9 | this.component = Crocodoc.getComponentForTest('page-img', this.scope); 10 | this.$el = $('
'); 11 | this.pageNum = 3; 12 | } 13 | }); 14 | 15 | test('preload() should make a request for the image when called', function () { 16 | this.mock(this.scope) 17 | .expects('get') 18 | .withArgs('page-img', this.pageNum) 19 | .returns($.when()); 20 | 21 | this.component.init(this.$el, this.pageNum); 22 | this.component.preload(); 23 | }); 24 | 25 | test('load() should preload the image when called', function () { 26 | this.stub(this.scope, 'get') 27 | .withArgs('page-img', this.pageNum) 28 | .returns($.when()); 29 | 30 | var spy = this.spy(this.component, 'preload'); 31 | 32 | this.component.init(this.$el, this.pageNum); 33 | this.component.load(); 34 | 35 | ok(spy.called, 'image should be preloaded'); 36 | }); 37 | 38 | test('load() should embed the image when the image successfully loads', function () { 39 | var img = new Image(); 40 | this.stub(this.scope, 'get') 41 | .withArgs('page-img', this.pageNum) 42 | .returns($.Deferred().resolve(img).promise()); 43 | 44 | this.component.init(this.$el, this.pageNum); 45 | this.component.load(); 46 | 47 | ok(this.$el.find('img').length > 0, 'image should be embedded'); 48 | }); 49 | 50 | test('load() should broadcast an asseterror message when the image fails to load', function () { 51 | var error = { message: 'not found' }; 52 | var mock = this.mock(this.scope); 53 | mock.expects('get') 54 | .withArgs('page-img', this.pageNum) 55 | .returns($.Deferred().reject(error).promise()); 56 | 57 | mock.expects('broadcast') 58 | .withArgs('asseterror', error); 59 | 60 | this.component.init(this.$el, this.pageNum); 61 | this.component.load(); 62 | }); 63 | 64 | test('destroy() should unload the img and empty the element when called', function () { 65 | var img = new Image(); 66 | this.stub(this.scope, 'get') 67 | .withArgs('page-img', this.pageNum) 68 | .returns($.Deferred().resolve(img).promise({ abort: function () {} })); 69 | 70 | var spy = this.spy(this.component, 'unload'); 71 | 72 | this.component.init(this.$el, this.pageNum); 73 | this.component.load(); 74 | 75 | this.component.destroy(); 76 | ok(this.$el.html() === '', 'the element should be emptied'); 77 | ok(spy.called, 'the img component should be unloaded'); 78 | }); 79 | 80 | test('unload() should abort the request if there is one when called', function () { 81 | var pageNum = 3, 82 | $deferred = $.Deferred().resolve(new Image()); 83 | 84 | var spy = this.spy(); 85 | 86 | this.stub(this.scope, 'get') 87 | .withArgs('page-img', pageNum) 88 | .returns($deferred.promise({ abort: spy })); 89 | 90 | this.component.init(this.$el, pageNum); 91 | this.component.load(); 92 | this.component.unload(); 93 | 94 | ok(spy.called, 'request should be aborted'); 95 | }); 96 | -------------------------------------------------------------------------------- /src/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/js/components/layout-horizontal-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-horizontal', { 2 | setup: function () { 3 | this.utilities = { 4 | common: Crocodoc.getUtilityForTest('common'), 5 | browser: { 6 | mobile: false 7 | } 8 | }; 9 | this.scope = Crocodoc.getScopeForTest(this); 10 | this.mixins = { 11 | 'layout-paged': { 12 | calculateZoomValue: function () {}, 13 | init: function () {}, 14 | handleResize: function () {}, 15 | handleScroll: function () {}, 16 | updateCurrentPage: function () {}, 17 | extend: function (obj) { 18 | return Crocodoc.getUtility('common').extend({}, this, obj); 19 | } 20 | } 21 | }; 22 | this.config = { 23 | 24 | }; 25 | 26 | this.component = Crocodoc.getComponentForTest('layout-horizontal', this.scope, this.mixins); 27 | } 28 | }); 29 | 30 | QUnit.cases([ 31 | { fitWidth: 1.1, fitHeight: 2, widestWidth: 100, tallestHeight: 50, mobile: false, value: 1.1 }, 32 | { fitWidth: 0.8, fitHeight: 2, widestWidth: 100, tallestHeight: 50, mobile: false, value: 0.8 }, 33 | { fitWidth: 1.8, fitHeight: 0.8, widestWidth: 100, tallestHeight: 50, mobile: false, value: 0.8 }, 34 | { fitWidth: 1.8, fitHeight: 1.8, widestWidth: 100, tallestHeight: 50, mobile: false, value: 1.8 }, 35 | { fitWidth: 0.9, fitHeight: 0.8, widestWidth: 100, tallestHeight: 150, mobile: false, value: 0.8 }, 36 | { fitWidth: 1.9, fitHeight: 0.8, widestWidth: 100, tallestHeight: 150, mobile: true, value: 0.8 }, 37 | ]).test('calculateZoomAutoValue() should return the correct zoom auto value when called', function (params) { 38 | var stub = this.stub(this.component, 'calculateZoomValue'); 39 | stub.withArgs(Crocodoc.ZOOM_FIT_WIDTH).returns(params.fitWidth); 40 | stub.withArgs(Crocodoc.ZOOM_FIT_HEIGHT).returns(params.fitHeight); 41 | 42 | this.utilities.browser.mobile = params.mobile; 43 | this.component.state = { 44 | widestPage: { 45 | actualWidth: params.widestWidth 46 | }, 47 | tallestPage: { 48 | actualHeight: params.tallestHeight 49 | } 50 | }; 51 | equal(this.component.calculateZoomAutoValue(), params.value, 'value is correct'); 52 | }); 53 | 54 | test('handleResize() should update the current page when called', function () { 55 | this.mock(this.component) 56 | .expects('updateCurrentPage'); 57 | this.component.handleResize(); 58 | }); 59 | 60 | test('handleScroll() should update the current page when called', function () { 61 | this.mock(this.component) 62 | .expects('updateCurrentPage'); 63 | this.component.handleScroll(); 64 | }); 65 | 66 | test('calculatePreviousPage() should return the correct page number when called', function () { 67 | var page = 8; 68 | this.component.state = { currentPage: page }; 69 | equal(this.component.calculatePreviousPage(), page - 1, 'the page was correct'); 70 | }); 71 | 72 | test('calculateNextPage() should return the correct page number when called', function () { 73 | var page = 8; 74 | this.component.state = { currentPage: page }; 75 | equal(this.component.calculateNextPage(), page + 1, 'the page was correct'); 76 | }); 77 | -------------------------------------------------------------------------------- /src/js/components/layout-presentation-two-page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview layout-presentation-two-page component definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * The presentation-two-page layout 8 | */ 9 | Crocodoc.addComponent('layout-' + LAYOUT_PRESENTATION_TWO_PAGE, ['layout-' + LAYOUT_PRESENTATION], function (scope, presentation) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var util = scope.getUtility('common'); 18 | 19 | //-------------------------------------------------------------------------- 20 | // Public 21 | //-------------------------------------------------------------------------- 22 | 23 | return presentation.extend({ 24 | /** 25 | * Initialize the presentation-two-page layout component 26 | * @returns {void} 27 | */ 28 | init: function () { 29 | this.twoPageMode = true; 30 | presentation.init.call(this); 31 | }, 32 | 33 | /** 34 | * Calculates the next page 35 | * @returns {int} The next page number 36 | */ 37 | calculateNextPage: function () { 38 | return this.state.currentPage + 2; 39 | }, 40 | 41 | /** 42 | * Calculates the previous page 43 | * @returns {int} The previous page number 44 | */ 45 | calculatePreviousPage: function () { 46 | return this.state.currentPage - 2; 47 | }, 48 | 49 | /** 50 | * Calculate the numeric value for a given zoom mode (or return the value if it's already numeric) 51 | * @param {string} mode The mode to zoom to 52 | * @returns {float} The zoom value 53 | */ 54 | calculateZoomValue: function (mode) { 55 | var baseVal = presentation.calculateZoomValue.call(this, mode); 56 | if (mode === ZOOM_FIT_WIDTH) { 57 | baseVal /= 2; 58 | } 59 | return baseVal; 60 | }, 61 | 62 | /** 63 | * Scroll to the given page number 64 | * @param {int} page The page number to scroll to 65 | * @returns {void} 66 | */ 67 | scrollToPage: function (page) { 68 | // pick the left page 69 | presentation.scrollToPage.call(this, page - (page + 1) % 2); 70 | }, 71 | 72 | /** 73 | * Calculates the current range of pages that are visible 74 | * @returns {Object} Range object with min and max values 75 | */ 76 | calculateVisibleRange: function () { 77 | var min = this.state.currentPage - 1, 78 | max = min + 1; 79 | return util.constrainRange(min, max, this.numPages); 80 | }, 81 | 82 | /** 83 | * Calculates the current range of pages that are fully visible 84 | * @NOTE: this can be incorrect for presentations that are zoomed in 85 | * past the size of the viewport... I'll fix it if it becomes an issue 86 | * @returns {Object} Range object with min and max values 87 | */ 88 | calculateFullyVisibleRange: function () { 89 | return this.calculateVisibleRange(); 90 | } 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/js/components/page-img.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview page-img component 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * page-img component used to display raster image instead of SVG content for 8 | * browsers that do not support SVG 9 | */ 10 | Crocodoc.addComponent('page-img', function (scope) { 11 | 12 | 'use strict'; 13 | 14 | //-------------------------------------------------------------------------- 15 | // Private 16 | //-------------------------------------------------------------------------- 17 | 18 | var browser = scope.getUtility('browser'); 19 | 20 | var $img, $el, 21 | $loadImgPromise, 22 | page, 23 | imageLoaded = false, 24 | removeOnUnload = browser.mobile; 25 | 26 | //-------------------------------------------------------------------------- 27 | // Public 28 | //-------------------------------------------------------------------------- 29 | 30 | return { 31 | /** 32 | * Initialize the page-img component 33 | * @param {Element} el The element to insert the image into 34 | * @param {number} pageNum The page number 35 | * @returns {void} 36 | */ 37 | init: function (el, pageNum) { 38 | $el = $(el); 39 | page = pageNum; 40 | }, 41 | 42 | /** 43 | * Destroy the page-img component 44 | * @returns {void} 45 | */ 46 | destroy: function () { 47 | removeOnUnload = true; 48 | this.unload(); 49 | $el.empty(); 50 | }, 51 | 52 | /** 53 | * Prepare the element for loading 54 | * @returns {void} 55 | */ 56 | prepare: function () { /* noop */ }, 57 | 58 | /** 59 | * Preload the image 60 | * @returns {void} 61 | */ 62 | preload: function () { 63 | if (!$loadImgPromise) { 64 | $loadImgPromise = scope.get('page-img', page); 65 | } 66 | }, 67 | 68 | /** 69 | * Load the image 70 | * @returns {$.Promise} A jQuery Promise object 71 | */ 72 | load: function () { 73 | this.preload(); 74 | 75 | $loadImgPromise.done(function loadImgSuccess(img) { 76 | if (!imageLoaded) { 77 | imageLoaded = true; 78 | $img = $(img).appendTo($el); 79 | } 80 | }); 81 | 82 | $loadImgPromise.fail(function loadImgFail(error) { 83 | imageLoaded = false; 84 | if (error) { 85 | scope.broadcast('asseterror', error); 86 | } 87 | }); 88 | 89 | return $loadImgPromise; 90 | }, 91 | 92 | /** 93 | * Unload the img if necessary 94 | * @returns {void} 95 | */ 96 | unload: function () { 97 | if ($loadImgPromise) { 98 | $loadImgPromise.abort(); 99 | $loadImgPromise = null; 100 | } 101 | if (removeOnUnload) { 102 | if ($img) { 103 | $img.remove(); 104 | $img = null; 105 | } 106 | imageLoaded = false; 107 | } 108 | } 109 | }; 110 | }); 111 | -------------------------------------------------------------------------------- /src/js/data-providers/page-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A standard data provider for page text 3 | * @author nsilva 4 | * @author lakenen 5 | */ 6 | Crocodoc.addDataProvider('page-text', function(scope) { 7 | 'use strict'; 8 | 9 | var MAX_TEXT_BOXES = 256; 10 | 11 | var util = scope.getUtility('common'), 12 | ajax = scope.getUtility('ajax'), 13 | config = scope.getConfig(), 14 | destroyed = false, 15 | cache = {}; 16 | 17 | /** 18 | * Process HTML text and return the embeddable result 19 | * @param {string} text The original HTML text 20 | * @returns {string} The processed HTML text 21 | * @private 22 | */ 23 | function processTextContent(text) { 24 | if (destroyed) { 25 | return; 26 | } 27 | 28 | // in the text layer, divs are only used for text boxes, so 29 | // they should provide an accurate count 30 | var numTextBoxes = util.countInStr(text, ' MAX_TEXT_BOXES) { 33 | return ''; 34 | } 35 | 36 | // remove reference to the styles 37 | text = text.replace(/'); 11 | this.component.init(this.$el); 12 | this.SCROLL_EVENT_THROTTLE_INTERVAL = 200; 13 | }, 14 | teardown: function () { 15 | // disable fake timers 16 | this.clock.restore(); 17 | this.component.destroy(); 18 | } 19 | }); 20 | 21 | test('scroller should broadcast scrollstart and scroll message when scroll event is fired', function () { 22 | var broadcastSpy = this.spy(this.scope, 'broadcast'); 23 | this.$el.trigger('scroll'); 24 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 25 | ok(broadcastSpy.calledWith('scrollstart', sinon.match.object), 'broadcasted scrollstart message'); 26 | ok(broadcastSpy.calledWith('scroll', sinon.match.object), 'broadcasted scroll message'); 27 | }); 28 | 29 | test('scroller should not broadcast the scrollstart message more than once when scroll event is fired', function () { 30 | var broadcastSpy = this.spy(this.scope, 'broadcast'); 31 | this.$el.trigger('scroll'); 32 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 33 | this.$el.trigger('scroll'); 34 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 35 | this.$el.trigger('scroll'); 36 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 37 | ok(broadcastSpy.withArgs('scrollstart', sinon.match.object).calledOnce, 'broadcasted scrollstart message'); 38 | }); 39 | 40 | test('scroller should broadcast scrollend message when scroll event is fired', function () { 41 | this.$el.trigger('scroll'); 42 | // ignore the expected scroll message 43 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 44 | this.mock(this.scope) 45 | .expects('broadcast') 46 | .withArgs('scrollend', sinon.match.object); 47 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 48 | }); 49 | 50 | test('scroller should broadcast scrollstart and scroll messages when touchstart event is fired', function () { 51 | var stub = this.stub(this.scope, 'broadcast'); 52 | this.$el.trigger('touchstart'); 53 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 54 | 55 | ok(stub.calledWith('scrollstart', sinon.match.object), 'scrollstart should be broadcast'); 56 | ok(stub.calledWith('scroll', sinon.match.object), 'scroll should be broadcast'); 57 | }); 58 | 59 | test('scroller should broadcast scroll message when touchmove event is fired', function () { 60 | var stub = this.stub(this.scope, 'broadcast'); 61 | this.$el.trigger('touchmove'); 62 | this.clock.tick(this.SCROLL_EVENT_THROTTLE_INTERVAL); 63 | ok(stub.calledWith('scroll', sinon.match.object), 'scroll should be broadcast'); 64 | }); 65 | 66 | test('scroller should broadcast scroll messages and eventually an scrollend message when touchstart, touchmove, and touchend event is fired', function () { 67 | var broadcastSpy = sinon.spy(this.scope, 'broadcast'); 68 | this.$el.trigger('touchstart'); 69 | this.$el.trigger('touchmove'); 70 | this.$el.trigger('touchend'); 71 | this.clock.tick(4000); // arbirary large amount of time 72 | ok(broadcastSpy.calledWith('scroll'), 'broadcasted scroll message'); 73 | ok(broadcastSpy.calledWith('scrollend'), 'broadcasted scrollend message'); 74 | }); 75 | -------------------------------------------------------------------------------- /test/framework/crocodoc-test-utils.js: -------------------------------------------------------------------------------- 1 | /*global Crocodoc*/ 2 | 3 | /** 4 | * Get a scope for testing 5 | * @param {object} [testContext] Optional map of utilities and components to use 6 | * in getUtility and createComponent respectively 7 | * @returns {object} The scope 8 | */ 9 | Crocodoc.getScopeForTest = function (testContext) { 10 | testContext = testContext || {}; 11 | testContext.utilities = testContext.utilities || {}; 12 | testContext.components = testContext.components || {}; 13 | testContext.dataProviders = testContext.dataProviders || {}; 14 | testContext.config = testContext.config || {}; 15 | return { 16 | getUtility: function (name) { 17 | return testContext.utilities[name] || (testContext.utilities[name] = {}); 18 | }, 19 | createComponent: function (name) { 20 | return testContext.components[name] || (testContext.components[name] = {}); 21 | }, 22 | getConfig: function () { 23 | return testContext.config; 24 | }, 25 | getDataProvider: function () { 26 | return testContext.dataProviders[name] || (testContext.dataProviders[name] = {}); 27 | }, 28 | get: function() {}, 29 | destroyComponent: function () {}, 30 | broadcast: function () {}, 31 | destroy: function () {}, 32 | ready: function () {} 33 | }; 34 | }; 35 | 36 | 37 | /** 38 | * Get a framework for testing 39 | * @param {object} [testContext] Optional map of utilities use in getUtility 40 | * @returns {object} The framework 41 | */ 42 | Crocodoc.getFrameworkForTest = function (testContext) { 43 | testContext = testContext || {}; 44 | testContext.utilities = testContext.utilities || {}; 45 | return { 46 | getUtility: function (name) { 47 | return testContext.utilities[name] || (testContext.utilities[name] = {}); 48 | } 49 | }; 50 | }; 51 | 52 | /** 53 | * Get a module for testing 54 | * @param {string} name The module name 55 | * @param {object} scope The scope to pass in 56 | * @param {object} [mixins] Optional map of mixins to use instead of creating instances of any dependencies 57 | * @returns {object?} The module or null 58 | */ 59 | Crocodoc.getComponentForTest = function (name, scope, mixins) { 60 | mixins = mixins || {}; 61 | var module = Crocodoc.components[name]; 62 | if (module) { 63 | var args = [], mixinName; 64 | for (var i = 0; i < module.mixins.length; ++i) { 65 | mixinName = module.mixins[i]; 66 | args.push(mixins[mixinName] || Crocodoc.getComponentForTest(mixinName, scope)); 67 | } 68 | args.unshift(scope); 69 | return module.creator.apply(module.creator, args); 70 | } 71 | return null; 72 | }; 73 | 74 | /** 75 | * Get a utility for testing 76 | * @param {string} name The utility name 77 | * @param {object} [framework] A mock framework 78 | * @returns {object?} The utility or null 79 | */ 80 | Crocodoc.getUtilityForTest = function (name, framework) { 81 | var util = Crocodoc.utilities[name]; 82 | if (util) { 83 | return util.creator(framework || Crocodoc); 84 | } 85 | return null; 86 | }; 87 | 88 | /** 89 | * Get a plugin for testing 90 | * @param {string} name The plugin name 91 | * @param {object} scope The scope to pass in 92 | * @returns {object?} The plugin or null 93 | */ 94 | Crocodoc.getPluginForTest = function (name, scope) { 95 | return this.getComponentForTest('plugin-' + name, scope); 96 | }; 97 | -------------------------------------------------------------------------------- /examples/presentations/pageflip.css: -------------------------------------------------------------------------------- 1 | /**** PAGE FLIP STUFF ****/ 2 | 3 | /*ABANDON ALL HOPE YE WHO ENTER HERE*/ 4 | 5 | .crocodoc-pageflip .crocodoc-page-visible, 6 | .crocodoc-pageflip .crocodoc-page-next, 7 | .crocodoc-pageflip .crocodoc-page-prev { 8 | visibility: visible !important; 9 | } 10 | 11 | .crocodoc-pageflip .crocodoc-page-before, 12 | .crocodoc-pageflip .crocodoc-page-after { 13 | visibility: visible; 14 | } 15 | 16 | /* Visible pages should always be on top */ 17 | .crocodoc-pageflip .crocodoc-page-visible { 18 | z-index: 2; 19 | } 20 | 21 | /* Next/Previous page should sit above other pages so it doesn't look weird when 22 | transitioning */ 23 | .crocodoc-pageflip .crocodoc-page-prev, 24 | .crocodoc-pageflip .crocodoc-page-next { 25 | z-index: 1; 26 | } 27 | 28 | .crocodoc-pageflip .crocodoc-doc { 29 | overflow: visible; 30 | -webkit-perspective: 2000px; 31 | perspective: 2000px; 32 | -webkit-transform-style: preserve-3d; 33 | transform-style: preserve-3d; 34 | -webkit-transform-origin: 50% 50%; 35 | transform-origin: 50% 50%; 36 | } 37 | 38 | .crocodoc-pageflip .crocodoc-page { 39 | -webkit-backface-visibility: hidden; 40 | -moz-backface-visibility: hidden; 41 | backface-visibility: hidden; 42 | -webkit-transform-style: preserve-3d; 43 | transform-style: preserve-3d; 44 | -webkit-transition: -webkit-transform 0.8s, z-index 0s linear 0.1s; 45 | transition: transform 0.8s, z-index 0s linear 0.1s; 46 | } 47 | 48 | .crocodoc-pageflip .crocodoc-page:nth-child(odd) { 49 | -webkit-transform-origin: right top; 50 | transform-origin: right top; 51 | } 52 | .crocodoc-pageflip .crocodoc-page:nth-child(even) { 53 | -webkit-transform-origin: left top; 54 | transform-origin: left top; 55 | } 56 | 57 | .crocodoc-pageflip .crocodoc-page-visible:nth-child(odd) { 58 | -webkit-transform: rotateY(0.1deg); 59 | transform: rotateY(0.1deg); 60 | } 61 | .crocodoc-pageflip .crocodoc-page-visible:nth-child(even) { 62 | -webkit-transform: rotateY(-0.1deg); 63 | transform: rotateY(-0.1deg); 64 | } 65 | 66 | .crocodoc-pageflip .crocodoc-page-before-buffer, 67 | .crocodoc-pageflip .crocodoc-page-after-buffer { 68 | -webkit-transition-delay: 0s, 0.4s; 69 | transition-delay: 0s, 0.4s; 70 | visibility: visible !important; 71 | } 72 | 73 | .crocodoc-pageflip .crocodoc-page-before, 74 | .crocodoc-pageflip .crocodoc-page-after { 75 | visibility: hidden; 76 | } 77 | 78 | .crocodoc-pageflip .crocodoc-page-before:nth-child(odd) { 79 | -webkit-transform: none; 80 | transform: none; 81 | } 82 | .crocodoc-pageflip .crocodoc-page-before:nth-child(even) { 83 | -webkit-transform: rotateY(-180deg); 84 | transform: rotateY(-180deg); 85 | } 86 | 87 | .crocodoc-pageflip .crocodoc-page-after:nth-child(odd) { 88 | -webkit-transform: rotateY(180deg); 89 | transform: rotateY(180deg); 90 | } 91 | .crocodoc-pageflip .crocodoc-page-after:nth-child(even) { 92 | -webkit-transform: none; 93 | transform: none; 94 | } 95 | 96 | /* Create a gradient to make it look more like a book */ 97 | .crocodoc-pageflip .crocodoc-page:nth-child(odd) .crocodoc-page-overlay { 98 | background-image: linear-gradient(to right, rgba(136,136,136,0) 0%,rgba(136,136,136,0) 65%,rgba(136,136,136,0.03) 88%,rgba(136,136,136,0.01) 94%,rgba(136,136,136,0.03) 95%,rgba(136,136,136,0.1) 100%); 99 | } 100 | .crocodoc-pageflip .crocodoc-page:nth-child(even) .crocodoc-page-overlay { 101 | background-image: linear-gradient(to right, rgba(0,0,0,0.08) 0%,rgba(0,0,0,0.05) 1%,rgba(0,0,0,0.01) 15%,rgba(0,0,0,0) 25%,rgba(0,0,0,0) 100%); 102 | } 103 | -------------------------------------------------------------------------------- /examples/thumbnails/thumbnails.js: -------------------------------------------------------------------------------- 1 | var url = 'https://view-api.box.com/1/sessions/5a34f299b35947b5ac1e0b4d83553392/assets'; 2 | // Create a viewer for the thumbnails 3 | var thumbnails = Crocodoc.createViewer('.thumbnails', { 4 | url: url, 5 | // Disable links and text selection, because they are not needed in thumbnails 6 | enableTextSelection: false, 7 | enableLinks: false, 8 | // This is kind of a hack at this point, but I basically just manually found 9 | // the zoom level that worked to have two pages side-by-side (and reduced 10 | // the minimum zoom level to accommodate) 11 | minZoom: 0.17, 12 | zoom: 0.17, 13 | layout: Crocodoc.LAYOUT_VERTICAL 14 | }); 15 | // Keep a reference to the currently fully-visible pages so we can auto-scroll 16 | // the thumbnails when necessary 17 | var visibleThumbnails = []; 18 | thumbnails.on('ready', function () { 19 | // wait for the thumbnails to be ready before loading the presentation 20 | presentation.load(); 21 | }); 22 | // Update the visible thumbnails when it's zoomed or page changes 23 | thumbnails.on('zoom', function (event) { 24 | visibleThumbnails = event.data.fullyVisiblePages; 25 | }); 26 | thumbnails.on('pagefocus', function (event) { 27 | visibleThumbnails = event.data.fullyVisiblePages; 28 | }); 29 | 30 | thumbnails.load(); 31 | 32 | // Create a viewer for the presentation slides (the large view) 33 | var presentation = Crocodoc.createViewer('.presentation', { 34 | url: url, 35 | layout: Crocodoc.LAYOUT_PRESENTATION 36 | }); 37 | 38 | 39 | // Bind 'ready' and 'pagefocus' event handlers to update the page controls 40 | presentation.on('ready', function (event) { 41 | updatePageControls(event.data.page, event.data.numPages); 42 | }); 43 | presentation.on('pagefocus', function (event) { 44 | updatePageControls(event.data.page, event.data.numPages); 45 | }); 46 | 47 | // Bind 'zoom' event to update the zoom controls 48 | presentation.on('zoom', function (event) { 49 | $('.zoom-in').prop('disabled', !event.data.canZoomIn); 50 | $('.zoom-out').prop('disabled', !event.data.canZoomOut); 51 | }); 52 | 53 | function updatePageControls(currentPage, numPages) { 54 | $('.page').get(0).textContent = currentPage + ' / ' + numPages; 55 | $('.scroll-previous').prop('disabled', currentPage === 1); 56 | $('.scroll-next').prop('disabled', currentPage === numPages); 57 | 58 | // scroll to the thumbnail if it's not fully visible 59 | if ($.inArray(currentPage, visibleThumbnails) === -1) { 60 | thumbnails.scrollTo(currentPage); 61 | } 62 | 63 | // remove the current-thumbnail class from the old thumbnail and add it to 64 | // the newly-focused one 65 | $('.thumbnails .crocodoc-page') 66 | .removeClass('current-thumbnail') 67 | .eq(currentPage - 1) 68 | .addClass('current-thumbnail'); 69 | } 70 | 71 | // Bind click events for controlling the viewer 72 | $('.scroll-previous').on('click', function () { 73 | presentation.scrollTo(Crocodoc.SCROLL_PREVIOUS); 74 | }); 75 | $('.scroll-next').on('click', function () { 76 | presentation.scrollTo(Crocodoc.SCROLL_NEXT); 77 | }); 78 | // Delegate clicks on .crocodoc-page elements in the thumbnails viewer to scroll 79 | // to the proper page 80 | $('.thumbnails').on('click', '.crocodoc-page', function () { 81 | var pageNum = $(this).index()+1; 82 | presentation.scrollTo(pageNum); 83 | }); 84 | 85 | // Bind some key events to flip through slides easily 86 | $(window).on('keydown', function (ev) { 87 | if (ev.keyCode === 37) { 88 | presentation.scrollTo(Crocodoc.SCROLL_PREVIOUS); 89 | } else if (ev.keyCode === 39) { 90 | presentation.scrollTo(Crocodoc.SCROLL_NEXT); 91 | } else { 92 | return; 93 | } 94 | ev.preventDefault(); 95 | }); 96 | -------------------------------------------------------------------------------- /examples/thumbnails/thumbnails.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | overflow: hidden; 7 | } 8 | 9 | .thumbnails { 10 | z-index: 1; 11 | position: absolute; 12 | bottom: 0; 13 | left: 0; 14 | top: 0; 15 | width: 400px; 16 | box-shadow: inset -3px 0 3px -2px rgba(0,0,0,0.4); 17 | background: #fff; 18 | } 19 | .main { 20 | position: absolute; 21 | bottom: 0; 22 | left: 400px; 23 | right: 0; 24 | top: 0; 25 | background: #f2f2f2; 26 | border-left: 1px solid #eee; 27 | } 28 | .presentation { 29 | width: 100%; 30 | height: 100%; 31 | } 32 | .presentation .crocodoc-viewport { 33 | overflow: visible; 34 | } 35 | 36 | .controls-container { 37 | position: absolute; 38 | bottom: 10px; 39 | width: 0; 40 | left: 50%; 41 | overflow: visible; 42 | height: 26px; 43 | z-index: 1; 44 | } 45 | .controls-wrapper { 46 | position: absolute; 47 | } 48 | .controls { 49 | background: black; 50 | background: rgba(0,0,0,0.7); 51 | color: white; 52 | font-family: Calibri,Arial,sans-serif; 53 | font-size: 12px; 54 | white-space: nowrap; 55 | border-radius: 4px; 56 | margin-left: -50%; 57 | width: 100%; 58 | } 59 | .crocodoc-page-inner { 60 | box-shadow: 1px 1px 3px rgba(0,0,0,0.6); 61 | } 62 | .crocodoc-page-visible.crocodoc-page-loading .crocodoc-page-inner { 63 | background: #fff url('../../src/images/spinner.gif') center center no-repeat; 64 | } 65 | 66 | .thumbnails .crocodoc-page { 67 | padding: 40px; 68 | } 69 | .thumbnails .crocodoc-page .crocodoc-page-svg { 70 | opacity: 0.8; 71 | } 72 | .thumbnails .current-thumbnail .crocodoc-page-svg { 73 | opacity: 1; 74 | } 75 | .thumbnails .crocodoc-page-inner { 76 | box-shadow: 0 0 2px rgba(0,0,0,0.6); 77 | } 78 | .thumbnails .crocodoc-page:hover .crocodoc-page-inner { 79 | box-shadow: 0 0 4px 2px rgb(60, 130, 200); 80 | } 81 | .thumbnails .current-thumbnail .crocodoc-page-inner { 82 | box-shadow: 0 0 4px 2px rgb(253, 165, 20) !important; 83 | } 84 | 85 | .thumbnails .crocodoc-page-inner .thumbnail-controls { 86 | position: absolute; 87 | bottom: 0; 88 | left: 0; 89 | width: 100%; 90 | height: 10%; 91 | } 92 | 93 | /* align the thumbnail pages to the left instead of center */ 94 | .thumbnails .crocodoc-pages { 95 | text-align: left; 96 | } 97 | .thumbnails .crocodoc-viewer-logo { 98 | display: none; 99 | } 100 | 101 | .presentation .crocodoc-page { 102 | -webkit-transition: opacity 0.2s, -webkit-transform 0.4s; 103 | -moz-transition: opacity 0.2s, -moz-transform 0.4s; 104 | transition: opacity 0.2s, transform 0.4s; 105 | visibility: visible; 106 | } 107 | .presentation .crocodoc-current-page { 108 | -webkit-transform: none; 109 | -moz-transform: none; 110 | -ms-transform: none; 111 | transform: none; 112 | -webkit-transition-delay: 0s, 0s; 113 | -moz-transition-delay: 0s, 0s; 114 | transition-delay: 0s, 0s; 115 | opacity: 1; 116 | } 117 | .presentation .crocodoc-page-before { 118 | -webkit-transform: translateX(-100%); 119 | -moz-transform: translateX(-100%); 120 | -ms-transform: translateX(-100%); 121 | transform: translateX(-100%); 122 | -webkit-transition-delay: 0.2s, 0s; 123 | -moz-transition-delay: 0.2s, 0s; 124 | transition-delay: 0.2s, 0s; 125 | opacity: 0; 126 | } 127 | .presentation .crocodoc-page-after { 128 | -webkit-transform: translateX(100%); 129 | -moz-transform: translateX(100%); 130 | -ms-transform: translateX(100%); 131 | transform: translateX(100%); 132 | -webkit-transition-delay: 0.2s, 0s; 133 | -moz-transition-delay: 0.2s, 0s; 134 | transition-delay: 0.2s, 0s; 135 | opacity: 0; 136 | } 137 | -------------------------------------------------------------------------------- /src/js/utilities/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview url utility definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * URL utility 8 | */ 9 | Crocodoc.addUtility('url', function (framework) { 10 | 11 | 'use strict'; 12 | 13 | var browser = framework.getUtility('browser'), 14 | parsedLocation; 15 | 16 | return { 17 | /** 18 | * Return the current page's URL 19 | * @returns {string} The current URL 20 | */ 21 | getCurrentURL: function () { 22 | return window.location.href; 23 | }, 24 | 25 | /** 26 | * Make the given path absolute 27 | * - if path doesn't contain protocol and domain, prepend the current protocol and domain 28 | * - if the path is relative (eg. doesn't begin with /), also fill in the current path 29 | * @param {string} path The path to make absolute 30 | * @returns {string} The absolute path 31 | */ 32 | makeAbsolute: function (path) { 33 | return this.parse(path).href; 34 | }, 35 | 36 | /** 37 | * Returns true if the given url is external to the current domain 38 | * @param {string} url The URL 39 | * @returns {Boolean} Whether or not the url is external 40 | */ 41 | isCrossDomain: function (url) { 42 | var parsedURL = this.parse(url); 43 | 44 | if (!parsedLocation) { 45 | parsedLocation = this.parse(this.getCurrentURL()); 46 | } 47 | 48 | // IE7 does not properly parse relative URLs, so the hostname is empty 49 | if (!parsedURL.hostname) { 50 | return false; 51 | } 52 | 53 | return parsedURL.protocol !== parsedLocation.protocol || 54 | parsedURL.hostname !== parsedLocation.hostname || 55 | parsedURL.port !== parsedLocation.port; 56 | }, 57 | 58 | /** 59 | * Append a query parameters string to the given URL 60 | * @param {string} url The URL 61 | * @param {string} str The query parameters 62 | * @returns {string} The new URL 63 | */ 64 | appendQueryParams: function (url, str) { 65 | if (url.indexOf('?') > -1) { 66 | return url + '&' + str; 67 | } else { 68 | return url + '?' + str; 69 | } 70 | }, 71 | 72 | /** 73 | * Parse a URL into protocol, host, port, etc 74 | * @param {string} url The URL to parse 75 | * @returns {object} The parsed URL parts 76 | */ 77 | parse: function (url) { 78 | var parsed = document.createElement('a'), 79 | pathname; 80 | 81 | parsed.href = url; 82 | 83 | // @NOTE: IE does not automatically parse relative urls, 84 | // but requesting href back from the element will return 85 | // an absolute URL, which can then be fed back in to get the 86 | // expected result. WTF? Yep! 87 | if (browser.ie && url !== parsed.href) { 88 | url = parsed.href; 89 | parsed.href = url; 90 | } 91 | 92 | // @NOTE: IE does not include the preceding '/' in pathname 93 | pathname = parsed.pathname; 94 | if (!/^\//.test(pathname)) { 95 | pathname = '/' + pathname; 96 | } 97 | 98 | return { 99 | href: parsed.href, 100 | protocol: parsed.protocol, // includes : 101 | host: parsed.host, // includes port 102 | hostname: parsed.hostname, // does not include port 103 | port: parsed.port, 104 | pathname: pathname, 105 | hash: parsed.hash, // inclues # 106 | search: parsed.search // incudes ? 107 | }; 108 | } 109 | }; 110 | }); 111 | -------------------------------------------------------------------------------- /examples/realtime/server.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | var http = require('http'), 3 | https = require('https'), 4 | url = require('url'), 5 | channels = {}, 6 | currentChannelId = 0; 7 | 8 | function readResponse(response, callback) { 9 | var body = ''; 10 | response.on('data', function (d) { 11 | body += d.toString(); 12 | }); 13 | response.on('end', function () { 14 | callback(JSON.parse(body)); 15 | }); 16 | response.on('error', callback); 17 | } 18 | 19 | function createSSEChannel(numPages) { 20 | var id = currentChannelId++; 21 | channels[id] = { 22 | page: 1, 23 | numPages: numPages, 24 | finished: false 25 | }; 26 | return '/sse?id=' + id; 27 | } 28 | 29 | function getMetadata(assetsURL, callback) { 30 | https.get(url.resolve(assetsURL, 'assets/info.json'), function (res) { 31 | if (res.statusCode === 200) { 32 | readResponse(res, function (metadata) { 33 | callback(null, metadata); 34 | }); 35 | } else { 36 | callback('error loading info.json'); 37 | } 38 | }); 39 | } 40 | 41 | function handleCreateRequest(parsedURL, response) { 42 | getMetadata(parsedURL.query.url, function (err, metadata) { 43 | if (err) { 44 | response.writeHead(500); 45 | response.end(err); 46 | return; 47 | } 48 | response.writeHead(200, { 49 | 'Content-Type': 'text/plain', 50 | 'Access-Control-Allow-Origin': '*' 51 | }); 52 | response.end(createSSEChannel(metadata.numpages)); 53 | }); 54 | } 55 | 56 | function handleSSERequest(parsedURL, request, response) { 57 | var channelId = parsedURL.query.id, 58 | channel = channels[channelId], 59 | lastEventId, 60 | timeoutId, 61 | i, c; 62 | 63 | if (!channel) { 64 | response.writeHead(404, { 65 | 'Access-Control-Allow-Origin': '*' 66 | }); 67 | response.end('channel not found'); 68 | return; 69 | } 70 | 71 | response.socket.setTimeout(0); 72 | response.writeHead(200, { 73 | 'Content-Type': 'text/event-stream', 74 | 'Cache-Control': 'no-cache', 75 | 'Connection': 'keep-alive', 76 | 'Access-Control-Allow-Origin': '*' 77 | }); 78 | 79 | response.write(':' + (new Array(2049)).join(' ') + '\n'); // 2kB padding for IE 80 | response.write('retry: 2000\n'); 81 | 82 | lastEventId = Number(request.headers['last-event-id']) || Number(parsedURL.query.lastEventId) || 0; 83 | 84 | timeoutId = 0; 85 | i = lastEventId; 86 | c = i + 100; // send 100 events before forcing a reconnect 87 | 88 | function sendEvent() { 89 | var data = {}, 90 | eventName; 91 | if (++i < c) { 92 | if (channel.page < channel.numPages) { 93 | data.pages = [channel.page++]; 94 | eventName = 'pageavailable.svg'; 95 | } else { 96 | eventName = 'finished.svg'; 97 | data = ''; 98 | } 99 | response.write('id: ' + i + '\n'); 100 | response.write('event: ' + eventName + '\n'); 101 | response.write('data: ' + (data ? JSON.stringify(data) : '') + '\n\n'); 102 | timeoutId = setTimeout(sendEvent, (Math.random() * 1000 + 50)); 103 | } else { 104 | response.end(); 105 | } 106 | } 107 | 108 | sendEvent(); 109 | 110 | response.on('close', function () { 111 | clearTimeout(timeoutId); 112 | }); 113 | } 114 | 115 | module.exports = function (port) { 116 | http.createServer(function (request, response) { 117 | var parsedURL = url.parse(request.url, true); 118 | if (parsedURL.pathname === '/create') { 119 | handleCreateRequest(parsedURL, response); 120 | } else if (parsedURL.pathname === '/sse') { 121 | handleSSERequest(parsedURL, request, response); 122 | } 123 | }).listen(port); 124 | }; 125 | -------------------------------------------------------------------------------- /test/plugins/fullscreen/fullscreen-test.js: -------------------------------------------------------------------------------- 1 | module('Plugin - fullscreen', { 2 | setup: function () { 3 | this.viewerAPI = { 4 | fire: function () {} 5 | }; 6 | this.utilities = { 7 | common: Crocodoc.getUtility('common') 8 | }; 9 | this.config = { 10 | api: this.viewerAPI, 11 | $el: $('
') 12 | }; 13 | this.scope = Crocodoc.getScopeForTest(this); 14 | this.plugin = Crocodoc.getPluginForTest('fullscreen', this.scope); 15 | } 16 | }); 17 | 18 | test('init() should extend the viewer API with the proper methods when called', function () { 19 | this.plugin.init(); 20 | equal(typeof this.viewerAPI.enterFullscreen, 'function', 'enterFullscreen'); 21 | equal(typeof this.viewerAPI.exitFullscreen, 'function', 'exitFullscreen'); 22 | equal(typeof this.viewerAPI.isFullscreen, 'function', 'isFullscreen'); 23 | equal(typeof this.viewerAPI.isFullscreenSupported, 'function', 'isFullscreenSupported'); 24 | }); 25 | 26 | test('init() should use documentElement as the fullscreen element when viewer is using window as viewport', function () { 27 | var spy = this.spy(window, '$'); 28 | this.config.useWindowAsViewport = true; 29 | this.plugin.init({ 30 | useFakeFullscreen: true 31 | }); 32 | 33 | this.viewerAPI.enterFullscreen(); 34 | this.viewerAPI.exitFullscreen(); 35 | ok(spy.calledWith(document.documentElement), 'documentElement should be used'); 36 | }); 37 | 38 | test('enterFullscreen() should call the proper function on the context of the element when called', function () { 39 | var el = { 40 | requestFullscreen: this.spy() 41 | }; 42 | this.plugin.init({ 43 | element: el 44 | }); 45 | 46 | this.viewerAPI.enterFullscreen(); 47 | ok(el.requestFullscreen.calledOn(el), 'requestFullscreen should be called on el'); 48 | }); 49 | 50 | test('exitFullscreen() should call the proper function on the context of the document when called', function () { 51 | document.cancelFullScreen = this.spy(); 52 | this.plugin.init(); 53 | 54 | this.viewerAPI.exitFullscreen(); 55 | ok(document.cancelFullScreen.calledOn(document), 'cancelFullScreen should be called on document'); 56 | }); 57 | 58 | test('enterFullscreen should enter fake fullscreen mode when called and native fullscreen is not supported', function () { 59 | var el = {}, 60 | spy = this.spy(); 61 | this.stub(window, '$').returns({ 62 | addClass: spy, 63 | on: this.stub(), 64 | off: this.stub(), 65 | 0: el 66 | }); 67 | this.plugin.init({ 68 | element: el, 69 | useFakeFullscreen: true 70 | }); 71 | 72 | this.viewerAPI.enterFullscreen(); 73 | ok(spy.calledWith('crocodoc-fakefullscreen'), 'should add class .crocodoc-fakefullscreen'); 74 | }); 75 | 76 | test('exitFullscreen should exit fake fullscreen mode when called and native fullscreen is not supported', function () { 77 | var el = {}, 78 | spy = this.spy(); 79 | this.stub(window, '$').returns({ 80 | addClass: function () {}, 81 | removeClass: spy, 82 | on: this.stub(), 83 | off: this.stub(), 84 | 0: el 85 | }); 86 | 87 | // all these properties are tested against before falling into fake fullscreen mode 88 | document.cancelFullScreen = 89 | document.exitFullscreen = 90 | document.mozCancelFullScreen = 91 | document.webkitCancelFullScreen = 92 | document.msExitFullscreen = null; 93 | 94 | this.plugin.init({ 95 | element: el, 96 | useFakeFullscreen: true 97 | }); 98 | 99 | this.viewerAPI.enterFullscreen(); 100 | this.viewerAPI.exitFullscreen(); 101 | ok(spy.calledWith('crocodoc-fakefullscreen'), 'should remove class .crocodoc-fakefullscreen'); 102 | }); 103 | 104 | test('destroy() should exit fullscreen mode when called', function () { 105 | this.plugin.init(); 106 | var spy = this.spy(this.viewerAPI, 'exitFullscreen'); 107 | this.plugin.destroy(); 108 | ok(spy.called, 'should exit fullscreen'); 109 | }); 110 | -------------------------------------------------------------------------------- /src/js/utilities/subpx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Subpixel rendering fix for browsers that do not support subpixel rendering 3 | * @author lakenen 4 | */ 5 | 6 | /*global window, document*/ 7 | Crocodoc.addUtility('subpx', function (framework) { 8 | 9 | 'use strict'; 10 | 11 | //-------------------------------------------------------------------------- 12 | // Private 13 | //-------------------------------------------------------------------------- 14 | 15 | var CSS_CLASS_SUBPX_FIX = 'crocodoc-subpx-fix', 16 | TEST_SPAN_TEMPLATE = '' + 17 | (new Array(100)).join('A') + ''; // repeat 'A' character; 18 | 19 | var util = framework.getUtility('common'); 20 | 21 | /** 22 | * Return true if subpixel rendering is supported 23 | * @returns {Boolean} 24 | * @private 25 | */ 26 | function isSubpixelRenderingSupported() { 27 | // Test if subpixel rendering is supported 28 | // @NOTE: jQuery.support.leadingWhitespace is apparently false if browser is IE6-8 29 | if (!$.support.leadingWhitespace) { 30 | return false; 31 | } else { 32 | //span #1 - desired font-size: 12.5px 33 | var span = $(util.template(TEST_SPAN_TEMPLATE, { size: 12.5 })) 34 | .appendTo(document.documentElement).get(0); 35 | var fontsize1 = $(span).css('font-size'); 36 | var width1 = $(span).width(); 37 | $(span).remove(); 38 | 39 | //span #2 - desired font-size: 12.6px 40 | span = $(util.template(TEST_SPAN_TEMPLATE, { size: 12.6 })) 41 | .appendTo(document.documentElement).get(0); 42 | var fontsize2 = $(span).css('font-size'); 43 | var width2 = $(span).width(); 44 | $(span).remove(); 45 | 46 | // is not mobile device? 47 | // @NOTE(plai): Mobile WebKit supports subpixel rendering even though the browser fails the following tests. 48 | // @NOTE(plai): When modifying these tests, make sure that these tests will work even when the browser zoom is changed. 49 | // @TODO(plai): Find a better way of testing for mobile Safari. 50 | if (!('ontouchstart' in window)) { 51 | 52 | //font sizes are the same? (Chrome and Safari will fail this) 53 | if (fontsize1 === fontsize2) { 54 | return false; 55 | } 56 | 57 | //widths are the same? (Firefox on Windows without GPU will fail this) 58 | if (width1 === width2) { 59 | return false; 60 | } 61 | } 62 | } 63 | 64 | return true; 65 | } 66 | 67 | var subpixelRenderingIsSupported = isSubpixelRenderingSupported(); 68 | 69 | //-------------------------------------------------------------------------- 70 | // Public 71 | //-------------------------------------------------------------------------- 72 | 73 | return { 74 | /** 75 | * Apply the subpixel rendering fix to the given element if necessary. 76 | * @NOTE: Fix is only applied if the "zoom" CSS property exists 77 | * (ie., this fix is never applied in Firefox) 78 | * @param {Element} el The element 79 | * @returns {Element} The element 80 | */ 81 | fix: function (el) { 82 | if (!subpixelRenderingIsSupported) { 83 | if (document.body.style.zoom !== undefined) { 84 | var $wrap = $('
').addClass(CSS_CLASS_SUBPX_FIX); 85 | $(el).wrap($wrap); 86 | } 87 | } 88 | return el; 89 | }, 90 | 91 | /** 92 | * Is sub-pixel text rendering supported? 93 | * @param {void} 94 | * @returns {boolean} true if sub-pixel tex rendering is supported 95 | */ 96 | isSubpxSupported: function() { 97 | return subpixelRenderingIsSupported; 98 | } 99 | }; 100 | }); 101 | -------------------------------------------------------------------------------- /src/js/utilities/support.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Support utility for feature detection / browser support 3 | * @author lakenen 4 | */ 5 | 6 | Crocodoc.addUtility('support', function () { 7 | 8 | 'use strict'; 9 | var prefixes = ['Moz', 'Webkit', 'O', 'ms'], 10 | xhrSupported = null, 11 | xhrCORSSupported = null; 12 | 13 | /** 14 | * Helper function to get the proper vendor property name 15 | * (`transition` => `WebkitTransition`) 16 | * @param {string} prop The property name to test for 17 | * @returns {string|boolean} The vendor-prefixed property name or false if the property is not supported 18 | */ 19 | function getVendorCSSPropertyName(prop) { 20 | var testDiv = document.createElement('div'), 21 | prop_, i, vendorProp; 22 | 23 | // Handle unprefixed versions (FF16+, for example) 24 | if (prop in testDiv.style) { 25 | return prop; 26 | } 27 | 28 | prop_ = prop.charAt(0).toUpperCase() + prop.substr(1); 29 | 30 | if (prop in testDiv.style) { 31 | return prop; 32 | } 33 | 34 | for (i = 0; i < prefixes.length; ++i) { 35 | vendorProp = prefixes[i] + prop_; 36 | if (vendorProp in testDiv.style) { 37 | if (vendorProp.indexOf('ms') === 0) { 38 | vendorProp = '-' + vendorProp; 39 | } 40 | return uncamel(vendorProp); 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | 47 | /** 48 | * Converts a camelcase string to a dasherized string. 49 | * (`marginLeft` => `margin-left`) 50 | * @param {stirng} str The camelcase string to convert 51 | * @returns {string} The dasherized string 52 | */ 53 | function uncamel(str) { 54 | return str.replace(/([A-Z])/g, function(letter) { return '-' + letter.toLowerCase(); }); 55 | } 56 | 57 | return { 58 | svg: document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1'), 59 | csstransform: getVendorCSSPropertyName('transform'), 60 | csstransition: getVendorCSSPropertyName('transition'), 61 | csszoom: getVendorCSSPropertyName('zoom'), 62 | 63 | /** 64 | * Return true if XHR is supported 65 | * @returns {boolean} 66 | */ 67 | isXHRSupported: function () { 68 | if (xhrSupported === null) { 69 | xhrSupported = !!this.getXHR(); 70 | } 71 | return xhrSupported; 72 | }, 73 | 74 | /** 75 | * Return true if XHR is supported and is CORS-enabled 76 | * @returns {boolean} 77 | */ 78 | isCORSSupported: function () { 79 | if (xhrCORSSupported === null) { 80 | xhrCORSSupported = this.isXHRSupported() && 81 | ('withCredentials' in this.getXHR()); 82 | } 83 | return xhrCORSSupported; 84 | }, 85 | 86 | /** 87 | * Return true if XDR is supported 88 | * @returns {boolean} 89 | */ 90 | isXDRSupported: function () { 91 | return typeof window.XDomainRequest !== 'undefined'; 92 | }, 93 | 94 | /** 95 | * Get a XHR object 96 | * @returns {XMLHttpRequest} An XHR object 97 | */ 98 | getXHR: function () { 99 | if (window.XMLHttpRequest) { 100 | return new window.XMLHttpRequest(); 101 | } else { 102 | try { 103 | return new ActiveXObject('MSXML2.XMLHTTP.3.0'); 104 | } 105 | catch(ex) { 106 | return null; 107 | } 108 | } 109 | }, 110 | 111 | /** 112 | * Get a CORS-enabled request object 113 | * @returns {XMLHttpRequest|XDomainRequest} The request object 114 | */ 115 | getXDR: function () { 116 | if (this.isXDRSupported()) { 117 | return new window.XDomainRequest(); 118 | } 119 | return null; 120 | } 121 | }; 122 | }); 123 | -------------------------------------------------------------------------------- /examples/realtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 51 | 52 | 53 |
54 |
55 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/js/components/page-links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview page-link component 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * page-links component definition 8 | */ 9 | Crocodoc.addComponent('page-links', function (scope) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var $el, 18 | browser = scope.getUtility('browser'); 19 | 20 | /** 21 | * Create a link element given link data 22 | * @param {Object} link The link data 23 | * @returns {void} 24 | * @private 25 | */ 26 | function createLink(link) { 27 | var $link = $('
').addClass(CSS_CLASS_PAGE_LINK), 28 | left = link.bbox[0], 29 | top = link.bbox[1], 30 | attr = {}; 31 | 32 | if (browser.ie) { 33 | // @NOTE: IE doesn't allow override of ctrl+click on anchor tags, 34 | // but there is a workaround to add a child element (which triggers 35 | // the onclick event first) 36 | $('') 37 | .appendTo($link) 38 | .on('click', handleClick); 39 | } 40 | 41 | $link.css({ 42 | left: left + 'pt', 43 | top: top + 'pt', 44 | width: link.bbox[2] - left + 'pt', 45 | height: link.bbox[3] - top + 'pt' 46 | }); 47 | 48 | if (link.uri) { 49 | if (/^http|^mailto/.test(link.uri)) { 50 | attr.href = encodeURI(link.uri); 51 | attr.target = '_blank'; 52 | attr.rel = 'noreferrer'; // strip referrer for privacy 53 | } else { 54 | // don't embed this link... we don't trust the protocol 55 | return; 56 | } 57 | } else if (link.destination) { 58 | attr.href = '#page-' + link.destination.pagenum; 59 | } 60 | 61 | $link.attr(attr); 62 | $link.data('link', link); 63 | $link.appendTo($el); 64 | } 65 | 66 | /** 67 | * Handle link clicks 68 | * @param {Event} event The event object 69 | * @returns {void} 70 | * @private 71 | */ 72 | function handleClick(event) { 73 | var targetEl = browser.ie ? event.target.parentNode : event.target, 74 | $link = $(targetEl), 75 | data = $link.data('link'); 76 | 77 | if (data) { 78 | scope.broadcast('linkclick', data); 79 | } 80 | event.preventDefault(); 81 | } 82 | 83 | //-------------------------------------------------------------------------- 84 | // Public 85 | //-------------------------------------------------------------------------- 86 | 87 | return { 88 | /** 89 | * Initialize the page-links component 90 | * @param {Array} links Links configuration array 91 | * @returns {void} 92 | * @TODO (possible): make a links data-provider instead of passing 93 | * them in as an argument? 94 | */ 95 | init: function (el, links) { 96 | $el = $(el); 97 | this.createLinks(links); 98 | if (!browser.ie) { 99 | // @NOTE: event handlers are individually bound in IE, because 100 | // the ctrl+click workaround fails when using event delegation 101 | $el.on('click', '.' + CSS_CLASS_PAGE_LINK, handleClick); 102 | } 103 | }, 104 | 105 | /** 106 | * Destroy the page-links component 107 | * @returns {void} 108 | */ 109 | destroy: function () { 110 | // @NOTE: individual click event handlers needed for IE are 111 | // implicitly removed by jQuery when we empty the links container 112 | $el.empty().off('click'); 113 | $el = browser = null; 114 | }, 115 | 116 | /** 117 | * Create and insert link elements into the element 118 | * @param {Array} links Array of link data 119 | * @returns {void} 120 | */ 121 | createLinks: function (links) { 122 | var i, len; 123 | for (i = 0, len = links.length; i < len; ++i) { 124 | createLink(links[i]); 125 | } 126 | } 127 | }; 128 | }); 129 | -------------------------------------------------------------------------------- /src/js/components/layout-horizontal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview layout-horizontal component definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * The horizontal layout 8 | */ 9 | Crocodoc.addComponent('layout-' + LAYOUT_HORIZONTAL, ['layout-paged'], function (scope, paged) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var util = scope.getUtility('common'), 18 | browser = scope.getUtility('browser'); 19 | 20 | //-------------------------------------------------------------------------- 21 | // Public 22 | //-------------------------------------------------------------------------- 23 | 24 | return paged.extend({ 25 | 26 | /** 27 | * Calculate the numeric value for zoom 'auto' for this layout mode 28 | * @returns {float} The zoom value 29 | */ 30 | calculateZoomAutoValue: function () { 31 | var state = this.state, 32 | fitWidth = this.calculateZoomValue(ZOOM_FIT_WIDTH), 33 | fitHeight = this.calculateZoomValue(ZOOM_FIT_HEIGHT); 34 | 35 | // landscape 36 | if (state.widestPage.actualWidth > state.tallestPage.actualHeight) { 37 | return Math.min(fitWidth, fitHeight); 38 | } 39 | // portrait 40 | else { 41 | if (browser.mobile) { 42 | return fitHeight; 43 | } 44 | // limit max zoom to 1.0 45 | return Math.min(1, fitHeight); 46 | } 47 | }, 48 | 49 | /** 50 | * Calculate which page is currently the "focused" page. 51 | * In horizontal mode, this is the page farthest to the left, 52 | * where at least half of the page is showing. 53 | * @returns {int} The current page 54 | */ 55 | calculateCurrentPage: function () { 56 | var prev, page, 57 | state = this.state, 58 | pages = state.pages; 59 | 60 | prev = util.bisectRight(pages, state.scrollLeft, 'x0') - 1; 61 | page = util.bisectRight(pages, state.scrollLeft + pages[prev].width / 2, 'x0') - 1; 62 | return 1 + page; 63 | }, 64 | 65 | /** 66 | * Calculates the next page 67 | * @returns {int} The next page number 68 | */ 69 | calculateNextPage: function () { 70 | return this.state.currentPage + 1; 71 | }, 72 | 73 | /** 74 | * Calculates the previous page 75 | * @returns {int} The previous page number 76 | */ 77 | calculatePreviousPage: function () { 78 | return this.state.currentPage - 1; 79 | }, 80 | 81 | /** 82 | * Handle resize mesages 83 | * @param {Object} data The message data 84 | * @returns {void} 85 | */ 86 | handleResize: function (data) { 87 | paged.handleResize.call(this, data); 88 | this.updateCurrentPage(); 89 | }, 90 | 91 | /** 92 | * Handle scroll mesages 93 | * @param {Object} data The message data 94 | * @returns {void} 95 | */ 96 | handleScroll: function (data) { 97 | paged.handleScroll.call(this, data); 98 | this.updateCurrentPage(); 99 | }, 100 | 101 | /** 102 | * Updates the layout elements (pages, doc, etc) CSS 103 | * appropriately for the current zoom level 104 | * @returns {void} 105 | */ 106 | updateLayout: function () { 107 | var state = this.state, 108 | zoomState = state.zoomState, 109 | zoom = zoomState.zoom, 110 | zoomedWidth = state.sumWidths, 111 | zoomedHeight = Math.floor(state.tallestPage.totalActualHeight * zoom), 112 | docWidth = Math.max(zoomedWidth, state.viewportDimensions.clientWidth), 113 | docHeight = Math.max(zoomedHeight, state.viewportDimensions.clientHeight); 114 | 115 | this.$doc.css({ 116 | height: docHeight, 117 | lineHeight: docHeight + 'px', 118 | width: docWidth 119 | }); 120 | } 121 | }); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /examples/slider/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 86 | 87 | 88 |
89 |
90 |
91 |
92 | Filter: 93 | | 94 | | 95 | | 96 | | 97 | 98 |
99 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /test/js/core/event-target-test.js: -------------------------------------------------------------------------------- 1 | module('Framework - EventTarget', { 2 | setup: function() { 3 | this.eventTarget = new Crocodoc.EventTarget(); 4 | } 5 | }); 6 | 7 | test('fire() should call the registered event handler for the event when called with a custom event', function() { 8 | this.eventTarget.on('myevent', this.mock()); 9 | this.eventTarget.fire('myevent'); 10 | }); 11 | 12 | test('fire() should call all registered event handlers for the event when called with a custom event', function() { 13 | this.eventTarget.on('myevent', this.mock()); 14 | this.eventTarget.on('myevent', this.mock()); 15 | this.eventTarget.fire('myevent'); 16 | }); 17 | 18 | test('fire() should only call the event handlers registered for the event when called with a custom event', function() { 19 | this.eventTarget.on('myevent1', this.mock()); 20 | this.eventTarget.on('myevent2', this.mock().never()); 21 | this.eventTarget.fire('myevent1'); 22 | }); 23 | 24 | test('fire() should call the event handler with a custom event object for custom event when called with a custom event object', function() { 25 | var handler = this.mock().withArgs(sinon.match({ 26 | type: 'myevent', 27 | data: undefined 28 | })); 29 | 30 | this.eventTarget.on('myevent', handler); 31 | this.eventTarget.fire('myevent'); 32 | }); 33 | 34 | test('fire() should call the event handler with a custom event object and extra data for custom event when called with extra data', function() { 35 | var handler = this.mock().withArgs(sinon.match({ 36 | type: 'myevent', 37 | data: { 38 | foo: 'bar', 39 | time: 'now' 40 | } 41 | })); 42 | 43 | this.eventTarget.on('myevent', handler); 44 | this.eventTarget.fire('myevent', { 45 | foo: 'bar', 46 | time: 'now' 47 | }); 48 | }); 49 | 50 | test('fire() should not call the event handler for custom event when off() is called for that event handler', function() { 51 | var handler = sinon.spy(); 52 | this.eventTarget.on('myevent', handler); 53 | 54 | this.eventTarget.off('myevent', handler); 55 | 56 | this.eventTarget.fire('myevent'); 57 | ok(handler.notCalled); 58 | }); 59 | 60 | test('fire() should return an event object with the proper methods and data when called', function () { 61 | var type = 'myevent', 62 | data = { some: 'data' }, 63 | event = this.eventTarget.fire(type, data); 64 | 65 | equal(event.type, type, 'event type should be correct'); 66 | equal(event.data, data, 'event data should be correct'); 67 | equal(typeof event.preventDefault, 'function', 'event.preventDefault should be a function'); 68 | equal(typeof event.isDefaultPrevented, 'function', 'event.isDefaultPrevented should be a function'); 69 | }); 70 | 71 | test('event.preventDefault() should prevent default behavior when called', function () { 72 | var event = this.eventTarget.fire('myevent'); 73 | event.preventDefault(); 74 | 75 | ok(event.isDefaultPrevented(), 'default behavior should be prevented'); 76 | }); 77 | 78 | test('registered event handler should be called only once when attached with one()', function () { 79 | this.eventTarget.one('myevent', this.mock().once()); 80 | this.eventTarget.fire('myevent'); 81 | this.eventTarget.fire('myevent'); 82 | }); 83 | 84 | test('registered event handler should be removed when attached with one() and off() is called for that handler', function () { 85 | var handler = this.mock().never(); 86 | this.eventTarget.one('myevent', handler); 87 | this.eventTarget.off('myevent', handler); 88 | this.eventTarget.fire('myevent'); 89 | }); 90 | 91 | test('off() should remove all event handlers for a given type when called without a handler', function () { 92 | var handler1 = this.mock().never(); 93 | var handler2 = this.mock().never(); 94 | this.eventTarget.on('myevent', handler1); 95 | this.eventTarget.on('myevent', handler2); 96 | this.eventTarget.off('myevent'); 97 | this.eventTarget.fire('myevent'); 98 | }); 99 | 100 | test('Event handler should be called even after another event handler for the same type removes itself', function() { 101 | 102 | var handler1 = function () { 103 | // this handler removes itself 104 | this.off('myevent', handler1); 105 | }, 106 | handler2 = sinon.spy(); 107 | 108 | this.eventTarget.on('myevent', handler1); 109 | this.eventTarget.on('myevent', handler2); 110 | 111 | this.eventTarget.fire('myevent'); 112 | ok(handler2.called); 113 | }); 114 | 115 | -------------------------------------------------------------------------------- /test/js/core/crocodoc-test.js: -------------------------------------------------------------------------------- 1 | 2 | module('Framework - Utilities'); 3 | 4 | test('getUtility() should call the creator function with the framework as an argument when called for an existing utility', function() { 5 | Crocodoc.addUtility('utility 1', this.mock().withArgs(Crocodoc)); 6 | Crocodoc.getUtility('utility 1'); 7 | }); 8 | 9 | test('getUtility() should return the object that is returned from the creator function when called for an existing utility', function() { 10 | var testService = {}; 11 | 12 | Crocodoc.addUtility('utility 2', this.stub().returns(testService)); 13 | 14 | equal(Crocodoc.getUtility('utility 2'), testService, 'constructed utility returned'); 15 | }); 16 | 17 | test('getUtility() should return the same object for each call when called for the same utility multiple times', function() { 18 | Crocodoc.addUtility('utility 3', this.mock().once().returns({})); 19 | 20 | var first = Crocodoc.getUtility('utility 3'); 21 | var second = Crocodoc.getUtility('utility 3'); 22 | equal(first, second, 'same utility returned'); 23 | }); 24 | 25 | test('getUtility() should return null when called for a non-existing utility', function() { 26 | var utility = Crocodoc.getUtility('non-existing'); 27 | equal(utility, null, 'null returned'); 28 | }); 29 | 30 | module('Framework - Components'); 31 | 32 | test('createComponent() should call the creator function with the passed scope as an argument when called for an existing module', function () { 33 | var testScope = {}; 34 | Crocodoc.addComponent('module 1', this.mock().withArgs(testScope)); 35 | Crocodoc.createComponent('module 1', testScope); 36 | }); 37 | 38 | test('createComponent() should call the creator function with the correct mixin components as arguments when called for an existing module that has requested mixins', function () { 39 | var testScope = {}, base1 = { foo: 'bar' }, base2 = { bar: 'foo' }; 40 | Crocodoc.addComponent('module base1', function () { return base1; }); 41 | Crocodoc.addComponent('module base2', function () { return base2; }); 42 | Crocodoc.addComponent('module 1', ['module base1','module base2'], this.mock().withArgs(testScope, base1, base2)); 43 | Crocodoc.createComponent('module 1', testScope); 44 | }); 45 | 46 | test('addComponent() should throw an error when called with a module name and mixins list that contains a circular dependency', function () { 47 | Crocodoc.addComponent('base1', ['base2'], function () {}); 48 | throws( 49 | function () { 50 | Crocodoc.addComponent('base2', ['base1'], function () {}); 51 | }, 52 | 'Exception was thrown' 53 | ); 54 | }); 55 | 56 | test('createComponent() should return null when called for a non-existing module', function() { 57 | var module = Crocodoc.createComponent('non-existing', {}); 58 | equal(module, null, 'null returned'); 59 | }); 60 | 61 | test('addDataProvider() should call addComponent when a model name and creator function are passed in', function() { 62 | this.mock(Crocodoc).expects('addComponent'); 63 | Crocodoc.addDataProvider('my-data-provider', function () {}); 64 | }); 65 | 66 | module('Framework - createViewer', { 67 | setup: function () { 68 | this.scope = { 69 | createComponent: function () { 70 | 71 | } 72 | }; 73 | this.viewerAPI = { 74 | id: 3, 75 | init: function () {} 76 | }; 77 | this.scopeStub = sinon.stub(Crocodoc, 'Scope').returns(this.scope); 78 | }, 79 | teardown: function () { 80 | this.scopeStub.restore(); 81 | } 82 | }); 83 | 84 | test('createViewer() should return a new instance of Crocodoc.Viewer when called', function () { 85 | var el = $(), options = {}; 86 | 87 | this.mock(Crocodoc) 88 | .expects('Viewer') 89 | .withArgs(el, options) 90 | .returns(this.viewerAPI); 91 | var instance = Crocodoc.createViewer(el, options); 92 | ok(this.viewerAPI === instance, 'returned the viewer instance'); 93 | }); 94 | 95 | test('getViewer() should return the viewer instance when called with a valid id', function () { 96 | var el = $(), options = {}; 97 | 98 | this.stub(Crocodoc, 'Viewer') 99 | .withArgs(el, options) 100 | .returns(this.viewerAPI); 101 | this.stub(Crocodoc.Viewer, 'get') 102 | .withArgs(this.viewerAPI.id) 103 | .returns(this.viewerAPI); 104 | 105 | var instance = Crocodoc.createViewer(el, options); 106 | 107 | equal(Crocodoc.getViewer(instance.id), instance, 'should be the same viewer'); 108 | }); 109 | -------------------------------------------------------------------------------- /test/js/components/layout-presentation-test.js: -------------------------------------------------------------------------------- 1 | module('Component - layout-presentation', { 2 | setup: function () { 3 | this.utilities = { 4 | common: Crocodoc.getUtilityForTest('common'), 5 | browser: { 6 | mobile: false 7 | } 8 | }; 9 | this.config = {}; 10 | this.scope = Crocodoc.getScopeForTest(this); 11 | this.mixins = { 12 | 'layout-paged': { 13 | calculateZoomValue: function () {}, 14 | init: function () {}, 15 | updateCurrentPage: function () {}, 16 | updatePageMargins: function () {}, 17 | updatePageClasses: function () {}, 18 | updateVisiblePages: function () {}, 19 | setCurrentPage: function () {}, 20 | extend: function (obj) { 21 | return Crocodoc.getUtility('common').extend({}, this, obj); 22 | } 23 | } 24 | }; 25 | 26 | this.component = Crocodoc.getComponentForTest('layout-presentation', this.scope, this.mixins); 27 | } 28 | }); 29 | 30 | test('init() should update page margins and classes and init a base layout when called', function () { 31 | var initSpy = this.spy(this.mixins['layout-paged'], 'init'); 32 | var mock = this.mock(this.component); 33 | mock.expects('updatePageMargins'); 34 | mock.expects('updatePageClasses'); 35 | this.component.init(); 36 | ok(initSpy.calledOn(this.component), 'init was called with the proper config on the proper context'); 37 | }); 38 | 39 | QUnit.cases([ 40 | { fitWidth: 1.1, fitHeight: 2, value: 1.1 }, 41 | { fitWidth: 2.8, fitHeight: 1.2, value: 1.2 } 42 | ]).test('calculateZoomAutoValue() should return the correct zoom auto value when called', function (params) { 43 | var stub = this.stub(this.component, 'calculateZoomValue'); 44 | stub.withArgs(Crocodoc.ZOOM_FIT_WIDTH).returns(params.fitWidth); 45 | stub.withArgs(Crocodoc.ZOOM_FIT_HEIGHT).returns(params.fitHeight); 46 | equal(this.component.calculateZoomAutoValue(), params.value, 'value is correct'); 47 | }); 48 | 49 | test('calculatePreviousPage() should return the correct page number when called', function () { 50 | var page = 8; 51 | this.component.state = { currentPage: page }; 52 | equal(this.component.calculatePreviousPage(), page - 1, 'the page was correct'); 53 | }); 54 | 55 | test('calculateNextPage() should return the correct page number when called', function () { 56 | var page = 8; 57 | this.component.state = { currentPage: page }; 58 | equal(this.component.calculateNextPage(), page + 1, 'the page was correct'); 59 | }); 60 | 61 | QUnit.cases([ 62 | { page: 1, expectedCurrent: 1 }, 63 | { currentPage: 1, page: 1, expectedCurrent: 1 }, 64 | { currentPage: 1, page: 2, expectedCurrent: 2, expectedPreceding: 1 }, 65 | { precedingPage: 1, currentPage: 2, page: 1, expectedCurrent: 1, expectedPreceding: 2 } 66 | ]).test('setCurrentPage() should update the preceding and current page classes correctly when called', function (params) { 67 | this.stub(this.component, 'updateVisiblePages'); 68 | this.stub(this.component, 'updatePageClasses'); 69 | 70 | var $doc = $('
'), 71 | $page = $('
'); 72 | 73 | $page.clone().appendTo($doc); 74 | $page.clone().appendTo($doc); 75 | $page.clone().appendTo($doc); 76 | 77 | var $pages = $doc.find('.crocodoc-page'); 78 | 79 | if (params.currentPage) { 80 | $pages.eq(params.currentPage - 1).addClass('crocodoc-current-page'); 81 | } 82 | if (params.precedingPage) { 83 | $pages.eq(params.precedingPage - 1).addClass('crocodoc-preceding-page'); 84 | } 85 | 86 | this.component.state = { currentPage: params.currentPage }; 87 | this.component.$doc = $doc; 88 | this.component.$pages = $pages; 89 | 90 | this.component.setCurrentPage(params.page); 91 | 92 | if (params.expectedCurrent) { 93 | ok($pages.eq(params.expectedCurrent - 1).hasClass('crocodoc-current-page'), 'should have current page class'); 94 | } 95 | if (params.expectedPreceding) { 96 | ok($pages.eq(params.expectedPreceding - 1).hasClass('crocodoc-preceding-page'), 'should have preceding page class'); 97 | equal($doc.find('.crocodoc-preceding-page').length, 1, 'should only be one preceding page'); 98 | } else { 99 | equal($doc.find('.crocodoc-preceding-page').length, 0, 'should not be a preceding page'); 100 | } 101 | equal($doc.find('.crocodoc-current-page').length, 1, 'should only be one current page'); 102 | }); 103 | -------------------------------------------------------------------------------- /test/js/data-providers/page-img-test.js: -------------------------------------------------------------------------------- 1 | module('Data Provider: page-img', { 2 | setup: function () { 3 | var me = this; 4 | this.$deferred = $.Deferred(); 5 | this.promise = { 6 | abort: function () {}, 7 | then: function () { return me.$deferred.promise(); }, 8 | promise: function (x) { return me.$deferred.promise(x); } 9 | }; 10 | this.utilities = { 11 | ajax: { 12 | fetch: function () {} 13 | }, 14 | common: { 15 | template: sinon.stub().returns(''), 16 | countInStr: sinon.stub().returns(10000) 17 | }, 18 | browser: {}, 19 | subpx: { 20 | isSubpxSupported: function () {} 21 | } 22 | }; 23 | this.config = { 24 | url: '', 25 | template: { 26 | img: 'page-{{page}}.png' 27 | }, 28 | queryString: '' 29 | }; 30 | this.scope = Crocodoc.getScopeForTest(this); 31 | this.dataProvider = Crocodoc.getComponentForTest('data-provider-page-img', this.scope); 32 | }, 33 | teardown: function () { 34 | this.scope.destroy(); 35 | } 36 | }); 37 | 38 | test('creator should return an object with a get function', function(){ 39 | equal(typeof this.dataProvider, 'object'); 40 | equal(typeof this.dataProvider.get, 'function'); 41 | }); 42 | 43 | test('getImage() should return a new Image object when called', function() { 44 | var img = this.dataProvider.getImage(); 45 | ok(img instanceof Image, 'should return a new image'); 46 | }); 47 | 48 | test('get() should return a $.Promise with an abort() function', function() { 49 | var img = { 50 | setAttribute: this.spy() 51 | }; 52 | this.stub(this.dataProvider, 'getImage').returns(img); 53 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 54 | propEqual(this.dataProvider.get('page-img', 1), $.Deferred().promise({abort:function(){}})); 55 | }); 56 | 57 | test('abort() should abort the image load when called on the returned promise', function() { 58 | var img = { 59 | setAttribute: function () {}, 60 | removeAttribute: this.spy() 61 | }; 62 | this.stub(this.dataProvider, 'getImage').returns(img); 63 | 64 | var promise = this.dataProvider.get('page-img', 2); 65 | promise.abort(); 66 | ok(img.removeAttribute.calledWith('src'), 'image load should be aborted'); 67 | }); 68 | 69 | test('getURL() should return the correct URL to the png file when called', function() { 70 | this.utilities.common.template = Crocodoc.getUtility('common').template; 71 | this.config.url = 'http://beep.boop/bop/'; 72 | equal(this.dataProvider.getURL(3), this.config.url + 'page-3.png', 'the URL should be correct'); 73 | }); 74 | 75 | test('get() should return a promise that is resolved with the img object when the image successfully loads', function () { 76 | var img = { 77 | setAttribute: this.spy() 78 | }; 79 | this.stub(this.dataProvider, 'getImage').returns(img); 80 | 81 | var promise = this.dataProvider.get('page-img', 2); 82 | 83 | img.onload(); 84 | promise.done(function (i) { 85 | equal(i, img, 'img object should be resolved'); 86 | }); 87 | }); 88 | 89 | test('get() should retry Crocodoc.ASSET_REQUEST_RETRIES times when the image fails to load', function () { 90 | var url = 'someurl.png'; 91 | var img = { 92 | setAttribute: function () {}, 93 | removeAttribute: function () {} 94 | }; 95 | this.stub(this.dataProvider, 'getURL').returns(url); 96 | this.stub(this.dataProvider, 'getImage').returns(img); 97 | 98 | var loadSpy = this.spy(img, 'setAttribute').withArgs('src', url); 99 | var abortSpy = this.spy(img, 'removeAttribute').withArgs('src'); 100 | 101 | var promise = this.dataProvider.get('page-img', 2); 102 | 103 | for (var i = 0; i < Crocodoc.ASSET_REQUEST_RETRIES; ++i) { 104 | img.onerror(); 105 | } 106 | equal(abortSpy.callCount, Crocodoc.ASSET_REQUEST_RETRIES, 'should be retried the correct number of times'); 107 | equal(loadSpy.callCount, Crocodoc.ASSET_REQUEST_RETRIES + 1, 'should be loaded the correct number of times'); 108 | }); 109 | 110 | test('get() should return a promise that is rejected with the error object when the image fails to load', function () { 111 | var img = { 112 | setAttribute: function () {}, 113 | removeAttribute: function () {} 114 | }; 115 | this.stub(this.dataProvider, 'getImage').returns(img); 116 | 117 | var promise = this.dataProvider.get('page-img', 2); 118 | 119 | img.onerror(); 120 | img.onerror(); 121 | promise.fail(function (err) { 122 | ok(err, 'error object should be rejected'); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /examples/page-content-flip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 93 | 94 | 95 | 96 |
97 | 98 | 114 | 115 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/js/components/scroller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Scroller component used to watch an element and fire 3 | * events `scrollstart`, `scroll`, and `scrollend` 4 | * @author lakenen 5 | */ 6 | 7 | /*global setTimeout, clearTimeout */ 8 | 9 | Crocodoc.addComponent('scroller', function (scope) { 10 | 11 | 'use strict'; 12 | 13 | var util = scope.getUtility('common'), 14 | browser = scope.getUtility('browser'); 15 | 16 | var GHOST_SCROLL_TIMEOUT = 3000, 17 | GHOST_SCROLL_INTERVAL = 30, 18 | SCROLL_EVENT_THROTTLE_INTERVAL = 200, 19 | SCROLL_END_TIMEOUT = browser.mobile ? 500 : 250; 20 | 21 | var $el, 22 | scrollendTID, 23 | scrollingStarted = false, 24 | touchStarted = false, 25 | touchEnded = false, 26 | touchMoved = false, 27 | touchEndTime = 0, 28 | ghostScrollStart = null; 29 | 30 | /** 31 | * Build event data object for firing scroll events 32 | * @returns {Object} Scroll event data object 33 | * @private 34 | */ 35 | function buildEventData() { 36 | return { 37 | scrollTop: $el.scrollTop(), 38 | scrollLeft: $el.scrollLeft() 39 | }; 40 | } 41 | 42 | /** 43 | * Broadcast a scroll event 44 | * @returns {void} 45 | * @private 46 | */ 47 | var fireScroll = util.throttle(SCROLL_EVENT_THROTTLE_INTERVAL, function () { 48 | scope.broadcast('scroll', buildEventData()); 49 | }); 50 | 51 | /** 52 | * Handle scrollend 53 | * @returns {void} 54 | * @private 55 | */ 56 | function handleScrollEnd() { 57 | scrollingStarted = false; 58 | ghostScrollStart = null; 59 | clearTimeout(scrollendTID); 60 | scope.broadcast('scrollend', buildEventData()); 61 | } 62 | 63 | /** 64 | * Handle scroll events 65 | * @returns {void} 66 | * @private 67 | */ 68 | function handleScroll() { 69 | // if we are just starting scrolling, fire scrollstart event 70 | if (!scrollingStarted) { 71 | scrollingStarted = true; 72 | scope.broadcast('scrollstart', buildEventData()); 73 | } 74 | clearTimeout(scrollendTID); 75 | scrollendTID = setTimeout(handleScrollEnd, SCROLL_END_TIMEOUT); 76 | fireScroll(); 77 | } 78 | 79 | /** 80 | * Handle touch start events 81 | * @returns {void} 82 | * @private 83 | */ 84 | function handleTouchstart() { 85 | touchStarted = true; 86 | touchEnded = false; 87 | touchMoved = false; 88 | handleScroll(); 89 | } 90 | 91 | /** 92 | * Handle touchmove events 93 | * @returns {void} 94 | * @private 95 | */ 96 | function handleTouchmove() { 97 | touchMoved = true; 98 | handleScroll(); 99 | } 100 | 101 | /** 102 | * Handle touchend events 103 | * @returns {void} 104 | * @private 105 | */ 106 | function handleTouchend() { 107 | touchStarted = false; 108 | touchEnded = true; 109 | touchEndTime = new Date().getTime(); 110 | if (touchMoved) { 111 | ghostScroll(); 112 | } 113 | } 114 | 115 | /** 116 | * Fire fake scroll events. 117 | * iOS doesn't fire events during the 'momentum' part of scrolling 118 | * so this is used to fake these events until the page stops moving. 119 | * @returns {void} 120 | * @private 121 | */ 122 | function ghostScroll() { 123 | clearTimeout(scrollendTID); 124 | if (ghostScrollStart === null) { 125 | ghostScrollStart = new Date().getTime(); 126 | } 127 | if (new Date().getTime() - ghostScrollStart > GHOST_SCROLL_TIMEOUT) { 128 | handleScrollEnd(); 129 | return; 130 | } 131 | fireScroll(); 132 | scrollendTID = setTimeout(ghostScroll, GHOST_SCROLL_INTERVAL); 133 | } 134 | 135 | return { 136 | /** 137 | * Initialize the scroller component 138 | * @param {Element} el The Element 139 | * @returns {void} 140 | */ 141 | init: function (el) { 142 | $el = $(el); 143 | $el.on('scroll', handleScroll); 144 | $el.on('touchstart', handleTouchstart); 145 | $el.on('touchmove', handleTouchmove); 146 | $el.on('touchend', handleTouchend); 147 | }, 148 | 149 | /** 150 | * Destroy the scroller component 151 | * @returns {void} 152 | */ 153 | destroy: function () { 154 | clearTimeout(scrollendTID); 155 | $el.off('scroll', handleScroll); 156 | $el.off('touchstart', handleTouchstart); 157 | $el.off('touchmove', handleTouchmove); 158 | $el.off('touchend', handleTouchend); 159 | } 160 | }; 161 | }); 162 | -------------------------------------------------------------------------------- /test/js/data-providers/page-svg-test.js: -------------------------------------------------------------------------------- 1 | module('Data Provider: page-svg', { 2 | setup: function () { 3 | var me = this; 4 | this.$deferred = $.Deferred(); 5 | this.promise = { 6 | abort: function () {}, 7 | then: function () { return me.$deferred.promise(); }, 8 | promise: function (x) { return me.$deferred.promise(x); } 9 | }; 10 | this.utilities = { 11 | ajax: { 12 | fetch: function () {} 13 | }, 14 | common: { 15 | template: sinon.stub().returns(''), 16 | countInStr: sinon.stub().returns(10000) 17 | }, 18 | browser: {}, 19 | subpx: { 20 | isSubpxSupported: function () {} 21 | } 22 | }; 23 | this.config = { 24 | url: '', 25 | template: { 26 | svg: 'page-{{page}}.svg' 27 | }, 28 | queryString: '', 29 | cssText: '' 30 | }; 31 | this.scope = Crocodoc.getScopeForTest(this); 32 | this.dataProvider = Crocodoc.getComponentForTest('data-provider-page-svg', this.scope); 33 | }, 34 | teardown: function () { 35 | this.scope.destroy(); 36 | this.dataProvider.destroy(); 37 | } 38 | }); 39 | 40 | test('creator should return an object with a get function', function(){ 41 | equal(typeof this.dataProvider, 'object'); 42 | equal(typeof this.dataProvider.get, 'function'); 43 | }); 44 | 45 | test('get() should return a $.Promise with an abort() function', function() { 46 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 47 | propEqual(this.dataProvider.get('page-svg', 1), $.Deferred().promise({abort:function(){}})); 48 | }); 49 | 50 | test('get() should return a cached promise when called a second time', function() { 51 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 52 | equal(this.dataProvider.get('page-svg', 1), this.dataProvider.get('page-svg', 1)); 53 | }); 54 | 55 | test('abort() should call abort on the promise returned from ajax.fetch when called on the returned promise', function() { 56 | this.stub(this.utilities.ajax, 'fetch').returns(this.promise); 57 | this.mock(this.promise).expects('abort').once(); 58 | 59 | var promise = this.dataProvider.get('page-svg', 2); 60 | promise.abort(); 61 | }); 62 | 63 | test('getURL() should return the correct URL to the svg file when called', function() { 64 | this.utilities.common.template = Crocodoc.getUtility('common').template; 65 | this.config.url = 'http://beep.boop/bop/'; 66 | equal(this.dataProvider.getURL(3), this.config.url + 'page-3.svg', 'the URL should be correct'); 67 | }); 68 | 69 | test('get() should apply the subpx hack if the browser is firefox and subpixel rendering is not supported', function () { 70 | var svgText = '\n\n'; 71 | 72 | this.stub(this.scope, 'get').withArgs('stylesheet').returns(this.$deferred.promise()); 73 | this.stub(this.utilities.ajax, 'fetch').returns(this.$deferred.promise()); 74 | 75 | this.utilities.browser.firefox = true; 76 | this.stub(this.utilities.subpx, 'isSubpxSupported').returns(false); 77 | 78 | this.$deferred.resolve(svgText); 79 | 80 | var promise = this.dataProvider.get('page-svg', 3); 81 | promise.done(function (text) { 82 | ok(text.indexOf('') > -1, 'should have subpx hack'); 83 | }); 84 | }); 85 | 86 | test('get() should replace the link tag with the stylesheet correctly for a self-closing tag', function () { 87 | var svgText = '', 88 | cssText = '.this-is-css { color: red; }'; 89 | 90 | this.stub(this.scope, 'get').withArgs('stylesheet').returns($.Deferred().resolve(cssText).promise()); 91 | this.stub(this.utilities.ajax, 'fetch').returns(this.$deferred.promise()); 92 | 93 | this.$deferred.resolve(svgText); 94 | 95 | var promise = this.dataProvider.get('page-svg', 3); 96 | promise.done(function (text) { 97 | ok(text.indexOf('') > -1, 'should have cssText'); 98 | }); 99 | }); 100 | 101 | test('get() should replace the link tag with the stylesheet correctly for a non-self-closing tag', function () { 102 | var svgText = '', 103 | cssText = '.this-is-css { color: red; }'; 104 | 105 | this.stub(this.scope, 'get').withArgs('stylesheet').returns($.Deferred().resolve(cssText).promise()); 106 | this.stub(this.utilities.ajax, 'fetch').returns(this.$deferred.promise()); 107 | 108 | this.$deferred.resolve(svgText); 109 | 110 | var promise = this.dataProvider.get('page-svg', 3); 111 | promise.done(function (text) { 112 | ok(text.indexOf('') > -1, 'should have cssText'); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /examples/presentations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/js/components/page-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview page-text component 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * page-text component 8 | */ 9 | Crocodoc.addComponent('page-text', function (scope) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var browser = scope.getUtility('browser'), 18 | subpx = scope.getUtility('subpx'); 19 | 20 | var destroyed = false, 21 | loaded = false, 22 | $textLayer, 23 | $loadTextPromise, 24 | page, 25 | viewerConfig = scope.getConfig(); 26 | 27 | /** 28 | * Return true if we should use the text layer, false otherwise 29 | * @returns {bool} 30 | * @private 31 | */ 32 | function shouldUseTextLayer() { 33 | return viewerConfig.enableTextSelection && !browser.ielt9; 34 | } 35 | 36 | /** 37 | * Handle success loading HTML text 38 | * @param {string} text The HTML text 39 | * @returns {void} 40 | * @private 41 | */ 42 | function loadTextLayerHTMLSuccess(text) { 43 | var doc, textEl; 44 | 45 | if (!text || loaded || destroyed) { 46 | return; 47 | } 48 | 49 | loaded = true; 50 | 51 | // create a document to parse the html text 52 | doc = document.implementation.createHTMLDocument(''); 53 | doc.getElementsByTagName('body')[0].innerHTML = text; 54 | text = null; 55 | 56 | // select just the element we want (CSS_CLASS_PAGE_TEXT) 57 | textEl = document.importNode(doc.querySelector('.' + CSS_CLASS_PAGE_TEXT), true); 58 | $textLayer.attr('class', textEl.getAttribute('class')); 59 | $textLayer.html(textEl.innerHTML); 60 | subpx.fix($textLayer); 61 | } 62 | 63 | function loadTextLayerHTMLFail(error) { 64 | if (error) { 65 | scope.broadcast('asseterror', error); 66 | } 67 | } 68 | 69 | /** 70 | * Load text html if necessary and insert it into the element 71 | * @returns {$.Promise} 72 | * @private 73 | */ 74 | function loadTextLayerHTML() { 75 | // already load(ed|ing)? 76 | if (!$loadTextPromise) { 77 | if (shouldUseTextLayer()) { 78 | $loadTextPromise = scope.get('page-text', page); 79 | } else { 80 | $loadTextPromise = $.Deferred().resolve().promise({ 81 | abort: function () {} 82 | }); 83 | } 84 | } 85 | 86 | return $loadTextPromise; 87 | } 88 | 89 | //-------------------------------------------------------------------------- 90 | // Public 91 | //-------------------------------------------------------------------------- 92 | 93 | return { 94 | /** 95 | * Initialize the page-text component 96 | * @param {jQuery} $el The jQuery element to load the text layer into 97 | * @returns {void} 98 | */ 99 | init: function ($el, pageNum) { 100 | $textLayer = $el; 101 | page = pageNum; 102 | }, 103 | 104 | /** 105 | * Destroy the page-text component 106 | * @returns {void} 107 | */ 108 | destroy: function () { 109 | destroyed = true; 110 | this.unload(); 111 | $textLayer.empty(); 112 | }, 113 | 114 | /** 115 | * Start loading HTML text 116 | * @returns {void} 117 | */ 118 | preload: function () { 119 | loadTextLayerHTML(); 120 | }, 121 | 122 | /** 123 | * Load the html text for the text layer and insert it into the element 124 | * if text layer is enabled and is not loading/has not already been loaded 125 | * @returns {$.Promise} A promise to load the text layer 126 | */ 127 | load: function () { 128 | return loadTextLayerHTML() 129 | .done(loadTextLayerHTMLSuccess) 130 | .fail(loadTextLayerHTMLFail); 131 | }, 132 | 133 | /** 134 | * Stop loading the text layer (no need to actually remove it) 135 | * @returns {void} 136 | */ 137 | unload: function () { 138 | if ($loadTextPromise && $loadTextPromise.state() !== 'resolved') { 139 | $loadTextPromise.abort(); 140 | $loadTextPromise = null; 141 | } 142 | }, 143 | 144 | /** 145 | * Enable text selection 146 | * @returns {void} 147 | */ 148 | enable: function () { 149 | $textLayer.css('display', ''); 150 | // we created an empty promise if text selection was previously disabled, 151 | // so we can scrap it so a new promise will be created next time this 152 | // page is requested 153 | if ($loadTextPromise && !loaded) { 154 | $loadTextPromise = null; 155 | } 156 | }, 157 | 158 | /** 159 | * Disable text selection 160 | * @returns {void} 161 | */ 162 | disable: function () { 163 | $textLayer.css('display', 'none'); 164 | } 165 | }; 166 | }); 167 | -------------------------------------------------------------------------------- /src/js/components/layout-vertical.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview layout-vertical component definition 3 | * @author lakenen 4 | */ 5 | 6 | /** 7 | * The vertical layout 8 | */ 9 | Crocodoc.addComponent('layout-' + LAYOUT_VERTICAL, ['layout-paged'], function (scope, paged) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var util = scope.getUtility('common'), 18 | browser = scope.getUtility('browser'); 19 | 20 | //-------------------------------------------------------------------------- 21 | // Public 22 | //-------------------------------------------------------------------------- 23 | 24 | return paged.extend({ 25 | 26 | /** 27 | * Calculate the numeric value for zoom 'auto' for this layout mode 28 | * @returns {float} The zoom value 29 | */ 30 | calculateZoomAutoValue: function () { 31 | var state = this.state, 32 | fitWidth = this.calculateZoomValue(ZOOM_FIT_WIDTH), 33 | fitHeight = this.calculateZoomValue(ZOOM_FIT_HEIGHT); 34 | 35 | if (state.widestPage.actualWidth > state.tallestPage.actualHeight) { 36 | // landscape 37 | // max zoom 1 for vertical mode 38 | return Math.min(1, fitWidth, fitHeight); 39 | } else { 40 | // portrait 41 | if (browser.mobile) { 42 | return fitWidth; 43 | } 44 | // limit max zoom to 100% of the doc 45 | return Math.min(1, fitWidth); 46 | } 47 | }, 48 | 49 | /** 50 | * Calculate which page is currently the "focused" page. 51 | * In vertical mode, this is the page at the top (and if multiple columns, the leftmost page), 52 | * where at least half of the page is showing. 53 | * @returns {int} The current page 54 | */ 55 | calculateCurrentPage: function () { 56 | var prevPageIndex, 57 | currentPageIndex, 58 | rowIndex, 59 | row, 60 | offset, 61 | state = this.state, 62 | pages = state.pages; 63 | 64 | prevPageIndex = util.bisectRight(pages, state.scrollTop, 'y0') - 1; 65 | if (prevPageIndex < 0) { 66 | return 1; 67 | } 68 | offset = state.scrollTop + pages[prevPageIndex].height / 2; 69 | currentPageIndex = util.bisectRight(pages, offset, 'y0') - 1; 70 | rowIndex = pages[currentPageIndex].rowIndex; 71 | row = state.rows[rowIndex]; 72 | return 1 + row[0]; 73 | 74 | }, 75 | 76 | /** 77 | * Calculates the next page 78 | * @returns {int} The next page number 79 | */ 80 | calculateNextPage: function () { 81 | var state = this.state, 82 | currentPage = state.pages[state.currentPage - 1], 83 | rowIndex = currentPage.rowIndex, 84 | nextRow = state.rows[rowIndex + 1]; 85 | return nextRow && nextRow[0] + 1 || state.currentPage; 86 | }, 87 | 88 | /** 89 | * Calculates the previous page 90 | * @returns {int} The previous page number 91 | */ 92 | calculatePreviousPage: function () { 93 | var state = this.state, 94 | currentPage = state.pages[state.currentPage - 1], 95 | rowIndex = currentPage.rowIndex, 96 | prevRow = state.rows[rowIndex - 1]; 97 | return prevRow && prevRow[0] + 1 || state.currentPage; 98 | }, 99 | 100 | /** 101 | * Handle resize mesages 102 | * @param {Object} data The message data 103 | * @returns {void} 104 | */ 105 | handleResize: function (data) { 106 | paged.handleResize.call(this, data); 107 | this.updateCurrentPage(); 108 | }, 109 | 110 | /** 111 | * Handle scroll mesages 112 | * @param {Object} data The message data 113 | * @returns {void} 114 | */ 115 | handleScroll: function (data) { 116 | paged.handleScroll.call(this, data); 117 | this.updateCurrentPage(); 118 | }, 119 | 120 | /** 121 | * Updates the layout elements (pages, doc, etc) CSS 122 | * appropriately for the current zoom level 123 | * @returns {void} 124 | */ 125 | updateLayout: function () { 126 | // vertical stuff 127 | var state = this.state, 128 | zoom = state.zoomState.zoom, 129 | zoomedWidth, 130 | docWidth; 131 | 132 | zoomedWidth = Math.floor(state.widestPage.totalActualWidth * zoom); 133 | 134 | // use clientWidth for the doc (prevent scrollbar) 135 | // use width:auto when possible 136 | if (zoomedWidth <= state.viewportDimensions.clientWidth) { 137 | docWidth = 'auto'; 138 | } else { 139 | docWidth = zoomedWidth; 140 | } 141 | 142 | this.$doc.css({ 143 | width: docWidth 144 | }); 145 | } 146 | }); 147 | }); 148 | 149 | --------------------------------------------------------------------------------