egroupware/api/js/webodf/collab/wodocollabtexteditor.js

666 lines
26 KiB
JavaScript
Raw Normal View History

/**
* Copyright (C) 2014 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 Wodo, require, navigator, dojo, runtime, document, window, core, ops, gui, odf*/
window.Wodo = window.Wodo || (function () {
"use strict";
var /** @type{!boolean} */
isInitalized = false,
/** @type{!Array.<!function():undefined>} */
pendingInstanceCreationCalls = [],
/** @type{!number} */
instanceCounter = 0,
// constructors
BorderContainer, ContentPane, FullWindowZoomHelper, EditorSession, Tools, MemberListView,
// const strings
/** @const
@type {!string} */
EVENT_UNKNOWNERROR = "unkownError",
/** @const
@type {!string} */
EVENT_METADATACHANGED = "metadataChanged",
/** @const
@type {!string} */
EVENT_BEFORESAVETOFILE = "beforeSaveToFile",
/** @const
@type {!string} */
EVENT_SAVEDTOFILE = "savedToFile",
/** @const
@type {!string} */
EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED = "hasLocalUnsyncedOperationsChanged",
/** @const
@type {!string} */
EVENT_HASSESSIONHOSTCONNECTIONCHANGED = "hasSessionHostConnectionChanged";
/**
* @return {undefined}
*/
function initCollabTextEditor() {
webodfModule.require([
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"webodf/editor/FullWindowZoomHelper",
"webodf/editor/EditorSession",
"webodf/editor/Tools",
"webodf/editor/MemberListView",
"webodf/editor/Translator"],
function (BC, CP, FWZH, ES, T, MLV, Translator) {
var locale = navigator.language || "en-US",
editorBase = dojo.config && dojo.config.paths && dojo.config.paths["webodf/editor"],
translationsDir = editorBase + '/translations',
t;
BorderContainer = BC;
ContentPane = CP;
FullWindowZoomHelper = FWZH;
EditorSession = ES;
Tools = T;
MemberListView = MLV;
// TODO: locale cannot be set by the user, also different for different editors
t = new Translator(translationsDir, locale, function (editorTranslator) {
runtime.setTranslator(editorTranslator.translate);
// Extend runtime with a convenient translation function
runtime.translateContent = function (node) {
var i,
element,
tag,
placeholder,
translatable = node.querySelectorAll("*[text-i18n]");
for (i = 0; i < translatable.length; i += 1) {
element = translatable[i];
tag = element.localName;
placeholder = element.getAttribute('text-i18n');
if (tag === "label"
|| tag === "span"
|| /h\d/i.test(tag)) {
element.textContent = runtime.tr(placeholder);
}
}
};
isInitalized = true;
pendingInstanceCreationCalls.forEach(function (create) { create(); });
return t; // return it so 't' is not unused
});
});
}
/**
* @constructor
* @param {!string} mainContainerElementId
* @param {!Object.<!string,!*>} editorOptions
*/
function CollabTextEditor(mainContainerElementId, editorOptions) {
instanceCounter = instanceCounter + 1;
/**
* Returns true if either all features are wanted and this one is not explicitely disabled
* or if not all features are wanted by default and it is explicitely enabled
* @param {?boolean|undefined} isFeatureEnabled explicit flag which enables a feature
* @param {!boolean=} isUnstable set to true if the feature is not stable (in collab mode)
* @return {!boolean}
*/
function isEnabled(isFeatureEnabled, isUnstable) {
if (isUnstable && !editorOptions.unstableFeaturesEnabled) {
return false;
}
return editorOptions.allFeaturesEnabled ? (isFeatureEnabled !== false) : isFeatureEnabled;
}
var //
mainContainerElement = document.getElementById(mainContainerElementId),
canvasElement,
canvasContainerElement,
toolbarElement,
toolbarContainerElement, // needed because dijit toolbar overwrites direct classList
editorElement,
/** @const
@type{!string} */
canvasElementId = "webodfeditor-canvas" + instanceCounter,
/** @const
@type{!string} */
canvasContainerElementId = "webodfeditor-canvascontainer" + instanceCounter,
/** @const
@type{!string} */
toolbarElementId = "webodfeditor-toolbar" + instanceCounter,
/** @const
@type{!string} */
editorElementId = "webodfeditor-editor" + instanceCounter,
//
fullWindowZoomHelper,
//
memberListElement,
membersElement,
/** @const
@type{!string} */
membersElementId = "webodfeditor-members" + instanceCounter,
//
mainContainer,
memberListView,
tools,
odfCanvas,
//
editorSession,
session,
//
saveOdtFile = editorOptions.saveCallback,
close = editorOptions.closeCallback,
//
directTextStylingEnabled = isEnabled(editorOptions.directTextStylingEnabled),
directParagraphStylingEnabled = isEnabled(editorOptions.directParagraphStylingEnabled),
paragraphStyleSelectingEnabled = isEnabled(editorOptions.paragraphStyleSelectingEnabled),
paragraphStyleEditingEnabled = isEnabled(editorOptions.paragraphStyleEditingEnabled),
imageEditingEnabled = isEnabled(editorOptions.imageEditingEnabled, true),
hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled, true),
reviewModeEnabled = isEnabled(editorOptions.reviewModeEnabled, true),
annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled),
undoRedoEnabled = false, // no proper mechanism yet for collab
zoomingEnabled = isEnabled(editorOptions.zoomingEnabled),
//
pendingMemberId,
pendingEditorReadyCallback,
//
eventNotifier = new core.EventNotifier([
EVENT_UNKNOWNERROR,
EVENT_METADATACHANGED,
EVENT_BEFORESAVETOFILE,
EVENT_SAVEDTOFILE,
EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED,
EVENT_HASSESSIONHOSTCONNECTIONCHANGED
]);
runtime.assert(Boolean(mainContainerElement), "No id of an existing element passed to Wodo.createCollabTextEditor(): "+mainContainerElementId);
/**
* @param {!Object} changes
* @return {undefined}
*/
function relayMetadataSignal(changes) {
eventNotifier.emit(EVENT_METADATACHANGED, changes);
}
/**
* @return {undefined}
*/
function createSession() {
var viewOptions = {
editInfoMarkersInitiallyVisible: true,
caretAvatarsInitiallyVisible: true,
caretBlinksOnRangeSelect: true
};
// create session around loaded document
session = new ops.Session(odfCanvas);
editorSession = new EditorSession(session, pendingMemberId, {
viewOptions: jQuery.extend(viewOptions,editorOptions.viewOptions),
directTextStylingEnabled: directTextStylingEnabled,
directParagraphStylingEnabled: directParagraphStylingEnabled,
paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
imageEditingEnabled: imageEditingEnabled,
hyperlinkEditingEnabled: hyperlinkEditingEnabled,
annotationsEnabled: annotationsEnabled,
zoomingEnabled: zoomingEnabled,
reviewModeEnabled: reviewModeEnabled
});
if (undoRedoEnabled) {
editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
}
memberListView.setEditorSession(editorSession);
// Relay any metadata changes to the Editor's consumer as an event
editorSession.sessionController.getMetadataController().subscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);
// and report back to caller
pendingEditorReadyCallback();
// reset
pendingEditorReadyCallback = null;
pendingMemberId = null;
}
/**
* @return {undefined}
*/
function startEditing() {
runtime.assert(editorSession, "editorSession should exist here.");
tools.setEditorSession(editorSession);
editorSession.sessionController.insertLocalCursor();
editorSession.sessionController.startEditing();
}
/**
* @return {undefined}
*/
function endEditing() {
runtime.assert(editorSession, "editorSession should exist here.");
tools.setEditorSession(undefined);
editorSession.sessionController.endEditing();
editorSession.sessionController.removeLocalCursor();
}
/**
* @param {!string} eventid
* @param {*} args
* @return {undefined}
*/
function fireEvent(eventid, args) {
eventNotifier.emit(eventid, args);
}
/**
* @param {!Object} error
* @return {undefined}
*/
function handleOperationRouterErrors(error) {
// TODO: translate error into Editor ids or at least document the possible values
fireEvent(EVENT_UNKNOWNERROR, error);
}
/**
* @param {!SessionBackend} sessionBackend
* @param {!function(!Error=):undefined} callback, passing an error object in case of error
* @return {undefined}
*/
function joinSession(sessionBackend, editorReadyCallback) {
runtime.assert(sessionBackend, "No sessionBackend passed.");
runtime.assert(sessionBackend.getMemberId(), "sessionBackend should deliver a memberId here.");
runtime.assert(!pendingEditorReadyCallback, "pendingEditorReadyCallback should not exist here.");
runtime.assert(!editorSession, "editorSession should not exist here.");
runtime.assert(!session, "session should not exist here.");
pendingMemberId = sessionBackend.getMemberId();
pendingEditorReadyCallback = function () {
// overwrite router
var opRouter = sessionBackend.createOperationRouter(odfCanvas.odfContainer(), handleOperationRouterErrors);
session.setOperationRouter(opRouter);
// forward events
// TODO: relying here on that opRouter uses the same id strings ATM, those should be defined at OperationRouter interface
opRouter.subscribe(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, function (hasUnsyncedOps) {
fireEvent(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, hasUnsyncedOps);
});
opRouter.subscribe(EVENT_HASSESSIONHOSTCONNECTIONCHANGED, function (hasSessionHostConnection) {
fireEvent(EVENT_HASSESSIONHOSTCONNECTIONCHANGED, hasSessionHostConnection);
});
opRouter.subscribe(EVENT_BEFORESAVETOFILE, function () {
fireEvent(EVENT_BEFORESAVETOFILE, null);
});
opRouter.subscribe(EVENT_SAVEDTOFILE, function () {
fireEvent(EVENT_SAVEDTOFILE, null);
});
// now get existing ops and after that let the user edit
opRouter.requestReplay(function done() {
startEditing();
if (editorReadyCallback) {
editorReadyCallback();
}
});
};
odfCanvas.load(sessionBackend.getGenesisUrl());
}
/**
* Leave the currently joined edit session, and do cleanup.
* @param {!function(!Error=)} callback, passing an error object in case of error
* @return {undefined}
*/
function leaveSession(callback) {
runtime.assert(session, "session should exist here.");
endEditing();
session.close(function(err) {
if (err) {
callback(err);
} else {
// now also destroy session, will not be reused for new document
memberListView.setEditorSession(undefined);
editorSession.sessionController.getMetadataController().unsubscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);
editorSession.destroy(function(err) {
if (err) {
callback(err);
} else {
editorSession = undefined;
session.destroy(function(err) {
if (err) {
callback(err);
} else {
session = undefined;
callback();
}
});
}
});
}
});
}
/**
* @return {!Element}
*/
function getCanvasContainerElement() {
return canvasContainerElement;
}
/**
* @param {!function(err:?Error, !Uint8Array=):undefined} cb receiving zip as bytearray
* @return {undefined}
*/
function getDocumentAsByteArray(cb) {
var odfContainer = odfCanvas.odfContainer();
if (odfContainer) {
odfContainer.createByteArray(function (ba) {
cb(null, ba);
}, function (err) {
cb(err || "Could not create bytearray.");
});
} else {
cb("No odfContainer!");
}
}
/**
* Sets the metadata fields from the given properties map.
* Avoid setting certain fields since they are automatically set:
* dc:creator
* dc:date
* meta:editing-cycles
* If you do wish to externally set these fields, try getting
* the server backend (if any) to inject operations into the timeline
* with the relevant properties.
*
* The following properties are never used and will be removed for semantic
* consistency from the document:
* meta:editing-duration
* meta:document-statistic
*
* Setting any of the above mentioned fields using this method will have no effect.
*
* @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value.
* @param {?Array.<!string>} removedProperties An array of metadata field names (prefixed).
* @return {undefined}
*/
function setMetadata(setProperties, removedProperties) {
runtime.assert(editorSession, "editorSession should exist here.");
editorSession.sessionController.getMetadataController().setMetadata(setProperties, removedProperties);
}
/**
* Returns the value of the requested document metadata field
* @param {!string} property A namespace-prefixed field name, for example
* dc:creator
* @return {?string}
*/
function getMetadata(property) {
runtime.assert(editorSession, "editorSession should exist here.");
return editorSession.sessionController.getMetadataController().getMetadata(property);
}
/**
* Sets the current state of the document to be either the unmodified state
* or a modified state.
* If @p modified is @true and the current state was already a modified state,
* this call has no effect and also does not remove the unmodified flag
* from the state which has it set.
*
* @name TextEditor#setDocumentModified
* @function
* @param {!boolean} modified
* @return {undefined}
*/
this.setDocumentModified = function (modified) {
runtime.assert(editorSession, "editorSession should exist here.");
if (undoRedoEnabled) {
editorSession.sessionController.getUndoManager().setDocumentModified(modified);
}
};
/**
* Returns if the current state of the document matches the unmodified state.
* @name TextEditor#isDocumentModified
* @function
* @return {!boolean}
*/
this.isDocumentModified = function () {
runtime.assert(editorSession, "editorSession should exist here.");
if (undoRedoEnabled) {
return editorSession.sessionController.getUndoManager().isDocumentModified();
}
return false;
};
/**
* @return {undefined}
*/
function setFocusToOdfCanvas() {
editorSession.sessionController.getEventManager().focus();
}
/**
* @param {!function(!Error=):undefined} callback, passing an error object in case of error
* @return {undefined}
*/
function destroyInternal(callback) {
mainContainerElement.removeChild(editorElement);
mainContainerElement.removeChild(membersElement);
callback();
}
/**
* @param {!function(!Error=):undefined} callback, passing an error object in case of error
* @return {undefined}
*/
function destroy(callback) {
var destroyCallbacks = [];
// TODO: decide if some forced close should be done here instead of enforcing proper API usage
runtime.assert(!session, "session should not exist here.");
// TODO: investigate what else needs to be done
mainContainer.destroyRecursive(true);
destroyCallbacks = destroyCallbacks.concat([
fullWindowZoomHelper.destroy,
memberListView.destroy,
tools.destroy,
odfCanvas.destroy,
destroyInternal
]);
core.Async.destroyAll(destroyCallbacks, callback);
}
/////////////////////////////////////////////////////////////////////
// Exposed API
// make sure no data structures are shared, copy as needed
//
this.joinSession = joinSession;
this.leaveSession = leaveSession;
this.getDocumentAsByteArray = getDocumentAsByteArray;
// setReadOnly: setReadOnly,
this.setMetadata = setMetadata;
this.getMetadata = getMetadata;
// temporary hack:
this.getCanvasContainerElement = getCanvasContainerElement;
this.addEventListener = eventNotifier.subscribe;
this.removeEventListener = eventNotifier.unsubscribe;
this.destroy = destroy;
/**
* @return {undefined}
*/
function init() {
var editorPane, memberListPane,
documentns = document.documentElement.namespaceURI;
function createElement(tagLocalName, id, className) {
var element;
element = document.createElementNS(documentns, tagLocalName);
if (id) {
element.id = id;
}
element.classList.add(className);
return element;
}
// create needed tree structure
canvasElement = createElement('div', canvasElementId, "webodfeditor-canvas");
canvasContainerElement = createElement('div', canvasContainerElementId, "webodfeditor-canvascontainer");
toolbarElement = createElement('span', toolbarElementId, "webodfeditor-toolbar");
toolbarContainerElement = createElement('span', undefined, "webodfeditor-toolbarcontainer");
editorElement = createElement('div', editorElementId, "webodfeditor-editor");
// put into tree
canvasContainerElement.appendChild(canvasElement);
toolbarContainerElement.appendChild(toolbarElement);
editorElement.appendChild(toolbarContainerElement);
editorElement.appendChild(canvasContainerElement);
mainContainerElement.appendChild(editorElement);
// memberlist plugin
memberListElement = createElement('div', undefined, "webodfeditor-memberList");
membersElement = createElement('div', membersElementId, "webodfeditor-members");
// put into tree
membersElement.appendChild(memberListElement);
mainContainerElement.appendChild(membersElement);
// style all elements with Dojo's claro.
// Not nice to do this on body, but then there is no other way known
// to style also all dialogs, which are attached directly to body
document.body.classList.add("claro");
// prevent browser translation service messing up internal address system
canvasElement.setAttribute("translate", "no");
canvasElement.classList.add("notranslate");
// create widgets
mainContainer = new BorderContainer({}, mainContainerElementId);
editorPane = new ContentPane({
region: 'center'
}, editorElementId);
mainContainer.addChild(editorPane);
memberListPane = new ContentPane({
region: 'right',
title: runtime.tr("Members")
}, membersElementId);
mainContainer.addChild(memberListPane);
memberListView = new MemberListView(memberListElement);
mainContainer.startup();
tools = new Tools(toolbarElementId, {
onToolDone: setFocusToOdfCanvas,
saveOdtFile: saveOdtFile,
close: close,
directTextStylingEnabled: directTextStylingEnabled,
directParagraphStylingEnabled: directParagraphStylingEnabled,
paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
imageInsertingEnabled: imageEditingEnabled,
hyperlinkEditingEnabled: hyperlinkEditingEnabled,
annotationsEnabled: annotationsEnabled,
undoRedoEnabled: undoRedoEnabled,
zoomingEnabled: zoomingEnabled
});
odfCanvas = new odf.OdfCanvas(canvasElement);
odfCanvas.enableAnnotations(annotationsEnabled, true);
odfCanvas.addListener("statereadychange", createSession);
fullWindowZoomHelper = new FullWindowZoomHelper(toolbarContainerElement, canvasContainerElement);
}
init();
}
/**
* @param {!string} mainContainerElementId
* @param {!Object.<!string,!*>} editorOptions
* @param {!function(err:?Error, editor:!CollabTextEditor=):undefined} onEditorCreated
* @return {undefined}
*/
function createInstance(mainContainerElementId, editorOptions, onEditorCreated) {
/**
* @return {undefined}
*/
function create() {
var editor = new CollabTextEditor(mainContainerElementId, editorOptions);
onEditorCreated(null, editor);
}
if (!isInitalized) {
pendingInstanceCreationCalls.push(create);
// first request?
if (pendingInstanceCreationCalls.length === 1) {
initCollabTextEditor();
}
} else {
create();
}
}
// exposed API
return {
createCollabTextEditor: createInstance,
// flags
EVENT_UNKNOWNERROR: EVENT_UNKNOWNERROR,
EVENT_METADATACHANGED: EVENT_METADATACHANGED,
EVENT_BEFORESAVETOFILE: EVENT_BEFORESAVETOFILE,
EVENT_SAVEDTOFILE: EVENT_SAVEDTOFILE,
EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED: EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED,
EVENT_HASSESSIONHOSTCONNECTIONCHANGED: EVENT_HASSESSIONHOSTCONNECTIONCHANGED
};
}());