').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 = $('