1 /**
  2  * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global runtime, odf, xmldom, webodf_css, core, gui */
 26 /*jslint sub: true*/
 27 
 28 (function () {
 29     "use strict";
 30     /**
 31      * A loading queue where various tasks related to loading can be placed
 32      * and will be run with 10 ms between them. This gives the ui a change to
 33      * to update.
 34      * @constructor
 35      */
 36     function LoadingQueue() {
 37         var /**@type{!Array.<!Function>}*/
 38             queue = [],
 39             taskRunning = false;
 40         /**
 41          * @param {!Function} task
 42          * @return {undefined}
 43          */
 44         function run(task) {
 45             taskRunning = true;
 46             runtime.setTimeout(function () {
 47                 try {
 48                     task();
 49                 } catch (/**@type{Error}*/e) {
 50                     runtime.log(String(e) + "\n" + e.stack);
 51                 }
 52                 taskRunning = false;
 53                 if (queue.length > 0) {
 54                     run(queue.pop());
 55                 }
 56             }, 10);
 57         }
 58         /**
 59          * @return {undefined}
 60          */
 61         this.clearQueue = function () {
 62             queue.length = 0;
 63         };
 64         /**
 65          * @param {!Function} loadingTask
 66          * @return {undefined}
 67          */
 68         this.addToQueue = function (loadingTask) {
 69             if (queue.length === 0 && !taskRunning) {
 70                 return run(loadingTask);
 71             }
 72             queue.push(loadingTask);
 73         };
 74     }
 75     /**
 76      * @constructor
 77      * @implements {core.Destroyable}
 78      * @param {!HTMLStyleElement} css
 79      */
 80     function PageSwitcher(css) {
 81         var sheet = /**@type{!CSSStyleSheet}*/(css.sheet),
 82             /**@type{number}*/
 83             position = 1;
 84         /**
 85          * @return {undefined}
 86          */
 87         function updateCSS() {
 88             while (sheet.cssRules.length > 0) {
 89                 sheet.deleteRule(0);
 90             }
 91             // The #shadowContent contains the master pages, with each page in the slideshow
 92             // corresponding to a master page in #shadowContent, and in the same order.
 93             // So, when showing a page, also make it's master page (behind it) visible.
 94             sheet.insertRule('#shadowContent draw|page {display:none;}', 0);
 95             sheet.insertRule('office|presentation draw|page {display:none;}', 1);
 96             sheet.insertRule("#shadowContent draw|page:nth-of-type(" +
 97                 position + ") {display:block;}", 2);
 98             sheet.insertRule("office|presentation draw|page:nth-of-type(" +
 99                 position + ") {display:block;}", 3);
100         }
101         /**
102          * @return {undefined}
103          */
104         this.showFirstPage = function () {
105             position = 1;
106             updateCSS();
107         };
108         /**
109          * @return {undefined}
110          */
111         this.showNextPage = function () {
112             position += 1;
113             updateCSS();
114         };
115         /**
116          * @return {undefined}
117          */
118         this.showPreviousPage = function () {
119             if (position > 1) {
120                 position -= 1;
121                 updateCSS();
122             }
123         };
124 
125         /**
126          * @param {!number} n  number of the page
127          * @return {undefined}
128          */
129         this.showPage = function (n) {
130             if (n > 0) {
131                 position = n;
132                 updateCSS();
133             }
134         };
135 
136         this.css = css;
137 
138         /**
139          * @param {!function(!Error=)} callback, passing an error object in case of error
140          * @return {undefined}
141          */
142         this.destroy = function (callback) {
143             css.parentNode.removeChild(css);
144             callback();
145         };
146     }
147     /**
148      * Register event listener on DOM element.
149      * @param {!Element} eventTarget
150      * @param {!string} eventType
151      * @param {!Function} eventHandler
152      * @return {undefined}
153      */
154     function listenEvent(eventTarget, eventType, eventHandler) {
155         if (eventTarget.addEventListener) {
156             eventTarget.addEventListener(eventType, eventHandler, false);
157         } else if (eventTarget.attachEvent) {
158             eventType = "on" + eventType;
159             eventTarget.attachEvent(eventType, eventHandler);
160         } else {
161             eventTarget["on" + eventType] = eventHandler;
162         }
163     }
164 
165     // variables per class (so not per instance!)
166     var /**@const@type {!string}*/drawns  = odf.Namespaces.drawns,
167         /**@const@type {!string}*/fons    = odf.Namespaces.fons,
168         /**@const@type {!string}*/officens = odf.Namespaces.officens,
169         /**@const@type {!string}*/stylens = odf.Namespaces.stylens,
170         /**@const@type {!string}*/svgns   = odf.Namespaces.svgns,
171         /**@const@type {!string}*/tablens = odf.Namespaces.tablens,
172         /**@const@type {!string}*/textns  = odf.Namespaces.textns,
173         /**@const@type {!string}*/xlinkns = odf.Namespaces.xlinkns,
174         /**@const@type {!string}*/presentationns = odf.Namespaces.presentationns,
175         /**@const@type {!string}*/webodfhelperns = "urn:webodf:names:helper",
176         xpath = xmldom.XPath,
177         domUtils = core.DomUtils;
178 
179     /**
180      * @param {!Element} element
181      * @return {undefined}
182      */
183     function clear(element) {
184         while (element.firstChild) {
185             element.removeChild(element.firstChild);
186         }
187     }
188     /**
189      * @param {!HTMLStyleElement} style
190      * @return {undefined}
191      */
192     function clearCSSStyleSheet(style) {
193         var stylesheet = /**@type{!CSSStyleSheet}*/(style.sheet),
194             cssRules = stylesheet.cssRules;
195 
196         while (cssRules.length) {
197             stylesheet.deleteRule(cssRules.length - 1);
198         }
199     }
200 
201     /**
202      * A new styles.xml has been loaded. Update the live document with it.
203      * @param {!odf.OdfContainer} odfcontainer
204      * @param {!odf.Formatting} formatting
205      * @param {!HTMLStyleElement} stylesxmlcss
206      * @return {undefined}
207      **/
208     function handleStyles(odfcontainer, formatting, stylesxmlcss) {
209         // update the css translation of the styles
210         var style2css = new odf.Style2CSS(),
211             list2css = new odf.ListStyleToCss(),
212             styleSheet = /**@type{!CSSStyleSheet}*/(stylesxmlcss.sheet),
213             styleTree = new odf.StyleTree(
214                 odfcontainer.rootElement.styles,
215                 odfcontainer.rootElement.automaticStyles).getStyleTree();
216 
217         style2css.style2css(
218             odfcontainer.getDocumentType(),
219             odfcontainer.rootElement,
220             styleSheet,
221             formatting.getFontMap(),
222             styleTree
223         );
224 
225         list2css.applyListStyles(
226             styleSheet,
227             styleTree,
228             odfcontainer.rootElement.body);
229 
230     }
231 
232     /**
233      * @param {!odf.OdfContainer} odfContainer
234      * @param {!HTMLStyleElement} fontcss
235      * @return {undefined}
236      **/
237     function handleFonts(odfContainer, fontcss) {
238         // update the css references to the fonts
239         var fontLoader = new odf.FontLoader();
240         fontLoader.loadFonts(odfContainer,
241             /**@type{!CSSStyleSheet}*/(fontcss.sheet));
242     }
243 
244     /**
245      * @param {!Element} clonedNode <draw:page/>
246      * @return {undefined}
247      */
248     function dropTemplateDrawFrames(clonedNode) {
249         // drop all frames which are just template frames
250         var i, element, presentationClass,
251             clonedDrawFrameElements = domUtils.getElementsByTagNameNS(clonedNode, drawns, 'frame');
252         for (i = 0; i < clonedDrawFrameElements.length; i += 1) {
253             element = /**@type{!Element}*/(clonedDrawFrameElements[i]);
254             presentationClass = element.getAttributeNS(presentationns, 'class');
255             if (presentationClass && ! /^(date-time|footer|header|page-number)$/.test(presentationClass)) {
256                 element.parentNode.removeChild(element);
257             }
258         }
259     }
260 
261     /**
262      * @param {!odf.OdfContainer} odfContainer
263      * @param {!Element} frame
264      * @param {!string} headerFooterId
265      * @return {?string}
266      */
267     function getHeaderFooter(odfContainer, frame, headerFooterId) {
268         var headerFooter = null,
269             i,
270             declElements = odfContainer.rootElement.body.getElementsByTagNameNS(presentationns, headerFooterId+'-decl'),
271             headerFooterName = frame.getAttributeNS(presentationns, 'use-'+headerFooterId+'-name'),
272             element;
273 
274         if (headerFooterName && declElements.length > 0) {
275             for (i = 0; i < declElements.length; i += 1) {
276                 element = /**@type{!Element}*/(declElements[i]);
277                 if (element.getAttributeNS(presentationns, 'name') === headerFooterName) {
278                     headerFooter = element.textContent;
279                     break;
280                 }
281             }
282         }
283         return headerFooter;
284     }
285 
286     /**
287      * @param {!Element} rootElement
288      * @param {string} ns
289      * @param {string} localName
290      * @param {?string} value
291      * @return {undefined}
292      */
293     function setContainerValue(rootElement, ns, localName, value) {
294         var i, containerList,
295             document = rootElement.ownerDocument,
296             e;
297 
298         containerList = domUtils.getElementsByTagNameNS(rootElement, ns, localName);
299         for (i = 0; i < containerList.length; i += 1) {
300             clear(containerList[i]);
301             if (value) {
302                 e = /**@type{!Element}*/(containerList[i]);
303                 e.appendChild(document.createTextNode(value));
304             }
305         }
306     }
307 
308     /**
309      * @param {string} styleid
310      * @param {!Element} frame
311      * @param {!CSSStyleSheet} stylesheet
312      * @return {undefined}
313      **/
314     function setDrawElementPosition(styleid, frame, stylesheet) {
315         frame.setAttributeNS(webodfhelperns, 'styleid', styleid);
316         var rule,
317             anchor = frame.getAttributeNS(textns, 'anchor-type'),
318             x = frame.getAttributeNS(svgns, 'x'),
319             y = frame.getAttributeNS(svgns, 'y'),
320             width = frame.getAttributeNS(svgns, 'width'),
321             height = frame.getAttributeNS(svgns, 'height'),
322             minheight = frame.getAttributeNS(fons, 'min-height'),
323             minwidth = frame.getAttributeNS(fons, 'min-width');
324 
325         if (anchor === "as-char") {
326             rule = 'display: inline-block;';
327         } else if (anchor || x || y) {
328             rule = 'position: absolute;';
329         } else if (width || height || minheight || minwidth) {
330             rule = 'display: block;';
331         }
332         if (x) {
333             rule += 'left: ' + x + ';';
334         }
335         if (y) {
336             rule += 'top: ' + y + ';';
337         }
338         if (width) {
339             rule += 'width: ' + width + ';';
340         }
341         if (height) {
342             rule += 'height: ' + height + ';';
343         }
344         if (minheight) {
345             rule += 'min-height: ' + minheight + ';';
346         }
347         if (minwidth) {
348             rule += 'min-width: ' + minwidth + ';';
349         }
350         if (rule) {
351             rule = 'draw|' + frame.localName + '[webodfhelper|styleid="' + styleid + '"] {' +
352                 rule + '}';
353             stylesheet.insertRule(rule, stylesheet.cssRules.length);
354         }
355     }
356     /**
357      * @param {!Element} image
358      * @return {string}
359      **/
360     function getUrlFromBinaryDataElement(image) {
361         var node = image.firstChild;
362         while (node) {
363             if (node.namespaceURI === officens &&
364                     node.localName === "binary-data") {
365                 // TODO: detect mime-type, assuming png for now
366                 // the base64 data can be  pretty printed, hence we need remove all the line breaks and whitespaces
367                 return "data:image/png;base64," + node.textContent.replace(/[\r\n\s]/g, '');
368             }
369             node = node.nextSibling;
370         }
371         return "";
372     }
373     /**
374      * @param {string} id
375      * @param {!odf.OdfContainer} container
376      * @param {!Element} image
377      * @param {!CSSStyleSheet} stylesheet
378      * @return {undefined}
379      **/
380     function setImage(id, container, image, stylesheet) {
381         image.setAttributeNS(webodfhelperns, 'styleid', id);
382         var url = image.getAttributeNS(xlinkns, 'href'),
383             /**@type{!odf.OdfPart}*/
384             part;
385         /**
386          * @param {?string} url
387          */
388         function callback(url) {
389             var rule;
390             if (url) { // if part cannot be loaded, url is null
391                 rule = "background-image: url(" + url + ");";
392                 rule = 'draw|image[webodfhelper|styleid="' + id + '"] {' + rule + '}';
393                 stylesheet.insertRule(rule, stylesheet.cssRules.length);
394             }
395         }
396         /**
397          * @param {!odf.OdfPart} p
398          */
399         function onchange(p) {
400             callback(p.url);
401         }
402         // look for a office:binary-data
403         if (url) {
404             try {
405                 part = container.getPart(url);
406                 part.onchange = onchange;
407                 part.load();
408             } catch (/**@type{*}*/e) {
409                 runtime.log('slight problem: ' + String(e));
410             }
411         } else {
412             url = getUrlFromBinaryDataElement(image);
413             callback(url);
414         }
415     }
416     /**
417      * @param {!Element} odfbody
418      * @return {undefined}
419      */
420     function formatParagraphAnchors(odfbody) {
421         var n,
422             i,
423             nodes = xpath.getODFElementsWithXPath(odfbody,
424                 ".//*[*[@text:anchor-type='paragraph']]",
425                 odf.Namespaces.lookupNamespaceURI);
426         for (i = 0; i < nodes.length; i += 1) {
427             n = nodes[i];
428             if (n.setAttributeNS) {
429                 n.setAttributeNS(webodfhelperns, "containsparagraphanchor", true);
430             }
431         }
432     }
433     /**
434      * Modify tables to support merged cells (col/row span)
435      * @param {!Element} odffragment
436      * @param {!string} documentns
437      * @return {undefined}
438      */
439     function modifyTables(odffragment, documentns) {
440         var i,
441             tableCells,
442             node;
443 
444         /**
445          * @param {!Element} node
446          * @return {undefined}
447          */
448         function modifyTableCell(node) {
449             // If we have a cell which spans columns or rows,
450             // then add col-span or row-span attributes.
451             if (node.hasAttributeNS(tablens, "number-columns-spanned")) {
452                 node.setAttributeNS(documentns, "colspan",
453                     node.getAttributeNS(tablens, "number-columns-spanned"));
454             }
455             if (node.hasAttributeNS(tablens, "number-rows-spanned")) {
456                 node.setAttributeNS(documentns, "rowspan",
457                     node.getAttributeNS(tablens, "number-rows-spanned"));
458             }
459         }
460         tableCells = domUtils.getElementsByTagNameNS(odffragment, tablens, 'table-cell');
461         for (i = 0; i < tableCells.length; i += 1) {
462             node = /**@type{!Element}*/(tableCells[i]);
463             modifyTableCell(node);
464         }
465     }
466 
467     /**
468      * Make the text:line-break elements behave like html br element.
469      * @param {!Element} odffragment
470      * @return {undefined}
471      */
472     function modifyLineBreakElements(odffragment) {
473         var document = odffragment.ownerDocument,
474             lineBreakElements = domUtils.getElementsByTagNameNS(odffragment, textns, "line-break");
475         lineBreakElements.forEach(function (lineBreak) {
476             // Make sure we don't add br more than once as this method is executed whenever user undo an operation.
477             if (!lineBreak.hasChildNodes()) {
478                 lineBreak.appendChild(document.createElement("br"));
479             }
480         });
481     }
482 
483     /**
484      * Expand ODF spaces of the form <text:s text:c=N/> to N consecutive
485      * <text:s/> elements. This makes things simpler for WebODF during
486      * handling of spaces, in particular during editing.
487      * @param {!Element} odffragment
488      * @return {undefined}
489      */
490     function expandSpaceElements(odffragment) {
491         var spaces,
492             doc = odffragment.ownerDocument;
493 
494         /**
495          * @param {!Element} space
496          * @return {undefined}
497          */
498         function expandSpaceElement(space) {
499             var j, count;
500             // If the space has any children, remove them and put a " " text
501             // node in place.
502             while (space.firstChild) {
503                 space.removeChild(space.firstChild);
504             }
505             space.appendChild(doc.createTextNode(" "));
506 
507             count = parseInt(space.getAttributeNS(textns, "c"), 10);
508             if (count > 1) {
509                 // Make it a 'simple' space node
510                 space.removeAttributeNS(textns, "c");
511                 // Prepend count-1 clones of this space node to itself
512                 for (j = 1; j < count; j += 1) {
513                     space.parentNode.insertBefore(space.cloneNode(true), space);
514                 }
515             }
516         }
517 
518         spaces = domUtils.getElementsByTagNameNS(odffragment, textns, "s");
519         spaces.forEach(expandSpaceElement);
520     }
521 
522     /**
523      * Expand tabs to contain tab characters. This eases cursor behaviour
524      * during editing
525      * @param {!Element} odffragment
526      */
527     function expandTabElements(odffragment) {
528         var tabs;
529 
530         tabs = domUtils.getElementsByTagNameNS(odffragment, textns, "tab");
531         tabs.forEach(function(tab) {
532             tab.textContent = "\t";
533         });
534     }
535     /**
536      * @param {!Element} odfbody
537      * @param {!CSSStyleSheet} stylesheet
538      * @return {undefined}
539      **/
540     function modifyDrawElements(odfbody, stylesheet) {
541         var node,
542             /**@type{!Array.<!Element>}*/
543             drawElements = [],
544             i;
545         // find all the draw:* elements
546         node = odfbody.firstElementChild;
547         while (node && node !== odfbody) {
548             if (node.namespaceURI === drawns) {
549                 drawElements[drawElements.length] = node;
550             }
551             if (node.firstElementChild) {
552                 node = node.firstElementChild;
553             } else {
554                 while (node && node !== odfbody && !node.nextElementSibling) {
555                     node = /**@type{!Element}*/(node.parentNode);
556                 }
557                 if (node && node.nextElementSibling) {
558                     node = node.nextElementSibling;
559                 }
560             }
561         }
562         // adjust all the frame positions
563         for (i = 0; i < drawElements.length; i += 1) {
564             node = drawElements[i];
565             setDrawElementPosition('frame' + String(i), node, stylesheet);
566         }
567         formatParagraphAnchors(odfbody);
568     }
569 
570     /**
571      * @param {!odf.Formatting} formatting
572      * @param {!odf.OdfContainer} odfContainer
573      * @param {!Element} shadowContent
574      * @param {!Element} odfbody
575      * @param {!CSSStyleSheet} stylesheet
576      * @return {undefined}
577      **/
578     function cloneMasterPages(formatting, odfContainer, shadowContent, odfbody, stylesheet) {
579         var masterPageName,
580             masterPageElement,
581             styleId,
582             clonedPageElement,
583             clonedElement,
584             clonedDrawElements,
585             pageNumber = 0,
586             i,
587             element,
588             elementToClone,
589             document = odfContainer.rootElement.ownerDocument;
590 
591         element = odfbody.firstElementChild;
592         // no master pages to expect?
593         if (!(element && element.namespaceURI === officens &&
594               (element.localName === "presentation" || element.localName === "drawing"))) {
595             return;
596         }
597 
598         element = element.firstElementChild;
599         while (element) {
600             // If there was a master-page-name attribute, then we are dealing with a draw:page.
601             // Get the referenced master page element from the master styles
602             masterPageName = element.getAttributeNS(drawns, 'master-page-name');
603             masterPageElement = masterPageName ? formatting.getMasterPageElement(masterPageName) : null;
604 
605             // If the referenced master page exists, create a new page and copy over it's contents into the new page,
606             // except for the ones that are placeholders. Also, call setDrawElementPosition on each of those child frames.
607             if (masterPageElement) {
608                 styleId = element.getAttributeNS(webodfhelperns, 'styleid');
609                 clonedPageElement = document.createElementNS(drawns, 'draw:page');
610 
611                 elementToClone = masterPageElement.firstElementChild;
612                 i = 0;
613                 while (elementToClone) {
614                     if (elementToClone.getAttributeNS(presentationns, 'placeholder') !== 'true') {
615                         clonedElement = /**@type{!Element}*/(elementToClone.cloneNode(true));
616                         clonedPageElement.appendChild(clonedElement);
617                     }
618                     elementToClone = elementToClone.nextElementSibling;
619                     i += 1;
620                 }
621                 // TODO: above already do not clone nodes which match the rule for being dropped
622                 dropTemplateDrawFrames(clonedPageElement);
623 
624                 // Position all elements
625                 clonedDrawElements = domUtils.getElementsByTagNameNS(clonedPageElement, drawns, '*');
626                 for (i = 0; i < clonedDrawElements.length; i += 1) {
627                     setDrawElementPosition(styleId + '_' + i, clonedDrawElements[i], stylesheet);
628                 }
629 
630                 // Append the cloned master page to the "Shadow Content" element outside the main ODF dom
631                 shadowContent.appendChild(clonedPageElement);
632 
633                 // Get the page number by counting the number of previous master pages in this shadowContent
634                 pageNumber = String(shadowContent.getElementsByTagNameNS(drawns, 'page').length);
635                 // Get the page-number tag in the cloned master page and set the text content to the calculated number
636                 setContainerValue(clonedPageElement, textns, 'page-number', pageNumber);
637 
638                 // Care for header
639                 setContainerValue(clonedPageElement, presentationns, 'header', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'header'));
640                 // Care for footer
641                 setContainerValue(clonedPageElement, presentationns, 'footer', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'footer'));
642 
643                 // Now call setDrawElementPosition on this new page to set the proper dimensions
644                 setDrawElementPosition(styleId, clonedPageElement, stylesheet);
645                 // Add a custom attribute with the style name of the normal page, so the CSS rules created for the styles of the normal page
646                 // to display/hide frames of certain classes from the master page can address the cloned master page belonging to that normal page
647                 // Cmp. addDrawPageFrameDisplayRules in Style2CSS
648                 clonedPageElement.setAttributeNS(webodfhelperns, 'page-style-name', element.getAttributeNS(drawns, 'style-name'));
649                 // TODO: investigate if the attributes draw:style-name and style:page-layoutname should be copied over
650                 // to the cloned page from the master page as well, or if this one below is enough already
651                 // And finally, add an attribute referring to the master page, so the CSS targeted for that master page will style this
652                 clonedPageElement.setAttributeNS(drawns, 'draw:master-page-name', masterPageElement.getAttributeNS(stylens, 'name'));
653             }
654 
655             element = element.nextElementSibling;
656         }
657     }
658 
659     /**
660      * @param {!odf.OdfContainer} container
661      * @param {!Element} plugin
662      * @return {undefined}
663      **/
664     function setVideo(container, plugin) {
665         var video, source, url, doc = plugin.ownerDocument,
666             /**@type{!odf.OdfPart}*/
667             part;
668 
669         url = plugin.getAttributeNS(xlinkns, 'href');
670 
671         /**
672          * @param {?string} url
673          * @param {string} mimetype
674          * @return {undefined}
675          */
676         function callback(url, mimetype) {
677             var ns = doc.documentElement.namespaceURI;
678             // test for video mimetypes
679             if (mimetype.substr(0, 6) === 'video/') {
680                 video = doc.createElementNS(ns, "video");
681                 video.setAttribute('controls', 'controls');
682 
683                 source = doc.createElementNS(ns, 'source');
684                 if (url) {
685                     source.setAttribute('src', url);
686                 }
687                 source.setAttribute('type', mimetype);
688 
689                 video.appendChild(source);
690                 plugin.parentNode.appendChild(video);
691             } else {
692                 plugin.innerHtml = 'Unrecognised Plugin';
693             }
694         }
695         /**
696          * @param {!odf.OdfPart} p
697          */
698         function onchange(p) {
699             callback(p.url, p.mimetype);
700         }
701         // look for a office:binary-data
702         if (url) {
703             try {
704                 part = container.getPart(url);
705                 part.onchange = onchange;
706                 part.load();
707             } catch (/**@type{*}*/e) {
708                 runtime.log('slight problem: ' + String(e));
709             }
710         } else {
711         // this will fail  atm - following function assumes PNG data]
712             runtime.log('using MP4 data fallback');
713             url = getUrlFromBinaryDataElement(plugin);
714             callback(url, 'video/mp4');
715         }
716     }
717 
718     /**
719      * @param {!HTMLHeadElement} head
720      * @return {?HTMLStyleElement}
721      */
722     function findWebODFStyleSheet(head) {
723         var style = head.firstElementChild;
724         while (style && !(style.localName === "style"
725                 && style.hasAttribute("webodfcss"))) {
726             style = style.nextElementSibling;
727         }
728         return /**@type{?HTMLStyleElement}*/(style);
729     }
730 
731     /**
732      * @param {!Document} document
733      * @return {!HTMLStyleElement}
734      */
735     function addWebODFStyleSheet(document) {
736         var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
737             css,
738             /**@type{?HTMLStyleElement}*/
739             style,
740             href,
741             count = document.styleSheets.length;
742         // make sure this is only added once per HTML document, e.g. in case of
743         // multiple odfCanvases
744         style = findWebODFStyleSheet(head);
745         if (style) {
746             count = parseInt(style.getAttribute("webodfcss"), 10);
747             style.setAttribute("webodfcss", count + 1);
748             return style;
749         }
750         if (String(typeof webodf_css) === "string") {
751             css = /**@type{!string}*/(webodf_css);
752         } else {
753             href = "webodf.css";
754             if (runtime.currentDirectory) {
755                 href = runtime.currentDirectory();
756                 if (href.length > 0 && href.substr(-1) !== "/") {
757                     href += "/";
758                 }
759                 href += "../webodf.css";
760             }
761             css = /**@type{!string}*/(runtime.readFileSync(href, "utf-8"));
762         }
763         style = /**@type{!HTMLStyleElement}*/(document.createElementNS(head.namespaceURI, 'style'));
764         style.setAttribute('media', 'screen, print, handheld, projection');
765         style.setAttribute('type', 'text/css');
766         style.setAttribute('webodfcss', '1');
767         style.appendChild(document.createTextNode(css));
768         head.appendChild(style);
769         return style;
770     }
771 
772     /**
773      * @param {!HTMLStyleElement} webodfcss
774      * @return {undefined}
775      */
776     function removeWebODFStyleSheet(webodfcss) {
777         var count = parseInt(webodfcss.getAttribute("webodfcss"), 10);
778         if (count === 1) {
779              webodfcss.parentNode.removeChild(webodfcss);
780         } else {
781              webodfcss.setAttribute("count", count - 1);
782         }
783     }
784 
785     /**
786      * @param {!Document} document Put and ODF Canvas inside this element.
787      * @return {!HTMLStyleElement}
788      */
789     function addStyleSheet(document) {
790         var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
791             style = document.createElementNS(head.namespaceURI, 'style'),
792             /**@type{string}*/
793             text = '';
794         style.setAttribute('type', 'text/css');
795         style.setAttribute('media', 'screen, print, handheld, projection');
796         odf.Namespaces.forEachPrefix(function(prefix, ns) {
797             text += "@namespace " + prefix + " url(" + ns + ");\n";
798         });
799         text += "@namespace webodfhelper url(" + webodfhelperns + ");\n";
800         style.appendChild(document.createTextNode(text));
801         head.appendChild(style);
802         return /**@type {!HTMLStyleElement}*/(style);
803     }
804     /**
805      * This class manages a loaded ODF document that is shown in an element.
806      * It takes care of giving visual feedback on loading, ensures that the
807      * stylesheets are loaded.
808      * @constructor
809      * @implements {gui.AnnotatableCanvas}
810      * @implements {ops.Canvas}
811      * @implements {core.Destroyable}
812      * @param {!HTMLElement} element Put and ODF Canvas inside this element.
813      * @param {!gui.Viewport=} viewport Viewport used for scrolling elements and ranges into view
814      */
815     odf.OdfCanvas = function OdfCanvas(element, viewport) {
816         runtime.assert((element !== null) && (element !== undefined),
817             "odf.OdfCanvas constructor needs DOM element");
818         runtime.assert((element.ownerDocument !== null) && (element.ownerDocument !== undefined),
819             "odf.OdfCanvas constructor needs DOM");
820         var self = this,
821             doc = /**@type{!Document}*/(element.ownerDocument),
822             /**@type{!odf.OdfContainer}*/
823             odfcontainer,
824             /**@type{!odf.Formatting}*/
825             formatting = new odf.Formatting(),
826             /**@type{!PageSwitcher}*/
827             pageSwitcher,
828             /**@type{HTMLDivElement}*/
829             sizer = null,
830             /**@type{HTMLDivElement}*/
831             annotationsPane = null,
832             allowAnnotations = false,
833             showAnnotationRemoveButton = false,
834             /**@type{gui.AnnotationViewManager}*/
835             annotationViewManager = null,
836             /**@type{!HTMLStyleElement}*/
837             webodfcss,
838             /**@type{!HTMLStyleElement}*/
839             fontcss,
840             /**@type{!HTMLStyleElement}*/
841             stylesxmlcss,
842             /**@type{!HTMLStyleElement}*/
843             positioncss,
844             shadowContent,
845             /**@type{!Object.<string,!Array.<!Function>>}*/
846             eventHandlers = {},
847             waitingForDoneTimeoutId,
848             /**@type{!core.ScheduledTask}*/redrawContainerTask,
849             shouldRefreshCss = false,
850             shouldRerenderAnnotations = false,
851             loadingQueue = new LoadingQueue(),
852             /**@type{!gui.ZoomHelper}*/
853             zoomHelper = new gui.ZoomHelper(),
854             /**@type{!gui.Viewport}*/
855             canvasViewport = viewport || new gui.SingleScrollViewport(/**@type{!HTMLElement}*/(element.parentNode));
856 
857         /**
858          * Load all the images that are inside an odf element.
859          * @param {!odf.OdfContainer} container
860          * @param {!Element} odffragment
861          * @param {!CSSStyleSheet} stylesheet
862          * @return {undefined}
863          */
864         function loadImages(container, odffragment, stylesheet) {
865             var i,
866                 images,
867                 node;
868             /**
869              * Do delayed loading for all the images
870              * @param {string} name
871              * @param {!odf.OdfContainer} container
872              * @param {!Element} node
873              * @param {!CSSStyleSheet} stylesheet
874              * @return {undefined}
875              */
876             function loadImage(name, container, node, stylesheet) {
877                 // load image with a small delay to give the html ui a chance to
878                 // update
879                 loadingQueue.addToQueue(function () {
880                     setImage(name, container, node, stylesheet);
881                 });
882             }
883             images = odffragment.getElementsByTagNameNS(drawns, 'image');
884             for (i = 0; i < images.length; i += 1) {
885                 node = /**@type{!Element}*/(images.item(i));
886                 loadImage('image' + String(i), container, node, stylesheet);
887             }
888         }
889         /**
890          * Load all the video that are inside an odf element.
891          * @param {!odf.OdfContainer} container
892          * @param {!Element} odffragment
893          * @return {undefined}
894          */
895         function loadVideos(container, odffragment) {
896             var i,
897                 plugins,
898                 node;
899             /**
900              * Do delayed loading for all the videos
901              * @param {!odf.OdfContainer} container
902              * @param {!Element} node
903              * @return {undefined}
904              */
905             function loadVideo(container, node) {
906                 // load video with a small delay to give the html ui a chance to
907                 // update
908                 loadingQueue.addToQueue(function () {
909                     setVideo(container, node);
910                 });
911             }
912             // embedded video is stored in a draw:plugin element
913             plugins = odffragment.getElementsByTagNameNS(drawns, 'plugin');
914             for (i = 0; i < plugins.length; i += 1) {
915                 node = /**@type{!Element}*/(plugins.item(i));
916                 loadVideo(container, node);
917             }
918         }
919 
920         /**
921          * Register an event handler
922          * @param {!string} eventType
923          * @param {!Function} eventHandler
924          * @return {undefined}
925          */
926         function addEventListener(eventType, eventHandler) {
927             var handlers;
928             if (eventHandlers.hasOwnProperty(eventType)) {
929                 handlers = eventHandlers[eventType];
930             } else {
931                 handlers = eventHandlers[eventType] = [];
932             }
933             if (eventHandler && handlers.indexOf(eventHandler) === -1) {
934                 handlers.push(eventHandler);
935             }
936         }
937         /**
938          * Fire an event
939          * @param {!string} eventType
940          * @param {Array.<Object>=} args
941          * @return {undefined}
942          */
943         function fireEvent(eventType, args) {
944             if (!eventHandlers.hasOwnProperty(eventType)) {
945                 return;
946             }
947             var handlers = eventHandlers[eventType], i;
948             for (i = 0; i < handlers.length; i += 1) {
949                 handlers[i].apply(null, args);
950             }
951         }
952 
953         /**
954          * @return {undefined}
955          */
956         function fixContainerSize() {
957             var minHeight,
958                 odfdoc = sizer.firstChild,
959                 zoomLevel = zoomHelper.getZoomLevel();
960 
961             if (!odfdoc) {
962                 return;
963             }
964 
965             // All zooming of the sizer within the canvas
966             // is done relative to the top-left corner.
967             sizer.style.WebkitTransformOrigin = "0% 0%";
968             sizer.style.MozTransformOrigin = "0% 0%";
969             sizer.style.msTransformOrigin = "0% 0%";
970             sizer.style.OTransformOrigin = "0% 0%";
971             sizer.style.transformOrigin = "0% 0%";
972 
973             if (annotationViewManager) {
974                 minHeight = annotationViewManager.getMinimumHeightForAnnotationPane();
975                 if (minHeight) {
976                     sizer.style.minHeight = minHeight;
977                 } else {
978                     sizer.style.removeProperty('min-height');
979                 }
980             }
981 
982             element.style.width = Math.round(zoomLevel * sizer.offsetWidth) + "px";
983             element.style.height = Math.round(zoomLevel * sizer.offsetHeight) + "px";
984             // Re-apply inline-block to canvas element on resizing.
985             // Chrome tends to forget this property after a relayout
986             element.style.display = "inline-block";
987         }
988 
989         /**
990          * @return {undefined}
991          */
992         function redrawContainer() {
993             if (shouldRefreshCss) {
994                 handleStyles(odfcontainer, formatting, stylesxmlcss);
995                 shouldRefreshCss = false;
996                 // different styles means different layout, thus different sizes
997             }
998             if (shouldRerenderAnnotations) {
999                 if (annotationViewManager) {
1000                     annotationViewManager.rerenderAnnotations();
1001                 }
1002                 shouldRerenderAnnotations = false;
1003             }
1004             fixContainerSize();
1005         }
1006 
1007         /**
1008          * A new content.xml has been loaded. Update the live document with it.
1009          * @param {!odf.OdfContainer} container
1010          * @param {!odf.ODFDocumentElement} odfnode
1011          * @return {undefined}
1012          **/
1013         function handleContent(container, odfnode) {
1014             var css = /**@type{!CSSStyleSheet}*/(positioncss.sheet);
1015             // only append the content at the end
1016             clear(element);
1017 
1018             sizer = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
1019             sizer.style.display = "inline-block";
1020             sizer.style.background = "white";
1021             // When the window is shrunk such that the
1022             // canvas container has a horizontal scrollbar,
1023             // zooming out seems to not make the scrollable
1024             // width disappear. This extra scrollable
1025             // width seems to be proportional to the
1026             // annotation pane's width. Setting the 'float'
1027             // of the sizer to 'left' fixes this in webkit.
1028             sizer.style.setProperty("float", "left", "important");
1029             sizer.appendChild(odfnode);
1030             element.appendChild(sizer);
1031 
1032             // An annotations pane div. Will only be shown when annotations are enabled
1033             annotationsPane = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
1034             annotationsPane.id = "annotationsPane";
1035             // A "Shadow Content" div. This will contain stuff like pages
1036             // extracted from <style:master-page>. These need to be nicely
1037             // styled, so we will populate this in the ODF body first. Once the
1038             // styling is handled, it can then be lifted out of the
1039             // ODF body and placed beside it, to not pollute the ODF dom.
1040             shadowContent = doc.createElementNS(element.namespaceURI, 'div');
1041             shadowContent.id = "shadowContent";
1042             shadowContent.style.position = 'absolute';
1043             shadowContent.style.top = 0;
1044             shadowContent.style.left = 0;
1045             container.getContentElement().appendChild(shadowContent);
1046 
1047             modifyDrawElements(odfnode.body, css);
1048             cloneMasterPages(formatting, container, shadowContent, odfnode.body, css);
1049             modifyTables(odfnode.body, element.namespaceURI);
1050             modifyLineBreakElements(odfnode.body);
1051             expandSpaceElements(odfnode.body);
1052             expandTabElements(odfnode.body);
1053             loadImages(container, odfnode.body, css);
1054             loadVideos(container, odfnode.body);
1055 
1056             sizer.insertBefore(shadowContent, sizer.firstChild);
1057             zoomHelper.setZoomableElement(sizer);
1058         }
1059 
1060         /**
1061         * Wraps all annotations and renders them using the Annotation View Manager.
1062         * @param {!Element} odffragment
1063         * @return {undefined}
1064         */
1065         function modifyAnnotations(odffragment) {
1066             var annotationNodes = /**@type{!Array.<!odf.AnnotationElement>}*/(domUtils.getElementsByTagNameNS(odffragment, officens, 'annotation'));
1067 
1068             annotationNodes.forEach(annotationViewManager.addAnnotation);
1069             annotationViewManager.rerenderAnnotations();
1070         }
1071 
1072         /**
1073          * This should create an annotations pane if non existent, and then populate it with annotations
1074          * If annotations are disallowed, it should remove the pane and all annotations
1075          * @param {!odf.ODFDocumentElement} odfnode
1076          */
1077         function handleAnnotations(odfnode) {
1078             if (allowAnnotations) {
1079                 if (!annotationsPane.parentNode) {
1080                     sizer.appendChild(annotationsPane);
1081                 }
1082                 if (annotationViewManager) {
1083                     annotationViewManager.forgetAnnotations();
1084                 }
1085                 annotationViewManager = new gui.AnnotationViewManager(self, odfnode.body, annotationsPane, showAnnotationRemoveButton);
1086                 modifyAnnotations(odfnode.body);
1087                 fixContainerSize();
1088             } else {
1089                 if (annotationsPane.parentNode) {
1090                     sizer.removeChild(annotationsPane);
1091                     annotationViewManager.forgetAnnotations();
1092                     fixContainerSize();
1093                 }
1094             }
1095         }
1096 
1097         /**
1098          * @param {boolean} suppressEvent Suppress the statereadychange event from firing. Used for refreshing the OdtContainer
1099          * @return {undefined}
1100          **/
1101         function refreshOdf(suppressEvent) {
1102 
1103             // synchronize the object a window.odfcontainer with the view
1104             function callback() {
1105                 // clean up
1106                 clearCSSStyleSheet(fontcss);
1107                 clearCSSStyleSheet(stylesxmlcss);
1108                 clearCSSStyleSheet(positioncss);
1109 
1110                 clear(element);
1111 
1112                 // setup
1113                 element.style.display = "inline-block";
1114                 var odfnode = odfcontainer.rootElement;
1115                 element.ownerDocument.importNode(odfnode, true);
1116 
1117                 formatting.setOdfContainer(odfcontainer);
1118                 handleFonts(odfcontainer, fontcss);
1119                 handleStyles(odfcontainer, formatting, stylesxmlcss);
1120                 // do content last, because otherwise the document is constantly
1121                 // updated whenever the css changes
1122                 handleContent(odfcontainer, odfnode);
1123                 handleAnnotations(odfnode);
1124 
1125                 if (!suppressEvent) {
1126                     loadingQueue.addToQueue(function () {
1127                         fireEvent("statereadychange", [odfcontainer]);
1128                     });
1129                 }
1130             }
1131 
1132             if (odfcontainer.state === odf.OdfContainer.DONE) {
1133                 callback();
1134             } else {
1135                 // so the ODF is not done yet. take care that we'll
1136                 // do the work once it is done:
1137 
1138                 // FIXME: use callback registry instead of replacing the onchange
1139                 runtime.log("WARNING: refreshOdf called but ODF was not DONE.");
1140 
1141                 waitingForDoneTimeoutId = runtime.setTimeout(function later_cb() {
1142                     if (odfcontainer.state === odf.OdfContainer.DONE) {
1143                         callback();
1144                     } else {
1145                         runtime.log("will be back later...");
1146                         waitingForDoneTimeoutId = runtime.setTimeout(later_cb, 500);
1147                     }
1148                 }, 100);
1149             }
1150         }
1151 
1152         /**
1153          * Updates the CSS rules to match the ODF document styles and also
1154          * updates the size of the canvas to match the new layout.
1155          * Needs to be called after changes to the styles of the ODF document.
1156          * @return {undefined}
1157          */
1158         this.refreshCSS = function () {
1159             shouldRefreshCss = true;
1160             redrawContainerTask.trigger();
1161         };
1162 
1163         /**
1164          * Updates the size of the canvas to the size of the content.
1165          * Needs to be called after changes to the content of the ODF document.
1166          * @return {undefined}
1167          */
1168         this.refreshSize = function () {
1169             redrawContainerTask.trigger();
1170         };
1171         /**
1172          * @return {!odf.OdfContainer}
1173          */
1174         this.odfContainer = function () {
1175             return odfcontainer;
1176         };
1177         /**
1178          * Set a odfcontainer manually.
1179          * @param {!odf.OdfContainer} container
1180          * @param {boolean=} suppressEvent Default value is false
1181          * @return {undefined}
1182          */
1183         this.setOdfContainer = function (container, suppressEvent) {
1184             odfcontainer = container;
1185             refreshOdf(suppressEvent === true);
1186         };
1187         /**
1188          * @param {string} url
1189          * @return {undefined}
1190          */
1191         function load(url) {
1192             // clean up
1193             loadingQueue.clearQueue();
1194 
1195             // FIXME: We need to support parametrized strings, because
1196             // drop-in word replacements are inadequate for translations;
1197             // see http://techbase.kde.org/Development/Tutorials/Localization/i18n_Mistakes#Pitfall_.232:_Word_Puzzles
1198             element.innerHTML = runtime.tr('Loading') + ' ' + url + '...';
1199             element.removeAttribute('style');
1200             // open the odf container
1201             odfcontainer = new odf.OdfContainer(url, function (container) {
1202                 // assignment might be necessary if the callback
1203                 // fires before the assignment above happens.
1204                 odfcontainer = container;
1205                 refreshOdf(false);
1206             });
1207         }
1208         this["load"] = load;
1209         this.load = load;
1210 
1211         /**
1212          * @param {function(?string):undefined} callback
1213          * @return {undefined}
1214          */
1215         this.save = function (callback) {
1216             odfcontainer.save(callback);
1217         };
1218 
1219         /**
1220          * @param {!string} eventName
1221          * @param {!function(*)} handler
1222          * @return {undefined}
1223          */
1224         this.addListener = function (eventName, handler) {
1225             switch (eventName) {
1226             case "click":
1227                 listenEvent(element, eventName, handler); break;
1228             default:
1229                 addEventListener(eventName, handler); break;
1230             }
1231         };
1232 
1233         /**
1234          * @return {!odf.Formatting}
1235          */
1236         this.getFormatting = function () {
1237             return formatting;
1238         };
1239 
1240         /**
1241          * @return {gui.AnnotationViewManager}
1242          */
1243         this.getAnnotationViewManager = function () {
1244             return annotationViewManager;
1245         };
1246 
1247         /**
1248          * Unstyles and untracks all annotations present in the document,
1249          * and then tracks them again with fresh rendering
1250          * @return {undefined}
1251          */
1252         this.refreshAnnotations = function () {
1253             handleAnnotations(odfcontainer.rootElement);
1254         };
1255 
1256         /**
1257          * Re-renders all annotations if enabled
1258          * @return {undefined}
1259          */
1260         this.rerenderAnnotations = function () {
1261             if (annotationViewManager) {
1262                 shouldRerenderAnnotations = true;
1263                 redrawContainerTask.trigger();
1264             }
1265         };
1266 
1267         /**
1268          * This returns the element inside the canvas which can be zoomed with
1269          * CSS and which contains the ODF document and the annotation sidebar.
1270          * @return {!HTMLElement}
1271          */
1272         this.getSizer = function () {
1273             return /**@type{!HTMLElement}*/(sizer);
1274         };
1275 
1276         /** Allows / disallows annotations
1277          * @param {!boolean} allow
1278          * @param {!boolean} showRemoveButton
1279          * @return {undefined}
1280          */
1281         this.enableAnnotations = function (allow, showRemoveButton) {
1282             if (allow !== allowAnnotations) {
1283                 allowAnnotations = allow;
1284                 showAnnotationRemoveButton = showRemoveButton;
1285                 if (odfcontainer) {
1286                     handleAnnotations(odfcontainer.rootElement);
1287                 }
1288             }
1289         };
1290 
1291         /**
1292          * Adds an annotation for the annotaiton manager to track
1293          * and wraps and highlights it
1294          * @param {!odf.AnnotationElement} annotation
1295          * @return {undefined}
1296          */
1297         this.addAnnotation = function (annotation) {
1298             if (annotationViewManager) {
1299                 annotationViewManager.addAnnotation(annotation);
1300                 fixContainerSize();
1301             }
1302         };
1303 
1304         /**
1305          * Stops annotations and unwraps it
1306          * @return {undefined}
1307          */
1308         this.forgetAnnotations = function () {
1309             if (annotationViewManager) {
1310                 annotationViewManager.forgetAnnotations();
1311                 fixContainerSize();
1312             }
1313         };
1314 
1315         /**
1316          * @return {!gui.ZoomHelper}
1317          */
1318         this.getZoomHelper = function () {
1319             return zoomHelper;
1320         };
1321 
1322         /**
1323          * @param {!number} zoom
1324          * @return {undefined}
1325          */
1326         this.setZoomLevel = function (zoom) {
1327             zoomHelper.setZoomLevel(zoom);
1328         };
1329         /**
1330          * @return {!number}
1331          */
1332         this.getZoomLevel = function () {
1333             return zoomHelper.getZoomLevel();
1334         };
1335         /**
1336          * @param {!number} width
1337          * @param {!number} height
1338          * @return {undefined}
1339          */
1340         this.fitToContainingElement = function (width, height) {
1341             var zoomLevel = zoomHelper.getZoomLevel(),
1342                 realWidth = element.offsetWidth / zoomLevel,
1343                 realHeight = element.offsetHeight / zoomLevel,
1344                 zoom;
1345 
1346             zoom = width / realWidth;
1347             if (height / realHeight < zoom) {
1348                 zoom = height / realHeight;
1349             }
1350             zoomHelper.setZoomLevel(zoom);
1351         };
1352         /**
1353          * @param {!number} width
1354          * @return {undefined}
1355          */
1356         this.fitToWidth = function (width) {
1357             var realWidth = element.offsetWidth / zoomHelper.getZoomLevel();
1358             zoomHelper.setZoomLevel(width / realWidth);
1359         };
1360         /**
1361          * @param {!number} width
1362          * @param {!number} height
1363          * @return {undefined}
1364          */
1365         this.fitSmart = function (width, height) {
1366             var realWidth, realHeight, newScale,
1367                 zoomLevel = zoomHelper.getZoomLevel();
1368 
1369             realWidth = element.offsetWidth / zoomLevel;
1370             realHeight = element.offsetHeight / zoomLevel;
1371 
1372             newScale = width / realWidth;
1373             if (height !== undefined) {
1374                 if (height / realHeight < newScale) {
1375                     newScale = height / realHeight;
1376                 }
1377             }
1378 
1379             zoomHelper.setZoomLevel(Math.min(1.0, newScale));
1380         };
1381         /**
1382          * @param {!number} height
1383          * @return {undefined}
1384          */
1385         this.fitToHeight = function (height) {
1386             var realHeight = element.offsetHeight / zoomHelper.getZoomLevel();
1387             zoomHelper.setZoomLevel(height / realHeight);
1388         };
1389         /**
1390          * @return {undefined}
1391          */
1392         this.showFirstPage = function () {
1393             pageSwitcher.showFirstPage();
1394         };
1395         /**
1396          * @return {undefined}
1397          */
1398         this.showNextPage = function () {
1399             pageSwitcher.showNextPage();
1400         };
1401         /**
1402          * @return {undefined}
1403          */
1404         this.showPreviousPage = function () {
1405             pageSwitcher.showPreviousPage();
1406         };
1407         /**
1408          * @param {!number} n  number of the page
1409          * @return {undefined}
1410          */
1411         this.showPage = function (n) {
1412             pageSwitcher.showPage(n);
1413             fixContainerSize();
1414         };
1415 
1416         /**
1417          * @return {!HTMLElement}
1418          */
1419         this.getElement = function () {
1420             return element;
1421         };
1422 
1423         /**
1424          * @return {!gui.Viewport}
1425          */
1426         this.getViewport = function () {
1427             return canvasViewport;
1428         };
1429 
1430         /**
1431          * Add additional css rules for newly inserted draw:frame and draw:image. eg. position, dimensions and background image
1432          * @param {!Element} frame
1433          */
1434         this.addCssForFrameWithImage = function (frame) {
1435             // TODO: frameid and imageid generation here is better brought in sync with that for the images on loading of a odf file.
1436             var frameName = frame.getAttributeNS(drawns, 'name'),
1437                 fc = frame.firstElementChild;
1438             setDrawElementPosition(frameName, frame,
1439                     /**@type{!CSSStyleSheet}*/(positioncss.sheet));
1440             if (fc) {
1441                 setImage(frameName + 'img', odfcontainer, fc,
1442                    /**@type{!CSSStyleSheet}*/( positioncss.sheet));
1443             }
1444         };
1445         /**
1446          * @param {!function(!Error=)} callback, passing an error object in case of error
1447          * @return {undefined}
1448          */
1449         this.destroy = function(callback) {
1450             var head = /**@type{!HTMLHeadElement}*/(doc.getElementsByTagName('head')[0]),
1451                 cleanup = [pageSwitcher.destroy, redrawContainerTask.destroy];
1452 
1453             runtime.clearTimeout(waitingForDoneTimeoutId);
1454             // TODO: anything to clean with annotationViewManager?
1455             if (annotationsPane && annotationsPane.parentNode) {
1456                 annotationsPane.parentNode.removeChild(annotationsPane);
1457             }
1458 
1459             zoomHelper.destroy(function () {
1460                 if (sizer) {
1461                     element.removeChild(sizer);
1462                     sizer = null;
1463                 }
1464             });
1465 
1466             // remove all styles
1467             removeWebODFStyleSheet(webodfcss);
1468             head.removeChild(fontcss);
1469             head.removeChild(stylesxmlcss);
1470             head.removeChild(positioncss);
1471 
1472             // TODO: loadingQueue, make sure it is empty
1473             core.Async.destroyAll(cleanup, callback);
1474         };
1475 
1476         function init() {
1477             webodfcss = addWebODFStyleSheet(doc);
1478             pageSwitcher = new PageSwitcher(addStyleSheet(doc));
1479             fontcss = addStyleSheet(doc);
1480             stylesxmlcss = addStyleSheet(doc);
1481             positioncss = addStyleSheet(doc);
1482             redrawContainerTask = core.Task.createRedrawTask(redrawContainer);
1483             zoomHelper.subscribe(gui.ZoomHelper.signalZoomChanged, fixContainerSize);
1484         }
1485 
1486         init();
1487     };
1488 }());
1489