ckeditor.admin.es6.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. /**
  2. * @file
  3. * CKEditor button and group configuration user interface.
  4. */
  5. (function($, Drupal, drupalSettings, _) {
  6. Drupal.ckeditor = Drupal.ckeditor || {};
  7. /**
  8. * Sets config behavior and creates config views for the CKEditor toolbar.
  9. *
  10. * @type {Drupal~behavior}
  11. *
  12. * @prop {Drupal~behaviorAttach} attach
  13. * Attaches admin behavior to the CKEditor buttons.
  14. * @prop {Drupal~behaviorDetach} detach
  15. * Detaches admin behavior from the CKEditor buttons on 'unload'.
  16. */
  17. Drupal.behaviors.ckeditorAdmin = {
  18. attach(context) {
  19. // Process the CKEditor configuration fragment once.
  20. const $configurationForm = $(context)
  21. .find('.ckeditor-toolbar-configuration')
  22. .once('ckeditor-configuration');
  23. if ($configurationForm.length) {
  24. const $textarea = $configurationForm
  25. // Hide the textarea that contains the serialized representation of the
  26. // CKEditor configuration.
  27. .find('.js-form-item-editor-settings-toolbar-button-groups')
  28. .hide()
  29. // Return the textarea child node from this expression.
  30. .find('textarea');
  31. // The HTML for the CKEditor configuration is assembled on the server
  32. // and sent to the client as a serialized DOM fragment.
  33. $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
  34. // Create a configuration model.
  35. Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
  36. $textarea,
  37. activeEditorConfig: JSON.parse($textarea.val()),
  38. hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig,
  39. });
  40. // Create the configuration Views.
  41. const viewDefaults = {
  42. model: Drupal.ckeditor.models.Model,
  43. el: $('.ckeditor-toolbar-configuration'),
  44. };
  45. Drupal.ckeditor.views = {
  46. controller: new Drupal.ckeditor.ControllerView(viewDefaults),
  47. visualView: new Drupal.ckeditor.VisualView(viewDefaults),
  48. keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
  49. auralView: new Drupal.ckeditor.AuralView(viewDefaults),
  50. };
  51. }
  52. },
  53. detach(context, settings, trigger) {
  54. // Early-return if the trigger for detachment is something else than
  55. // unload.
  56. if (trigger !== 'unload') {
  57. return;
  58. }
  59. // We're detaching because CKEditor as text editor has been disabled; this
  60. // really means that all CKEditor toolbar buttons have been removed.
  61. // Hence,all editor features will be removed, so any reactions from
  62. // filters will be undone.
  63. const $configurationForm = $(context)
  64. .find('.ckeditor-toolbar-configuration')
  65. .findOnce('ckeditor-configuration');
  66. if (
  67. $configurationForm.length &&
  68. Drupal.ckeditor.models &&
  69. Drupal.ckeditor.models.Model
  70. ) {
  71. const config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
  72. const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
  73. const $activeToolbar = $('.ckeditor-toolbar-configuration').find(
  74. '.ckeditor-toolbar-active',
  75. );
  76. for (let i = 0; i < buttons.length; i++) {
  77. $activeToolbar.trigger('CKEditorToolbarChanged', [
  78. 'removed',
  79. buttons[i],
  80. ]);
  81. }
  82. }
  83. },
  84. };
  85. /**
  86. * CKEditor configuration UI methods of Backbone objects.
  87. *
  88. * @namespace
  89. */
  90. Drupal.ckeditor = {
  91. /**
  92. * A hash of View instances.
  93. *
  94. * @type {object}
  95. */
  96. views: {},
  97. /**
  98. * A hash of Model instances.
  99. *
  100. * @type {object}
  101. */
  102. models: {},
  103. /**
  104. * Translates changes in CKEditor config DOM structure to the config model.
  105. *
  106. * If the button is moved within an existing group, the DOM structure is
  107. * simply translated to a configuration model. If the button is moved into a
  108. * new group placeholder, then a process is launched to name that group
  109. * before the button move is translated into configuration.
  110. *
  111. * @param {Backbone.View} view
  112. * The Backbone View that invoked this function.
  113. * @param {jQuery} $button
  114. * A jQuery set that contains an li element that wraps a button element.
  115. * @param {function} callback
  116. * A callback to invoke after the button group naming modal dialog has
  117. * been closed.
  118. *
  119. */
  120. registerButtonMove(view, $button, callback) {
  121. const $group = $button.closest('.ckeditor-toolbar-group');
  122. // If dropped in a placeholder button group, the user must name it.
  123. if ($group.hasClass('placeholder')) {
  124. if (view.isProcessing) {
  125. return;
  126. }
  127. view.isProcessing = true;
  128. Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
  129. } else {
  130. view.model.set('isDirty', true);
  131. callback(true);
  132. }
  133. },
  134. /**
  135. * Translates changes in CKEditor config DOM structure to the config model.
  136. *
  137. * Each row has a placeholder group at the end of the row. A user may not
  138. * move an existing button group past the placeholder group at the end of a
  139. * row.
  140. *
  141. * @param {Backbone.View} view
  142. * The Backbone View that invoked this function.
  143. * @param {jQuery} $group
  144. * A jQuery set that contains an li element that wraps a group of buttons.
  145. */
  146. registerGroupMove(view, $group) {
  147. // Remove placeholder classes if necessary.
  148. let $row = $group.closest('.ckeditor-row');
  149. if ($row.hasClass('placeholder')) {
  150. $row.removeClass('placeholder');
  151. }
  152. // If there are any rows with just a placeholder group, mark the row as a
  153. // placeholder.
  154. $row
  155. .parent()
  156. .children()
  157. .each(function() {
  158. $row = $(this);
  159. if (
  160. $row.find('.ckeditor-toolbar-group').not('.placeholder').length ===
  161. 0
  162. ) {
  163. $row.addClass('placeholder');
  164. }
  165. });
  166. view.model.set('isDirty', true);
  167. },
  168. /**
  169. * Opens a dialog with a form for changing the title of a button group.
  170. *
  171. * @param {Backbone.View} view
  172. * The Backbone View that invoked this function.
  173. * @param {jQuery} $group
  174. * A jQuery set that contains an li element that wraps a group of buttons.
  175. * @param {function} callback
  176. * A callback to invoke after the button group naming modal dialog has
  177. * been closed.
  178. */
  179. openGroupNameDialog(view, $group, callback) {
  180. callback = callback || function() {};
  181. /**
  182. * Validates the string provided as a button group title.
  183. *
  184. * @param {HTMLElement} form
  185. * The form DOM element that contains the input with the new button
  186. * group title string.
  187. *
  188. * @return {bool}
  189. * Returns true when an error exists, otherwise returns false.
  190. */
  191. function validateForm(form) {
  192. if (form.elements[0].value.length === 0) {
  193. const $form = $(form);
  194. if (!$form.hasClass('errors')) {
  195. $form
  196. .addClass('errors')
  197. .find('input')
  198. .addClass('error')
  199. .attr('aria-invalid', 'true');
  200. $(
  201. `<div class="description" >${Drupal.t(
  202. 'Please provide a name for the button group.',
  203. )}</div>`,
  204. ).insertAfter(form.elements[0]);
  205. }
  206. return true;
  207. }
  208. return false;
  209. }
  210. /**
  211. * Attempts to close the dialog; Validates user input.
  212. *
  213. * @param {string} action
  214. * The dialog action chosen by the user: 'apply' or 'cancel'.
  215. * @param {HTMLElement} form
  216. * The form DOM element that contains the input with the new button
  217. * group title string.
  218. */
  219. function closeDialog(action, form) {
  220. /**
  221. * Closes the dialog when the user cancels or supplies valid data.
  222. */
  223. function shutdown() {
  224. // eslint-disable-next-line no-use-before-define
  225. dialog.close(action);
  226. // The processing marker can be deleted since the dialog has been
  227. // closed.
  228. delete view.isProcessing;
  229. }
  230. /**
  231. * Applies a string as the name of a CKEditor button group.
  232. *
  233. * @param {jQuery} $group
  234. * A jQuery set that contains an li element that wraps a group of
  235. * buttons.
  236. * @param {string} name
  237. * The new name of the CKEditor button group.
  238. */
  239. function namePlaceholderGroup($group, name) {
  240. // If it's currently still a placeholder, then that means we're
  241. // creating a new group, and we must do some extra work.
  242. if ($group.hasClass('placeholder')) {
  243. // Remove all whitespace from the name, lowercase it and ensure
  244. // HTML-safe encoding, then use this as the group ID for CKEditor
  245. // configuration UI accessibility purposes only.
  246. const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(
  247. name.toLowerCase().replace(/\s/g, '-'),
  248. )}`;
  249. $group
  250. // Update the group container.
  251. .removeAttr('aria-label')
  252. .attr('data-drupal-ckeditor-type', 'group')
  253. .attr('tabindex', 0)
  254. // Update the group heading.
  255. .children('.ckeditor-toolbar-group-name')
  256. .attr('id', groupID)
  257. .end()
  258. // Update the group items.
  259. .children('.ckeditor-toolbar-group-buttons')
  260. .attr('aria-labelledby', groupID);
  261. }
  262. $group
  263. .attr('data-drupal-ckeditor-toolbar-group-name', name)
  264. .children('.ckeditor-toolbar-group-name')
  265. .text(name);
  266. }
  267. // Invoke a user-provided callback and indicate failure.
  268. if (action === 'cancel') {
  269. shutdown();
  270. callback(false, $group);
  271. return;
  272. }
  273. // Validate that a group name was provided.
  274. if (form && validateForm(form)) {
  275. return;
  276. }
  277. // React to application of a valid group name.
  278. if (action === 'apply') {
  279. shutdown();
  280. // Apply the provided name to the button group label.
  281. namePlaceholderGroup(
  282. $group,
  283. Drupal.checkPlain(form.elements[0].value),
  284. );
  285. // Remove placeholder classes so that new placeholders will be
  286. // inserted.
  287. $group
  288. .closest('.ckeditor-row.placeholder')
  289. .addBack()
  290. .removeClass('placeholder');
  291. // Invoke a user-provided callback and indicate success.
  292. callback(true, $group);
  293. // Signal that the active toolbar DOM structure has changed.
  294. view.model.set('isDirty', true);
  295. }
  296. }
  297. // Create a Drupal dialog that will get a button group name from the user.
  298. const $ckeditorButtonGroupNameForm = $(
  299. Drupal.theme('ckeditorButtonGroupNameForm'),
  300. );
  301. const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
  302. title: Drupal.t('Button group name'),
  303. dialogClass: 'ckeditor-name-toolbar-group',
  304. resizable: false,
  305. buttons: [
  306. {
  307. text: Drupal.t('Apply'),
  308. click() {
  309. closeDialog('apply', this);
  310. },
  311. primary: true,
  312. },
  313. {
  314. text: Drupal.t('Cancel'),
  315. click() {
  316. closeDialog('cancel');
  317. },
  318. },
  319. ],
  320. open() {
  321. const form = this;
  322. const $form = $(this);
  323. const $widget = $form.parent();
  324. $widget.find('.ui-dialog-titlebar-close').remove();
  325. // Set a click handler on the input and button in the form.
  326. $widget.on('keypress.ckeditor', 'input, button', event => {
  327. // React to enter key press.
  328. if (event.keyCode === 13) {
  329. const $target = $(event.currentTarget);
  330. const data = $target.data('ui-button');
  331. let action = 'apply';
  332. // Assume 'apply', but take into account that the user might have
  333. // pressed the enter key on the dialog buttons.
  334. if (data && data.options && data.options.label) {
  335. action = data.options.label.toLowerCase();
  336. }
  337. closeDialog(action, form);
  338. event.stopPropagation();
  339. event.stopImmediatePropagation();
  340. event.preventDefault();
  341. }
  342. });
  343. // Announce to the user that a modal dialog is open.
  344. let text = Drupal.t(
  345. 'Editing the name of the new button group in a dialog.',
  346. );
  347. if (
  348. typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !==
  349. 'undefined'
  350. ) {
  351. text = Drupal.t(
  352. 'Editing the name of the "@groupName" button group in a dialog.',
  353. {
  354. '@groupName': $group.attr(
  355. 'data-drupal-ckeditor-toolbar-group-name',
  356. ),
  357. },
  358. );
  359. }
  360. Drupal.announce(text);
  361. },
  362. close(event) {
  363. // Automatically destroy the DOM element that was used for the dialog.
  364. $(event.target).remove();
  365. },
  366. });
  367. // A modal dialog is used because the user must provide a button group
  368. // name or cancel the button placement before taking any other action.
  369. dialog.showModal();
  370. $(
  371. document
  372. .querySelector('.ckeditor-name-toolbar-group')
  373. .querySelector('input'),
  374. )
  375. // When editing, set the "group name" input in the form to the current
  376. // value.
  377. .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
  378. // Focus on the "group name" input in the form.
  379. .trigger('focus');
  380. },
  381. };
  382. /**
  383. * Automatically shows/hides settings of buttons-only CKEditor plugins.
  384. *
  385. * @type {Drupal~behavior}
  386. *
  387. * @prop {Drupal~behaviorAttach} attach
  388. * Attaches show/hide behavior to Plugin Settings buttons.
  389. */
  390. Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
  391. attach(context) {
  392. const $context = $(context);
  393. const $ckeditorPluginSettings = $context
  394. .find('#ckeditor-plugin-settings')
  395. .once('ckeditor-plugin-settings');
  396. if ($ckeditorPluginSettings.length) {
  397. // Hide all button-dependent plugin settings initially.
  398. $ckeditorPluginSettings
  399. .find('[data-ckeditor-buttons]')
  400. .each(function() {
  401. const $this = $(this);
  402. if ($this.data('verticalTab')) {
  403. $this.data('verticalTab').tabHide();
  404. } else {
  405. // On very narrow viewports, Vertical Tabs are disabled.
  406. $this.hide();
  407. }
  408. $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
  409. });
  410. // Whenever a button is added or removed, check if we should show or
  411. // hide the corresponding plugin settings. (Note that upon
  412. // initialization, each button that already is part of the toolbar still
  413. // is considered "added", hence it also works correctly for buttons that
  414. // were added previously.)
  415. $context
  416. .find('.ckeditor-toolbar-active')
  417. .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
  418. .on(
  419. 'CKEditorToolbarChanged.ckeditorAdminPluginSettings',
  420. (event, action, button) => {
  421. const $pluginSettings = $ckeditorPluginSettings.find(
  422. `[data-ckeditor-buttons~=${button}]`,
  423. );
  424. // No settings for this button.
  425. if ($pluginSettings.length === 0) {
  426. return;
  427. }
  428. const verticalTab = $pluginSettings.data('verticalTab');
  429. const activeButtons = $pluginSettings.data(
  430. 'ckeditorButtonPluginSettingsActiveButtons',
  431. );
  432. if (action === 'added') {
  433. activeButtons.push(button);
  434. // Show this plugin's settings if >=1 of its buttons are active.
  435. if (verticalTab) {
  436. verticalTab.tabShow();
  437. } else {
  438. // On very narrow viewports, Vertical Tabs remain fieldsets.
  439. $pluginSettings.show();
  440. }
  441. } else {
  442. // Remove this button from the list of active buttons.
  443. activeButtons.splice(activeButtons.indexOf(button), 1);
  444. // Show this plugin's settings 0 of its buttons are active.
  445. if (activeButtons.length === 0) {
  446. if (verticalTab) {
  447. verticalTab.tabHide();
  448. } else {
  449. // On very narrow viewports, Vertical Tabs are disabled.
  450. $pluginSettings.hide();
  451. }
  452. }
  453. }
  454. $pluginSettings.data(
  455. 'ckeditorButtonPluginSettingsActiveButtons',
  456. activeButtons,
  457. );
  458. },
  459. );
  460. }
  461. },
  462. };
  463. /**
  464. * Themes a blank CKEditor row.
  465. *
  466. * @return {string}
  467. * A HTML string for a CKEditor row.
  468. */
  469. Drupal.theme.ckeditorRow = function() {
  470. return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
  471. };
  472. /**
  473. * Themes a blank CKEditor button group.
  474. *
  475. * @return {string}
  476. * A HTML string for a CKEditor button group.
  477. */
  478. Drupal.theme.ckeditorToolbarGroup = function() {
  479. let group = '';
  480. group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t(
  481. 'Place a button to create a new button group.',
  482. )}">`;
  483. group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t(
  484. 'New group',
  485. )}</h3>`;
  486. group +=
  487. '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
  488. group += '</li>';
  489. return group;
  490. };
  491. /**
  492. * Themes a form for changing the title of a CKEditor button group.
  493. *
  494. * @return {string}
  495. * A HTML string for the form for the title of a CKEditor button group.
  496. */
  497. Drupal.theme.ckeditorButtonGroupNameForm = function() {
  498. return '<form><input name="group-name" required="required"></form>';
  499. };
  500. /**
  501. * Themes a button that will toggle the button group names in active config.
  502. *
  503. * @return {string}
  504. * A HTML string for the button to toggle group names.
  505. */
  506. Drupal.theme.ckeditorButtonGroupNamesToggle = function() {
  507. return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
  508. };
  509. /**
  510. * Themes a button that will prompt the user to name a new button group.
  511. *
  512. * @return {string}
  513. * A HTML string for the button to create a name for a new button group.
  514. */
  515. Drupal.theme.ckeditorNewButtonGroup = function() {
  516. return `<li class="ckeditor-add-new-group"><button aria-label="${Drupal.t(
  517. 'Add a CKEditor button group to the end of this row.',
  518. )}">${Drupal.t('Add group')}</button></li>`;
  519. };
  520. })(jQuery, Drupal, drupalSettings, _);