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 = new 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 = clonedNode.getElementsByTagNameNS(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 = rootElement.getElementsByTagNameNS(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 = odffragment.getElementsByTagNameNS(tablens, 'table-cell');
461         for (i = 0; i < tableCells.length; i += 1) {
462             node = /**@type{!Element}*/(tableCells.item(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             pageNumber = 0,
585             i,
586             element,
587             elementToClone,
588             document = odfContainer.rootElement.ownerDocument;
589 
590         element = odfbody.firstElementChild;
591         // no master pages to expect?
592         if (!(element && element.namespaceURI === officens &&
593               (element.localName === "presentation" || element.localName === "drawing"))) {
594             return;
595         }
596 
597         element = element.firstElementChild;
598         while (element) {
599             // If there was a master-page-name attribute, then we are dealing with a draw:page.
600             // Get the referenced master page element from the master styles
601             masterPageName = element.getAttributeNS(drawns, 'master-page-name');
602             masterPageElement = masterPageName ? formatting.getMasterPageElement(masterPageName) : null;
603 
604             // If the referenced master page exists, create a new page and copy over it's contents into the new page,
605             // except for the ones that are placeholders. Also, call setDrawElementPosition on each of those child frames.
606             if (masterPageElement) {
607                 styleId = element.getAttributeNS(webodfhelperns, 'styleid');
608                 clonedPageElement = document.createElementNS(drawns, 'draw:page');
609 
610                 elementToClone = masterPageElement.firstElementChild;
611                 i = 0;
612                 while (elementToClone) {
613                     if (elementToClone.getAttributeNS(presentationns, 'placeholder') !== 'true') {
614                         clonedElement = /**@type{!Element}*/(elementToClone.cloneNode(true));
615                         clonedPageElement.appendChild(clonedElement);
616                         setDrawElementPosition(styleId + '_' + i, clonedElement, stylesheet);
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                 // Append the cloned master page to the "Shadow Content" element outside the main ODF dom
625                 shadowContent.appendChild(clonedPageElement);
626 
627                 // Get the page number by counting the number of previous master pages in this shadowContent
628                 pageNumber = String(shadowContent.getElementsByTagNameNS(drawns, 'page').length);
629                 // Get the page-number tag in the cloned master page and set the text content to the calculated number
630                 setContainerValue(clonedPageElement, textns, 'page-number', pageNumber);
631 
632                 // Care for header
633                 setContainerValue(clonedPageElement, presentationns, 'header', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'header'));
634                 // Care for footer
635                 setContainerValue(clonedPageElement, presentationns, 'footer', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'footer'));
636 
637                 // Now call setDrawElementPosition on this new page to set the proper dimensions
638                 setDrawElementPosition(styleId, clonedPageElement, stylesheet);
639                 // And finally, add an attribute referring to the master page, so the CSS targeted for that master page will style this
640                 clonedPageElement.setAttributeNS(drawns, 'draw:master-page-name', masterPageElement.getAttributeNS(stylens, 'name'));
641             }
642 
643             element = element.nextElementSibling;
644         }
645     }
646 
647     /**
648      * @param {!odf.OdfContainer} container
649      * @param {!Element} plugin
650      * @return {undefined}
651      **/
652     function setVideo(container, plugin) {
653         var video, source, url, doc = plugin.ownerDocument,
654             /**@type{!odf.OdfPart}*/
655             part;
656 
657         url = plugin.getAttributeNS(xlinkns, 'href');
658 
659         /**
660          * @param {?string} url
661          * @param {string} mimetype
662          * @return {undefined}
663          */
664         function callback(url, mimetype) {
665             var ns = doc.documentElement.namespaceURI;
666             // test for video mimetypes
667             if (mimetype.substr(0, 6) === 'video/') {
668                 video = doc.createElementNS(ns, "video");
669                 video.setAttribute('controls', 'controls');
670 
671                 source = doc.createElementNS(ns, 'source');
672                 if (url) {
673                     source.setAttribute('src', url);
674                 }
675                 source.setAttribute('type', mimetype);
676 
677                 video.appendChild(source);
678                 plugin.parentNode.appendChild(video);
679             } else {
680                 plugin.innerHtml = 'Unrecognised Plugin';
681             }
682         }
683         /**
684          * @param {!odf.OdfPart} p
685          */
686         function onchange(p) {
687             callback(p.url, p.mimetype);
688         }
689         // look for a office:binary-data
690         if (url) {
691             try {
692                 part = container.getPart(url);
693                 part.onchange = onchange;
694                 part.load();
695             } catch (/**@type{*}*/e) {
696                 runtime.log('slight problem: ' + String(e));
697             }
698         } else {
699         // this will fail  atm - following function assumes PNG data]
700             runtime.log('using MP4 data fallback');
701             url = getUrlFromBinaryDataElement(plugin);
702             callback(url, 'video/mp4');
703         }
704     }
705 
706     /**
707      * @param {!HTMLHeadElement} head
708      * @return {?HTMLStyleElement}
709      */
710     function findWebODFStyleSheet(head) {
711         var style = head.firstElementChild;
712         while (style && !(style.localName === "style"
713                 && style.hasAttribute("webodfcss"))) {
714             style = style.nextElementSibling;
715         }
716         return /**@type{?HTMLStyleElement}*/(style);
717     }
718 
719     /**
720      * @param {!Document} document
721      * @return {!HTMLStyleElement}
722      */
723     function addWebODFStyleSheet(document) {
724         var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
725             css,
726             /**@type{?HTMLStyleElement}*/
727             style,
728             href,
729             count = document.styleSheets.length;
730         // make sure this is only added once per HTML document, e.g. in case of
731         // multiple odfCanvases
732         style = findWebODFStyleSheet(head);
733         if (style) {
734             count = parseInt(style.getAttribute("webodfcss"), 10);
735             style.setAttribute("webodfcss", count + 1);
736             return style;
737         }
738         if (String(typeof webodf_css) === "string") {
739             css = /**@type{!string}*/(webodf_css);
740         } else {
741             href = "webodf.css";
742             if (runtime.currentDirectory) {
743                 href = runtime.currentDirectory();
744                 if (href.length > 0 && href.substr(-1) !== "/") {
745                     href += "/";
746                 }
747                 href += "../webodf.css";
748             }
749             css = /**@type{!string}*/(runtime.readFileSync(href, "utf-8"));
750         }
751         style = /**@type{!HTMLStyleElement}*/(document.createElementNS(head.namespaceURI, 'style'));
752         style.setAttribute('media', 'screen, print, handheld, projection');
753         style.setAttribute('type', 'text/css');
754         style.setAttribute('webodfcss', '1');
755         style.appendChild(document.createTextNode(css));
756         head.appendChild(style);
757         return style;
758     }
759 
760     /**
761      * @param {!HTMLStyleElement} webodfcss
762      * @return {undefined}
763      */
764     function removeWebODFStyleSheet(webodfcss) {
765         var count = parseInt(webodfcss.getAttribute("webodfcss"), 10);
766         if (count === 1) {
767              webodfcss.parentNode.removeChild(webodfcss);
768         } else {
769              webodfcss.setAttribute("count", count - 1);
770         }
771     }
772 
773     /**
774      * @param {!Document} document Put and ODF Canvas inside this element.
775      * @return {!HTMLStyleElement}
776      */
777     function addStyleSheet(document) {
778         var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
779             style = document.createElementNS(head.namespaceURI, 'style'),
780             /**@type{string}*/
781             text = '';
782         style.setAttribute('type', 'text/css');
783         style.setAttribute('media', 'screen, print, handheld, projection');
784         odf.Namespaces.forEachPrefix(function(prefix, ns) {
785             text += "@namespace " + prefix + " url(" + ns + ");\n";
786         });
787         text += "@namespace webodfhelper url(" + webodfhelperns + ");\n";
788         style.appendChild(document.createTextNode(text));
789         head.appendChild(style);
790         return /**@type {!HTMLStyleElement}*/(style);
791     }
792     /**
793      * This class manages a loaded ODF document that is shown in an element.
794      * It takes care of giving visual feedback on loading, ensures that the
795      * stylesheets are loaded.
796      * @constructor
797      * @implements {gui.AnnotatableCanvas}
798      * @implements {ops.Canvas}
799      * @implements {core.Destroyable}
800      * @param {!HTMLElement} element Put and ODF Canvas inside this element.
801      */
802     odf.OdfCanvas = function OdfCanvas(element) {
803         runtime.assert((element !== null) && (element !== undefined),
804             "odf.OdfCanvas constructor needs DOM element");
805         runtime.assert((element.ownerDocument !== null) && (element.ownerDocument !== undefined),
806             "odf.OdfCanvas constructor needs DOM");
807         var self = this,
808             doc = /**@type{!Document}*/(element.ownerDocument),
809             /**@type{!odf.OdfContainer}*/
810             odfcontainer,
811             /**@type{!odf.Formatting}*/
812             formatting = new odf.Formatting(),
813             /**@type{!PageSwitcher}*/
814             pageSwitcher,
815             /**@type{HTMLDivElement}*/
816             sizer = null,
817             /**@type{HTMLDivElement}*/
818             annotationsPane = null,
819             allowAnnotations = false,
820             showAnnotationRemoveButton = false,
821             /**@type{gui.AnnotationViewManager}*/
822             annotationViewManager = null,
823             /**@type{!HTMLStyleElement}*/
824             webodfcss,
825             /**@type{!HTMLStyleElement}*/
826             fontcss,
827             /**@type{!HTMLStyleElement}*/
828             stylesxmlcss,
829             /**@type{!HTMLStyleElement}*/
830             positioncss,
831             shadowContent,
832             /**@type{!Object.<string,!Array.<!Function>>}*/
833             eventHandlers = {},
834             waitingForDoneTimeoutId,
835             /**@type{!core.ScheduledTask}*/redrawContainerTask,
836             shouldRefreshCss = false,
837             shouldRerenderAnnotations = false,
838             loadingQueue = new LoadingQueue(),
839             /**@type{!gui.ZoomHelper}*/
840             zoomHelper = new gui.ZoomHelper();
841 
842         /**
843          * Load all the images that are inside an odf element.
844          * @param {!odf.OdfContainer} container
845          * @param {!Element} odffragment
846          * @param {!CSSStyleSheet} stylesheet
847          * @return {undefined}
848          */
849         function loadImages(container, odffragment, stylesheet) {
850             var i,
851                 images,
852                 node;
853             /**
854              * Do delayed loading for all the images
855              * @param {string} name
856              * @param {!odf.OdfContainer} container
857              * @param {!Element} node
858              * @param {!CSSStyleSheet} stylesheet
859              * @return {undefined}
860              */
861             function loadImage(name, container, node, stylesheet) {
862                 // load image with a small delay to give the html ui a chance to
863                 // update
864                 loadingQueue.addToQueue(function () {
865                     setImage(name, container, node, stylesheet);
866                 });
867             }
868             images = odffragment.getElementsByTagNameNS(drawns, 'image');
869             for (i = 0; i < images.length; i += 1) {
870                 node = /**@type{!Element}*/(images.item(i));
871                 loadImage('image' + String(i), container, node, stylesheet);
872             }
873         }
874         /**
875          * Load all the video that are inside an odf element.
876          * @param {!odf.OdfContainer} container
877          * @param {!Element} odffragment
878          * @return {undefined}
879          */
880         function loadVideos(container, odffragment) {
881             var i,
882                 plugins,
883                 node;
884             /**
885              * Do delayed loading for all the videos
886              * @param {!odf.OdfContainer} container
887              * @param {!Element} node
888              * @return {undefined}
889              */
890             function loadVideo(container, node) {
891                 // load video with a small delay to give the html ui a chance to
892                 // update
893                 loadingQueue.addToQueue(function () {
894                     setVideo(container, node);
895                 });
896             }
897             // embedded video is stored in a draw:plugin element
898             plugins = odffragment.getElementsByTagNameNS(drawns, 'plugin');
899             for (i = 0; i < plugins.length; i += 1) {
900                 node = /**@type{!Element}*/(plugins.item(i));
901                 loadVideo(container, node);
902             }
903         }
904 
905         /**
906          * Register an event handler
907          * @param {!string} eventType
908          * @param {!Function} eventHandler
909          * @return {undefined}
910          */
911         function addEventListener(eventType, eventHandler) {
912             var handlers;
913             if (eventHandlers.hasOwnProperty(eventType)) {
914                 handlers = eventHandlers[eventType];
915             } else {
916                 handlers = eventHandlers[eventType] = [];
917             }
918             if (eventHandler && handlers.indexOf(eventHandler) === -1) {
919                 handlers.push(eventHandler);
920             }
921         }
922         /**
923          * Fire an event
924          * @param {!string} eventType
925          * @param {Array.<Object>=} args
926          * @return {undefined}
927          */
928         function fireEvent(eventType, args) {
929             if (!eventHandlers.hasOwnProperty(eventType)) {
930                 return;
931             }
932             var handlers = eventHandlers[eventType], i;
933             for (i = 0; i < handlers.length; i += 1) {
934                 handlers[i].apply(null, args);
935             }
936         }
937 
938         /**
939          * @return {undefined}
940          */
941         function fixContainerSize() {
942             var minHeight,
943                 odfdoc = sizer.firstChild,
944                 zoomLevel = zoomHelper.getZoomLevel();
945 
946             if (!odfdoc) {
947                 return;
948             }
949 
950             // All zooming of the sizer within the canvas
951             // is done relative to the top-left corner.
952             sizer.style.WebkitTransformOrigin = "0% 0%";
953             sizer.style.MozTransformOrigin = "0% 0%";
954             sizer.style.msTransformOrigin = "0% 0%";
955             sizer.style.OTransformOrigin = "0% 0%";
956             sizer.style.transformOrigin = "0% 0%";
957 
958             if (annotationViewManager) {
959                 minHeight = annotationViewManager.getMinimumHeightForAnnotationPane();
960                 if (minHeight) {
961                     sizer.style.minHeight = minHeight;
962                 } else {
963                     sizer.style.removeProperty('min-height');
964                 }
965             }
966 
967             element.style.width = Math.round(zoomLevel * sizer.offsetWidth) + "px";
968             element.style.height = Math.round(zoomLevel * sizer.offsetHeight) + "px";
969             // Re-apply inline-block to canvas element on resizing.
970             // Chrome tends to forget this property after a relayout
971             element.style.display = "inline-block";
972         }
973 
974         /**
975          * @return {undefined}
976          */
977         function redrawContainer() {
978             if (shouldRefreshCss) {
979                 handleStyles(odfcontainer, formatting, stylesxmlcss);
980                 shouldRefreshCss = false;
981                 // different styles means different layout, thus different sizes
982             }
983             if (shouldRerenderAnnotations) {
984                 if (annotationViewManager) {
985                     annotationViewManager.rerenderAnnotations();
986                 }
987                 shouldRerenderAnnotations = false;
988             }
989             fixContainerSize();
990         }
991 
992         /**
993          * A new content.xml has been loaded. Update the live document with it.
994          * @param {!odf.OdfContainer} container
995          * @param {!odf.ODFDocumentElement} odfnode
996          * @return {undefined}
997          **/
998         function handleContent(container, odfnode) {
999             var css = /**@type{!CSSStyleSheet}*/(positioncss.sheet);
1000             // only append the content at the end
1001             clear(element);
1002 
1003             sizer = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
1004             sizer.style.display = "inline-block";
1005             sizer.style.background = "white";
1006             // When the window is shrunk such that the
1007             // canvas container has a horizontal scrollbar,
1008             // zooming out seems to not make the scrollable
1009             // width disappear. This extra scrollable
1010             // width seems to be proportional to the
1011             // annotation pane's width. Setting the 'float'
1012             // of the sizer to 'left' fixes this in webkit.
1013             sizer.style.setProperty("float", "left", "important");
1014             sizer.appendChild(odfnode);
1015             element.appendChild(sizer);
1016 
1017             // An annotations pane div. Will only be shown when annotations are enabled
1018             annotationsPane = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
1019             annotationsPane.id = "annotationsPane";
1020             // A "Shadow Content" div. This will contain stuff like pages
1021             // extracted from <style:master-page>. These need to be nicely
1022             // styled, so we will populate this in the ODF body first. Once the
1023             // styling is handled, it can then be lifted out of the
1024             // ODF body and placed beside it, to not pollute the ODF dom.
1025             shadowContent = doc.createElementNS(element.namespaceURI, 'div');
1026             shadowContent.id = "shadowContent";
1027             shadowContent.style.position = 'absolute';
1028             shadowContent.style.top = 0;
1029             shadowContent.style.left = 0;
1030             container.getContentElement().appendChild(shadowContent);
1031 
1032             modifyDrawElements(odfnode.body, css);
1033             cloneMasterPages(formatting, container, shadowContent, odfnode.body, css);
1034             modifyTables(odfnode.body, element.namespaceURI);
1035             modifyLineBreakElements(odfnode.body);
1036             expandSpaceElements(odfnode.body);
1037             expandTabElements(odfnode.body);
1038             loadImages(container, odfnode.body, css);
1039             loadVideos(container, odfnode.body);
1040 
1041             sizer.insertBefore(shadowContent, sizer.firstChild);
1042             zoomHelper.setZoomableElement(sizer);
1043         }
1044 
1045         /**
1046         * Wraps all annotations and renders them using the Annotation View Manager.
1047         * @param {!Element} odffragment
1048         * @return {undefined}
1049         */
1050         function modifyAnnotations(odffragment) {
1051             var annotationNodes = /**@type{!Array.<!odf.AnnotationElement>}*/(domUtils.getElementsByTagNameNS(odffragment, officens, 'annotation'));
1052 
1053             annotationNodes.forEach(annotationViewManager.addAnnotation);
1054             annotationViewManager.rerenderAnnotations();
1055         }
1056 
1057         /**
1058          * This should create an annotations pane if non existent, and then populate it with annotations
1059          * If annotations are disallowed, it should remove the pane and all annotations
1060          * @param {!odf.ODFDocumentElement} odfnode
1061          */
1062         function handleAnnotations(odfnode) {
1063             if (allowAnnotations) {
1064                 if (!annotationsPane.parentNode) {
1065                     sizer.appendChild(annotationsPane);
1066                 }
1067                 if (annotationViewManager) {
1068                     annotationViewManager.forgetAnnotations();
1069                 }
1070                 annotationViewManager = new gui.AnnotationViewManager(self, odfnode.body, annotationsPane, showAnnotationRemoveButton);
1071                 modifyAnnotations(odfnode.body);
1072                 fixContainerSize();
1073             } else {
1074                 if (annotationsPane.parentNode) {
1075                     sizer.removeChild(annotationsPane);
1076                     annotationViewManager.forgetAnnotations();
1077                     fixContainerSize();
1078                 }
1079             }
1080         }
1081 
1082         /**
1083          * @param {boolean} suppressEvent Suppress the statereadychange event from firing. Used for refreshing the OdtContainer
1084          * @return {undefined}
1085          **/
1086         function refreshOdf(suppressEvent) {
1087 
1088             // synchronize the object a window.odfcontainer with the view
1089             function callback() {
1090                 // clean up
1091                 clearCSSStyleSheet(fontcss);
1092                 clearCSSStyleSheet(stylesxmlcss);
1093                 clearCSSStyleSheet(positioncss);
1094 
1095                 clear(element);
1096 
1097                 // setup
1098                 element.style.display = "inline-block";
1099                 var odfnode = odfcontainer.rootElement;
1100                 element.ownerDocument.importNode(odfnode, true);
1101 
1102                 formatting.setOdfContainer(odfcontainer);
1103                 handleFonts(odfcontainer, fontcss);
1104                 handleStyles(odfcontainer, formatting, stylesxmlcss);
1105                 // do content last, because otherwise the document is constantly
1106                 // updated whenever the css changes
1107                 handleContent(odfcontainer, odfnode);
1108                 handleAnnotations(odfnode);
1109 
1110                 if (!suppressEvent) {
1111                     loadingQueue.addToQueue(function () {
1112                         fireEvent("statereadychange", [odfcontainer]);
1113                     });
1114                 }
1115             }
1116 
1117             if (odfcontainer.state === odf.OdfContainer.DONE) {
1118                 callback();
1119             } else {
1120                 // so the ODF is not done yet. take care that we'll
1121                 // do the work once it is done:
1122 
1123                 // FIXME: use callback registry instead of replacing the onchange
1124                 runtime.log("WARNING: refreshOdf called but ODF was not DONE.");
1125 
1126                 waitingForDoneTimeoutId = runtime.setTimeout(function later_cb() {
1127                     if (odfcontainer.state === odf.OdfContainer.DONE) {
1128                         callback();
1129                     } else {
1130                         runtime.log("will be back later...");
1131                         waitingForDoneTimeoutId = runtime.setTimeout(later_cb, 500);
1132                     }
1133                 }, 100);
1134             }
1135         }
1136 
1137         /**
1138          * Updates the CSS rules to match the ODF document styles and also
1139          * updates the size of the canvas to match the new layout.
1140          * Needs to be called after changes to the styles of the ODF document.
1141          * @return {undefined}
1142          */
1143         this.refreshCSS = function () {
1144             shouldRefreshCss = true;
1145             redrawContainerTask.trigger();
1146         };
1147 
1148         /**
1149          * Updates the size of the canvas to the size of the content.
1150          * Needs to be called after changes to the content of the ODF document.
1151          * @return {undefined}
1152          */
1153         this.refreshSize = function () {
1154             redrawContainerTask.trigger();
1155         };
1156         /**
1157          * @return {!odf.OdfContainer}
1158          */
1159         this.odfContainer = function () {
1160             return odfcontainer;
1161         };
1162         /**
1163          * Set a odfcontainer manually.
1164          * @param {!odf.OdfContainer} container
1165          * @param {boolean=} suppressEvent Default value is false
1166          * @return {undefined}
1167          */
1168         this.setOdfContainer = function (container, suppressEvent) {
1169             odfcontainer = container;
1170             refreshOdf(suppressEvent === true);
1171         };
1172         /**
1173          * @param {string} url
1174          * @return {undefined}
1175          */
1176         function load(url) {
1177             // clean up
1178             loadingQueue.clearQueue();
1179 
1180             // FIXME: We need to support parametrized strings, because
1181             // drop-in word replacements are inadequate for translations;
1182             // see http://techbase.kde.org/Development/Tutorials/Localization/i18n_Mistakes#Pitfall_.232:_Word_Puzzles
1183             element.innerHTML = runtime.tr('Loading') + ' ' + url + '...';
1184             element.removeAttribute('style');
1185             // open the odf container
1186             odfcontainer = new odf.OdfContainer(url, function (container) {
1187                 // assignment might be necessary if the callback
1188                 // fires before the assignment above happens.
1189                 odfcontainer = container;
1190                 refreshOdf(false);
1191             });
1192         }
1193         this["load"] = load;
1194         this.load = load;
1195 
1196         /**
1197          * @param {function(?string):undefined} callback
1198          * @return {undefined}
1199          */
1200         this.save = function (callback) {
1201             odfcontainer.save(callback);
1202         };
1203 
1204         /**
1205          * @param {!string} eventName
1206          * @param {!function(*)} handler
1207          * @return {undefined}
1208          */
1209         this.addListener = function (eventName, handler) {
1210             switch (eventName) {
1211             case "click":
1212                 listenEvent(element, eventName, handler); break;
1213             default:
1214                 addEventListener(eventName, handler); break;
1215             }
1216         };
1217 
1218         /**
1219          * @return {!odf.Formatting}
1220          */
1221         this.getFormatting = function () {
1222             return formatting;
1223         };
1224 
1225         /**
1226          * @return {gui.AnnotationViewManager}
1227          */
1228         this.getAnnotationViewManager = function () {
1229             return annotationViewManager;
1230         };
1231 
1232         /**
1233          * Unstyles and untracks all annotations present in the document,
1234          * and then tracks them again with fresh rendering
1235          * @return {undefined}
1236          */
1237         this.refreshAnnotations = function () {
1238             handleAnnotations(odfcontainer.rootElement);
1239         };
1240 
1241         /**
1242          * Re-renders all annotations if enabled
1243          * @return {undefined}
1244          */
1245         this.rerenderAnnotations = function () {
1246             if (annotationViewManager) {
1247                 shouldRerenderAnnotations = true;
1248                 redrawContainerTask.trigger();
1249             }
1250         };
1251 
1252         /**
1253          * This returns the element inside the canvas which can be zoomed with
1254          * CSS and which contains the ODF document and the annotation sidebar.
1255          * @return {!HTMLElement}
1256          */
1257         this.getSizer = function () {
1258             return /**@type{!HTMLElement}*/(sizer);
1259         };
1260 
1261         /** Allows / disallows annotations
1262          * @param {!boolean} allow
1263          * @param {!boolean} showRemoveButton
1264          * @return {undefined}
1265          */
1266         this.enableAnnotations = function (allow, showRemoveButton) {
1267             if (allow !== allowAnnotations) {
1268                 allowAnnotations = allow;
1269                 showAnnotationRemoveButton = showRemoveButton;
1270                 if (odfcontainer) {
1271                     handleAnnotations(odfcontainer.rootElement);
1272                 }
1273             }
1274         };
1275 
1276         /**
1277          * Adds an annotation for the annotaiton manager to track
1278          * and wraps and highlights it
1279          * @param {!odf.AnnotationElement} annotation
1280          * @return {undefined}
1281          */
1282         this.addAnnotation = function (annotation) {
1283             if (annotationViewManager) {
1284                 annotationViewManager.addAnnotation(annotation);
1285                 fixContainerSize();
1286             }
1287         };
1288 
1289         /**
1290          * Stops annotations and unwraps it
1291          * @return {undefined}
1292          */
1293         this.forgetAnnotations = function () {
1294             if (annotationViewManager) {
1295                 annotationViewManager.forgetAnnotations();
1296                 fixContainerSize();
1297             }
1298         };
1299 
1300         /**
1301          * @return {!gui.ZoomHelper}
1302          */
1303         this.getZoomHelper = function () {
1304             return zoomHelper;
1305         };
1306 
1307         /**
1308          * @param {!number} zoom
1309          * @return {undefined}
1310          */
1311         this.setZoomLevel = function (zoom) {
1312             zoomHelper.setZoomLevel(zoom);
1313         };
1314         /**
1315          * @return {!number}
1316          */
1317         this.getZoomLevel = function () {
1318             return zoomHelper.getZoomLevel();
1319         };
1320         /**
1321          * @param {!number} width
1322          * @param {!number} height
1323          * @return {undefined}
1324          */
1325         this.fitToContainingElement = function (width, height) {
1326             var zoomLevel = zoomHelper.getZoomLevel(),
1327                 realWidth = element.offsetWidth / zoomLevel,
1328                 realHeight = element.offsetHeight / zoomLevel,
1329                 zoom;
1330 
1331             zoom = width / realWidth;
1332             if (height / realHeight < zoom) {
1333                 zoom = height / realHeight;
1334             }
1335             zoomHelper.setZoomLevel(zoom);
1336         };
1337         /**
1338          * @param {!number} width
1339          * @return {undefined}
1340          */
1341         this.fitToWidth = function (width) {
1342             var realWidth = element.offsetWidth / zoomHelper.getZoomLevel();
1343             zoomHelper.setZoomLevel(width / realWidth);
1344         };
1345         /**
1346          * @param {!number} width
1347          * @param {!number} height
1348          * @return {undefined}
1349          */
1350         this.fitSmart = function (width, height) {
1351             var realWidth, realHeight, newScale,
1352                 zoomLevel = zoomHelper.getZoomLevel();
1353 
1354             realWidth = element.offsetWidth / zoomLevel;
1355             realHeight = element.offsetHeight / zoomLevel;
1356 
1357             newScale = width / realWidth;
1358             if (height !== undefined) {
1359                 if (height / realHeight < newScale) {
1360                     newScale = height / realHeight;
1361                 }
1362             }
1363 
1364             zoomHelper.setZoomLevel(Math.min(1.0, newScale));
1365         };
1366         /**
1367          * @param {!number} height
1368          * @return {undefined}
1369          */
1370         this.fitToHeight = function (height) {
1371             var realHeight = element.offsetHeight / zoomHelper.getZoomLevel();
1372             zoomHelper.setZoomLevel(height / realHeight);
1373         };
1374         /**
1375          * @return {undefined}
1376          */
1377         this.showFirstPage = function () {
1378             pageSwitcher.showFirstPage();
1379         };
1380         /**
1381          * @return {undefined}
1382          */
1383         this.showNextPage = function () {
1384             pageSwitcher.showNextPage();
1385         };
1386         /**
1387          * @return {undefined}
1388          */
1389         this.showPreviousPage = function () {
1390             pageSwitcher.showPreviousPage();
1391         };
1392         /**
1393          * @param {!number} n  number of the page
1394          * @return {undefined}
1395          */
1396         this.showPage = function (n) {
1397             pageSwitcher.showPage(n);
1398             fixContainerSize();
1399         };
1400 
1401         /**
1402          * @return {!HTMLElement}
1403          */
1404         this.getElement = function () {
1405             return element;
1406         };
1407 
1408         /**
1409          * Add additional css rules for newly inserted draw:frame and draw:image. eg. position, dimensions and background image
1410          * @param {!Element} frame
1411          */
1412         this.addCssForFrameWithImage = function (frame) {
1413             // TODO: frameid and imageid generation here is better brought in sync with that for the images on loading of a odf file.
1414             var frameName = frame.getAttributeNS(drawns, 'name'),
1415                 fc = frame.firstElementChild;
1416             setDrawElementPosition(frameName, frame,
1417                     /**@type{!CSSStyleSheet}*/(positioncss.sheet));
1418             if (fc) {
1419                 setImage(frameName + 'img', odfcontainer, fc,
1420                    /**@type{!CSSStyleSheet}*/( positioncss.sheet));
1421             }
1422         };
1423         /**
1424          * @param {!function(!Error=)} callback, passing an error object in case of error
1425          * @return {undefined}
1426          */
1427         this.destroy = function(callback) {
1428             var head = /**@type{!HTMLHeadElement}*/(doc.getElementsByTagName('head')[0]),
1429                 cleanup = [pageSwitcher.destroy, redrawContainerTask.destroy];
1430 
1431             runtime.clearTimeout(waitingForDoneTimeoutId);
1432             // TODO: anything to clean with annotationViewManager?
1433             if (annotationsPane && annotationsPane.parentNode) {
1434                 annotationsPane.parentNode.removeChild(annotationsPane);
1435             }
1436 
1437             zoomHelper.destroy(function () {
1438                 if (sizer) {
1439                     element.removeChild(sizer);
1440                     sizer = null;
1441                 }
1442             });
1443 
1444             // remove all styles
1445             removeWebODFStyleSheet(webodfcss);
1446             head.removeChild(fontcss);
1447             head.removeChild(stylesxmlcss);
1448             head.removeChild(positioncss);
1449 
1450             // TODO: loadingQueue, make sure it is empty
1451             core.Async.destroyAll(cleanup, callback);
1452         };
1453 
1454         function init() {
1455             webodfcss = addWebODFStyleSheet(doc);
1456             pageSwitcher = new PageSwitcher(addStyleSheet(doc));
1457             fontcss = addStyleSheet(doc);
1458             stylesxmlcss = addStyleSheet(doc);
1459             positioncss = addStyleSheet(doc);
1460             redrawContainerTask = core.Task.createRedrawTask(redrawContainer);
1461             zoomHelper.subscribe(gui.ZoomHelper.signalZoomChanged, fixContainerSize);
1462         }
1463 
1464         init();
1465     };
1466 }());
1467