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