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 ops, odf, runtime*/
 26 
 27 /**
 28  * This operation inserts the given text
 29  * at the specified position, and if
 30  * the moveCursor flag is specified and
 31  * is set as true, moves the cursor to
 32  * the end of the inserted text.
 33  * Otherwise, the cursor remains at the
 34  * same position as before.
 35  * @constructor
 36  * @implements ops.Operation
 37  */
 38 ops.OpInsertText = function OpInsertText() {
 39     "use strict";
 40 
 41     var tab = "\t",
 42         memberid,
 43         timestamp,
 44         /**@type{number}*/
 45         position,
 46         /**@type{boolean}*/
 47         moveCursor,
 48         /**@type{string}*/
 49         text,
 50         odfUtils = odf.OdfUtils;
 51 
 52     /**
 53      * @param {!ops.OpInsertText.InitSpec} data
 54      */
 55     this.init = function (data) {
 56         memberid = data.memberid;
 57         timestamp = data.timestamp;
 58         position = data.position;
 59         text = data.text;
 60         moveCursor = data.moveCursor === 'true' || data.moveCursor === true;
 61     };
 62 
 63     this.isEdit = true;
 64     this.group = undefined;
 65 
 66     /**
 67      * This is a workaround for a bug where webkit forgets to relayout
 68      * the text when a new character is inserted at the beginning of a line in
 69      * a Text Node.
 70      * @param {!Node} textNode
 71      * @return {undefined}
 72      */
 73     function triggerLayoutInWebkit(textNode) {
 74         var parent = textNode.parentNode,
 75             next = textNode.nextSibling;
 76 
 77         parent.removeChild(textNode);
 78         parent.insertBefore(textNode, next);
 79     }
 80 
 81     /**
 82      * Returns true if the supplied character is a non-tab ODF whitespace character
 83      * @param {!string} character
 84      * @return {!boolean}
 85      */
 86     function isNonTabWhiteSpace(character) {
 87         return character !== tab && odfUtils.isODFWhitespace(character);
 88     }
 89 
 90     /**
 91      * Returns true if the particular character in the text string is a space character that is immediately
 92      * preceded by another space character (or is the first or last space in the text block).
 93      * Logic is based on http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#element-text_s
 94      * @param {!string} text
 95      * @param {!number} index
 96      * @return {boolean}
 97      */
 98     function requiresSpaceElement(text, index) {
 99         return isNonTabWhiteSpace(text[index]) && (index === 0 || index === text.length - 1 || isNonTabWhiteSpace(text[index - 1]));
100     }
101 
102     /**
103      * @param {!ops.Document} document
104      */
105     this.execute = function (document) {
106         var odtDocument = /**@type{ops.OdtDocument}*/(document),
107             domPosition,
108             previousNode,
109             /**@type{!Element}*/
110             parentElement,
111             nextNode = null,
112             ownerDocument = odtDocument.getDOMDocument(),
113             paragraphElement,
114             textns = "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
115             toInsertIndex = 0,
116             spaceElement,
117             cursor = odtDocument.getCursor(memberid),
118             i;
119 
120         /**
121          * @param {string} toInsertText
122          */
123         function insertTextNode(toInsertText) {
124             parentElement.insertBefore(ownerDocument.createTextNode(toInsertText), nextNode);
125         }
126 
127         odtDocument.upgradeWhitespacesAtPosition(position);
128         domPosition = odtDocument.getTextNodeAtStep(position);
129 
130         if (domPosition) {
131             previousNode = domPosition.textNode;
132             nextNode = previousNode.nextSibling;
133             parentElement = /**@type{!Element}*/(previousNode.parentNode);
134             paragraphElement = odfUtils.getParagraphElement(previousNode);
135 
136             // first do the insertion with any contained tabs or spaces
137             for (i = 0; i < text.length; i += 1) {
138                 if (text[i] === tab || requiresSpaceElement(text, i)) {
139                     // no nodes inserted yet?
140                     if (toInsertIndex === 0) {
141                         // if inserting in the middle the given text node needs to be split up
142                         // if previousNode becomes empty, it will be cleaned up on finishing
143                         if (domPosition.offset !== previousNode.length) {
144                             nextNode = previousNode.splitText(domPosition.offset);
145                         }
146                         // normal text to insert before this space?
147                         if (0 < i) {
148                             previousNode.appendData(text.substring(0, i));
149                         }
150                     } else {
151                         // normal text to insert before this space?
152                         if (toInsertIndex < i) {
153                             insertTextNode(text.substring(toInsertIndex, i));
154                         }
155                     }
156                     toInsertIndex = i + 1;
157 
158                     // insert appropriate spacing element
159                     if (text[i] === tab) {
160                         spaceElement = ownerDocument.createElementNS(textns, "text:tab");
161                         spaceElement.appendChild(ownerDocument.createTextNode("\t"));
162                     } else {
163                         if (text[i] !== " ") {
164                             runtime.log("WARN: InsertText operation contains non-tab, non-space whitespace character (character code " + text.charCodeAt(i) + ")");
165                         }
166                         spaceElement = ownerDocument.createElementNS(textns, "text:s");
167                         spaceElement.appendChild(ownerDocument.createTextNode(" "));
168                     }
169                     parentElement.insertBefore(spaceElement, nextNode);
170                 }
171             }
172 
173             // then insert rest
174             // text can be completely inserted, no spaces/tabs?
175             if (toInsertIndex === 0) {
176                 previousNode.insertData(domPosition.offset, text);
177             } else if (toInsertIndex < text.length) {
178                 insertTextNode(text.substring(toInsertIndex));
179             }
180 
181             // FIXME A workaround.
182             triggerLayoutInWebkit(previousNode);
183 
184             // Clean up the possibly created empty text node
185             if (previousNode.length === 0) {
186                 previousNode.parentNode.removeChild(previousNode);
187             }
188 
189             odtDocument.emit(ops.OdtDocument.signalStepsInserted, {position: position});
190 
191             if (cursor && moveCursor) {
192                 // Explicitly place the cursor in the desired position after insertion
193                 // TODO: At the moment the inserted text already appears before the
194                 // cursor, so the cursor is effectively at position + text.length
195                 // already. So this ought to be optimized, by perhaps removing
196                 // the textnode + cursor reordering logic from OdtDocument's
197                 // getTextNodeAtStep.
198                 odtDocument.moveCursor(memberid, position + text.length, 0);
199                 odtDocument.emit(ops.Document.signalCursorMoved, cursor);
200             }
201 
202             odtDocument.downgradeWhitespacesAtPosition(position);
203             odtDocument.downgradeWhitespacesAtPosition(position + text.length);
204 
205             odtDocument.getOdfCanvas().refreshSize();
206             odtDocument.emit(ops.OdtDocument.signalParagraphChanged, {
207                 paragraphElement: paragraphElement,
208                 memberId: memberid,
209                 timeStamp: timestamp
210             });
211 
212             odtDocument.getOdfCanvas().rerenderAnnotations();
213             return true;
214         }
215         return false;
216     };
217 
218     /**
219      * @return {!ops.OpInsertText.Spec}
220      */
221     this.spec = function () {
222         return {
223             optype: "InsertText",
224             memberid: memberid,
225             timestamp: timestamp,
226             position: position,
227             text: text,
228             moveCursor: moveCursor
229         };
230     };
231 };
232 /**@typedef{{
233     optype:string,
234     memberid:string,
235     timestamp:number,
236     position:number,
237     text:string,
238     moveCursor:boolean
239 }}*/
240 ops.OpInsertText.Spec;
241 /**@typedef{{
242     memberid:string,
243     timestamp:(number|undefined),
244     position:number,
245     text:string,
246     moveCursor:(string|boolean|undefined)
247 }}*/
248 ops.OpInsertText.InitSpec;
249