toolshed-8.x-1.x-dev/assets/Dock.es6.js

assets/Dock.es6.js
/* eslint no-bitwise: ["error", { "allow": ["^"] }] */
(($, Toolshed) => {
  /**
   * Define the namespace for defining docking libraries & tools.
   */
  Toolshed.Dock = {
    /**
     * Creates a new instance of a docker for the edge and parses
     * options from CSS class attributes.
     *
     * @param {jQuery} $elem
     *   HTML element that is being docked.
     * @param {jQuery} $bounds
     *   HTML element which defines the bounds.
     * @param {Object} settings
     *   Object containing the docker settings.
     *   {
     *     edge: {string} ['top'|'left'|'bottom'|'right'],
     *     offset: {int} 0
     *     collapsible: {bool} false,
     *     trackMutations: {bool} false,
     *     animate: {Object|bool} {
     *       type: {string} [slide],
     *       // Animation will last for 200 milliseconds.
     *       duration: {int} 200,
     *       // Animation starts after 250% of the element dimension.
     *       // This value is ignored of no animatable options are enabled.
     *       // NOTE: can be also be a constant pixel value.
     *     }
     *   }
     */
    createItem($elem, $bounds, settings = {}) {
      const config = { edge: 'TOP', offset: 0 };

      /*
       * Determine the set of active docker settings by parsing CSS class
       * information. Options are classes that start with "tsdock-opt-{{option}}"
       * or "tsdock-edge-[top|left|bottom|right]".
       *
       * Options can only get activated here, and will get applied with the
       * current defaults for that option. For instance, "tsdock-opt-sticky"
       * will make the docker, sticky using the default animation configurations.
       */
      if (!settings || settings.detectOpts) {
        let match = null;
        const optRegex = /(?:^|\s)tsdock--(opt|edge)-([-\w]+)(?:\s|$)/g;
        const elClasses = $elem.attr('class');

        // eslint-disable-next-line no-cond-assign
        while ((match = optRegex.exec(elClasses)) !== null) {
          if (match[1] === 'opt') {
            config[match[2]] = true;
          }
          else if (match[1] === 'edge') {
            [,, config.edge] = match;
          }
        }
      }

      // Build the docker now that all settings have been applied to it.
      const docker = new Toolshed.Dock.DockItem($elem, $bounds, { ...config, ...settings });
      Toolshed.Dock.addDocker(config.edge.toUpperCase() || 'TOP', docker);
    },

    /**
     * Add docker items into a docked container.
     *
     * @param {string} edge
     *   The edge to add the docking content to.
     * @param {Drupal.Toolshed.Dock.DockItem} item
     *   The dockable item to place into the container.
     */
    addDocker(edge, item) {
      if (Toolshed.Dock.containers[edge]) {
        Toolshed.Dock.containers[edge].addItem(item);
      }
    },
  };

  /**
   * Containers for holding items that are docked to them. DockContainers
   * will listen to Window events and manage the items that they wrap.
   */
  Toolshed.Dock.DockContainer = class {
    constructor() {
      this.active = false;
      this.container = null;
      this.items = [];
    }

    isActive() {
      return this.active;
    }

    /**
     * Add a new docking item to this docking container.
     *
     * @param {Drupal.Toolshed.Dock.DockItem} item
     *   The DockItem to add to this container.
     */
    addItem(item) {
      item.dockTo = this;
      this.items.push(item);

      // Defer building and listening to events until a dockable item is added.
      if (!this.active) {
        this.init();
      }
    }

    /**
     * Remove the DockItem from this container.
     *
     * @param {Drupal.Toolshed.Dock.DockItem} item
     *   The DockItem to find and remove from the container.
     */
    removeItem(item) {
      this.items = this.items.filter((cmp) => cmp !== item);
      delete item.dockTo;

      if (!this.items.length && this.container) {
        this.container.hide();
      }
    }

    /**
     * Register events that may make changes to docking, and init positioning.
     */
    init() {
      this.container = $('<div class="tsdock-container"/>').appendTo($('body'));
      this.initContainer();
      this.active = true;

      Toolshed.events.scroll.add(this);
      Toolshed.events.resize.add(this);

      // Initialize the positioning of the dock.
      this.onResize(new Event('resize'), Toolshed.winRect);
    }

    /**
     * Event handler for the window scroll change events.
     *
     * @param {Event} e
     *   The scroll event object for this event.
     * @param {Drupal.Toolshed.Geom.Rect} win
     *   The current bounds of the window.
     * @param {Object} scroll
     *   Object containing a top and left item to represent the current
     *   scroll offsets of the document in relation to the window.
     */
    onScroll(e, win, scroll) {
      const viewable = new Toolshed.Geom.Rect(win);
      viewable.offset(scroll.left, scroll.top);

      this.items.forEach((item) => {
        if (item.isDocked ^ this.isDocking(item, viewable)) {
          return item.isDocked ? item.deactivateDock() : item.activateDock(this.container);
        }
      }, this);
    }

    onResize(e, rect) {
      const offset = {
        top: document.documentElement.scrollTop || document.body.scrollTop,
        left: document.documentElement.scrollLeft || document.body.scrollLeft,
      };

      if (rect.top !== this.container.offset().top) {
        this.container.css({ top: rect.top });
      }

      // Window resizes could change the scroll position, but won't trigger a
      // scroll event on their own. Force a calculation of positioning.
      this.onScroll(e, rect, offset);
    }

    destroy() {
      // Unregister these event listeners, so these items are not lingering.
      Toolshed.events.scroll.remove(this);
      Toolshed.events.resize.remove(this);

      if (this.container) {
        this.container.remove();
      }
    }
  };

  Toolshed.Dock.TopDockContainer = class extends Toolshed.Dock.DockContainer {
    /**
     * Docking container specific handling of the docking container.
     */
    initContainer() {
      this.container.css({
        position: 'fixed',
        top: 0,
        width: '100%',
        boxSizing: 'border-box',
      });
    }

    /**
     * Determine if the content fits and is in the viewable window area.
     *
     * @param {Drupal.Toolshed.Geom.Rect} item
     *   Rect of the dockable content.
     * @param {Drupal.Toolshed.Geom.Rect} win
     *   Viewable window space.
     *
     * @return {Boolean}
     *   TRUE if the docking content is outside the viewable window.
     */
    isDocking(item, win) { // eslint-disable-line class-methods-use-this
      const cnt = item.getContainerRect();
      let top = Math.floor(item.placeholder.offset().top + item.config.offset);

      if (item.config.offset < 0) {
        top += item.placeholder.height();
      }

      return (top < win.top)
        && (cnt.bottom > win.top)
        && item.elem.outerHeight() < cnt.getHeight();
    }
  };

  /**
   * A dockable item that goes into a dock container.
   */
  Toolshed.Dock.DockItem = class {
    /**
     * Create a new instance of a dockable item.
     *
     * @param {jQuery} $elem
     *   The element that is being docked within this docking container.
     * @param {jQuery} $bounds
     *   The DOM element that is used to determine the bounds of when
     *   this item is being docked.
     * @param {Object} settings
     *   Settings that control how this item behaves while docking and
     *   undocking from a dock container.
     */
    constructor($elem, $bounds, settings) {
      this.elem = $elem;
      this.bounds = $bounds;
      this.config = settings;

      this.elem.addClass('tsdock-item');
      this.isDocked = false;

      // Apply animation settings, or use the defaults if they are provided.
      if (this.config.animate) {
        this.mode = this.config.animate.type || 'slide';
      }

      this.init();
    }

    /**
     * NULL function, meant to be a placeholder for edges that might
     * need to have custom initialization.
     */
    init() {
      // Create a new placeholder, that will keep track of the space
      // used by the docked element, while it's being docked to the container.
      this.placeholder = this.elem.wrap('<div class="tsdock__placeholder"/>').parent();
      this.placeholder.css({ position: this.elem.css('position') });
      this.height = this.elem.outerHeight();

      // If available, try to track the size of the docked element
      // and make updates to the docking system if dimensions change.
      if (this.config.trackMutations && MutationObserver) {
        this.observer = new MutationObserver(this._mutated.bind(this));
        this.observer.observe(this.elem[0], {
          attributes: true,
          childList: true,
          subtree: true,
          characterData: true,
        });
      }
    }

    /**
     * Mutation event listener. Will be registered by relevant docker types
     * and trigger when the docking element is modified in the appropriate ways.
     */
    _mutations() {
      // Disable mutation events while we process the current docking information.
      this.observer.disconnect();

      // In most cases we only care if the height has changed.
      const height = this.elem.outerHeight();
      if (this.height !== height) {
        this.height = height || 0;

        if (this.placeholder) {
          this.placeholder.height(height);
        }

        const win = new Toolshed.Geom.Rect(Toolshed.winRect);
        const scrollPos = (document.documentElement.scrollTop || document.body.scrollTop);
        this.scroll(scrollPos, win);
      }

      this.observer.observe(this.elem[0], {
        attributes: true,
        childList: true,
        subtree: true,
        characterData: true,
      });
    }

    getContainerRect() {
      const { top, left } = this.bounds.offset();
      return new Toolshed.Geom.Rect(
        top,
        left,
        top + this.bounds.outerHeight(),
        left + this.bounds.outerWidth(),
      );
    }

    /**
     * Turn on docking for this instance.
     *
     * This should make the element dock to the respective edge and set the
     * correct behaviors for items when they are docked.
     *
     * @param {jQuery} addTo
     *   Element to add the docked item into.
     */
    activateDock(addTo) {
      if (!this.isDocked) {
        this.isDocked = true;
        this.placeholder.height(this.height);

        addTo.append(this.elem);
        this.elem.addClass('tsdock-item--docked');
        this.elem.trigger('ToolshedDocking.docked');
      }
    }

    /**
     * Turn docking off for this docked item.
     */
    deactivateDock() {
      if (this.isDocked) {
        this.isDocked = false;
        this.placeholder.append(this.elem);
        this.elem.removeClass('tsdock-item--docked');

        // Reset the placeholder to size according to the placeholder.
        this.placeholder.css({ height: '' });
        this.elem.trigger('ToolshedDocking.undocked');
      }
    }

    destroy() {
      if (this.observer) this.observer.disconnect();
      this.deactivateDock();

      if (this.placeholder) {
        this.elem.unwrap('.tsdock__placeholder');
      }
    }
  };

  Toolshed.Dock.containers = {
    TOP: new Toolshed.Dock.TopDockContainer(),
  };
})(jQuery, Drupal.Toolshed);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc