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