@blockquote-web-components/blockquote-mixin-slot-content
Version:
Webcomponent blockquote-mixin-slot-content following open-wc recommendations
301 lines (287 loc) • 7.82 kB
JavaScript
import {dedupeMixin} from '@open-wc/dedupe-mixin';
/**
* Checks if a node's text content contains only whitespace.
*
* @param {Node} nod - The node to check.
* @returns {boolean} - True if the node's text content contains only whitespace, false otherwise.
*/
const hasOnlyWhitespace = (nod) => !/[^\t\n\r ]/.test(nod?.textContent ?? '');
/**
* Checks if a node is a comment node or a text node with only whitespace.
*
* @param {Node} nod - The node to check.
* @returns {boolean} - True if the node is ignorable, false otherwise.
*/
const isIgnorableNode = (nod) =>
nod.nodeType === Node.COMMENT_NODE || (nod.nodeType === Node.TEXT_NODE && hasOnlyWhitespace(nod));
/**
* 
*
* `BlockquoteMixinSlotContent` is a mixin for managing the flattened set of nodes assigned to a slot when the node(s) contained in some slot change.
*
* It implements the event handling pattern called [event delegation](https://javascript.info/event-delegation).
*
* ### Demo
*
* [](https://stackblitz.com/github/oscarmarina/blockquote-web-components/tree/main/packages/mixins/blockquote-mixin-slot-content)
*
* ### Example:
*
* ```js
* class SlotElement extends BlockquoteMixinSlotContent(LitElement) {
* // ...
* constructor() {
* super();
* this.addEventListener('slotchanges', this._onSlotChanges);
* }
*
* _onSlotChanges = (ev) => {
* const { detail } = ev;
* console.log(detail);
* }
* // ...
* }
* ```
*
* ## Caveats with whitespace:
*
* It's important to understand some of the nuances between text nodes that contain _`text`_ and text nodes that contain only [`whitespace`](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#what_is_whitespace).
*
* ```html
* <slot-element>
* nodeText1
* <p>Element 1</p>
* <p>Element 2</p>
* </slot-element>
* ```
*
* #### assignedNodes returns 5 Nodes
*
* ```js
* assignedNodes: Array(5)
* 0: text // textContent: "\n nodeText1\n "
* 1: p // textContent: "Element 1"
* 2: text // textContent: "\n "
* 3: p // textContent: "Element 2"
* 4: text // textContent: "\n "
* length: 5
* ```
*
* Also, another problem with whitespace is that when the content is just whitespace, assignedNodes are no longer able to return `flattened` nodes.
*
* #### without `whitespace`
*
* ```html
* <slot-element>
* #shadow-root
* <slot>
* <img src="icon.svg" />
* </slot>
* </slot-element>
*
* <slot-element>Hello</slot-element>
* ```
*
* ```js
* document.querySelector('slot-element').textContent = '';
*
* // assignedNodes [`img`]
* ```
*
* <hr>
*
* #### with `whitespace`
*
* ```html
* <slot-element>
* #shadow-root
* <slot>
* <img src="icon.svg" />
* </slot>
* </slot-element>
*
* <slot-element>Hello</slot-element>
* ```
*
* ```js
* document.querySelector('slot-element').textContent = ' ';
*
* // assignedNodes [`#text`]
* // * missing `flattened` node
* ```
*
* <hr>
*
* ## The `detail property` - keeping that in mind.
*
* The mixin will return only nodes [whose content is not whitespace](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#whitespace_helper_functions)
* and equally for flattened nodes.
*
* ```html
* <slot-element>
* #shadow-root
* <slot>
* <img src="icon.svg" />
* </slot>
* </slot-element>
*
* <slot-element>
* <p>sdfas</p>
* <p>2jfie</p>
* </slot-element>
* ```
*
* #### `event.detail`
*
* ```json
* {
* "assignedSlotContent": {
* "slotName": "",
* "assignedSlot": slot
* },
* "assignedNodesContent": {
* "assignedNodesByNode": [
* {
* "flatten": false,
* "assignedNodes": p,
* "assignedSlot": slot
* },
* {
* "flatten": false,
* "assignedNodes": p,
* "assignedSlot": slot
* }
* ],
* "assignedNodes": [p, p]
* },
* "flattenedNodesContent": {
* "assignedNodesByNode": [
* {
* "flatten": true,
* "assignedNodes": img,
* "assignedSlot": null
* }
* ],
* "assignedNodes": [img]
* },
* "originalEvent": {
* "event": {
* "isTrusted": true,
* "type": "slotchange",
* "target": null,
* "currentTarget": null,
* // ...
* },
* "assignedNodes": [text, p, text, p, text, text, text]
* }
* }
* ```
*
* <hr>
*
* ```html
* <slot-element>
* #shadow-root
* <slot></slot>
* </slot-element>
*
* <slot-element> </slot-element>
* ```
*
* #### `event.detail`
*
* ```json
* {
* "assignedSlotContent": {
* "slotName": "",
* "assignedSlot": null
* },
* "assignedNodesContent": {
* "assignedNodesByNode": [],
* "assignedNodes": []
* },
* "flattenedNodesContent": {
* "assignedNodesByNode": [],
* "assignedNodes": []
* },
* "originalEvent": {
* "event": {
* "isTrusted": true,
* "type": "slotchange",
* "target": null,
* "currentTarget": null,
* // ...
* },
* "assignedNodes": [text]
* }
* }
* ```
*
* @fires slotchanges
*/
const BlockquoteSlotContentBase = (Base) =>
class BlockquoteSlotContent extends Base {
/**
* @param {HTMLSlotElement} slotNode
*/
#processSlotContent(slotNode) {
const allNodes = [...slotNode.assignedNodes(), ...slotNode.childNodes];
const nodesWithContent = allNodes
.filter((nod) => !isIgnorableNode(nod))
.map((nod) => ({
isFlattened: /** @type {*} */ (nod).assignedSlot === null,
assignedNodes: nod.nodeType === Node.TEXT_NODE ? nod.textContent?.trim() : nod,
assignedSlot: /** @type {*} */ (nod).assignedSlot,
}));
return {
assignedContent: nodesWithContent.filter((nod) => nod.isFlattened === false),
flattenedContent: nodesWithContent.filter((nod) => nod.isFlattened === true),
};
}
/**
* @param {*} content
*/
#createContentStructure(content) {
return {
assignedNodesByNode: content,
assignedNodes: content.map((/** @type {*} */ nod) => nod.assignedNodes),
};
}
/**
* @param {Event} ev
*/
#onSlotChange = (ev) => {
const slotNode = /** @type {HTMLSlotElement} */ (ev.target);
if (slotNode) {
const contentSlotName = slotNode.name || slotNode.getAttribute('name') || '';
const originalAssignedNodes = slotNode.assignedNodes({flatten: true});
const contentSlots = this.#processSlotContent(slotNode);
const detail = {
assignedSlotContent: {
slotName: contentSlotName,
assignedSlot: contentSlots.assignedContent[0]?.assignedSlot || null,
},
assignedNodesContent: this.#createContentStructure(contentSlots.assignedContent),
flattenedNodesContent: this.#createContentStructure(contentSlots.flattenedContent),
originalEvent: {
event: ev,
assignedNodes: originalAssignedNodes,
},
};
const event = new CustomEvent('slotchanges', {
composed: true,
detail,
});
this.shadowRoot?.dispatchEvent(event);
}
};
connectedCallback() {
super.connectedCallback?.();
this.shadowRoot?.addEventListener('slotchange', this.#onSlotChange);
}
disconnectedCallback() {
super.disconnectedCallback?.();
this.shadowRoot?.removeEventListener('slotchange', this.#onSlotChange);
}
};
export const BlockquoteMixinSlotContent = dedupeMixin(BlockquoteSlotContentBase);