refreshless-8.x-1.x-dev/modules/refreshless_turbo/js/stylesheet_manager.js
modules/refreshless_turbo/js/stylesheet_manager.js
(function(Drupal, drupalSettings, $) {
'use strict';
/**
* Our event namespace.
*
* @type {String}
*
* @see https://learn.jquery.com/events/event-basics/#namespacing-events
*/
const eventNamespace = 'refreshless-turbo-stylesheet-manager';
/**
* Order attribute name added to stylesheet <link> elements by the back-end.
*
* @type {String}
*/
const weightAttributeName =
drupalSettings.refreshless.stylesheetOrderAttributeName;
/**
* RefreshLess Turbo stylesheet manager class.
*/
class StylesheetManager {
/**
* The context element to attach to; usually the <html> element.
*
* @type {HTMLElement}
*/
#context;
constructor(context) {
this.#context = context;
this.#bindEventHandlers();
}
/**
* Destroy this instance.
*/
destroy() {
this.#unbindEventHandlers();
}
/**
* Bind all of our event handlers.
*/
#bindEventHandlers() {
// @see https://ambientimpact.com/web/snippets/javascript-template-literal-as-object-property-name
$(this.#context).on({
// This event can be triggered to sort stylesheets at any time.
[`refreshless:sort-stylesheets.${eventNamespace}`]: (event) => {
const $closest = $(event.target).closest(this.#context);
if ($closest.length === 0) {
console.warn(
'%cRefreshLess%c: %s requires %o to either be or be within %o!',
'font-style: italic', 'font-style: normal',
event.type, event.target, this.#context,
);
return;
}
this.#sort($closest);
},
[`turbo:before-stylesheets-merge.${eventNamespace}`]: (event) => {
this.#beforeMergeEventHandler(event);
},
[`turbo:stylesheets-merged.${eventNamespace}`]: (event) => {
this.#mergedEventHandler(event);
},
[`turbo:stylesheets-loaded.${eventNamespace}`]: (event) => {
this.#loadedEventHandler(event);
},
[`turbo:before-stylesheets-remove.${eventNamespace}`]: (event) => {
this.#beforeRemoveEventHandler(event);
},
[`turbo:stylesheets-removed.${eventNamespace}`]: (event) => {
this.#removedEventHandler(event);
},
});
}
/**
* Unbind all of our event handlers.
*/
#unbindEventHandlers() {
$(this.#context).off(`.${eventNamespace}`);
}
/**
* 'turbo:before-stylesheets-merge' event handler.
*
* This prevents duplicate stylesheets that only differ by their weight
* attribute being merged in by transfering a new weight attribute value
* to the existing stylesheet pointing to the same href, only merging a
* stylesheet if one with that href isn't already present in the document.
*
* @param {jQuery.Event} event
*/
#beforeMergeEventHandler(event) {
const alteredNewStylesheets = [];
$(event.detail.newStylesheets)
.filter(`[${weightAttributeName}]`)
.each((i, element) => {
const $existing = $(event.detail.oldStylesheets).filter(
`[href="${$(element).attr('href')}"][${weightAttributeName}]`,
);
// If there is no existing stylesheet with the same URL, allow this
// stylesheet to be merged and continue to the next one.
if ($existing.length === 0) {
alteredNewStylesheets.push(element);
return;
}
// If an existing stylesheet exists, transfer the new element's weight
// attribute to the existing one, and don't merge in the new element.
$existing.attr(
weightAttributeName, $(element).attr(weightAttributeName),
);
});
event.detail.newStylesheets = alteredNewStylesheets;
const beforeMergeEvent = new CustomEvent(
'refreshless:before-stylesheets-merge', {
detail: event.detail,
},
);
this.#context.dispatchEvent(beforeMergeEvent);
}
/**
* Sort all stylesheets in the <head> based on their weight attribute.
*
* @param {HTMLElement|jQuery} context
* The context; usually the <html> element.
*
* @return {Boolean}
* True if sorting occurred, false if no sorting was required.
*/
#sort(context) {
const $elements = $(
`head link[rel="stylesheet"][${weightAttributeName}]`, context,
);
/**
* True if the sort order was changed, false otherwise.
*
* This prevents unnececessary DOM manipulation if no sorting occurred.
*
* @type {Boolean}
*/
let wasSorted = false;
/**
* <link> elements sorted by the order property generated by the back-end.
*
* @type {HTMLLinkElement[]}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
*/
const sorted = $elements.toArray().sort((a, b) => {
const aWeight = Number.parseInt($(a).attr(weightAttributeName));
const bWeight = Number.parseInt($(b).attr(weightAttributeName));
if (aWeight < bWeight) {
wasSorted = true;
return -1;
} else if (aWeight > bWeight) {
wasSorted = true;
return 1;
}
return 0;
});
if (wasSorted === false) {
return false;
}
// Append all the sorted <link> elements. This will be done in the newly
// sorted order.
$('head', context).append(sorted);
return true;
}
/**
* 'turbo:stylesheets-merged' event handler.
*
* This sorts all stylesheets using their weight attribute to ensure CSS
* specificity is preserved.
*
* @param {jQuery.Event} event
*
* @see https://www.drupal.org/project/refreshless/issues/3399314
* CSS specificity issue.
*/
#mergedEventHandler(event) {
this.#sort(event.target);
const mergedEvent = new CustomEvent(
'refreshless:stylesheets-merged', {detail: event.detail},
);
this.#context.dispatchEvent(mergedEvent);
}
/**
* 'turbo:stylesheets-loaded' event handler.
*
* @param {jQuery.Event} event
*/
#loadedEventHandler(event) {
const loadedEvent = new CustomEvent(
'refreshless:stylesheets-loaded', {detail: event.detail},
);
this.#context.dispatchEvent(loadedEvent);
}
/**
* 'turbo:before-stylesheets-remove' event handler.
*
* @param {jQuery.Event} event
*/
#beforeRemoveEventHandler(event) {
const beforeRemoveEvent = new CustomEvent(
'refreshless:before-stylesheets-remove', {
detail: event.detail,
},
);
this.#context.dispatchEvent(beforeRemoveEvent);
}
/**
* 'turbo:stylesheets-removed' event handler.
*
* @param {jQuery.Event} event
*/
#removedEventHandler(event) {
const removedEvent = new CustomEvent(
'refreshless:stylesheets-removed', {detail: event.detail},
);
this.#context.dispatchEvent(removedEvent);
}
}
// Merge Drupal.RefreshLess.classes into the existing Drupal global.
$.extend(true, Drupal, {RefreshLess: {classes: {
TurboStylesheetManager: StylesheetManager,
}}});
})(Drupal, drupalSettings, jQuery);
