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