Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

772 linhas
24 KiB

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