Source: modules/ckeditor/js/views/VisualView.es6.js

/**
 * @file
 * A Backbone View that provides the visual UX view of CKEditor toolbar
 *   configuration.
 */

(function (Drupal, Backbone, $, Sortable) {
  Drupal.ckeditor.VisualView = Backbone.View.extend(
    /** @lends Drupal.ckeditor.VisualView# */ {
      events: {
        'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
        'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick',
        'click .ckeditor-add-new-group button': 'onAddGroupButtonClick',
      },

      /**
       * Backbone View for CKEditor toolbar configuration; visual UX.
       *
       * @constructs
       *
       * @augments Backbone.View
       */
      initialize() {
        this.listenTo(
          this.model,
          'change:isDirty change:groupNamesVisible',
          this.render,
        );

        // Add a toggle for the button group names.
        $(Drupal.theme('ckeditorButtonGroupNamesToggle')).prependTo(
          this.$el.find('#ckeditor-active-toolbar').parent(),
        );

        this.render();
      },

      /**
       * Render function for rendering the toolbar configuration.
       *
       * @param {*} model
       *   Model used for the view.
       * @param {string} [value]
       *   The value that was changed.
       * @param {object} changedAttributes
       *   The attributes that was changed.
       *
       * @return {Drupal.ckeditor.VisualView}
       *   The {@link Drupal.ckeditor.VisualView} object.
       */
      render(model, value, changedAttributes) {
        this.insertPlaceholders();
        this.applySorting();

        // Toggle button group names.
        let groupNamesVisible = this.model.get('groupNamesVisible');
        // If a button was just placed in the active toolbar, ensure that the
        // button group names are visible.
        if (
          changedAttributes &&
          changedAttributes.changes &&
          changedAttributes.changes.isDirty
        ) {
          this.model.set({ groupNamesVisible: true }, { silent: true });
          groupNamesVisible = true;
        }
        this.$el
          .find('[data-toolbar="active"]')
          .toggleClass('ckeditor-group-names-are-visible', groupNamesVisible);
        this.$el
          .find('.ckeditor-groupnames-toggle')
          .text(
            groupNamesVisible
              ? Drupal.t('Hide group names')
              : Drupal.t('Show group names'),
          )
          .attr('aria-pressed', groupNamesVisible);

        return this;
      },

      /**
       * Handles clicks to a button group name.
       *
       * @param {jQuery.Event} event
       *   The click event on the button group.
       */
      onGroupNameClick(event) {
        const $group = $(event.currentTarget).closest(
          '.ckeditor-toolbar-group',
        );
        Drupal.ckeditor.openGroupNameDialog(this, $group);

        event.stopPropagation();
        event.preventDefault();
      },

      /**
       * Handles clicks on the button group names toggle button.
       *
       * @param {jQuery.Event} event
       *   The click event on the toggle button.
       */
      onGroupNamesToggleClick(event) {
        this.model.set(
          'groupNamesVisible',
          !this.model.get('groupNamesVisible'),
        );
        event.preventDefault();
      },

      /**
       * Prompts the user to provide a name for a new button group; inserts it.
       *
       * @param {jQuery.Event} event
       *   The event of the button click.
       */
      onAddGroupButtonClick(event) {
        /**
         * Inserts a new button if the openGroupNameDialog function returns true.
         *
         * @param {bool} success
         *   A flag that indicates if the user created a new group (true) or
         *   canceled out of the dialog (false).
         * @param {jQuery} $group
         *   A jQuery DOM fragment that represents the new button group. It has
         *   not been added to the DOM yet.
         */
        function insertNewGroup(success, $group) {
          if (success) {
            $group.appendTo(
              $(event.currentTarget)
                .closest('.ckeditor-row')
                .children('.ckeditor-toolbar-groups'),
            );
            // Focus on the new group.
            $group.trigger('focus');
          }
        }

        // Pass in a DOM fragment of a placeholder group so that the new group
        // name can be applied to it.
        Drupal.ckeditor.openGroupNameDialog(
          this,
          $(Drupal.theme('ckeditorToolbarGroup')),
          insertNewGroup,
        );

        event.preventDefault();
      },

      /**
       * Handles Sortable stop sort of a button group.
       *
       * @param {CustomEvent} event
       *   The event triggered on the group drag.
       */
      endGroupDrag(event) {
        const $item = $(event.item);
        Drupal.ckeditor.registerGroupMove(this, $item);
      },

      /**
       * Handles Sortable start sort of a button.
       *
       * @param {CustomEvent} event
       *   The event triggered on the button drag.
       */
      startButtonDrag(event) {
        this.$el.find('a:focus').trigger('blur');

        // Show the button group names as soon as the user starts dragging.
        this.model.set('groupNamesVisible', true);
      },

      /**
       * Handles Sortable stop sort of a button.
       *
       * @param {CustomEvent} event
       *   The event triggered on the button drag.
       */
      endButtonDrag(event) {
        const $item = $(event.item);

        Drupal.ckeditor.registerButtonMove(this, $item, (success) => {
          // Refocus the target button so that the user can continue
          // from a known place.
          $item.find('a').trigger('focus');
        });
      },

      /**
       * Invokes Sortable() on new buttons and groups in a CKEditor config.
       * Array.prototype.forEach is used here because of the lack of support for
       * NodeList.forEach in older browsers.
       */
      applySorting() {
        // Make the buttons sortable.
        Array.prototype.forEach.call(
          this.el.querySelectorAll('.ckeditor-buttons:not(.js-sortable)'),
          (buttons) => {
            buttons.classList.add('js-sortable');
            Sortable.create(buttons, {
              ghostClass: 'ckeditor-button-placeholder',
              group: 'ckeditor-buttons',
              onStart: this.startButtonDrag.bind(this),
              onEnd: this.endButtonDrag.bind(this),
            });
          },
        );

        Array.prototype.forEach.call(
          this.el.querySelectorAll(
            '.ckeditor-toolbar-groups:not(.js-sortable)',
          ),
          (buttons) => {
            buttons.classList.add('js-sortable');
            Sortable.create(buttons, {
              ghostClass: 'ckeditor-toolbar-group-placeholder',
              onEnd: this.endGroupDrag.bind(this),
            });
          },
        );

        Array.prototype.forEach.call(
          this.el.querySelectorAll(
            '.ckeditor-multiple-buttons:not(.js-sortable)',
          ),
          (buttons) => {
            buttons.classList.add('js-sortable');
            Sortable.create(buttons, {
              group: {
                name: 'ckeditor-buttons',
                pull: 'clone',
              },
              onEnd: this.endButtonDrag.bind(this),
            });
          },
        );
      },

      /**
       * Wraps the invocation of methods to insert blank groups and rows.
       */
      insertPlaceholders() {
        this.insertPlaceholderRow();
        this.insertNewGroupButtons();
      },

      /**
       * Inserts a blank row at the bottom of the CKEditor configuration.
       */
      insertPlaceholderRow() {
        let $rows = this.$el.find('.ckeditor-row');
        // Add a placeholder row. to the end of the list if one does not exist.
        if (!$rows.eq(-1).hasClass('placeholder')) {
          this.$el
            .find('.ckeditor-toolbar-active')
            .children('.ckeditor-active-toolbar-configuration')
            .append(Drupal.theme('ckeditorRow'));
        }
        // Update the $rows variable to include the new row.
        $rows = this.$el.find('.ckeditor-row');
        // Remove blank rows except the last one.
        const len = $rows.length;
        $rows
          .filter((index, row) => {
            // Do not remove the last row.
            if (index + 1 === len) {
              return false;
            }
            return (
              $(row).find('.ckeditor-toolbar-group').not('.placeholder')
                .length === 0
            );
          })
          // Then get all rows that are placeholders and remove them.
          .remove();
      },

      /**
       * Inserts a button in each row that will add a new CKEditor button group.
       */
      insertNewGroupButtons() {
        // Insert an add group button to each row.
        this.$el.find('.ckeditor-row').each(function () {
          const $row = $(this);
          const $groups = $row.find('.ckeditor-toolbar-group');
          const $button = $row.find('.ckeditor-add-new-group');
          if ($button.length === 0) {
            $row
              .children('.ckeditor-toolbar-groups')
              .append(Drupal.theme('ckeditorNewButtonGroup'));
          }
          // If a placeholder group exists, make sure it's at the end of the row.
          else if (!$groups.eq(-1).hasClass('ckeditor-add-new-group')) {
            $button.appendTo($row.children('.ckeditor-toolbar-groups'));
          }
        });
      },
    },
  );
})(Drupal, Backbone, jQuery, Sortable);