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