1 /**
  2  * Copyright (C) 2014 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 window, document, alert, navigator, require, dojo, runtime, core, gui, ops, odf, WodoFromSource*/
 26 
 27 /**
 28  * Namespace of the Wodo.TextEditor
 29  * @namespace
 30  * @name Wodo
 31  */
 32 window.Wodo = window.Wodo || (function () {
 33     "use strict";
 34 
 35     function getInstallationPath() {
 36         /**
 37          * Sees to get the url of this script on top of the stack trace.
 38          * @param {!string|undefined} stack
 39          * @return {!string|undefined}
 40          */
 41         function getScriptUrlFromStack(stack) {
 42             var url, matches;
 43 
 44             if (typeof stack === "string" && stack) {
 45                 /*jslint regexp: true*/
 46                 matches = stack.match(/((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/);
 47                 /*jslint regexp: false*/
 48                 url = matches && matches[1];
 49             }
 50             if (typeof url === "string" && url) {
 51                 return url;
 52             }
 53             return undefined;
 54         }
 55 
 56         /**
 57          * Tries by various tricks to get the url of this script.
 58          * To be called if document.currentScript is not supported
 59          * @return {!string|undefined}
 60          */
 61         function getCurrentScriptElementSrcByTricks() {
 62             var scriptElements = document.getElementsByTagName("script");
 63 
 64             // if there is only one script, it must be this
 65             if (scriptElements.length === 1) {
 66                 return scriptElements[0].src;
 67             }
 68 
 69             // otherwise get it from the stacktrace
 70             try {
 71                 throw new Error();
 72             } catch (err) {
 73                 return getScriptUrlFromStack(err.stack);
 74             }
 75         }
 76 
 77         var path = ".", scriptElementSrc,
 78             a, pathname, pos;
 79 
 80         if (document.currentScript && document.currentScript.src) {
 81             scriptElementSrc = document.currentScript.src;
 82         } else {
 83             scriptElementSrc = getCurrentScriptElementSrcByTricks();
 84         }
 85 
 86         if (scriptElementSrc) {
 87             a = document.createElement('a');
 88             a.href = scriptElementSrc;
 89             pathname = a.pathname;
 90             if (pathname.charAt(0) !== "/") {
 91                 // Various versions of Internet Explorer seems to neglect the leading slash under some conditions
 92                 // (not when watching it with the dev tools of course!). This was confirmed in IE10 + IE11
 93                 pathname = "/" + pathname;
 94             }
 95 
 96             pos = pathname.lastIndexOf("/");
 97             if (pos !== -1) {
 98                 path = pathname.substr(0, pos);
 99             }
100         } else {
101             alert("Could not estimate installation path of the Wodo.TextEditor.");
102         }
103         return path;
104     }
105 
106     var /** @inner @const
107             @type{!string} */
108         installationPath = getInstallationPath(),
109         /** @inner @type{!boolean} */
110         isInitalized = false,
111         /** @inner @type{!Array.<!function():undefined>} */
112         pendingInstanceCreationCalls = [],
113         /** @inner @type{!number} */
114         instanceCounter = 0,
115         // TODO: avatar image url needs base-url setting.
116         // so far Wodo itself does not have a setup call,
117         // but then the avatar is also not used yet here
118         defaultUserData = {
119             fullName: "",
120             color:    "black",
121             imageUrl: "avatar-joe.png"
122         },
123         /** @inner @const
124             @type{!Array.<!string>} */
125         userDataFieldNames = ["fullName", "color", "imageUrl"],
126         /** @inner @const
127             @type{!string} */
128         memberId = "localuser",
129         // constructors
130         BorderContainer, ContentPane, FullWindowZoomHelper, EditorSession, Tools,
131         /** @inner @const
132             @type{!string} */
133         MODUS_FULLEDITING = "fullediting",
134         /** @inner @const
135             @type{!string} */
136         MODUS_REVIEW = "review",
137         /** @inner @const
138             @type{!string} */
139         EVENT_UNKNOWNERROR = "unknownError",
140         /** @inner @const
141             @type {!string} */
142         EVENT_DOCUMENTMODIFIEDCHANGED = "documentModifiedChanged",
143         /** @inner @const
144             @type {!string} */
145         EVENT_METADATACHANGED = "metadataChanged";
146 
147     window.dojoConfig = (function () {
148         var WebODFEditorDojoLocale = "C";
149 
150         if (navigator && navigator.language && navigator.language.match(/^(de)/)) {
151             WebODFEditorDojoLocale = navigator.language.substr(0, 2);
152         }
153 
154         return {
155             locale: WebODFEditorDojoLocale,
156             paths: {
157                 "webodf/editor": installationPath,
158                 "dijit":         installationPath + "/dijit",
159                 "dojox":         installationPath + "/dojox",
160                 "dojo":          installationPath + "/dojo",
161                 "resources":     installationPath + "/resources"
162             }
163         };
164     }());
165 
166     /**
167      * @return {undefined}
168      */
169     function initTextEditor() {
170         require([
171             "dijit/layout/BorderContainer",
172             "dijit/layout/ContentPane",
173             "webodf/editor/FullWindowZoomHelper",
174             "webodf/editor/EditorSession",
175             "webodf/editor/Tools",
176             "webodf/editor/Translator"],
177             function (BC, CP, FWZH, ES, T, Translator) {
178                 var locale = navigator.language || "en-US",
179                     editorBase = dojo.config && dojo.config.paths && dojo.config.paths["webodf/editor"],
180                     translationsDir = editorBase + '/translations',
181                     t;
182 
183                 BorderContainer = BC;
184                 ContentPane = CP;
185                 FullWindowZoomHelper = FWZH;
186                 EditorSession = ES;
187                 Tools = T;
188 
189                 // TODO: locale cannot be set by the user, also different for different editors
190                 t = new Translator(translationsDir, locale, function (editorTranslator) {
191                     runtime.setTranslator(editorTranslator.translate);
192                     // Extend runtime with a convenient translation function
193                     runtime.translateContent = function (node) {
194                         var i,
195                             element,
196                             tag,
197                             placeholder,
198                             translatable = node.querySelectorAll("*[text-i18n]");
199 
200                         for (i = 0; i < translatable.length; i += 1) {
201                             element = translatable[i];
202                             tag = element.localName;
203                             placeholder = element.getAttribute('text-i18n');
204                             if (tag === "label"
205                                     || tag === "span"
206                                     || /h\d/i.test(tag)) {
207                                 element.textContent = runtime.tr(placeholder);
208                             }
209                         }
210                     };
211 
212                     defaultUserData.fullName = runtime.tr("Unknown Author");
213 
214                     isInitalized = true;
215                     pendingInstanceCreationCalls.forEach(function (create) { create(); });
216                 });
217 
218                 // only done to make jslint see the var used
219                 return t;
220             }
221         );
222     }
223 
224     /**
225      * Creates a new record with userdata, and for all official fields
226      * copies over the value from the original or, if not present there,
227      * sets it to the default value.
228      * @param {?Object.<!string,!string>|undefined} original, defaults to {}
229      * @return {!Object.<!string,!string>}
230      */
231     function cloneUserData(original) {
232         var result = {};
233 
234         if (!original) {
235             original = {};
236         }
237 
238         userDataFieldNames.forEach(function (fieldName) {
239             result[fieldName] = original[fieldName] || defaultUserData[fieldName];
240         });
241 
242         return result;
243     }
244 
245     /**
246      * @name TextEditor
247      * @constructor
248      * @param {!string} mainContainerElementId
249      * @param {!Object.<!string,!*>} editorOptions
250      */
251     function TextEditor(mainContainerElementId, editorOptions) {
252         instanceCounter = instanceCounter + 1;
253 
254         /**
255         * Returns true if either all features are wanted and this one is not explicitely disabled
256         * or if not all features are wanted by default and it is explicitely enabled
257         * @param {?boolean|undefined} isFeatureEnabled explicit flag which enables a feature
258         * @return {!boolean}
259         */
260         function isEnabled(isFeatureEnabled) {
261             return editorOptions.allFeaturesEnabled ? (isFeatureEnabled !== false) : isFeatureEnabled;
262         }
263 
264         var userData,
265             //
266             mainContainerElement = document.getElementById(mainContainerElementId),
267             canvasElement,
268             canvasContainerElement,
269             toolbarElement,
270             toolbarContainerElement, // needed because dijit toolbar overwrites direct classList
271             editorElement,
272             /** @inner @const
273                 @type{!string} */
274             canvasElementId = "webodfeditor-canvas" + instanceCounter,
275             /** @inner @const
276                 @type{!string} */
277             canvasContainerElementId = "webodfeditor-canvascontainer" + instanceCounter,
278             /** @inner @const
279                 @type{!string} */
280             toolbarElementId = "webodfeditor-toolbar" + instanceCounter,
281             /** @inner @const
282                 @type{!string} */
283             editorElementId = "webodfeditor-editor" + instanceCounter,
284             //
285             fullWindowZoomHelper,
286             //
287             mainContainer,
288             tools,
289             odfCanvas,
290             //
291             editorSession,
292             session,
293             //
294             loadOdtFile = editorOptions.loadCallback,
295             saveOdtFile = editorOptions.saveCallback,
296             saveAsOdtFile = editorOptions.saveAsCallback,
297             downloadOdtFile = editorOptions.downloadCallback,
298             close =       editorOptions.closeCallback,
299             //
300             reviewModeEnabled = (editorOptions.modus === MODUS_REVIEW),
301             directTextStylingEnabled = isEnabled(editorOptions.directTextStylingEnabled),
302             directParagraphStylingEnabled = isEnabled(editorOptions.directParagraphStylingEnabled),
303             paragraphStyleSelectingEnabled = (!reviewModeEnabled) && isEnabled(editorOptions.paragraphStyleSelectingEnabled),
304             paragraphStyleEditingEnabled =   (!reviewModeEnabled) && isEnabled(editorOptions.paragraphStyleEditingEnabled),
305             imageEditingEnabled =            (!reviewModeEnabled) && isEnabled(editorOptions.imageEditingEnabled),
306             hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled),
307             annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled),
308             undoRedoEnabled = isEnabled(editorOptions.undoRedoEnabled),
309             zoomingEnabled = isEnabled(editorOptions.zoomingEnabled),
310             //
311             pendingMemberId,
312             pendingEditorReadyCallback,
313             //
314             eventNotifier = new core.EventNotifier([
315                 EVENT_UNKNOWNERROR,
316                 EVENT_DOCUMENTMODIFIEDCHANGED,
317                 EVENT_METADATACHANGED
318             ]);
319 
320         runtime.assert(Boolean(mainContainerElement), "No id of an existing element passed to Wodo.createTextEditor(): " + mainContainerElementId);
321 
322         /**
323          * @param {!Object} changes
324          * @return {undefined}
325          */
326         function relayMetadataSignal(changes) {
327             eventNotifier.emit(EVENT_METADATACHANGED, changes);
328         }
329 
330         /**
331          * @param {!Object} changes
332          * @return {undefined}
333          */
334         function relayModifiedSignal(modified) {
335             eventNotifier.emit(EVENT_DOCUMENTMODIFIEDCHANGED, modified);
336         }
337 
338         /**
339          * @return {undefined}
340          */
341         function createSession() {
342             var viewOptions = {
343                     editInfoMarkersInitiallyVisible: false,
344                     caretAvatarsInitiallyVisible: false,
345                     caretBlinksOnRangeSelect: true
346                 };
347 
348             // create session around loaded document
349             session = new ops.Session(odfCanvas);
350             editorSession = new EditorSession(session, pendingMemberId, {
351                 viewOptions: viewOptions,
352                 directTextStylingEnabled: directTextStylingEnabled,
353                 directParagraphStylingEnabled: directParagraphStylingEnabled,
354                 paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
355                 paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
356                 imageEditingEnabled: imageEditingEnabled,
357                 hyperlinkEditingEnabled: hyperlinkEditingEnabled,
358                 annotationsEnabled: annotationsEnabled,
359                 zoomingEnabled: zoomingEnabled,
360                 reviewModeEnabled: reviewModeEnabled
361             });
362             if (undoRedoEnabled) {
363                 editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
364                 editorSession.sessionController.getUndoManager().subscribe(gui.UndoManager.signalDocumentModifiedChanged, relayModifiedSignal);
365             }
366 
367             // Relay any metadata changes to the Editor's consumer as an event
368             editorSession.sessionController.getMetadataController().subscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);
369 
370             // and report back to caller
371             pendingEditorReadyCallback();
372             // reset
373             pendingEditorReadyCallback = null;
374             pendingMemberId = null;
375         }
376 
377         /**
378          * @return {undefined}
379          */
380         function startEditing() {
381             runtime.assert(editorSession, "editorSession should exist here.");
382 
383             tools.setEditorSession(editorSession);
384             editorSession.sessionController.insertLocalCursor();
385             editorSession.sessionController.startEditing();
386         }
387 
388         /**
389          * @return {undefined}
390          */
391         function endEditing() {
392             runtime.assert(editorSession, "editorSession should exist here.");
393 
394             tools.setEditorSession(undefined);
395             editorSession.sessionController.endEditing();
396             editorSession.sessionController.removeLocalCursor();
397         }
398 
399         /**
400          * Loads an ODT document into the editor.
401          * @name TextEditor#openDocumentFromUrl
402          * @function
403          * @param {!string} docUrl url from which the ODT document can be loaded
404          * @param {!function(!Error=):undefined} callback Called once the document has been opened, passes an error object in case of error
405          * @return {undefined}
406          */
407         this.openDocumentFromUrl = function (docUrl, editorReadyCallback) {
408             runtime.assert(docUrl, "document should be defined here.");
409             runtime.assert(!pendingEditorReadyCallback, "pendingEditorReadyCallback should not exist here.");
410             runtime.assert(!editorSession, "editorSession should not exist here.");
411             runtime.assert(!session, "session should not exist here.");
412 
413             pendingMemberId = memberId;
414             pendingEditorReadyCallback = function () {
415                 var op = new ops.OpAddMember();
416                 op.init({
417                     memberid: memberId,
418                     setProperties: userData
419                 });
420                 session.enqueue([op]);
421                 startEditing();
422                 if (editorReadyCallback) {
423                     editorReadyCallback();
424                 }
425             };
426 
427             odfCanvas.load(docUrl);
428         };
429 
430         /**
431          * Closes the document, and does cleanup.
432          * @name TextEditor#closeDocument
433          * @function
434          * @param {!function(!Error=):undefined} callback  Called once the document has been closed, passes an error object in case of error
435          * @return {undefined}
436          */
437         this.closeDocument = function (callback) {
438             runtime.assert(session, "session should exist here.");
439 
440             endEditing();
441 
442             var op = new ops.OpRemoveMember();
443             op.init({
444                 memberid: memberId
445             });
446             session.enqueue([op]);
447 
448             session.close(function (err) {
449                 if (err) {
450                     callback(err);
451                 } else {
452                     editorSession.sessionController.getMetadataController().unsubscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);
453                     editorSession.destroy(function (err) {
454                         if (err) {
455                             callback(err);
456                         } else {
457                             editorSession = undefined;
458                             session.destroy(function (err) {
459                                 if (err) {
460                                     callback(err);
461                                 } else {
462                                     session = undefined;
463                                     callback();
464                                 }
465                             });
466                         }
467                     });
468                 }
469             });
470         };
471 
472         /**
473          * @name TextEditor#getDocumentAsByteArray
474          * @function
475          * @param {!function(err:?Error, file:!Uint8Array=):undefined} callback Called with the current document as ODT file as bytearray, passes an error object in case of error
476          * @return {undefined}
477          */
478         this.getDocumentAsByteArray = function (callback) {
479             var odfContainer = odfCanvas.odfContainer();
480 
481             if (odfContainer) {
482                 odfContainer.createByteArray(function (ba) {
483                     callback(null, ba);
484                 }, function (errorString) {
485                     callback(new Error(errorString || "Could not create bytearray from OdfContainer."));
486                 });
487             } else {
488                 callback(new Error("No odfContainer set!"));
489             }
490         };
491 
492         /**
493          * Sets the metadata fields from the given properties map.
494          * Avoid setting certain fields since they are automatically set:
495          *    dc:creator
496          *    dc:date
497          *    meta:editing-cycles
498          *
499          * The following properties are never used and will be removed for semantic
500          * consistency from the document:
501          *     meta:editing-duration
502          *     meta:document-statistic
503          *
504          * Setting any of the above mentioned fields using this method will have no effect.
505          *
506          * @name TextEditor#setMetadata
507          * @function
508          * @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value.
509          * @param {?Array.<!string>} removedProperties An array of metadata field names (prefixed).
510          * @return {undefined}
511          */
512         this.setMetadata = function (setProperties, removedProperties) {
513             runtime.assert(editorSession, "editorSession should exist here.");
514 
515             editorSession.sessionController.getMetadataController().setMetadata(setProperties, removedProperties);
516         };
517 
518         /**
519          * Returns the value of the requested document metadata field.
520          * @name TextEditor#getMetadata
521          * @function
522          * @param {!string} property A namespace-prefixed field name, for example
523          * dc:creator
524          * @return {?string}
525          */
526         this.getMetadata = function (property) {
527             runtime.assert(editorSession, "editorSession should exist here.");
528 
529             return editorSession.sessionController.getMetadataController().getMetadata(property);
530         };
531 
532         /**
533          * Sets the data for the person that is editing the document.
534          * The supported fields are:
535          *     "fullName": the full name of the editing person
536          *     "color": color to use for the user specific UI elements
537          * @name TextEditor#setUserData
538          * @function
539          * @param {?Object.<!string,!string>|undefined} data
540          * @return {undefined}
541          */
542         function setUserData(data) {
543             userData = cloneUserData(data);
544         }
545         this.setUserData = setUserData;
546 
547         /**
548          * Returns the data set for the person that is editing the document.
549          * @name TextEditor#getUserData
550          * @function
551          * @return {!Object.<!string,!string>}
552          */
553         this.getUserData = function () {
554             return cloneUserData(userData);
555         };
556 
557         /**
558          * Sets the current state of the document to be either the unmodified state
559          * or a modified state.
560          * If @p modified is @true and the current state was already a modified state,
561          * this call has no effect and also does not remove the unmodified flag
562          * from the state which has it set.
563          *
564          * @name TextEditor#setDocumentModified
565          * @function
566          * @param {!boolean} modified
567          * @return {undefined}
568          */
569         this.setDocumentModified = function (modified) {
570             runtime.assert(editorSession, "editorSession should exist here.");
571 
572             if (undoRedoEnabled) {
573                 editorSession.sessionController.getUndoManager().setDocumentModified(modified);
574             }
575         };
576 
577         /**
578          * Returns if the current state of the document matches the unmodified state.
579          * @name TextEditor#isDocumentModified
580          * @function
581          * @return {!boolean}
582          */
583         this.isDocumentModified = function () {
584             runtime.assert(editorSession, "editorSession should exist here.");
585 
586             if (undoRedoEnabled) {
587                 return editorSession.sessionController.getUndoManager().isDocumentModified();
588             }
589 
590             return false;
591         };
592 
593         /**
594          * @return {undefined}
595          */
596         function setFocusToOdfCanvas() {
597             editorSession.sessionController.getEventManager().focus();
598         }
599 
600         /**
601          * @param {!function(!Error=):undefined} callback passes an error object in case of error
602          * @return {undefined}
603          */
604         function destroyInternal(callback) {
605             mainContainerElement.removeChild(editorElement);
606 
607             callback();
608         }
609 
610         /**
611          * Destructs the editor object completely.
612          * @name TextEditor#destroy
613          * @function
614          * @param {!function(!Error=):undefined} callback Called once the destruction has been completed, passes an error object in case of error
615          * @return {undefined}
616          */
617         this.destroy = function (callback) {
618             var destroyCallbacks = [];
619 
620             // TODO: decide if some forced close should be done here instead of enforcing proper API usage
621             runtime.assert(!session, "session should not exist here.");
622 
623             // TODO: investigate what else needs to be done
624             mainContainer.destroyRecursive(true);
625 
626             destroyCallbacks = destroyCallbacks.concat([
627                 fullWindowZoomHelper.destroy,
628                 tools.destroy,
629                 odfCanvas.destroy,
630                 destroyInternal
631             ]);
632 
633             core.Async.destroyAll(destroyCallbacks, callback);
634         };
635 
636         // TODO:
637         // this.openDocumentFromByteArray = openDocumentFromByteArray; see also https://github.com/kogmbh/WebODF/issues/375
638         // setReadOnly: setReadOnly,
639 
640         /**
641          * Registers a callback which should be called if the given event happens.
642          * @name TextEditor#addEventListener
643          * @function
644          * @param {!string} eventId
645          * @param {!Function} callback
646          * @return {undefined}
647          */
648         this.addEventListener = eventNotifier.subscribe;
649         /**
650          * Unregisters a callback for the given event.
651          * @name TextEditor#removeEventListener
652          * @function
653          * @param {!string} eventId
654          * @param {!Function} callback
655          * @return {undefined}
656          */
657         this.removeEventListener = eventNotifier.unsubscribe;
658 
659 
660         /**
661          * @return {undefined}
662          */
663         function init() {
664             var editorPane,
665                 /** @inner @const
666                     @type{!string} */
667                 documentns = document.documentElement.namespaceURI;
668 
669             /**
670              * @param {!string} tagLocalName
671              * @param {!string|undefined} id
672              * @param {!string} className
673              * @return {!Element}
674              */
675             function createElement(tagLocalName, id, className) {
676                 var element;
677                 element = document.createElementNS(documentns, tagLocalName);
678                 if (id) {
679                     element.id = id;
680                 }
681                 element.classList.add(className);
682                 return element;
683             }
684 
685             // create needed tree structure
686             canvasElement = createElement('div', canvasElementId, "webodfeditor-canvas");
687             canvasContainerElement = createElement('div', canvasContainerElementId, "webodfeditor-canvascontainer");
688             toolbarElement = createElement('span', toolbarElementId, "webodfeditor-toolbar");
689             toolbarContainerElement = createElement('span', undefined, "webodfeditor-toolbarcontainer");
690             editorElement = createElement('div', editorElementId, "webodfeditor-editor");
691 
692             // put into tree
693             canvasContainerElement.appendChild(canvasElement);
694             toolbarContainerElement.appendChild(toolbarElement);
695             editorElement.appendChild(toolbarContainerElement);
696             editorElement.appendChild(canvasContainerElement);
697             mainContainerElement.appendChild(editorElement);
698 
699             // style all elements with Dojo's claro.
700             // Not nice to do this on body, but then there is no other way known
701             // to style also all dialogs, which are attached directly to body
702             document.body.classList.add("claro");
703 
704             // prevent browser translation service messing up internal address system
705             // TODO: this should be done more centrally, but where exactly?
706             canvasElement.setAttribute("translate", "no");
707             canvasElement.classList.add("notranslate");
708 
709             // create widgets
710             mainContainer = new BorderContainer({}, mainContainerElementId);
711 
712             editorPane = new ContentPane({
713                 region: 'center'
714             }, editorElementId);
715             mainContainer.addChild(editorPane);
716 
717             mainContainer.startup();
718 
719             tools = new Tools(toolbarElementId, {
720                 onToolDone: setFocusToOdfCanvas,
721                 loadOdtFile: loadOdtFile,
722                 saveOdtFile: saveOdtFile,
723                 saveAsOdtFile: saveAsOdtFile,
724                 downloadOdtFile: downloadOdtFile,
725                 close: close,
726                 directTextStylingEnabled: directTextStylingEnabled,
727                 directParagraphStylingEnabled: directParagraphStylingEnabled,
728                 paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
729                 paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
730                 imageInsertingEnabled: imageEditingEnabled,
731                 hyperlinkEditingEnabled: hyperlinkEditingEnabled,
732                 annotationsEnabled: annotationsEnabled,
733                 undoRedoEnabled: undoRedoEnabled,
734                 zoomingEnabled: zoomingEnabled,
735                 aboutEnabled: true
736             });
737 
738             odfCanvas = new odf.OdfCanvas(canvasElement);
739             odfCanvas.enableAnnotations(annotationsEnabled, true);
740 
741             odfCanvas.addListener("statereadychange", createSession);
742 
743             fullWindowZoomHelper = new FullWindowZoomHelper(toolbarContainerElement, canvasContainerElement);
744 
745             setUserData(editorOptions.userData);
746         }
747 
748         init();
749     }
750 
751     function loadDojoAndStuff(callback) {
752         var head = document.getElementsByTagName("head")[0],
753             frag = document.createDocumentFragment(),
754             link,
755             script;
756 
757         // append two link and two script elements to the header
758         link = document.createElement("link");
759         link.rel = "stylesheet";
760         link.href = installationPath + "/app/resources/app.css";
761         link.type = "text/css";
762         link.async = false;
763         frag.appendChild(link);
764         link = document.createElement("link");
765         link.rel = "stylesheet";
766         link.href = installationPath + "/wodotexteditor.css";
767         link.type = "text/css";
768         link.async = false;
769         frag.appendChild(link);
770         script = document.createElement("script");
771         script.src = installationPath + "/dojo-amalgamation.js";
772         script["data-dojo-config"] = "async: true";
773         script.charset = "utf-8";
774         script.type = "text/javascript";
775         script.async = false;
776         frag.appendChild(script);
777         script = document.createElement("script");
778         script.src = installationPath + "/webodf.js";
779         script.charset = "utf-8";
780         script.type = "text/javascript";
781         script.async = false;
782         script.onload = callback;
783         frag.appendChild(script);
784         head.appendChild(frag);
785     }
786 
787     /**
788      * Creates a text editor object and returns it on success in the passed callback.
789      * @name Wodo#createTextEditor
790      * @function
791      * @param {!string} editorContainerElementId id of the existing div element which will contain the editor (should be empty before)
792      * @param editorOptions options to configure the features of the editor. All entries are optional
793      * @param [editorOptions.modus=Wodo.MODUS_FULLEDITING] set the editing modus. Current options: Wodo.MODUS_FULLEDITING, Wodo.MODUS_REVIEW
794      * @param [editorOptions.loadCallback] parameter-less callback method, adds a "Load" button to the toolbar which triggers this method
795      * @param [editorOptions.saveCallback] parameter-less callback method, adds a "Save" button to the toolbar which triggers this method
796      * @param [editorOptions.saveAsCallback] parameter-less callback method, adds a "Save as" button to the toolbar which triggers this method
797      * @param [editorOptions.downloadCallback] parameter-less callback method, adds a "Download" button to the right of the toolbar which triggers this method
798      * @param [editorOptions.closeCallback] parameter-less callback method, adds a "Save" button to the toolbar which triggers this method
799      * @param [editorOptions.allFeaturesEnabled=false] if set to 'true', switches the default for all features from 'false' to 'true'
800      * @param [editorOptions.directTextStylingEnabled=false] if set to 'true', enables the direct styling of text (e.g. bold/italic or font)
801      * @param [editorOptions.directParagraphStylingEnabled=false] if set to 'true', enables the direct styling of paragraphs (e.g. indentation or alignement)
802      * @param [editorOptions.paragraphStyleSelectingEnabled=false] if set to 'true', enables setting of defined paragraph styles to paragraphs
803      * @param [editorOptions.paragraphStyleEditingEnabled=false] if set to 'true', enables the editing of defined paragraph styles
804      * @param [editorOptions.imageEditingEnabled=false] if set to 'true', enables the insertion of images
805      * @param [editorOptions.hyperlinkEditingEnabled=false] if set to 'true', enables the editing of hyperlinks
806      * @param [editorOptions.annotationsEnabled=false] if set to 'true', enables the display and the editing of annotations
807      * @param [editorOptions.undoRedoEnabled=false] if set to 'true', enables the Undo and Redo of editing actions
808      * @param [editorOptions.zoomingEnabled=false] if set to 'true', enables the zooming tool
809      * @param [editorOptions.userData] data about the user editing the document
810      * @param [editorOptions.userData.fullName] full name of the user, used for annotations and in the metadata of the document
811      * @param [editorOptions.userData.color="black"] color to use for any user related indicators like cursor or annotations
812      * @param {!function(err:?Error, editor:!TextEditor=):undefined} onEditorCreated
813      * @return {undefined}
814      */
815     function createTextEditor(editorContainerElementId, editorOptions, onEditorCreated) {
816         /**
817          * @return {undefined}
818          */
819         function create() {
820             var editor = new TextEditor(editorContainerElementId, editorOptions);
821             onEditorCreated(null, editor);
822         }
823 
824         if (!isInitalized) {
825             pendingInstanceCreationCalls.push(create);
826             // first request?
827             if (pendingInstanceCreationCalls.length === 1) {
828                 if (String(typeof WodoFromSource) === "undefined") {
829                     loadDojoAndStuff(initTextEditor);
830                 } else {
831                     initTextEditor();
832                 }
833             }
834         } else {
835             create();
836         }
837     }
838 
839 
840     /**
841      * @lends Wodo#
842      */
843     return {
844         createTextEditor: createTextEditor,
845         // flags
846         /** Id of full editing modus */
847         MODUS_FULLEDITING: MODUS_FULLEDITING,
848         /** Id of review modus */
849         MODUS_REVIEW: MODUS_REVIEW,
850         /** Id of event for an unkown error */
851         EVENT_UNKNOWNERROR: EVENT_UNKNOWNERROR,
852         /** Id of event if documentModified state changes */
853         EVENT_DOCUMENTMODIFIEDCHANGED: EVENT_DOCUMENTMODIFIEDCHANGED,
854         /** Id of event if metadata changes */
855         EVENT_METADATACHANGED: EVENT_METADATACHANGED
856     };
857 }());
858