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