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