tinymce4Adapter.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /* global popups, tinyMceEditors, MediabrowserUtility, Base64 */
  6. /* eslint-disable strict */
  7. define([
  8. 'jquery',
  9. 'underscore',
  10. 'tinymce4',
  11. 'mage/adminhtml/events',
  12. 'mage/adminhtml/wysiwyg/events',
  13. 'mage/translate',
  14. 'prototype',
  15. 'jquery/ui'
  16. ], function (jQuery, _, tinyMCE4, varienGlobalEvents, wysiwygEvents) {
  17. 'use strict';
  18. var tinyMce4Wysiwyg = Class.create();
  19. tinyMce4Wysiwyg.prototype = {
  20. mediaBrowserOpener: null,
  21. mediaBrowserTargetElementId: null,
  22. magentoVariablesPlugin: null,
  23. mode: 'exact',
  24. /**
  25. * @param {*} htmlId
  26. * @param {Object} config
  27. */
  28. initialize: function (htmlId, config) {
  29. this.id = htmlId;
  30. this.config = config;
  31. _.bindAll(
  32. this,
  33. 'beforeSetContent',
  34. 'saveContent',
  35. 'onChangeContent',
  36. 'openFileBrowser',
  37. 'updateTextArea',
  38. 'onUndo',
  39. 'removeEvents'
  40. );
  41. varienGlobalEvents.attachEventHandler('tinymceChange', this.onChangeContent);
  42. varienGlobalEvents.attachEventHandler('tinymceBeforeSetContent', this.beforeSetContent);
  43. varienGlobalEvents.attachEventHandler('tinymceSetContent', this.updateTextArea);
  44. varienGlobalEvents.attachEventHandler('tinymceSaveContent', this.saveContent);
  45. varienGlobalEvents.attachEventHandler('tinymceUndo', this.onUndo);
  46. if (typeof tinyMceEditors === 'undefined') {
  47. window.tinyMceEditors = $H({});
  48. }
  49. tinyMceEditors.set(this.id, this);
  50. },
  51. /**
  52. * Ensures the undo operation works properly
  53. */
  54. onUndo: function () {
  55. this.addContentEditableAttributeBackToNonEditableNodes();
  56. },
  57. /**
  58. * Setup TinyMCE4 editor
  59. */
  60. setup: function (mode) {
  61. var deferreds = [],
  62. settings,
  63. self = this;
  64. this.turnOff();
  65. if (this.config.plugins) {
  66. this.config.plugins.forEach(function (plugin) {
  67. var deferred;
  68. self.addPluginToToolbar(plugin.name, '|');
  69. if (!plugin.src) {
  70. return;
  71. }
  72. deferred = jQuery.Deferred();
  73. deferreds.push(deferred);
  74. require([plugin.src], function (factoryFn) {
  75. if (typeof factoryFn === 'function') {
  76. factoryFn(plugin.options);
  77. }
  78. tinyMCE4.PluginManager.load(plugin.name, plugin.src);
  79. deferred.resolve();
  80. });
  81. });
  82. }
  83. if (jQuery.isReady) {
  84. tinyMCE4.dom.Event.domLoaded = true;
  85. }
  86. settings = this.getSettings();
  87. if (mode === 'inline') {
  88. settings.inline = true;
  89. if (!isNaN(settings.toolbarZIndex)) {
  90. tinyMCE4.ui.FloatPanel.zIndex = settings.toolbarZIndex;
  91. }
  92. this.removeEvents(self.id);
  93. }
  94. jQuery.when.apply(jQuery, deferreds).done(function () {
  95. tinyMCE4.init(settings);
  96. this.getPluginButtons().hide();
  97. this.eventBus.attachEventHandler('open_browser_callback', tinyMceEditors.get(self.id).openFileBrowser);
  98. }.bind(this));
  99. },
  100. /**
  101. * Remove events from instance.
  102. *
  103. * @param {String} wysiwygId
  104. */
  105. removeEvents: function (wysiwygId) {
  106. var editor;
  107. if (typeof tinyMceEditors !== 'undefined' && tinyMceEditors.get(wysiwygId)) {
  108. editor = tinyMceEditors.get(wysiwygId);
  109. varienGlobalEvents.removeEventHandler('tinymceChange', editor.onChangeContent);
  110. }
  111. },
  112. /**
  113. * Add plugin to the toolbar if not added.
  114. *
  115. * @param {String} plugin
  116. * @param {String} separator
  117. */
  118. addPluginToToolbar: function (plugin, separator) {
  119. var plugins = this.config.tinymce4.plugins.split(' '),
  120. toolbar = this.config.tinymce4.toolbar.split(' ');
  121. if (plugins.indexOf(plugin) === -1) {
  122. plugins.push(plugin);
  123. }
  124. if (toolbar.indexOf(plugin) === -1) {
  125. toolbar.push(separator || '', plugin);
  126. }
  127. this.config.tinymce4.plugins = plugins.join(' ');
  128. this.config.tinymce4.toolbar = toolbar.join(' ');
  129. },
  130. /**
  131. * Set the status of the toolbar to disabled or enabled (true for enabled, false for disabled)
  132. * @param {Boolean} enabled
  133. */
  134. setToolbarStatus: function (enabled) {
  135. var controlIds = this.get(this.getId()).theme.panel.rootControl.controlIdLookup;
  136. _.each(controlIds, function (controlId) {
  137. controlId.disabled(!enabled);
  138. controlId.canFocus = enabled;
  139. if (controlId.tooltip) {
  140. controlId.tooltip().state.set('rendered', enabled);
  141. if (enabled) {
  142. jQuery(controlId.getEl()).children('button').andSelf().removeAttr('style');
  143. } else {
  144. jQuery(controlId.getEl()).children('button').andSelf().attr('style', 'color: inherit;' +
  145. 'background-color: inherit;' +
  146. 'border-color: transparent;'
  147. );
  148. }
  149. }
  150. });
  151. },
  152. /**
  153. * @return {Object}
  154. */
  155. getSettings: function () {
  156. var settings,
  157. eventBus = this.eventBus;
  158. settings = {
  159. selector: '#' + this.getId(),
  160. theme: 'modern',
  161. 'entity_encoding': 'raw',
  162. 'convert_urls': false,
  163. 'content_css': this.config.tinymce4['content_css'],
  164. 'relative_urls': true,
  165. menubar: false,
  166. plugins: this.config.tinymce4.plugins,
  167. toolbar: this.config.tinymce4.toolbar,
  168. adapter: this,
  169. /**
  170. * @param {Object} editor
  171. */
  172. setup: function (editor) {
  173. var onChange;
  174. editor.on('BeforeSetContent', function (evt) {
  175. varienGlobalEvents.fireEvent('tinymceBeforeSetContent', evt);
  176. eventBus.fireEvent(wysiwygEvents.beforeSetContent);
  177. });
  178. editor.on('SaveContent', function (evt) {
  179. varienGlobalEvents.fireEvent('tinymceSaveContent', evt);
  180. eventBus.fireEvent(wysiwygEvents.afterSave);
  181. });
  182. editor.on('paste', function (evt) {
  183. varienGlobalEvents.fireEvent('tinymcePaste', evt);
  184. eventBus.fireEvent(wysiwygEvents.afterPaste);
  185. });
  186. editor.on('PostProcess', function (evt) {
  187. varienGlobalEvents.fireEvent('tinymceSaveContent', evt);
  188. eventBus.fireEvent(wysiwygEvents.afterSave);
  189. });
  190. editor.on('undo', function (evt) {
  191. varienGlobalEvents.fireEvent('tinymceUndo', evt);
  192. eventBus.fireEvent(wysiwygEvents.afterUndo);
  193. });
  194. editor.on('focus', function () {
  195. eventBus.fireEvent(wysiwygEvents.afterFocus);
  196. });
  197. editor.on('blur', function () {
  198. eventBus.fireEvent(wysiwygEvents.afterBlur);
  199. });
  200. /**
  201. * @param {*} evt
  202. */
  203. onChange = function (evt) {
  204. varienGlobalEvents.fireEvent('tinymceChange', evt);
  205. eventBus.fireEvent(wysiwygEvents.afterChangeContent);
  206. };
  207. editor.on('Change', onChange);
  208. editor.on('keyup', onChange);
  209. editor.on('ExecCommand', function (cmd) {
  210. varienGlobalEvents.fireEvent('tinymceExecCommand', cmd);
  211. });
  212. editor.on('init', function (args) {
  213. varienGlobalEvents.fireEvent('wysiwygEditorInitialized', args.target);
  214. eventBus.fireEvent(wysiwygEvents.afterInitialization);
  215. });
  216. }
  217. };
  218. if (this.config.baseStaticUrl && this.config.baseStaticDefaultUrl) {
  219. settings['document_base_url'] = this.config.baseStaticUrl;
  220. }
  221. // Set the document base URL
  222. if (this.config['document_base_url']) {
  223. settings['document_base_url'] = this.config['document_base_url'];
  224. }
  225. if (this.config['files_browser_window_url']) {
  226. /**
  227. * @param {*} fieldName
  228. * @param {*} url
  229. * @param {*} objectType
  230. * @param {*} w
  231. */
  232. settings['file_browser_callback'] = function (fieldName, url, objectType, w) {
  233. var payload = {
  234. win: w,
  235. type: objectType,
  236. field: fieldName
  237. };
  238. varienGlobalEvents.fireEvent('open_browser_callback', payload);
  239. this.eventBus.fireEvent('open_browser_callback', payload);
  240. }.bind(this);
  241. }
  242. if (this.config.width) {
  243. settings.width = this.config.width;
  244. }
  245. if (this.config.height) {
  246. settings.height = this.config.height;
  247. }
  248. if (this.config.plugins) {
  249. settings.magentoPluginsOptions = {};
  250. _.each(this.config.plugins, function (plugin) {
  251. settings.magentoPluginsOptions[plugin.name] = plugin.options;
  252. });
  253. }
  254. if (this.config.settings) {
  255. Object.extend(settings, this.config.settings);
  256. }
  257. return settings;
  258. },
  259. /**
  260. * @param {String} id
  261. */
  262. get: function (id) {
  263. return tinyMCE4.get(id);
  264. },
  265. /**
  266. * @return {String|null}
  267. */
  268. getId: function () {
  269. return this.id || (this.activeEditor() ? this.activeEditor().id : null) || tinyMceEditors.values()[0].id;
  270. },
  271. /**
  272. * @return {Object}
  273. */
  274. activeEditor: function () {
  275. return tinyMCE4.activeEditor;
  276. },
  277. /**
  278. * Insert content to active editor.
  279. *
  280. * @param {String} content
  281. * @param {Boolean} ui
  282. */
  283. insertContent: function (content, ui) {
  284. this.activeEditor().execCommand('mceInsertContent', typeof ui !== 'undefined' ? ui : false, content);
  285. },
  286. /**
  287. * Replace entire contents of wysiwyg with string content parameter
  288. *
  289. * @param {String} content
  290. */
  291. setContent: function (content) {
  292. this.get(this.getId()).setContent(content);
  293. },
  294. /**
  295. * Set caret location in WYSIWYG editor.
  296. *
  297. * @param {Object} targetElement
  298. */
  299. setCaretOnElement: function (targetElement) {
  300. this.activeEditor().selection.select(targetElement);
  301. this.activeEditor().selection.collapse();
  302. },
  303. /**
  304. * @param {Object} o
  305. */
  306. openFileBrowser: function (o) {
  307. var typeTitle = this.translate('Select Images'),
  308. storeId = this.config['store_id'] !== null ? this.config['store_id'] : 0,
  309. frameDialog = jQuery('div.mce-container[role="dialog"]'),
  310. wUrl = this.config['files_browser_window_url'] +
  311. 'target_element_id/' + this.getId() + '/' +
  312. 'store/' + storeId + '/';
  313. this.mediaBrowserOpener = o.win;
  314. this.mediaBrowserTargetElementId = o.field;
  315. if (typeof o.type !== 'undefined' && o.type !== '') { //eslint-disable-line eqeqeq
  316. wUrl = wUrl + 'type/' + o.type + '/';
  317. }
  318. frameDialog.hide();
  319. jQuery('#mce-modal-block').hide();
  320. require(['mage/adminhtml/browser'], function () {
  321. MediabrowserUtility.openDialog(wUrl, false, false, typeTitle, {
  322. /**
  323. * Closed.
  324. */
  325. closed: function () {
  326. frameDialog.show();
  327. jQuery('#mce-modal-block').show();
  328. }
  329. });
  330. });
  331. },
  332. /**
  333. * @param {String} string
  334. * @return {String}
  335. */
  336. translate: function (string) {
  337. return jQuery.mage.__ ? jQuery.mage.__(string) : string;
  338. },
  339. /**
  340. * @return {null}
  341. */
  342. getMediaBrowserOpener: function () {
  343. return this.mediaBrowserOpener;
  344. },
  345. /**
  346. * @return {null}
  347. */
  348. getMediaBrowserTargetElementId: function () {
  349. return this.mediaBrowserTargetElementId;
  350. },
  351. /**
  352. * @return {jQuery|*|HTMLElement}
  353. */
  354. getToggleButton: function () {
  355. return $('toggle' + this.getId());
  356. },
  357. /**
  358. * Get plugins button.
  359. */
  360. getPluginButtons: function () {
  361. return jQuery('#buttons' + this.getId() + ' > button.plugin');
  362. },
  363. /**
  364. * @param {*} mode
  365. * @return {wysiwygSetup}
  366. */
  367. turnOn: function (mode) {
  368. this.closePopups();
  369. this.setup(mode);
  370. this.getPluginButtons().hide();
  371. tinyMCE4.execCommand('mceAddControl', false, this.getId());
  372. return this;
  373. },
  374. /**
  375. * @param {String} name
  376. */
  377. closeEditorPopup: function (name) {
  378. if (typeof popups !== 'undefined' && popups[name] !== undefined && !popups[name].closed) {
  379. popups[name].close();
  380. }
  381. },
  382. /**
  383. * @return {wysiwygSetup}
  384. */
  385. turnOff: function () {
  386. this.closePopups();
  387. this.getPluginButtons().show();
  388. tinyMCE4.execCommand('mceRemoveEditor', false, this.getId());
  389. return this;
  390. },
  391. /**
  392. * Close popups.
  393. */
  394. closePopups: function () {
  395. // close all popups to avoid problems with updating parent content area
  396. varienGlobalEvents.fireEvent('wysiwygClosePopups');
  397. this.closeEditorPopup('browser_window' + this.getId());
  398. },
  399. /**
  400. * @return {Boolean}
  401. */
  402. toggle: function () {
  403. if (!tinyMCE4.get(this.getId())) {
  404. this.turnOn();
  405. return true;
  406. }
  407. this.turnOff();
  408. return false;
  409. },
  410. /**
  411. * On form validation.
  412. */
  413. onFormValidation: function () {
  414. if (tinyMCE4.get(this.getId())) {
  415. $(this.getId()).value = tinyMCE4.get(this.getId()).getContent();
  416. }
  417. },
  418. /**
  419. * On change content.
  420. */
  421. onChangeContent: function () {
  422. // Add "changed" to tab class if it exists
  423. var tab;
  424. this.updateTextArea();
  425. if (this.config['tab_id']) {
  426. tab = $$('a[id$=' + this.config['tab_id'] + ']')[0];
  427. if ($(tab) != undefined && $(tab).hasClassName('tab-item-link')) { //eslint-disable-line eqeqeq
  428. $(tab).addClassName('changed');
  429. }
  430. }
  431. },
  432. /**
  433. * @param {Object} o
  434. */
  435. beforeSetContent: function (o) {
  436. o.content = this.encodeContent(o.content);
  437. },
  438. /**
  439. * @param {Object} o
  440. */
  441. saveContent: function (o) {
  442. o.content = this.decodeContent(o.content);
  443. },
  444. /**
  445. * Return the content stored in the WYSIWYG field
  446. * @param {String} id
  447. * @return {String}
  448. */
  449. getContent: function (id) {
  450. return id ? this.get(id).getContent() : this.get(this.getId()).getContent();
  451. },
  452. /**
  453. * @returns {Object}
  454. */
  455. getAdapterPrototype: function () {
  456. return tinyMce4Wysiwyg;
  457. },
  458. /**
  459. * Fix range selection placement when typing. This fixes MAGETWO-84769
  460. * @param {Object} editor
  461. */
  462. fixRangeSelection: function (editor) {
  463. var selection = editor.selection,
  464. dom = editor.dom,
  465. rng = dom.createRng(),
  466. doc = editor.getDoc(),
  467. markerHtml,
  468. marker;
  469. // Validate the range we're trying to fix is contained within the current editors document
  470. if (!selection.getContent().length && jQuery.contains(doc, selection.getRng().startContainer)) {
  471. markerHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
  472. selection.setContent(markerHtml);
  473. marker = dom.get('mce_marker');
  474. rng.setStartBefore(marker);
  475. rng.setEndBefore(marker);
  476. dom.remove(marker);
  477. selection.setRng(rng);
  478. }
  479. },
  480. /**
  481. * Update text area.
  482. */
  483. updateTextArea: function () {
  484. var editor = this.get(this.getId()),
  485. content;
  486. if (!editor || editor.id !== this.activeEditor().id) {
  487. return;
  488. }
  489. this.addContentEditableAttributeBackToNonEditableNodes();
  490. this.fixRangeSelection(editor);
  491. content = editor.getContent();
  492. content = this.decodeContent(content);
  493. this.getTextArea().val(content).trigger('change');
  494. },
  495. /**
  496. * @return {Object} jQuery textarea element
  497. */
  498. getTextArea: function () {
  499. return jQuery('#' + this.getId());
  500. },
  501. /**
  502. * Set the status of the editor and toolbar
  503. *
  504. * @param {Boolean} enabled
  505. */
  506. setEnabledStatus: function (enabled) {
  507. if (this.activeEditor()) {
  508. this.activeEditor().getBody().setAttribute('contenteditable', enabled);
  509. this.activeEditor().readonly = !enabled;
  510. this.setToolbarStatus(enabled);
  511. }
  512. if (enabled) {
  513. this.getTextArea().removeProp('disabled');
  514. } else {
  515. this.getTextArea().prop('disabled', 'disabled');
  516. }
  517. },
  518. /**
  519. * Retrieve directives URL with substituted directive value.
  520. *
  521. * @param {String} directive
  522. */
  523. makeDirectiveUrl: function (directive) {
  524. return this.config['directives_url']
  525. .replace(/directive/, 'directive/___directive/' + directive)
  526. .replace(/\/$/, '');
  527. },
  528. /**
  529. * Convert {{directive}} style attributes syntax to absolute URLs
  530. * @param {Object} content
  531. * @return {*}
  532. */
  533. encodeDirectives: function (content) {
  534. // collect all HTML tags with attributes that contain directives
  535. return content.gsub(/<([a-z0-9\-\_]+[^>]+?)([a-z0-9\-\_]+="[^"]*?\{\{.+?\}\}.*?".*?)>/i, function (match) {
  536. var attributesString = match[2],
  537. decodedDirectiveString;
  538. // process tag attributes string
  539. attributesString = attributesString.gsub(/([a-z0-9\-\_]+)="(.*?)(\{\{.+?\}\})(.*?)"/i, function (m) {
  540. decodedDirectiveString = encodeURIComponent(Base64.mageEncode(m[3].replace(/&quot;/g, '"')));
  541. return m[1] + '="' + m[2] + this.makeDirectiveUrl(decodedDirectiveString) + m[4] + '"';
  542. }.bind(this));
  543. return '<' + match[1] + attributesString + '>';
  544. }.bind(this));
  545. },
  546. /**
  547. * Convert absolute URLs to {{directive}} style attributes syntax
  548. * @param {Object} content
  549. * @return {*}
  550. */
  551. decodeDirectives: function (content) {
  552. var directiveUrl = this.makeDirectiveUrl('%directive%').split('?')[0], // remove query string from directive
  553. // escape special chars in directives url to use in regular expression
  554. regexEscapedDirectiveUrl = directiveUrl.replace(/([$^.?*!+:=()\[\]{}|\\])/g, '\\$1'),
  555. regexDirectiveUrl = regexEscapedDirectiveUrl
  556. .replace(
  557. '%directive%',
  558. '([a-zA-Z0-9,_-]+(?:%2[A-Z]|)+\/?)(?:(?!").)*'
  559. ) + '/?(\\\\?[^"]*)?', // allow optional query string
  560. reg = new RegExp(regexDirectiveUrl);
  561. return content.gsub(reg, function (match) {
  562. return Base64.mageDecode(decodeURIComponent(match[1]).replace(/\/$/, '')).replace(/"/g, '&quot;');
  563. });
  564. },
  565. /**
  566. * @param {Object} attributes
  567. * @return {Object}
  568. */
  569. parseAttributesString: function (attributes) {
  570. var result = {};
  571. // Decode &quot; entity, as regex below does not support encoded quote
  572. attributes = attributes.replace(/&quot;/g, '"');
  573. attributes.gsub(
  574. /(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/,
  575. function (match) {
  576. result[match[1]] = match[2];
  577. }
  578. );
  579. return result;
  580. },
  581. /**
  582. * @param {Object} content
  583. * @return {*}
  584. */
  585. decodeContent: function (content) {
  586. if (this.config['add_directives']) {
  587. content = this.decodeDirectives(content);
  588. }
  589. content = varienGlobalEvents.fireEventReducer('wysiwygDecodeContent', content);
  590. return content;
  591. },
  592. /**
  593. * @param {Object} content
  594. * @return {*}
  595. */
  596. encodeContent: function (content) {
  597. if (this.config['add_directives']) {
  598. content = this.encodeDirectives(content);
  599. }
  600. content = varienGlobalEvents.fireEventReducer('wysiwygEncodeContent', content);
  601. return content;
  602. },
  603. /**
  604. * Reinstate contenteditable attributes on .mceNonEditable nodes
  605. */
  606. addContentEditableAttributeBackToNonEditableNodes: function () {
  607. jQuery('.mceNonEditable', this.activeEditor().getDoc()).attr('contenteditable', false);
  608. }
  609. };
  610. return tinyMce4Wysiwyg.prototype;
  611. });