node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
1,283 lines (1,197 loc) • 56.3 kB
JavaScript
/**
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global runtime, odf, xmldom, webodf_css, core, gui */
/*jslint sub: true*/
(function () {
"use strict";
/**
* A loading queue where various tasks related to loading can be placed
* and will be run with 10 ms between them. This gives the ui a change to
* to update.
* @constructor
*/
function LoadingQueue() {
var /**@type{!Array.<!Function>}*/
queue = [],
taskRunning = false;
/**
* @param {!Function} task
* @return {undefined}
*/
function run(task) {
taskRunning = true;
runtime.setTimeout(function () {
try {
task();
} catch (/**@type{Error}*/e) {
runtime.log(String(e) + "\n" + e.stack);
}
taskRunning = false;
if (queue.length > 0) {
run(queue.pop());
}
}, 10);
}
/**
* @return {undefined}
*/
this.clearQueue = function () {
queue.length = 0;
};
/**
* @param {!Function} loadingTask
* @return {undefined}
*/
this.addToQueue = function (loadingTask) {
if (queue.length === 0 && !taskRunning) {
return run(loadingTask);
}
queue.push(loadingTask);
};
}
/**
* @constructor
* @implements {core.Destroyable}
* @param {!HTMLStyleElement} css
*/
function PageSwitcher(css) {
var sheet = /**@type{!CSSStyleSheet}*/(css.sheet),
/**@type{number}*/
position = 1;
/**
* @return {undefined}
*/
function updateCSS() {
while (sheet.cssRules.length > 0) {
sheet.deleteRule(0);
}
// The #shadowContent contains the master pages, with each page in the slideshow
// corresponding to a master page in #shadowContent, and in the same order.
// So, when showing a page, also make it's master page (behind it) visible.
sheet.insertRule('#shadowContent draw|page {display:none;}', 0);
sheet.insertRule('office|presentation draw|page {display:none;}', 1);
sheet.insertRule("#shadowContent draw|page:nth-of-type(" +
position + ") {display:block;}", 2);
sheet.insertRule("office|presentation draw|page:nth-of-type(" +
position + ") {display:block;}", 3);
}
/**
* @return {undefined}
*/
this.showFirstPage = function () {
position = 1;
updateCSS();
};
/**
* @return {undefined}
*/
this.showNextPage = function () {
position += 1;
updateCSS();
};
/**
* @return {undefined}
*/
this.showPreviousPage = function () {
if (position > 1) {
position -= 1;
updateCSS();
}
};
/**
* @param {!number} n number of the page
* @return {undefined}
*/
this.showPage = function (n) {
if (n > 0) {
position = n;
updateCSS();
}
};
this.css = css;
/**
* @param {!function(!Error=)} callback, passing an error object in case of error
* @return {undefined}
*/
this.destroy = function (callback) {
css.parentNode.removeChild(css);
callback();
};
}
/**
* Register event listener on DOM element.
* @param {!Element} eventTarget
* @param {!string} eventType
* @param {!Function} eventHandler
* @return {undefined}
*/
function listenEvent(eventTarget, eventType, eventHandler) {
if (eventTarget.addEventListener) {
eventTarget.addEventListener(eventType, eventHandler, false);
} else if (eventTarget.attachEvent) {
eventType = "on" + eventType;
eventTarget.attachEvent(eventType, eventHandler);
} else {
eventTarget["on" + eventType] = eventHandler;
}
}
// variables per class (so not per instance!)
var /**@const@type {!string}*/drawns = odf.Namespaces.drawns,
/**@const@type {!string}*/fons = odf.Namespaces.fons,
/**@const@type {!string}*/officens = odf.Namespaces.officens,
/**@const@type {!string}*/stylens = odf.Namespaces.stylens,
/**@const@type {!string}*/svgns = odf.Namespaces.svgns,
/**@const@type {!string}*/tablens = odf.Namespaces.tablens,
/**@const@type {!string}*/textns = odf.Namespaces.textns,
/**@const@type {!string}*/xlinkns = odf.Namespaces.xlinkns,
/**@const@type {!string}*/presentationns = odf.Namespaces.presentationns,
/**@const@type {!string}*/webodfhelperns = "urn:webodf:names:helper",
xpath = xmldom.XPath,
domUtils = core.DomUtils;
/**
* @param {!HTMLStyleElement} style
* @return {undefined}
*/
function clearCSSStyleSheet(style) {
var stylesheet = /**@type{!CSSStyleSheet}*/(style.sheet),
cssRules = stylesheet.cssRules;
while (cssRules.length) {
stylesheet.deleteRule(cssRules.length - 1);
}
}
/**
* A new styles.xml has been loaded. Update the live document with it.
* @param {!odf.OdfContainer} odfcontainer
* @param {!odf.Formatting} formatting
* @param {!HTMLStyleElement} stylesxmlcss
* @return {undefined}
**/
function handleStyles(odfcontainer, formatting, stylesxmlcss) {
// update the css translation of the styles
var style2css = new odf.Style2CSS(),
list2css = new odf.ListStyleToCss(),
styleSheet = /**@type{!CSSStyleSheet}*/(stylesxmlcss.sheet),
styleTree = new odf.StyleTree(
odfcontainer.rootElement.styles,
odfcontainer.rootElement.automaticStyles).getStyleTree();
style2css.style2css(
odfcontainer.getDocumentType(),
odfcontainer.rootElement,
styleSheet,
formatting.getFontMap(),
styleTree
);
list2css.applyListStyles(
styleSheet,
styleTree,
odfcontainer.rootElement.body);
}
/**
* @param {!odf.OdfContainer} odfContainer
* @param {!HTMLStyleElement} fontcss
* @return {undefined}
**/
function handleFonts(odfContainer, fontcss) {
// update the css references to the fonts
var fontLoader = new odf.FontLoader();
fontLoader.loadFonts(odfContainer,
/**@type{!CSSStyleSheet}*/(fontcss.sheet));
}
/**
* @param {!Element} clonedNode <draw:page/>
* @return {undefined}
*/
function dropTemplateDrawFrames(clonedNode) {
// drop all frames which are just template frames
var i, element, presentationClass,
clonedDrawFrameElements = domUtils.getElementsByTagNameNS(clonedNode, drawns, 'frame');
for (i = 0; i < clonedDrawFrameElements.length; i += 1) {
element = /**@type{!Element}*/(clonedDrawFrameElements[i]);
presentationClass = element.getAttributeNS(presentationns, 'class');
if (presentationClass && ! /^(date-time|footer|header|page-number)$/.test(presentationClass)) {
element.parentNode.removeChild(element);
}
}
}
/**
* @param {!odf.OdfContainer} odfContainer
* @param {!Element} frame
* @param {!string} headerFooterId
* @return {?string}
*/
function getHeaderFooter(odfContainer, frame, headerFooterId) {
var headerFooter = null,
i,
declElements = odfContainer.rootElement.body.getElementsByTagNameNS(presentationns, headerFooterId+'-decl'),
headerFooterName = frame.getAttributeNS(presentationns, 'use-'+headerFooterId+'-name'),
element;
if (headerFooterName && declElements.length > 0) {
for (i = 0; i < declElements.length; i += 1) {
element = /**@type{!Element}*/(declElements[i]);
if (element.getAttributeNS(presentationns, 'name') === headerFooterName) {
headerFooter = element.textContent;
break;
}
}
}
return headerFooter;
}
/**
* @param {!Element} rootElement
* @param {string} ns
* @param {string} localName
* @param {?string} value
* @return {undefined}
*/
function setContainerValue(rootElement, ns, localName, value) {
var i, containerList,
document = rootElement.ownerDocument,
e;
containerList = domUtils.getElementsByTagNameNS(rootElement, ns, localName);
for (i = 0; i < containerList.length; i += 1) {
domUtils.removeAllChildNodes(containerList[i]);
if (value) {
e = /**@type{!Element}*/(containerList[i]);
e.appendChild(document.createTextNode(value));
}
}
}
/**
* @param {string} styleid
* @param {!Element} frame
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
**/
function setDrawElementPosition(styleid, frame, stylesheet) {
frame.setAttributeNS(webodfhelperns, 'styleid', styleid);
var rule,
anchor = frame.getAttributeNS(textns, 'anchor-type'),
x = frame.getAttributeNS(svgns, 'x'),
y = frame.getAttributeNS(svgns, 'y'),
width = frame.getAttributeNS(svgns, 'width'),
height = frame.getAttributeNS(svgns, 'height'),
minheight = frame.getAttributeNS(fons, 'min-height'),
minwidth = frame.getAttributeNS(fons, 'min-width');
if (anchor === "as-char") {
rule = 'display: inline-block;';
} else if (anchor || x || y) {
rule = 'position: absolute;';
} else if (width || height || minheight || minwidth) {
rule = 'display: block;';
}
if (x) {
rule += 'left: ' + x + ';';
}
if (y) {
rule += 'top: ' + y + ';';
}
if (width) {
rule += 'width: ' + width + ';';
}
if (height) {
rule += 'height: ' + height + ';';
}
if (minheight) {
rule += 'min-height: ' + minheight + ';';
}
if (minwidth) {
rule += 'min-width: ' + minwidth + ';';
}
if (rule) {
rule = 'draw|' + frame.localName + '[webodfhelper|styleid="' + styleid + '"] {' +
rule + '}';
stylesheet.insertRule(rule, stylesheet.cssRules.length);
}
}
/**
* @param {!Element} image
* @return {string}
**/
function getUrlFromBinaryDataElement(image) {
var node = image.firstChild;
while (node) {
if (node.namespaceURI === officens &&
node.localName === "binary-data") {
// TODO: detect mime-type, assuming png for now
// the base64 data can be pretty printed, hence we need remove all the line breaks and whitespaces
return "data:image/png;base64," + node.textContent.replace(/[\r\n\s]/g, '');
}
node = node.nextSibling;
}
return "";
}
/**
* @param {string} id
* @param {!odf.OdfContainer} container
* @param {!Element} image
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
**/
function setImage(id, container, image, stylesheet) {
image.setAttributeNS(webodfhelperns, 'styleid', id);
var url = image.getAttributeNS(xlinkns, 'href'),
/**@type{!odf.OdfPart}*/
part;
/**
* @param {?string} url
*/
function callback(url) {
var rule;
if (url) { // if part cannot be loaded, url is null
rule = "background-image: url(" + url + ");";
rule = 'draw|image[webodfhelper|styleid="' + id + '"] {' + rule + '}';
stylesheet.insertRule(rule, stylesheet.cssRules.length);
}
}
/**
* @param {!odf.OdfPart} p
*/
function onchange(p) {
callback(p.url);
}
// look for a office:binary-data
if (url) {
try {
part = container.getPart(url);
part.onchange = onchange;
part.load();
} catch (/**@type{*}*/e) {
runtime.log('slight problem: ' + String(e));
}
} else {
url = getUrlFromBinaryDataElement(image);
callback(url);
}
}
/**
* @param {!Element} odfbody
* @return {undefined}
*/
function formatParagraphAnchors(odfbody) {
var n,
i,
nodes = xpath.getODFElementsWithXPath(odfbody,
".//*[*[@text:anchor-type='paragraph']]",
odf.Namespaces.lookupNamespaceURI);
for (i = 0; i < nodes.length; i += 1) {
n = nodes[i];
if (n.setAttributeNS) {
n.setAttributeNS(webodfhelperns, "containsparagraphanchor", true);
}
}
}
/**
* Modify tables to support merged cells (col/row span)
* @param {!Element} odffragment
* @param {!string} documentns
* @return {undefined}
*/
function modifyTables(odffragment, documentns) {
var i,
tableCells,
node;
/**
* @param {!Element} node
* @return {undefined}
*/
function modifyTableCell(node) {
// If we have a cell which spans columns or rows,
// then add col-span or row-span attributes.
if (node.hasAttributeNS(tablens, "number-columns-spanned")) {
node.setAttributeNS(documentns, "colspan",
node.getAttributeNS(tablens, "number-columns-spanned"));
}
if (node.hasAttributeNS(tablens, "number-rows-spanned")) {
node.setAttributeNS(documentns, "rowspan",
node.getAttributeNS(tablens, "number-rows-spanned"));
}
}
tableCells = domUtils.getElementsByTagNameNS(odffragment, tablens, 'table-cell');
for (i = 0; i < tableCells.length; i += 1) {
node = /**@type{!Element}*/(tableCells[i]);
modifyTableCell(node);
}
}
/**
* Make the text:line-break elements behave like html br element.
* @param {!Element} odffragment
* @return {undefined}
*/
function modifyLineBreakElements(odffragment) {
var document = odffragment.ownerDocument,
lineBreakElements = domUtils.getElementsByTagNameNS(odffragment, textns, "line-break");
lineBreakElements.forEach(function (lineBreak) {
// Make sure we don't add br more than once as this method is executed whenever user undo an operation.
if (!lineBreak.hasChildNodes()) {
lineBreak.appendChild(document.createElement("br"));
}
});
}
/**
* Expand ODF spaces of the form <text:s text:c=N/> to N consecutive
* <text:s/> elements. This makes things simpler for WebODF during
* handling of spaces, in particular during editing.
* @param {!Element} odffragment
* @return {undefined}
*/
function expandSpaceElements(odffragment) {
var spaces,
doc = odffragment.ownerDocument;
/**
* @param {!Element} space
* @return {undefined}
*/
function expandSpaceElement(space) {
var j, count;
// If the space has any children, remove them and put a " " text
// node in place.
domUtils.removeAllChildNodes(space);
space.appendChild(doc.createTextNode(" "));
count = parseInt(space.getAttributeNS(textns, "c"), 10);
if (count > 1) {
// Make it a 'simple' space node
space.removeAttributeNS(textns, "c");
// Prepend count-1 clones of this space node to itself
for (j = 1; j < count; j += 1) {
space.parentNode.insertBefore(space.cloneNode(true), space);
}
}
}
spaces = domUtils.getElementsByTagNameNS(odffragment, textns, "s");
spaces.forEach(expandSpaceElement);
}
/**
* Expand tabs to contain tab characters. This eases cursor behaviour
* during editing
* @param {!Element} odffragment
*/
function expandTabElements(odffragment) {
var tabs;
tabs = domUtils.getElementsByTagNameNS(odffragment, textns, "tab");
tabs.forEach(function(tab) {
tab.textContent = "\t";
});
}
/**
* @param {!Element} odfbody
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
**/
function modifyDrawElements(odfbody, stylesheet) {
var node,
/**@type{!Array.<!Element>}*/
drawElements = [],
i;
// find all the draw:* elements
node = odfbody.firstElementChild;
while (node && node !== odfbody) {
if (node.namespaceURI === drawns) {
drawElements[drawElements.length] = node;
}
if (node.firstElementChild) {
node = node.firstElementChild;
} else {
while (node && node !== odfbody && !node.nextElementSibling) {
node = /**@type{!Element}*/(node.parentNode);
}
if (node && node.nextElementSibling) {
node = node.nextElementSibling;
}
}
}
// adjust all the frame positions
for (i = 0; i < drawElements.length; i += 1) {
node = drawElements[i];
setDrawElementPosition('frame' + String(i), node, stylesheet);
}
formatParagraphAnchors(odfbody);
}
/**
* @param {!odf.Formatting} formatting
* @param {!odf.OdfContainer} odfContainer
* @param {!Element} shadowContent
* @param {!Element} odfbody
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
**/
function cloneMasterPages(formatting, odfContainer, shadowContent, odfbody, stylesheet) {
var masterPageName,
masterPageElement,
styleId,
clonedPageElement,
clonedElement,
clonedDrawElements,
pageNumber = 0,
i,
element,
elementToClone,
document = odfContainer.rootElement.ownerDocument;
element = odfbody.firstElementChild;
// no master pages to expect?
if (!(element && element.namespaceURI === officens &&
(element.localName === "presentation" || element.localName === "drawing"))) {
return;
}
element = element.firstElementChild;
while (element) {
// If there was a master-page-name attribute, then we are dealing with a draw:page.
// Get the referenced master page element from the master styles
masterPageName = element.getAttributeNS(drawns, 'master-page-name');
masterPageElement = masterPageName ? formatting.getMasterPageElement(masterPageName) : null;
// If the referenced master page exists, create a new page and copy over it's contents into the new page,
// except for the ones that are placeholders. Also, call setDrawElementPosition on each of those child frames.
if (masterPageElement) {
styleId = element.getAttributeNS(webodfhelperns, 'styleid');
clonedPageElement = document.createElementNS(drawns, 'draw:page');
elementToClone = masterPageElement.firstElementChild;
i = 0;
while (elementToClone) {
if (elementToClone.getAttributeNS(presentationns, 'placeholder') !== 'true') {
clonedElement = /**@type{!Element}*/(elementToClone.cloneNode(true));
clonedPageElement.appendChild(clonedElement);
}
elementToClone = elementToClone.nextElementSibling;
i += 1;
}
// TODO: above already do not clone nodes which match the rule for being dropped
dropTemplateDrawFrames(clonedPageElement);
// Position all elements
clonedDrawElements = domUtils.getElementsByTagNameNS(clonedPageElement, drawns, '*');
for (i = 0; i < clonedDrawElements.length; i += 1) {
setDrawElementPosition(styleId + '_' + i, clonedDrawElements[i], stylesheet);
}
// Append the cloned master page to the "Shadow Content" element outside the main ODF dom
shadowContent.appendChild(clonedPageElement);
// Get the page number by counting the number of previous master pages in this shadowContent
pageNumber = String(shadowContent.getElementsByTagNameNS(drawns, 'page').length);
// Get the page-number tag in the cloned master page and set the text content to the calculated number
setContainerValue(clonedPageElement, textns, 'page-number', pageNumber);
// Care for header
setContainerValue(clonedPageElement, presentationns, 'header', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'header'));
// Care for footer
setContainerValue(clonedPageElement, presentationns, 'footer', getHeaderFooter(odfContainer, /**@type{!Element}*/(element), 'footer'));
// Now call setDrawElementPosition on this new page to set the proper dimensions
setDrawElementPosition(styleId, clonedPageElement, stylesheet);
// Add a custom attribute with the style name of the normal page, so the CSS rules created for the styles of the normal page
// to display/hide frames of certain classes from the master page can address the cloned master page belonging to that normal page
// Cmp. addDrawPageFrameDisplayRules in Style2CSS
clonedPageElement.setAttributeNS(webodfhelperns, 'page-style-name', element.getAttributeNS(drawns, 'style-name'));
// TODO: investigate if the attributes draw:style-name and style:page-layoutname should be copied over
// to the cloned page from the master page as well, or if this one below is enough already
// And finally, add an attribute referring to the master page, so the CSS targeted for that master page will style this
clonedPageElement.setAttributeNS(drawns, 'draw:master-page-name', masterPageElement.getAttributeNS(stylens, 'name'));
}
element = element.nextElementSibling;
}
}
/**
* @param {!odf.OdfContainer} container
* @param {!Element} plugin
* @return {undefined}
**/
function setVideo(container, plugin) {
var video, source, url, doc = plugin.ownerDocument,
/**@type{!odf.OdfPart}*/
part;
url = plugin.getAttributeNS(xlinkns, 'href');
/**
* @param {?string} url
* @param {string} mimetype
* @return {undefined}
*/
function callback(url, mimetype) {
var ns = doc.documentElement.namespaceURI;
// test for video mimetypes
if (mimetype.substr(0, 6) === 'video/') {
video = doc.createElementNS(ns, "video");
video.setAttribute('controls', 'controls');
source = doc.createElementNS(ns, 'source');
if (url) {
source.setAttribute('src', url);
}
source.setAttribute('type', mimetype);
video.appendChild(source);
plugin.parentNode.appendChild(video);
} else {
plugin.innerHtml = 'Unrecognised Plugin';
}
}
/**
* @param {!odf.OdfPart} p
*/
function onchange(p) {
callback(p.url, p.mimetype);
}
// look for a office:binary-data
if (url) {
try {
part = container.getPart(url);
part.onchange = onchange;
part.load();
} catch (/**@type{*}*/e) {
runtime.log('slight problem: ' + String(e));
}
} else {
// this will fail atm - following function assumes PNG data]
runtime.log('using MP4 data fallback');
url = getUrlFromBinaryDataElement(plugin);
callback(url, 'video/mp4');
}
}
/**
* @param {!HTMLHeadElement} head
* @return {?HTMLStyleElement}
*/
function findWebODFStyleSheet(head) {
var style = head.firstElementChild;
while (style && !(style.localName === "style"
&& style.hasAttribute("webodfcss"))) {
style = style.nextElementSibling;
}
return /**@type{?HTMLStyleElement}*/(style);
}
/**
* @param {!Document} document
* @return {!HTMLStyleElement}
*/
function addWebODFStyleSheet(document) {
var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
css,
/**@type{?HTMLStyleElement}*/
style,
href,
count = document.styleSheets.length;
// make sure this is only added once per HTML document, e.g. in case of
// multiple odfCanvases
style = findWebODFStyleSheet(head);
if (style) {
count = parseInt(style.getAttribute("webodfcss"), 10);
style.setAttribute("webodfcss", count + 1);
return style;
}
if (String(typeof webodf_css) === "string") {
css = /**@type{!string}*/(webodf_css);
} else {
href = "webodf.css";
if (runtime.currentDirectory) {
href = runtime.currentDirectory();
if (href.length > 0 && href.substr(-1) !== "/") {
href += "/";
}
href += "../webodf.css";
}
css = /**@type{!string}*/(runtime.readFileSync(href, "utf-8"));
}
style = /**@type{!HTMLStyleElement}*/(document.createElementNS(head.namespaceURI, 'style'));
style.setAttribute('media', 'screen, print, handheld, projection');
style.setAttribute('type', 'text/css');
style.setAttribute('webodfcss', '1');
style.appendChild(document.createTextNode(css));
head.appendChild(style);
return style;
}
/**
* @param {!HTMLStyleElement} webodfcss
* @return {undefined}
*/
function removeWebODFStyleSheet(webodfcss) {
var count = parseInt(webodfcss.getAttribute("webodfcss"), 10);
if (count === 1) {
webodfcss.parentNode.removeChild(webodfcss);
} else {
webodfcss.setAttribute("count", count - 1);
}
}
/**
* @param {!Document} document Put and ODF Canvas inside this element.
* @return {!HTMLStyleElement}
*/
function addStyleSheet(document) {
var head = /**@type{!HTMLHeadElement}*/(document.getElementsByTagName('head')[0]),
style = document.createElementNS(head.namespaceURI, 'style'),
/**@type{string}*/
text = '';
style.setAttribute('type', 'text/css');
style.setAttribute('media', 'screen, print, handheld, projection');
odf.Namespaces.forEachPrefix(function(prefix, ns) {
text += "@namespace " + prefix + " url(" + ns + ");\n";
});
text += "@namespace webodfhelper url(" + webodfhelperns + ");\n";
style.appendChild(document.createTextNode(text));
head.appendChild(style);
return /**@type {!HTMLStyleElement}*/(style);
}
/**
* This class manages a loaded ODF document that is shown in an element.
* It takes care of giving visual feedback on loading, ensures that the
* stylesheets are loaded.
* @constructor
* @implements {gui.AnnotatableCanvas}
* @implements {ops.Canvas}
* @implements {core.Destroyable}
* @param {!HTMLElement} element Put and ODF Canvas inside this element.
* @param {!gui.Viewport=} viewport Viewport used for scrolling elements and ranges into view
*/
odf.OdfCanvas = function OdfCanvas(element, viewport) {
runtime.assert((element !== null) && (element !== undefined),
"odf.OdfCanvas constructor needs DOM element");
runtime.assert((element.ownerDocument !== null) && (element.ownerDocument !== undefined),
"odf.OdfCanvas constructor needs DOM");
var self = this,
doc = /**@type{!Document}*/(element.ownerDocument),
/**@type{!odf.OdfContainer}*/
odfcontainer,
/**@type{!odf.Formatting}*/
formatting = new odf.Formatting(),
/**@type{!PageSwitcher}*/
pageSwitcher,
/**@type{HTMLDivElement}*/
sizer = null,
/**@type{HTMLDivElement}*/
annotationsPane = null,
allowAnnotations = false,
showAnnotationRemoveButton = false,
/**@type{gui.AnnotationViewManager}*/
annotationViewManager = null,
/**@type{!HTMLStyleElement}*/
webodfcss,
/**@type{!HTMLStyleElement}*/
fontcss,
/**@type{!HTMLStyleElement}*/
stylesxmlcss,
/**@type{!HTMLStyleElement}*/
positioncss,
shadowContent,
/**@type{!Object.<string,!Array.<!Function>>}*/
eventHandlers = {},
waitingForDoneTimeoutId,
/**@type{!core.ScheduledTask}*/redrawContainerTask,
shouldRefreshCss = false,
shouldRerenderAnnotations = false,
loadingQueue = new LoadingQueue(),
/**@type{!gui.ZoomHelper}*/
zoomHelper = new gui.ZoomHelper(),
/**@type{!gui.Viewport}*/
canvasViewport = viewport || new gui.SingleScrollViewport(/**@type{!HTMLElement}*/(element.parentNode));
/**
* Load all the images that are inside an odf element.
* @param {!odf.OdfContainer} container
* @param {!Element} odffragment
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
*/
function loadImages(container, odffragment, stylesheet) {
var i,
images,
node;
/**
* Do delayed loading for all the images
* @param {string} name
* @param {!odf.OdfContainer} container
* @param {!Element} node
* @param {!CSSStyleSheet} stylesheet
* @return {undefined}
*/
function loadImage(name, container, node, stylesheet) {
// load image with a small delay to give the html ui a chance to
// update
loadingQueue.addToQueue(function () {
setImage(name, container, node, stylesheet);
});
}
images = odffragment.getElementsByTagNameNS(drawns, 'image');
for (i = 0; i < images.length; i += 1) {
node = /**@type{!Element}*/(images.item(i));
loadImage('image' + String(i), container, node, stylesheet);
}
}
/**
* Load all the video that are inside an odf element.
* @param {!odf.OdfContainer} container
* @param {!Element} odffragment
* @return {undefined}
*/
function loadVideos(container, odffragment) {
var i,
plugins,
node;
/**
* Do delayed loading for all the videos
* @param {!odf.OdfContainer} container
* @param {!Element} node
* @return {undefined}
*/
function loadVideo(container, node) {
// load video with a small delay to give the html ui a chance to
// update
loadingQueue.addToQueue(function () {
setVideo(container, node);
});
}
// embedded video is stored in a draw:plugin element
plugins = odffragment.getElementsByTagNameNS(drawns, 'plugin');
for (i = 0; i < plugins.length; i += 1) {
node = /**@type{!Element}*/(plugins.item(i));
loadVideo(container, node);
}
}
/**
* Register an event handler
* @param {!string} eventType
* @param {!Function} eventHandler
* @return {undefined}
*/
function addEventListener(eventType, eventHandler) {
var handlers;
if (eventHandlers.hasOwnProperty(eventType)) {
handlers = eventHandlers[eventType];
} else {
handlers = eventHandlers[eventType] = [];
}
if (eventHandler && handlers.indexOf(eventHandler) === -1) {
handlers.push(eventHandler);
}
}
/**
* Fire an event
* @param {!string} eventType
* @param {Array.<Object>=} args
* @return {undefined}
*/
function fireEvent(eventType, args) {
if (!eventHandlers.hasOwnProperty(eventType)) {
return;
}
var handlers = eventHandlers[eventType], i;
for (i = 0; i < handlers.length; i += 1) {
handlers[i].apply(null, args);
}
}
/**
* @return {undefined}
*/
function fixContainerSize() {
var minHeight,
odfdoc = sizer.firstChild,
zoomLevel = zoomHelper.getZoomLevel();
if (!odfdoc) {
return;
}
// All zooming of the sizer within the canvas
// is done relative to the top-left corner.
sizer.style.WebkitTransformOrigin = "0% 0%";
sizer.style.MozTransformOrigin = "0% 0%";
sizer.style.msTransformOrigin = "0% 0%";
sizer.style.OTransformOrigin = "0% 0%";
sizer.style.transformOrigin = "0% 0%";
if (annotationViewManager) {
minHeight = annotationViewManager.getMinimumHeightForAnnotationPane();
if (minHeight) {
sizer.style.minHeight = minHeight;
} else {
sizer.style.removeProperty('min-height');
}
}
element.style.width = Math.round(zoomLevel * sizer.offsetWidth) + "px";
element.style.height = Math.round(zoomLevel * sizer.offsetHeight) + "px";
// Re-apply inline-block to canvas element on resizing.
// Chrome tends to forget this property after a relayout
element.style.display = "inline-block";
}
/**
* @return {undefined}
*/
function redrawContainer() {
if (shouldRefreshCss) {
handleStyles(odfcontainer, formatting, stylesxmlcss);
shouldRefreshCss = false;
// different styles means different layout, thus different sizes
}
if (shouldRerenderAnnotations) {
if (annotationViewManager) {
annotationViewManager.rerenderAnnotations();
}
shouldRerenderAnnotations = false;
}
fixContainerSize();
}
/**
* A new content.xml has been loaded. Update the live document with it.
* @param {!odf.OdfContainer} container
* @param {!odf.ODFDocumentElement} odfnode
* @return {undefined}
**/
function handleContent(container, odfnode) {
var css = /**@type{!CSSStyleSheet}*/(positioncss.sheet);
// only append the content at the end
domUtils.removeAllChildNodes(element);
sizer = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
sizer.style.display = "inline-block";
sizer.style.background = "white";
// When the window is shrunk such that the
// canvas container has a horizontal scrollbar,
// zooming out seems to not make the scrollable
// width disappear. This extra scrollable
// width seems to be proportional to the
// annotation pane's width. Setting the 'float'
// of the sizer to 'left' fixes this in webkit.
sizer.style.setProperty("float", "left", "important");
sizer.appendChild(odfnode);
element.appendChild(sizer);
// An annotations pane div. Will only be shown when annotations are enabled
annotationsPane = /**@type{!HTMLDivElement}*/(doc.createElementNS(element.namespaceURI, 'div'));
annotationsPane.id = "annotationsPane";
// A "Shadow Content" div. This will contain stuff like pages
// extracted from <style:master-page>. These need to be nicely
// styled, so we will populate this in the ODF body first. Once the
// styling is handled, it can then be lifted out of the
// ODF body and placed beside it, to not pollute the ODF dom.
shadowContent = doc.createElementNS(element.namespaceURI, 'div');
shadowContent.id = "shadowContent";
shadowContent.style.position = 'absolute';
shadowContent.style.top = 0;
shadowContent.style.left = 0;
container.getContentElement().appendChild(shadowContent);
modifyDrawElements(odfnode.body, css);
cloneMasterPages(formatting, container, shadowContent, odfnode.body, css);
modifyTables(odfnode.body, element.namespaceURI);
modifyLineBreakElements(odfnode.body);
expandSpaceElements(odfnode.body);
expandTabElements(odfnode.body);
loadImages(container, odfnode.body, css);
loadVideos(container, odfnode.body);
sizer.insertBefore(shadowContent, sizer.firstChild);
zoomHelper.setZoomableElement(sizer);
}
/**
* This should create an annotations pane if non existent, and then populate it with annotations
* If annotations are disallowed, it should remove the pane and all annotations
* @param {!odf.ODFDocumentElement} odfnode
*/
function handleAnnotations(odfnode) {
var annotationNodes;
if (allowAnnotations) {
if (!annotationsPane.parentNode) {
sizer.appendChild(annotationsPane);
}
if (annotationViewManager) {
annotationViewManager.forgetAnnotations();
}
annotationViewManager = new gui.AnnotationViewManager(self, odfnode.body, annotationsPane, showAnnotationRemoveButton);
annotationNodes = /**@type{!Array.<!odf.AnnotationElement>}*/(domUtils.getElementsByTagNameNS(odfnode.body, officens, 'annotation'));
annotationViewManager.addAnnotations(annotationNodes);
fixContainerSize();
} else {
if (annotationsPane.parentNode) {
sizer.removeChild(annotationsPane);
annotationViewManager.forgetAnnotations();
fixContainerSize();
}
}
}
/**
* @param {boolean} suppressEvent Suppress the statereadychange event from firing. Used for refreshing the OdtContainer
* @return {undefined}
**/
function refreshOdf(suppressEvent) {
// synchronize the object a window.odfcontainer with the view
function callback() {
// clean up
clearCSSStyleSheet(fontcss);
clearCSSStyleSheet(stylesxmlcss);
clearCSSStyleSheet(positioncss);
domUtils.removeAllChildNodes(element);
// setup
element.style.display = "inline-block";
var odfnode = odfcontainer.rootElement;
element.ownerDocument.importNode(odfnode, true);
formatting.setOdfContainer(odfcontainer);
handleFonts(odfcontainer, fontcss);
handleStyles(odfcontainer, formatting, stylesxmlcss);
// do content last, because otherwise the document is constantly
// updated whenever the css changes
handleContent(odfcontainer, odfnode);
handleAnnotations(odfnode);
if (!suppressEvent) {
loadingQueue.addToQueue(function () {
fireEvent("statereadychange", [odfcontainer]);
});
}
}
if (odfcontainer.state === odf.OdfContainer.DONE) {
callback();
} else {
// so the ODF is not done yet. take care that we'll
// do the work once it is done:
// FIXME: use callback registry instead of replacing the onchange
runtime.log("WARNING: refreshOdf called but ODF was not DONE.");
waitingForDoneTimeoutId = runtime.setTimeout(function later_cb() {
if (odfcontainer.state === odf.OdfContainer.DONE) {
callback();
} else {
runtime.log("will be back later...");
waitingForDoneTimeoutId = runtime.setTimeout(later_cb, 500);
}
}, 100);
}
}
/**
* Updates the CSS rules to match the ODF document styles and also
* updates the size of the canvas to match the new layout.
* Needs to be called after changes to the styles of the ODF document.
* @return {undefined}
*/
this.refreshCSS = function () {
shouldRefreshCss = true;
redrawContainerTask.trigger();
};
/**
* Updates the size of the canvas to the size of the content.
* Needs to be called after changes to the content of the ODF document.
* @return {undefined}
*/
this.refreshSize = function () {
redrawContainerTask.trigger();
};
/**
* @return {!odf.OdfContainer}
*/
this.odfContainer = function () {
return odfcontainer;
};
/**
* Set a odfcontainer manually.
* @param {!odf.OdfContainer} container
* @param {boolean=} suppressEvent Default value is false
* @return {undefined}
*/
this.setOdfContainer = function (container, suppressEvent) {
odfcontainer = container;
refreshOdf(suppressEvent === true);
};
/**
* @param {string} url
* @return {undefined}
*/
function load(url) {
// clean up
loadingQueue.clearQueue();
// FIXME: We need to support parametrized strings, because
// drop-in word replacements are inadequate for translations;
// see http://techbase.kde.org/Development/Tutorials/Localization/i18n_Mistakes#Pitfall_.232:_Word_Puzzles
domUtils.removeAllChildNodes(element);
element.appendChild(element.ownerDocument.createTextNode(runtime.tr('Loading') + url + '...'));
element.removeAttribute('style');
// open the odf container
odfcontainer = new odf.OdfContainer(url, function (container) {
// assignment might be necessary if the callback
// fires before the assignment above happens.
odfcontainer = container;
refreshOdf(false);
});
}
this["load"] = load;
this.load = load;
/**
* @param {function(?string):undefined} callback
* @return {undefined}
*/
this.save = function (callback) {
odfcontainer.save(callback);
};
/**
* @param {!string} eventName
* @param {!function(*)} handler
* @return {undefined}
*/
this.addListener = function (eventName, handler) {
switch (eventName) {
case "click":
listenEvent(element, eventName, handler); break;
default:
addEventListener(eventName, handler); break;
}
};
/**
* @return {!odf.Formatting}
*/
this.getFormatting = function () {
return formatting;
};
/**
* @return {gui.AnnotationViewManager}
*/
this.getAnnotationViewManager = function () {
return annotationViewManager;
};
/**
* Unstyles and untracks all annotations present in the document,
* and then tracks them again with fresh rendering
* @return {undefined}
*/
this.refreshAnnotations = function () {
handleAnnotations(odfcontainer.rootElement);
};
/**
* Re-renders all annotations if enabled
* @return {undefined}
*/
this.rerenderAnnotations = function () {
if (annotationViewManager) {
shouldRerenderAnnotations = true;
redrawContainerTask.trigger();
}
};
/**
* This returns the element inside the canvas which can be zoomed with
* CSS and which contains the ODF document and the annotation sidebar.
* @return {!HTMLElement}
*/
this.getSizer = function () {
return /**@type{!HTMLElement}*/(sizer);
};
/** Allows / disallows annotations
* @param {!boolean} allow
* @param {!boolean} showRemoveButton
* @return {undefined}
*/
this.enableAnnotations = function (allow, showRemoveButton) {
if (allow !== allowAnnotations) {
allowAnnotations = allow;
showAnnotationRemoveButton = showRemoveButton;
if (odfcontainer) {
handleAnnotations(odfcontainer.rootElement);
}
}
};
/**
* Adds an annotation for the annotaiton manager to track
* and wraps and highlights it
* @param {!odf.AnnotationElement} annotation
* @return {undefined}
*/
this.addAnnotation = function (annotation) {
if (annotationViewManager) {
annotationViewManager.addAnnotations([annotation]);
fixContainerSize();