Source: modules/block/js/block.es6.js

/**
 * @file
 * Block behaviors.
 */

(function ($, window, Drupal) {
  /**
   * Provide the summary information for the block settings vertical tabs.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches the behavior for the block settings summaries.
   */
  Drupal.behaviors.blockSettingsSummary = {
    attach() {
      // The drupalSetSummary method required for this behavior is not available
      // on the Blocks administration page, so we need to make sure this
      // behavior is processed only if drupalSetSummary is defined.
      if (typeof $.fn.drupalSetSummary === 'undefined') {
        return;
      }

      /**
       * Create a summary for checkboxes in the provided context.
       *
       * @param {HTMLDocument|HTMLElement} context
       *   A context where one would find checkboxes to summarize.
       *
       * @return {string}
       *   A string with the summary.
       */
      function checkboxesSummary(context) {
        const vals = [];
        const $checkboxes = $(context).find(
          'input[type="checkbox"]:checked + label',
        );
        const il = $checkboxes.length;
        for (let i = 0; i < il; i++) {
          vals.push($($checkboxes[i]).html());
        }
        if (!vals.length) {
          vals.push(Drupal.t('Not restricted'));
        }
        return vals.join(', ');
      }

      $(
        '[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"]',
      ).drupalSetSummary(checkboxesSummary);

      $(
        '[data-drupal-selector="edit-visibility-request-path"]',
      ).drupalSetSummary((context) => {
        const $pages = $(context).find(
          'textarea[name="visibility[request_path][pages]"]',
        );
        if (!$pages.val()) {
          return Drupal.t('Not restricted');
        }

        return Drupal.t('Restricted to certain pages');
      });
    },
  };

  /**
   * Move a block in the blocks table between regions via select list.
   *
   * This behavior is dependent on the tableDrag behavior, since it uses the
   * objects initialized in that behavior to update the row.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches the tableDrag behavior for blocks in block administration.
   */
  Drupal.behaviors.blockDrag = {
    attach(context, settings) {
      // tableDrag is required and we should be on the blocks admin page.
      if (
        typeof Drupal.tableDrag === 'undefined' ||
        typeof Drupal.tableDrag.blocks === 'undefined'
      ) {
        return;
      }

      /**
       * Function to check empty regions and toggle classes based on this.
       *
       * @param {jQuery} table
       *   The jQuery object representing the table to inspect.
       * @param {jQuery} rowObject
       *   The jQuery object representing the table row.
       */
      function checkEmptyRegions(table, rowObject) {
        table.find('tr.region-message').each(function () {
          const $this = $(this);
          // If the dragged row is in this region, but above the message row,
          // swap it down one space.
          if ($this.prev('tr').get(0) === rowObject.element) {
            // Prevent a recursion problem when using the keyboard to move rows
            // up.
            if (
              rowObject.method !== 'keyboard' ||
              rowObject.direction === 'down'
            ) {
              rowObject.swap('after', this);
            }
          }
          // This region has become empty.
          if (
            $this.next('tr').is(':not(.draggable)') ||
            $this.next('tr').length === 0
          ) {
            $this.removeClass('region-populated').addClass('region-empty');
          }
          // This region has become populated.
          else if ($this.is('.region-empty')) {
            $this.removeClass('region-empty').addClass('region-populated');
          }
        });
      }

      /**
       * Function to update the last placed row with the correct classes.
       *
       * @param {jQuery} table
       *   The jQuery object representing the table to inspect.
       * @param {jQuery} rowObject
       *   The jQuery object representing the table row.
       */
      function updateLastPlaced(table, rowObject) {
        // Remove the color-success class from new block if applicable.
        table.find('.color-success').removeClass('color-success');

        const $rowObject = $(rowObject);
        if (!$rowObject.is('.drag-previous')) {
          table.find('.drag-previous').removeClass('drag-previous');
          $rowObject.addClass('drag-previous');
        }
      }

      /**
       * Update block weights in the given region.
       *
       * @param {jQuery} table
       *   Table with draggable items.
       * @param {string} region
       *   Machine name of region containing blocks to update.
       */
      function updateBlockWeights(table, region) {
        // Calculate minimum weight.
        let weight = -Math.round(table.find('.draggable').length / 2);
        // Update the block weights.
        table
          .find(`.region-${region}-message`)
          .nextUntil('.region-title')
          .find('select.block-weight')
          .val(
            // Increment the weight before assigning it to prevent using the
            // absolute minimum available weight. This way we always have an
            // unused upper and lower bound, which makes manually setting the
            // weights easier for users who prefer to do it that way.
            () => ++weight,
          );
      }

      const table = $('#blocks');
      // Get the blocks tableDrag object.
      const tableDrag = Drupal.tableDrag.blocks;
      // Add a handler for when a row is swapped, update empty regions.
      tableDrag.row.prototype.onSwap = function (swappedRow) {
        checkEmptyRegions(table, this);
        updateLastPlaced(table, this);
      };

      // Add a handler so when a row is dropped, update fields dropped into
      // new regions.
      tableDrag.onDrop = function () {
        const dragObject = this;
        const $rowElement = $(dragObject.rowObject.element);
        // Use "region-message" row instead of "region" row because
        // "region-{region_name}-message" is less prone to regexp match errors.
        const regionRow = $rowElement.prevAll('tr.region-message').get(0);
        const regionName = regionRow.className.replace(
          /([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/,
          '$2',
        );
        const regionField = $rowElement.find('select.block-region-select');
        // Check whether the newly picked region is available for this block.
        if (regionField.find(`option[value=${regionName}]`).length === 0) {
          // If not, alert the user and keep the block in its old region
          // setting.
          window.alert(Drupal.t('The block cannot be placed in this region.'));
          // Simulate that there was a selected element change, so the row is
          // put back to from where the user tried to drag it.
          regionField.trigger('change');
        }

        // Update region and weight fields if the region has been changed.
        if (!regionField.is(`.block-region-${regionName}`)) {
          const weightField = $rowElement.find('select.block-weight');
          const oldRegionName = weightField[0].className.replace(
            /([^ ]+[ ]+)*block-weight-([^ ]+)([ ]+[^ ]+)*/,
            '$2',
          );
          regionField
            .removeClass(`block-region-${oldRegionName}`)
            .addClass(`block-region-${regionName}`);
          weightField
            .removeClass(`block-weight-${oldRegionName}`)
            .addClass(`block-weight-${regionName}`);
          regionField.val(regionName);
        }

        updateBlockWeights(table, regionName);
      };

      // Add the behavior to each region select list.
      $(context)
        .find('select.block-region-select')
        .once('block-region-select')
        .on('change', function (event) {
          // Make our new row and select field.
          const row = $(this).closest('tr');
          const select = $(this);
          // Find the correct region and insert the row as the last in the
          // region.
          tableDrag.rowObject = new tableDrag.row(row[0]);
          const regionMessage = table.find(
            `.region-${select[0].value}-message`,
          );
          const regionItems = regionMessage.nextUntil(
            '.region-message, .region-title',
          );
          if (regionItems.length) {
            regionItems.last().after(row);
          }
          // We found that regionMessage is the last row.
          else {
            regionMessage.after(row);
          }
          updateBlockWeights(table, select[0].value);
          // Modify empty regions with added or removed fields.
          checkEmptyRegions(table, tableDrag.rowObject);
          // Update last placed block indication.
          updateLastPlaced(table, row);
          // Show unsaved changes warning.
          if (!tableDrag.changed) {
            $(Drupal.theme('tableDragChangedWarning'))
              .insertBefore(tableDrag.table)
              .hide()
              .fadeIn('slow');
            tableDrag.changed = true;
          }
          // Remove focus from selectbox.
          select.trigger('blur');
        });
    },
  };
})(jQuery, window, Drupal);