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