refreshless-8.x-1.x-dev/modules/refreshless_turbo_gin/js/dropbutton.js
modules/refreshless_turbo_gin/js/dropbutton.js
/**
* @file
* Gin dropbutton replacement for RefreshLess.
*
* Gin's dropbutton behaviour does not have a detach, and also attaches
* anonymous, non-namespaced event subscribers to the window which can't be
* removed directly. This solves both of those problems by rewriting as a class
* that supports detaching/destroying the instance.
*/
(function(html, Drupal, $, once) {
'use strict';
/**
* Our event namespace.
*
* @type {String}
*
* @see https://learn.jquery.com/events/event-basics/#namespacing-events
*/
const eventNamespace = 'gin-dropbutton';
/**
* once() identfier.
*
* This is the same as Gin's behaviour. Probably doesn't matter, but might as
* well play it safe and match the original.
*
* @type {String}
*/
const onceName = 'ginDropbutton';
/**
* Property name on dropbutton elements that we save our instance to.
*
* @type {String}
*/
const ginPropName = 'ginDropButton';
/**
* Property name on dropbutton elements that our core instance is saved to.
*
* @type {String}
*/
const corePropName = 'drupalDropButton';
/**
* Represents a JavaScript-enhanced dropbutton for the Gin theme.
*/
class GinDropButton {
/**
* The DropButton instance we're wrapping.
*
* @type {DropButton}
*/
#wrappedInstance;
/**
* The second level list Gin wraps all secondary actions under.
*
* @type {jQuery}
*
* @see gin/templates/form/links--dropbutton.html.twig
*/
#$menu;
constructor(wrappedInstance) {
this.#wrappedInstance = wrappedInstance;
this.#$menu = this.$dropbutton.find('.dropbutton__items');
this.#bindEventHandlers();
}
/**
* Destroy this instance by detaching from the element.
*/
destroy() {
this.#unbindEventHandlers();
}
/**
* Bind all of our event handlers.
*/
#bindEventHandlers() {
this.$toggle.on(`click.${eventNamespace}`, (event) => {
this.update(false);
});
$(window).on({
[`scroll.${eventNamespace}`]: this.#resizeAndScrollHandler,
[`resize.${eventNamespace}`]: this.#resizeAndScrollHandler,
});
}
/**
* Unbind all of our event handlers.
*/
#unbindEventHandlers() {
this.$toggle.off(`click.${eventNamespace}`);
$(window).off(`.${eventNamespace}`, this.#resizeAndScrollHandler);
}
#resizeAndScrollHandler = (event) => Drupal.debounce(this.update(), 100);
update(onlyIfOpen = true) {
if (onlyIfOpen === true && this.#wrappedInstance.isOpen() === false) {
return;
}
const dir = html.dir ?? 'ltr';
const $menuContainer = this.$menu.parent();
const toggleHeight = this.$primaryAction.outerHeight(true);
const menuWidth = this.$menu.outerWidth(true);
const menuHeight = this.$menu.outerHeight(true);
const boundingRect = $menuContainer[0].getBoundingClientRect();
const windowWidth = $(window).width();
const windowHeight = $(window).height();
const spaceBelow = windowHeight - boundingRect.bottom;
const spaceLeft = boundingRect.left;
const spaceRight = windowWidth - boundingRect.right;
const menuStyles = {
position: 'fixed',
};
const leftAlignStyles = {
left: `${boundingRect.left}px`,
right: 'auto'
};
const rightAlignStyles = {
left: 'auto',
right: `${windowWidth - boundingRect.right}px`
};
if (dir === 'ltr') {
if (spaceRight >= menuWidth) {
Object.assign(menuStyles, leftAlignStyles);
} else {
Object.assign(menuStyles, rightAlignStyles);
}
} else {
if (spaceLeft >= menuWidth) {
Object.assign(menuStyles, rightAlignStyles);
} else {
Object.assign(menuStyles, leftAlignStyles);
}
}
if (spaceBelow >= menuHeight) {
menuStyles.top = `${boundingRect.bottom}px`;
} else {
menuStyles.top = `${
boundingRect.top - toggleHeight - menuHeight
}px`;
}
this.$menu.css(menuStyles);
}
get $dropbutton() {
return this.#wrappedInstance.$dropbutton;
}
get $toggle() {
return this.#wrappedInstance.$toggle;
}
get $list() {
return this.#wrappedInstance.$list;
}
get $menu() {
return this.#$menu;
}
get $actions() {
return this.#wrappedInstance.$actions;
}
get $primaryAction() {
return this.#wrappedInstance.$primaryAction;
}
get $secondaryActions() {
return this.#wrappedInstance.$secondaryActions;
}
}
Drupal.behaviors.ginDropbutton = {
attach(context, settings) {
$(once(
onceName, '.dropbutton-multiple:has(.dropbutton--gin)', context,
)).each((i, element) => {
$(element).prop(ginPropName, new GinDropButton($(element).prop(
corePropName,
)));
});
},
detach(context, settings, trigger) {
$(once.remove(
// Note that this can't use the '.dropbutton-multiple' selector because
// that will never match as the class will have already been removed by
// the dropbutton destroy() method at this point. Instead, use the
// '.dropbutton-wrapper' selector that is left untouched.
onceName, '.dropbutton-wrapper:has(.dropbutton--gin)', context,
)).each((i, element) => {
$(element).prop(ginPropName)?.destroy();
$(element).removeProp(ginPropName);
});
},
};
})(document.documentElement, Drupal, jQuery, once);
