node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
249 lines (224 loc) • 9.25 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 ops, odf, runtime*/
/**
* This operation inserts the given text
* at the specified position, and if
* the moveCursor flag is specified and
* is set as true, moves the cursor to
* the end of the inserted text.
* Otherwise, the cursor remains at the
* same position as before.
* @constructor
* @implements ops.Operation
*/
ops.OpInsertText = function OpInsertText() {
"use strict";
var tab = "\t",
memberid,
timestamp,
/**@type{number}*/
position,
/**@type{boolean}*/
moveCursor,
/**@type{string}*/
text,
odfUtils = odf.OdfUtils;
/**
* @param {!ops.OpInsertText.InitSpec} data
*/
this.init = function (data) {
memberid = data.memberid;
timestamp = data.timestamp;
position = data.position;
text = data.text;
moveCursor = data.moveCursor === 'true' || data.moveCursor === true;
};
this.isEdit = true;
this.group = undefined;
/**
* This is a workaround for a bug where webkit forgets to relayout
* the text when a new character is inserted at the beginning of a line in
* a Text Node.
* @param {!Node} textNode
* @return {undefined}
*/
function triggerLayoutInWebkit(textNode) {
var parent = textNode.parentNode,
next = textNode.nextSibling;
parent.removeChild(textNode);
parent.insertBefore(textNode, next);
}
/**
* Returns true if the supplied character is a non-tab ODF whitespace character
* @param {!string} character
* @return {!boolean}
*/
function isNonTabWhiteSpace(character) {
return character !== tab && odfUtils.isODFWhitespace(character);
}
/**
* Returns true if the particular character in the text string is a space character that is immediately
* preceded by another space character (or is the first or last space in the text block).
* Logic is based on http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#element-text_s
* @param {!string} text
* @param {!number} index
* @return {boolean}
*/
function requiresSpaceElement(text, index) {
return isNonTabWhiteSpace(text[index]) && (index === 0 || index === text.length - 1 || isNonTabWhiteSpace(text[index - 1]));
}
/**
* @param {!ops.Document} document
*/
this.execute = function (document) {
var odtDocument = /**@type{ops.OdtDocument}*/(document),
domPosition,
previousNode,
/**@type{!Element}*/
parentElement,
nextNode = null,
ownerDocument = odtDocument.getDOMDocument(),
paragraphElement,
textns = "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
toInsertIndex = 0,
spaceElement,
cursor = odtDocument.getCursor(memberid),
i;
/**
* @param {string} toInsertText
*/
function insertTextNode(toInsertText) {
parentElement.insertBefore(ownerDocument.createTextNode(toInsertText), nextNode);
}
odtDocument.upgradeWhitespacesAtPosition(position);
domPosition = odtDocument.getTextNodeAtStep(position);
if (domPosition) {
previousNode = domPosition.textNode;
nextNode = previousNode.nextSibling;
parentElement = /**@type{!Element}*/(previousNode.parentNode);
paragraphElement = odfUtils.getParagraphElement(previousNode);
// first do the insertion with any contained tabs or spaces
for (i = 0; i < text.length; i += 1) {
if (text[i] === tab || requiresSpaceElement(text, i)) {
// no nodes inserted yet?
if (toInsertIndex === 0) {
// if inserting in the middle the given text node needs to be split up
// if previousNode becomes empty, it will be cleaned up on finishing
if (domPosition.offset !== previousNode.length) {
nextNode = previousNode.splitText(domPosition.offset);
}
// normal text to insert before this space?
if (0 < i) {
previousNode.appendData(text.substring(0, i));
}
} else {
// normal text to insert before this space?
if (toInsertIndex < i) {
insertTextNode(text.substring(toInsertIndex, i));
}
}
toInsertIndex = i + 1;
// insert appropriate spacing element
if (text[i] === tab) {
spaceElement = ownerDocument.createElementNS(textns, "text:tab");
spaceElement.appendChild(ownerDocument.createTextNode("\t"));
} else {
if (text[i] !== " ") {
runtime.log("WARN: InsertText operation contains non-tab, non-space whitespace character (character code " + text.charCodeAt(i) + ")");
}
spaceElement = ownerDocument.createElementNS(textns, "text:s");
spaceElement.appendChild(ownerDocument.createTextNode(" "));
}
parentElement.insertBefore(spaceElement, nextNode);
}
}
// then insert rest
// text can be completely inserted, no spaces/tabs?
if (toInsertIndex === 0) {
previousNode.insertData(domPosition.offset, text);
} else if (toInsertIndex < text.length) {
insertTextNode(text.substring(toInsertIndex));
}
// FIXME A workaround.
triggerLayoutInWebkit(previousNode);
// Clean up the possibly created empty text node
if (previousNode.length === 0) {
previousNode.parentNode.removeChild(previousNode);
}
odtDocument.emit(ops.OdtDocument.signalStepsInserted, {position: position});
if (cursor && moveCursor) {
// Explicitly place the cursor in the desired position after insertion
// TODO: At the moment the inserted text already appears before the
// cursor, so the cursor is effectively at position + text.length
// already. So this ought to be optimized, by perhaps removing
// the textnode + cursor reordering logic from OdtDocument's
// getTextNodeAtStep.
odtDocument.moveCursor(memberid, position + text.length, 0);
odtDocument.emit(ops.Document.signalCursorMoved, cursor);
}
odtDocument.downgradeWhitespacesAtPosition(position);
odtDocument.downgradeWhitespacesAtPosition(position + text.length);
odtDocument.getOdfCanvas().refreshSize();
odtDocument.emit(ops.OdtDocument.signalParagraphChanged, {
paragraphElement: paragraphElement,
memberId: memberid,
timeStamp: timestamp
});
odtDocument.getOdfCanvas().rerenderAnnotations();
return true;
}
return false;
};
/**
* @return {!ops.OpInsertText.Spec}
*/
this.spec = function () {
return {
optype: "InsertText",
memberid: memberid,
timestamp: timestamp,
position: position,
text: text,
moveCursor: moveCursor
};
};
};
/**@typedef{{
optype:string,
memberid:string,
timestamp:number,
position:number,
text:string,
moveCursor:boolean
}}*/
ops.OpInsertText.Spec;
/**@typedef{{
memberid:string,
timestamp:(number|undefined),
position:number,
text:string,
moveCursor:(string|boolean|undefined)
}}*/
ops.OpInsertText.InitSpec;