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*/ 26 /*jslint nomen: true, evil: true, bitwise: true */ 27 28 /** 29 * This operation splits the paragraph at the given 30 * position. If the `moveCursor` flag is specified 31 * and is set as true, the cursor is moved to the 32 * beginning of the next paragraph. Otherwise, it 33 * remains in it's original position. 34 * The paragraph style for the new paragraph is specified by the 35 * paragraphStyleName. If blank or empty, the new paragraph will 36 * have no specified style. 37 * 38 * @constructor 39 * @implements ops.Operation 40 */ 41 ops.OpSplitParagraph = function OpSplitParagraph() { 42 "use strict"; 43 44 var memberid, timestamp, 45 /**@type{number}*/ 46 sourceParagraphPosition, 47 /**@type{number}*/ 48 position, 49 /**@type{boolean}*/ 50 moveCursor, 51 /**@type{!string}*/ 52 paragraphStyleName, 53 odfUtils = odf.OdfUtils, 54 /**@const*/ 55 textns = odf.Namespaces.textns; 56 57 /** 58 * @param {!ops.OpSplitParagraph.InitSpec} data 59 */ 60 this.init = function (data) { 61 memberid = data.memberid; 62 timestamp = data.timestamp; 63 position = data.position; 64 sourceParagraphPosition = data.sourceParagraphPosition; 65 paragraphStyleName = data.paragraphStyleName; 66 moveCursor = data.moveCursor === 'true' || data.moveCursor === true; 67 }; 68 69 this.isEdit = true; 70 this.group = undefined; 71 72 /** 73 * @param {!ops.Document} document 74 */ 75 this.execute = function (document) { 76 var odtDocument = /**@type{!ops.OdtDocument}*/(document), 77 domPosition, paragraphNode, targetNode, 78 node, splitNode, splitChildNode, keptChildNode, 79 cursor = odtDocument.getCursor(memberid); 80 81 odtDocument.upgradeWhitespacesAtPosition(position); 82 domPosition = odtDocument.getTextNodeAtStep(position); 83 if (!domPosition) { 84 return false; 85 } 86 87 paragraphNode = odfUtils.getParagraphElement(domPosition.textNode); 88 if (!paragraphNode) { 89 return false; 90 } 91 92 if (odfUtils.isListItem(paragraphNode.parentNode)) { 93 targetNode = paragraphNode.parentNode; 94 } else { 95 targetNode = paragraphNode; 96 } 97 98 // There can be a chain of multiple nodes between the text node 99 // where the split is done and the containing paragraph nodes, 100 // e.g. text:span nodes 101 // So all nodes in this chain need to be split up, i.e. they need 102 // to be cloned, and then the clone and any next siblings have to 103 // be moved to the new paragraph node, which is also cloned from 104 // the current one. 105 106 // start with text node the cursor is in, needs special treatment 107 // if text node is split at the beginning, do not split but simply 108 // move the whole text node 109 if (domPosition.offset === 0) { 110 keptChildNode = domPosition.textNode.previousSibling; 111 splitChildNode = null; 112 } else { 113 keptChildNode = domPosition.textNode; 114 // if text node is to be split at the end, don't split at all 115 if (domPosition.offset >= domPosition.textNode.length) { 116 splitChildNode = null; 117 } else { 118 // splitText always returns {!Text} here 119 splitChildNode = /**@type{!Text}*/( 120 domPosition.textNode.splitText(domPosition.offset) 121 ); 122 } 123 } 124 125 // then handle all nodes until (incl.) the paragraph node: 126 // create a clone and add as childs the split node of the node below 127 // and any next siblings of it 128 node = domPosition.textNode; 129 while (node !== targetNode) { 130 node = node.parentNode; 131 132 // split off the node copy 133 // TODO: handle unique attributes, e.g. xml:id 134 splitNode = node.cloneNode(false); 135 // add the split child node 136 if (splitChildNode) { 137 splitNode.appendChild(splitChildNode); 138 } 139 if (keptChildNode) { 140 // Move all child nodes that should appear after the split to the new node 141 while (keptChildNode && keptChildNode.nextSibling) { 142 splitNode.appendChild(keptChildNode.nextSibling); 143 } 144 } else { 145 // All children of the original node should be moved after the split 146 while (node.firstChild) { 147 splitNode.appendChild(node.firstChild); 148 } 149 } 150 node.parentNode.insertBefore(splitNode, node.nextSibling); 151 152 // prepare next level 153 keptChildNode = node; 154 splitChildNode = splitNode; 155 } 156 157 if (odfUtils.isListItem(splitChildNode)) { 158 splitChildNode = splitChildNode.childNodes.item(0); 159 } 160 161 if (paragraphStyleName) { 162 /**@type{!Element}*/(splitChildNode).setAttributeNS(textns, "text:style-name", paragraphStyleName); 163 } else { 164 /**@type{!Element}*/(splitChildNode).removeAttributeNS(textns, "style-name"); 165 } 166 167 // clean up any empty text node which was created by odtDocument.getTextNodeAtStep 168 if (domPosition.textNode.length === 0) { 169 domPosition.textNode.parentNode.removeChild(domPosition.textNode); 170 } 171 odtDocument.emit(ops.OdtDocument.signalStepsInserted, {position: position}); 172 173 if (cursor && moveCursor) { 174 odtDocument.moveCursor(memberid, position + 1, 0); 175 odtDocument.emit(ops.Document.signalCursorMoved, cursor); 176 } 177 178 odtDocument.fixCursorPositions(); 179 odtDocument.getOdfCanvas().refreshSize(); 180 // mark both paragraphs as edited 181 odtDocument.emit(ops.OdtDocument.signalParagraphChanged, { 182 paragraphElement: paragraphNode, 183 memberId: memberid, 184 timeStamp: timestamp 185 }); 186 odtDocument.emit(ops.OdtDocument.signalParagraphChanged, { 187 paragraphElement: splitChildNode, 188 memberId: memberid, 189 timeStamp: timestamp 190 }); 191 192 odtDocument.getOdfCanvas().rerenderAnnotations(); 193 return true; 194 }; 195 196 /** 197 * @return {!ops.OpSplitParagraph.Spec} 198 */ 199 this.spec = function () { 200 return { 201 optype: "SplitParagraph", 202 memberid: memberid, 203 timestamp: timestamp, 204 position: position, 205 sourceParagraphPosition: sourceParagraphPosition, 206 paragraphStyleName: paragraphStyleName, 207 moveCursor: moveCursor 208 }; 209 }; 210 }; 211 /**@typedef{{ 212 optype:string, 213 memberid:string, 214 timestamp:number, 215 position:number, 216 sourceParagraphPosition:number, 217 paragraphStyleName:string, 218 moveCursor:boolean 219 }}*/ 220 ops.OpSplitParagraph.Spec; 221 /**@typedef{{ 222 memberid:string, 223 timestamp:(number|undefined), 224 position:number, 225 sourceParagraphPosition:number, 226 paragraphStyleName:string, 227 moveCursor:(string|boolean|undefined) 228 }}*/ 229 ops.OpSplitParagraph.InitSpec; 230