wodotexteditor.js 34 KB

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