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