123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- /**
- * Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
- *
- * @licstart
- * This file is part of WebODF.
- *
- * WebODF is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License (GNU AGPL)
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * WebODF is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with WebODF. If not, see <http://www.gnu.org/licenses/>.
- * @licend
- *
- * @source: http://www.webodf.org/
- * @source: https://github.com/kogmbh/WebODF/
- */
- /*global define, runtime, core, gui, ops, document */
- define("webodf/editor/EditorSession", [
- "dojo/text!resources/fonts/fonts.css"
- ], function (fontsCSS) { // fontsCSS is retrieved as a string, using dojo's text retrieval AMD plugin
- "use strict";
- runtime.loadClass("core.Async");
- runtime.loadClass("core.DomUtils");
- runtime.loadClass("odf.OdfUtils");
- runtime.loadClass("ops.OdtDocument");
- runtime.loadClass("ops.OdtStepsTranslator");
- runtime.loadClass("ops.Session");
- runtime.loadClass("odf.Namespaces");
- runtime.loadClass("odf.OdfCanvas");
- runtime.loadClass("odf.OdfUtils");
- runtime.loadClass("gui.CaretManager");
- runtime.loadClass("gui.Caret");
- runtime.loadClass("gui.SessionController");
- runtime.loadClass("gui.SessionView");
- runtime.loadClass("gui.HyperlinkTooltipView");
- runtime.loadClass("gui.TrivialUndoManager");
- runtime.loadClass("gui.SvgSelectionView");
- runtime.loadClass("gui.SelectionViewManager");
- runtime.loadClass("core.EventNotifier");
- runtime.loadClass("gui.ShadowCursor");
- runtime.loadClass("gui.CommonConstraints");
- /**
- * Instantiate a new editor session attached to an existing operation session
- * @param {!ops.Session} session
- * @param {!string} localMemberId
- * @param {{viewOptions:gui.SessionViewOptions,directParagraphStylingEnabled:boolean,annotationsEnabled:boolean}} config
- * @constructor
- */
- var EditorSession = function EditorSession(session, localMemberId, config) {
- var self = this,
- currentParagraphNode = null,
- currentCommonStyleName = null,
- currentStyleName = null,
- caretManager,
- selectionViewManager,
- hyperlinkTooltipView,
- odtDocument = session.getOdtDocument(),
- textns = odf.Namespaces.textns,
- fontStyles = document.createElement('style'),
- formatting = odtDocument.getFormatting(),
- domUtils = new core.DomUtils(),
- odfUtils = new odf.OdfUtils(),
- eventNotifier = new core.EventNotifier([
- EditorSession.signalMemberAdded,
- EditorSession.signalMemberUpdated,
- EditorSession.signalMemberRemoved,
- EditorSession.signalCursorAdded,
- EditorSession.signalCursorMoved,
- EditorSession.signalCursorRemoved,
- EditorSession.signalParagraphChanged,
- EditorSession.signalCommonStyleCreated,
- EditorSession.signalCommonStyleDeleted,
- EditorSession.signalParagraphStyleModified,
- EditorSession.signalUndoStackChanged]),
- shadowCursor = new gui.ShadowCursor(odtDocument),
- sessionConstraints;
- /**
- * @return {Array.<!string>}
- */
- function getAvailableFonts() {
- var availableFonts, regex, matches;
- availableFonts = {};
- regex = /font-family *: *(?:\'([^']*)\'|\"([^"]*)\")/gm;
- matches = regex.exec(fontsCSS);
- while (matches) {
- availableFonts[matches[1] || matches[2]] = 1;
- matches = regex.exec(fontsCSS);
- }
- availableFonts = Object.keys(availableFonts);
- return availableFonts;
- }
- function checkParagraphStyleName() {
- var newStyleName,
- newCommonStyleName;
- newStyleName = currentParagraphNode.getAttributeNS(textns, 'style-name');
- if (newStyleName !== currentStyleName) {
- currentStyleName = newStyleName;
- // check if common style is still the same
- newCommonStyleName = formatting.getFirstCommonParentStyleNameOrSelf(newStyleName);
- if (!newCommonStyleName) {
- // Default style, empty-string name
- currentCommonStyleName = newStyleName = currentStyleName = "";
- self.emit(EditorSession.signalParagraphChanged, {
- type: 'style',
- node: currentParagraphNode,
- styleName: currentCommonStyleName
- });
- return;
- }
- // a common style
- if (newCommonStyleName !== currentCommonStyleName) {
- currentCommonStyleName = newCommonStyleName;
- self.emit(EditorSession.signalParagraphChanged, {
- type: 'style',
- node: currentParagraphNode,
- styleName: currentCommonStyleName
- });
- }
- }
- }
- /**
- * Creates a NCName from the passed string
- * @param {!string} name
- * @return {!string}
- */
- function createNCName(name) {
- var letter,
- result = "",
- i;
- // encode
- for (i = 0; i < name.length; i++) {
- letter = name[i];
- // simple approach, can be improved to not skip other allowed chars
- if (letter.match(/[a-zA-Z0-9.-_]/) !== null) {
- result += letter;
- } else {
- result += "_" + letter.charCodeAt(0).toString(16) + "_";
- }
- }
- // ensure leading char is from proper range
- if (result.match(/^[a-zA-Z_]/) === null) {
- result = "_" + result;
- }
- return result;
- }
- function uniqueParagraphStyleNCName(name) {
- var result,
- i = 0,
- ncMemberId = createNCName(localMemberId),
- ncName = createNCName(name);
- // create default paragraph style
- // localMemberId is used to avoid id conflicts with ids created by other members
- result = ncName + "_" + ncMemberId;
- // then loop until result is really unique
- while (formatting.hasParagraphStyle(result)) {
- result = ncName + "_" + i + "_" + ncMemberId;
- i++;
- }
- return result;
- }
- function trackCursor(cursor) {
- var node;
- node = odtDocument.getParagraphElement(cursor.getNode());
- if (!node) {
- return;
- }
- currentParagraphNode = node;
- checkParagraphStyleName();
- }
- function trackCurrentParagraph(info) {
- var cursor = odtDocument.getCursor(localMemberId),
- range = cursor && cursor.getSelectedRange(),
- paragraphRange = odtDocument.getDOMDocument().createRange();
- paragraphRange.selectNode(info.paragraphElement);
- if ((range && domUtils.rangesIntersect(range, paragraphRange)) || info.paragraphElement === currentParagraphNode) {
- self.emit(EditorSession.signalParagraphChanged, info);
- checkParagraphStyleName();
- }
- paragraphRange.detach();
- }
- function onMemberAdded(member) {
- self.emit(EditorSession.signalMemberAdded, member.getMemberId());
- }
- function onMemberUpdated(member) {
- self.emit(EditorSession.signalMemberUpdated, member.getMemberId());
- }
- function onMemberRemoved(memberId) {
- self.emit(EditorSession.signalMemberRemoved, memberId);
- }
- function onCursorAdded(cursor) {
- self.emit(EditorSession.signalCursorAdded, cursor.getMemberId());
- trackCursor(cursor);
- }
- function onCursorRemoved(memberId) {
- self.emit(EditorSession.signalCursorRemoved, memberId);
- }
- function onCursorMoved(cursor) {
- // Emit 'cursorMoved' only when *I* am moving the cursor, not the other users
- if (cursor.getMemberId() === localMemberId) {
- self.emit(EditorSession.signalCursorMoved, cursor);
- trackCursor(cursor);
- }
- }
- function onStyleCreated(newStyleName) {
- self.emit(EditorSession.signalCommonStyleCreated, newStyleName);
- }
- function onStyleDeleted(styleName) {
- self.emit(EditorSession.signalCommonStyleDeleted, styleName);
- }
- function onParagraphStyleModified(styleName) {
- self.emit(EditorSession.signalParagraphStyleModified, styleName);
- }
- /**
- * Call all subscribers for the given event with the specified argument
- * @param {!string} eventid
- * @param {Object} args
- */
- this.emit = function (eventid, args) {
- eventNotifier.emit(eventid, args);
- };
- /**
- * Subscribe to a given event with a callback
- * @param {!string} eventid
- * @param {!Function} cb
- */
- this.subscribe = function (eventid, cb) {
- eventNotifier.subscribe(eventid, cb);
- };
- /**
- * @param {!string} eventid
- * @param {!Function} cb
- * @return {undefined}
- */
- this.unsubscribe = function (eventid, cb) {
- eventNotifier.unsubscribe(eventid, cb);
- };
- this.getCursorPosition = function () {
- return odtDocument.getCursorPosition(localMemberId);
- };
- this.getCursorSelection = function () {
- return odtDocument.getCursorSelection(localMemberId);
- };
- this.getOdfCanvas = function () {
- return odtDocument.getOdfCanvas();
- };
- this.getCurrentParagraph = function () {
- return currentParagraphNode;
- };
- this.getAvailableParagraphStyles = function () {
- return formatting.getAvailableParagraphStyles();
- };
- this.getCurrentParagraphStyle = function () {
- return currentCommonStyleName;
- };
- /**
- * Round the step up to the next step
- * @param {!number} step
- * @return {!boolean}
- */
- function roundUp(step) {
- return step === ops.OdtStepsTranslator.NEXT_STEP;
- }
- /**
- * Applies the paragraph style with the given
- * style name to all the paragraphs within
- * the cursor selection.
- * @param {!string} styleName
- * @return {undefined}
- */
- this.setCurrentParagraphStyle = function (styleName) {
- var range = odtDocument.getCursor(localMemberId).getSelectedRange(),
- paragraphs = odfUtils.getParagraphElements(range),
- opQueue = [];
- paragraphs.forEach(function (paragraph) {
- var paragraphStartPoint = odtDocument.convertDomPointToCursorStep(paragraph, 0, roundUp),
- paragraphStyleName = paragraph.getAttributeNS(odf.Namespaces.textns, "style-name"),
- opSetParagraphStyle;
- if (paragraphStyleName !== styleName) {
- opSetParagraphStyle = new ops.OpSetParagraphStyle();
- opSetParagraphStyle.init({
- memberid: localMemberId,
- styleName: styleName,
- position: paragraphStartPoint
- });
- opQueue.push(opSetParagraphStyle);
- }
- });
- if (opQueue.length > 0) {
- session.enqueue(opQueue);
- }
- };
- this.insertTable = function (initialRows, initialColumns, tableStyleName, tableColumnStyleName, tableCellStyleMatrix) {
- var op = new ops.OpInsertTable();
- op.init({
- memberid: localMemberId,
- position: self.getCursorPosition(),
- initialRows: initialRows,
- initialColumns: initialColumns,
- tableStyleName: tableStyleName,
- tableColumnStyleName: tableColumnStyleName,
- tableCellStyleMatrix: tableCellStyleMatrix
- });
- session.enqueue([op]);
- };
- /**
- * Takes a style name and returns the corresponding paragraph style
- * element. If the style name is an empty string, the default style
- * is returned.
- * @param {!string} styleName
- * @return {Element}
- */
- this.getParagraphStyleElement = function (styleName) {
- return (styleName === "")
- ? formatting.getDefaultStyleElement('paragraph')
- : odtDocument.getParagraphStyleElement(styleName);
- };
- /**
- * Returns if the style is used anywhere in the document
- * @param {!Element} styleElement
- * @return {boolean}
- */
- this.isStyleUsed = function (styleElement) {
- return formatting.isStyleUsed(styleElement);
- };
- function getDefaultParagraphStyleAttributes() {
- var styleNode = formatting.getDefaultStyleElement('paragraph');
- if (styleNode) {
- return formatting.getInheritedStyleAttributes(styleNode);
- }
- return null;
- }
- /**
- * Returns the attributes of a given paragraph style name
- * (with inheritance). If the name is an empty string,
- * the attributes of the default style are returned.
- * @param {!string} styleName
- * @return {Object}
- */
- this.getParagraphStyleAttributes = function (styleName) {
- return (styleName === "")
- ? getDefaultParagraphStyleAttributes()
- : odtDocument.getParagraphStyleAttributes(styleName);
- };
- /**
- * Creates and enqueues a paragraph-style cloning operation.
- * Returns the created id for the new style.
- * @param {!string} styleName id of the style to update
- * @param {!{paragraphProperties,textProperties}} setProperties properties which are set
- * @param {!{paragraphPropertyNames,textPropertyNames}=} removedProperties properties which are removed
- * @return {undefined}
- */
- this.updateParagraphStyle = function (styleName, setProperties, removedProperties) {
- var op;
- op = new ops.OpUpdateParagraphStyle();
- op.init({
- memberid: localMemberId,
- styleName: styleName,
- setProperties: setProperties,
- removedProperties: (!removedProperties) ? {} : removedProperties
- });
- session.enqueue([op]);
- };
- /**
- * Creates and enqueues a paragraph-style cloning operation.
- * Returns the created id for the new style.
- * @param {!string} styleName id of the style to clone
- * @param {!string} newStyleDisplayName display name of the new style
- * @return {!string}
- */
- this.cloneParagraphStyle = function (styleName, newStyleDisplayName) {
- var newStyleName = uniqueParagraphStyleNCName(newStyleDisplayName),
- styleNode = self.getParagraphStyleElement(styleName),
- formatting = odtDocument.getFormatting(),
- op, setProperties, attributes, i;
- setProperties = formatting.getStyleAttributes(styleNode);
- // copy any attributes directly on the style
- attributes = styleNode.attributes;
- for (i = 0; i < attributes.length; i += 1) {
- // skip...
- // * style:display-name -> not copied, set to new string below
- // * style:name -> not copied, set from op by styleName property
- // * style:family -> "paragraph" always, set by op
- if (!/^(style:display-name|style:name|style:family)/.test(attributes[i].name)) {
- setProperties[attributes[i].name] = attributes[i].value;
- }
- }
- setProperties['style:display-name'] = newStyleDisplayName;
- op = new ops.OpAddStyle();
- op.init({
- memberid: localMemberId,
- styleName: newStyleName,
- styleFamily: 'paragraph',
- setProperties: setProperties
- });
- session.enqueue([op]);
- return newStyleName;
- };
- this.deleteStyle = function (styleName) {
- var op;
- op = new ops.OpRemoveStyle();
- op.init({
- memberid: localMemberId,
- styleName: styleName,
- styleFamily: 'paragraph'
- });
- session.enqueue([op]);
- };
- /**
- * Returns an array of the declared fonts in the ODF document,
- * with 'duplicates' like Arial1, Arial2, etc removed. The alphabetically
- * first font name for any given family is kept.
- * The elements of the array are objects containing the font's name and
- * the family.
- * @return {Array.<!Object>}
- */
- this.getDeclaredFonts = function () {
- var fontMap = formatting.getFontMap(),
- usedFamilies = [],
- array = [],
- sortedNames,
- key,
- value,
- i;
- // Sort all the keys in the font map alphabetically
- sortedNames = Object.keys(fontMap);
- sortedNames.sort();
- for (i = 0; i < sortedNames.length; i += 1) {
- key = sortedNames[i];
- value = fontMap[key];
- // Use the font declaration only if the family is not already used.
- // Therefore we are able to discard the alphabetic successors of the first
- // font name.
- if (usedFamilies.indexOf(value) === -1) {
- array.push({
- name: key,
- family: value
- });
- if (value) {
- usedFamilies.push(value);
- }
- }
- }
- return array;
- };
- this.getSelectedHyperlinks = function () {
- var cursor = odtDocument.getCursor(localMemberId);
- // no own cursor yet/currently added?
- if (!cursor) {
- return [];
- }
- return odfUtils.getHyperlinkElements(cursor.getSelectedRange());
- };
- this.getSelectedRange = function () {
- var cursor = odtDocument.getCursor(localMemberId);
- return cursor && cursor.getSelectedRange();
- };
- function undoStackModified(e) {
- self.emit(EditorSession.signalUndoStackChanged, e);
- }
- this.undo = function () {
- self.sessionController.undo();
- };
- this.redo = function () {
- self.sessionController.redo();
- };
- /**
- * @param {!string} memberId
- * @return {?ops.Member}
- */
- this.getMember = function (memberId) {
- return odtDocument.getMember(memberId);
- };
- /**
- * @param {!function(!Object=)} callback passing an error object in case of error
- * @return {undefined}
- */
- function destroy(callback) {
- var head = document.getElementsByTagName('head')[0],
- eventManager = self.sessionController.getEventManager();
- head.removeChild(fontStyles);
- odtDocument.unsubscribe(ops.Document.signalMemberAdded, onMemberAdded);
- odtDocument.unsubscribe(ops.Document.signalMemberUpdated, onMemberUpdated);
- odtDocument.unsubscribe(ops.Document.signalMemberRemoved, onMemberRemoved);
- odtDocument.unsubscribe(ops.Document.signalCursorAdded, onCursorAdded);
- odtDocument.unsubscribe(ops.Document.signalCursorRemoved, onCursorRemoved);
- odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorMoved);
- odtDocument.unsubscribe(ops.OdtDocument.signalCommonStyleCreated, onStyleCreated);
- odtDocument.unsubscribe(ops.OdtDocument.signalCommonStyleDeleted, onStyleDeleted);
- odtDocument.unsubscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified);
- odtDocument.unsubscribe(ops.OdtDocument.signalParagraphChanged, trackCurrentParagraph);
- odtDocument.unsubscribe(ops.OdtDocument.signalUndoStackChanged, undoStackModified);
- eventManager.unsubscribe("mousemove", hyperlinkTooltipView.showTooltip);
- eventManager.unsubscribe("mouseout", hyperlinkTooltipView.hideTooltip);
- delete self.sessionView;
- delete self.sessionController;
- callback();
- }
- /**
- * @param {!function(!Error=)} callback passing an error object in case of error
- * @return {undefined}
- */
- this.destroy = function(callback) {
- var cleanup = [
- self.sessionView.destroy,
- caretManager.destroy,
- selectionViewManager.destroy,
- self.sessionController.destroy,
- hyperlinkTooltipView.destroy,
- destroy
- ];
- core.Async.destroyAll(cleanup, callback);
- };
- function init() {
- var head = document.getElementsByTagName('head')[0],
- eventManager;
- // TODO: fonts.css should be rather done by odfCanvas, or?
- fontStyles.type = 'text/css';
- fontStyles.media = 'screen, print, handheld, projection';
- fontStyles.appendChild(document.createTextNode(fontsCSS));
- head.appendChild(fontStyles);
- self.sessionController = new gui.SessionController(session, localMemberId, shadowCursor, {
- annotationsEnabled: config.annotationsEnabled,
- directTextStylingEnabled: config.directTextStylingEnabled,
- directParagraphStylingEnabled: config.directParagraphStylingEnabled
- });
- sessionConstraints = self.sessionController.getSessionConstraints();
- eventManager = self.sessionController.getEventManager();
- hyperlinkTooltipView = new gui.HyperlinkTooltipView(session.getOdtDocument().getOdfCanvas(),
- self.sessionController.getHyperlinkClickHandler().getModifier);
- eventManager.subscribe("mousemove", hyperlinkTooltipView.showTooltip);
- eventManager.subscribe("mouseout", hyperlinkTooltipView.hideTooltip);
- caretManager = new gui.CaretManager(self.sessionController);
- selectionViewManager = new gui.SelectionViewManager(gui.SvgSelectionView);
- self.sessionView = new gui.SessionView(config.viewOptions, localMemberId, session, sessionConstraints, caretManager, selectionViewManager);
- self.availableFonts = getAvailableFonts();
- selectionViewManager.registerCursor(shadowCursor, true);
- // Session Constraints can be applied once the controllers are instantiated.
- if (config.reviewModeEnabled) {
- // Disallow deleting other authors' annotations.
- sessionConstraints.setState(gui.CommonConstraints.EDIT.ANNOTATIONS.ONLY_DELETE_OWN, true);
- sessionConstraints.setState(gui.CommonConstraints.EDIT.REVIEW_MODE, true);
- }
- // Custom signals, that make sense in the Editor context. We do not want to expose webodf's ops signals to random bits of the editor UI.
- odtDocument.subscribe(ops.Document.signalMemberAdded, onMemberAdded);
- odtDocument.subscribe(ops.Document.signalMemberUpdated, onMemberUpdated);
- odtDocument.subscribe(ops.Document.signalMemberRemoved, onMemberRemoved);
- odtDocument.subscribe(ops.Document.signalCursorAdded, onCursorAdded);
- odtDocument.subscribe(ops.Document.signalCursorRemoved, onCursorRemoved);
- odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorMoved);
- odtDocument.subscribe(ops.OdtDocument.signalCommonStyleCreated, onStyleCreated);
- odtDocument.subscribe(ops.OdtDocument.signalCommonStyleDeleted, onStyleDeleted);
- odtDocument.subscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified);
- odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, trackCurrentParagraph);
- odtDocument.subscribe(ops.OdtDocument.signalUndoStackChanged, undoStackModified);
- }
- init();
- };
- /**@const*/EditorSession.signalMemberAdded = "memberAdded";
- /**@const*/EditorSession.signalMemberUpdated = "memberUpdated";
- /**@const*/EditorSession.signalMemberRemoved = "memberRemoved";
- /**@const*/EditorSession.signalCursorAdded = "cursorAdded";
- /**@const*/EditorSession.signalCursorRemoved = "cursorRemoved";
- /**@const*/EditorSession.signalCursorMoved = "cursorMoved";
- /**@const*/EditorSession.signalParagraphChanged = "paragraphChanged";
- /**@const*/EditorSession.signalCommonStyleCreated = "styleCreated";
- /**@const*/EditorSession.signalCommonStyleDeleted = "styleDeleted";
- /**@const*/EditorSession.signalParagraphStyleModified = "paragraphStyleModified";
- /**@const*/EditorSession.signalUndoStackChanged = "signalUndoStackChanged";
- return EditorSession;
- });
|