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