1 /**
  2  * Copyright (C) 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 runtime, gui, ops, odf, core*/
 26 
 27 /**
 28  * Provides a method to paste text at the current cursor
 29  * position, and processes the input string to understand
 30  * special structuring such as paragraph splits.
 31  * @implements {core.Destroyable}
 32  * @param {!ops.Session} session
 33  * @param {!gui.SessionConstraints} sessionConstraints
 34  * @param {!gui.SessionContext} sessionContext
 35  * @param {!string} inputMemberId
 36  * @constructor
 37  */
 38 gui.PasteController = function PasteController(session, sessionConstraints, sessionContext, inputMemberId) {
 39     "use strict";
 40 
 41     var odtDocument = session.getOdtDocument(),
 42         isEnabled = false,
 43         /**@const*/
 44         textns = odf.Namespaces.textns,
 45         /**@const*/
 46         NEXT = core.StepDirection.NEXT,
 47         odfUtils = odf.OdfUtils;
 48 
 49     /**
 50      * @return {undefined}
 51      */
 52     function updateEnabledState() {
 53         if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) {
 54             isEnabled = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation());
 55         } else {
 56             isEnabled = true;
 57         }
 58     }
 59 
 60     /**
 61      * @param {!ops.OdtCursor} cursor
 62      * @return {undefined}
 63      */
 64     function onCursorEvent(cursor) {
 65         if (cursor.getMemberId() === inputMemberId) {
 66             updateEnabledState();
 67         }
 68     }
 69 
 70     /**
 71      * @return {!boolean}
 72      */
 73     this.isEnabled = function () {
 74         return isEnabled;
 75     };
 76 
 77     /**
 78      * @param {!string} data
 79      * @return {undefined}
 80      */
 81     this.paste = function (data) {
 82         if (!isEnabled) {
 83             return;
 84         }
 85 
 86         var originalCursorPosition = odtDocument.getCursorPosition(inputMemberId),
 87             cursorNode = odtDocument.getCursor(inputMemberId).getNode(),
 88             originalParagraph = /**@type{!Element}*/(odfUtils.getParagraphElement(cursorNode)),
 89             paragraphStyle = originalParagraph.getAttributeNS(textns, "style-name") || "",
 90             /**@type{number}*/
 91             cursorPosition = originalCursorPosition,
 92             operations = [],
 93             currentParagraphStartPosition = odtDocument.convertDomPointToCursorStep(originalParagraph, 0, NEXT),
 94             paragraphs;
 95 
 96         paragraphs = data.replace(/\r/g, "").split("\n");
 97         paragraphs.forEach(function (text) {
 98             var insertTextOp = new ops.OpInsertText(),
 99                 splitParagraphOp = new ops.OpSplitParagraph();
100 
101             insertTextOp.init({
102                 memberid: inputMemberId,
103                 position: cursorPosition,
104                 text: text,
105                 moveCursor: true
106             });
107             operations.push(insertTextOp);
108             cursorPosition += text.length;
109 
110             splitParagraphOp.init({
111                 memberid: inputMemberId,
112                 position: cursorPosition,
113                 paragraphStyleName: paragraphStyle,
114                 sourceParagraphPosition: currentParagraphStartPosition,
115                 moveCursor: true
116             });
117             operations.push(splitParagraphOp);
118             cursorPosition += 1; // Splitting a paragraph introduces 1 walkable position, bumping the cursor forward
119             currentParagraphStartPosition = cursorPosition; // Reset the source paragraph to the newly created one
120         });
121 
122         // Discard the last split paragraph op as unnecessary.
123         // Reasoning through the scenarios, this produces the most intuitive behaviour:
124         // 1. Paste a single line - No line split should be added
125         // 2. Paste two lines - Only one paragraph split is necessary per new paragraph. As pasting MUST occur within an
126         //                      existing paragraph, only a single split should occur.
127         operations.pop();
128 
129         session.enqueue(operations);
130     };
131 
132     /**
133      * @param {!function(!Error=)} callback, passing an error object in case of error
134      * @return {undefined}
135      */
136     this.destroy = function (callback) {
137         odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent);
138         sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
139         callback();
140     };
141 
142     function init() {
143         odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent);
144         sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
145         updateEnabledState();
146     }
147     init();
148 };
149