chore: new operations functions to replace direct DOM access

pull/18058/head
paoloricciuti 6 months ago
parent ac40179686
commit 0a0212c86e

@ -69,6 +69,7 @@ export const STALE_REACTION = new (class StaleReactionError extends Error {
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})(); })();
// TODO: DOM access
export const IS_XHTML = export const IS_XHTML =
// We gotta write it like this because after downleveling the pure comment may end up in the wrong location // We gotta write it like this because after downleveling the pure comment may end up in the wrong location
!!globalThis.document?.contentType && !!globalThis.document?.contentType &&
@ -77,3 +78,10 @@ export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3; export const TEXT_NODE = 3;
export const COMMENT_NODE = 8; export const COMMENT_NODE = 8;
export const DOCUMENT_FRAGMENT_NODE = 11; export const DOCUMENT_FRAGMENT_NODE = 11;
export const CUSTOM_RENDERER_NODE_TYPE_MAP = /** @type {const} */ ({
fragment: DOCUMENT_FRAGMENT_NODE,
element: ELEMENT_NODE,
text: TEXT_NODE,
comment: COMMENT_NODE
});

@ -5,9 +5,15 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, REACTION_RAN } from '#client/constants'; import {
TEXT_NODE,
REACTION_RAN,
CUSTOM_RENDERER_NODE_TYPE_MAP,
COMMENT_NODE
} from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js'; import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js'; import { NAMESPACE_HTML } from '../../../constants.js';
import { renderer } from '../custom-renderer/state.js';
// export these for reference in the compiled code, making global name deduplication unnecessary // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */
@ -78,6 +84,7 @@ export function init_operations() {
* @returns {Text} * @returns {Text}
*/ */
export function create_text(value = '') { export function create_text(value = '') {
if (renderer) return /** @type {Text} */ (renderer.createTextNode(value));
return document.createTextNode(value); return document.createTextNode(value);
} }
@ -87,6 +94,7 @@ export function create_text(value = '') {
*/ */
/*@__NO_SIDE_EFFECTS__*/ /*@__NO_SIDE_EFFECTS__*/
export function get_first_child(node) { export function get_first_child(node) {
if (renderer) return /** @type {TemplateNode | null} */ (renderer.getFirstChild(node));
return /** @type {TemplateNode | null} */ (first_child_getter.call(node)); return /** @type {TemplateNode | null} */ (first_child_getter.call(node));
} }
@ -96,6 +104,7 @@ export function get_first_child(node) {
*/ */
/*@__NO_SIDE_EFFECTS__*/ /*@__NO_SIDE_EFFECTS__*/
export function get_next_sibling(node) { export function get_next_sibling(node) {
if (renderer) return /** @type {TemplateNode | null} */ (renderer.getNextSibling(node));
return /** @type {TemplateNode | null} */ (next_sibling_getter.call(node)); return /** @type {TemplateNode | null} */ (next_sibling_getter.call(node));
} }
@ -115,10 +124,10 @@ export function child(node, is_text) {
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty // Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) { if (child === null) {
child = hydrate_node.appendChild(create_text()); child = /** @type {TemplateNode} */ (append_child(hydrate_node, create_text()));
} else if (is_text && child.nodeType !== TEXT_NODE) { } else if (is_text && node_type(child) !== TEXT_NODE) {
var text = create_text(); var text = create_text();
child?.before(text); insert_before(child, text);
set_hydrate_node(text); set_hydrate_node(text);
return text; return text;
} }
@ -142,7 +151,8 @@ export function first_child(node, is_text = false) {
var first = get_first_child(node); var first = get_first_child(node);
// TODO prevent user comments with the empty string when preserveComments is true // TODO prevent user comments with the empty string when preserveComments is true
if (first instanceof Comment && first.data === '') return get_next_sibling(first); // TODO RENDERER: can we find another way to not have an `isComment` on the renderer?
if (is_comment(first) && get_node_value(first) === '') return get_next_sibling(first);
return first; return first;
} }
@ -150,10 +160,10 @@ export function first_child(node, is_text = false) {
if (is_text) { if (is_text) {
// if an {expression} is empty during SSR, there might be no // if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (hydrate_node?.nodeType !== TEXT_NODE) { if (node_type(hydrate_node) !== TEXT_NODE) {
var text = create_text(); var text = create_text();
hydrate_node?.before(text); if (hydrate_node) insert_before(hydrate_node, text);
set_hydrate_node(text); set_hydrate_node(text);
return text; return text;
} }
@ -187,15 +197,15 @@ export function sibling(node, count = 1, is_text = false) {
if (is_text) { if (is_text) {
// if a sibling {expression} is empty during SSR, there might be no // if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (next_sibling?.nodeType !== TEXT_NODE) { if (node_type(next_sibling) !== TEXT_NODE) {
var text = create_text(); var text = create_text();
// If the next sibling is `null` and we're handling text then it's because // If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text // the SSR content was empty for the text, so we need to generate a new text
// node and insert it after the last sibling // node and insert it after the last sibling
if (next_sibling === null) { if (next_sibling === null) {
last_sibling?.after(text); if (last_sibling) insert_after(last_sibling, text);
} else { } else {
next_sibling.before(text); insert_before(next_sibling, text);
} }
set_hydrate_node(text); set_hydrate_node(text);
return text; return text;
@ -214,6 +224,15 @@ export function sibling(node, count = 1, is_text = false) {
* @returns {void} * @returns {void}
*/ */
export function clear_text_content(node) { export function clear_text_content(node) {
if (renderer) {
var child = renderer.getFirstChild(node);
while (child !== null) {
var next = renderer.getNextSibling(child);
renderer.remove(child);
child = next;
}
return;
}
node.textContent = ''; node.textContent = '';
} }
@ -239,6 +258,10 @@ export function should_defer_append() {
* @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} * @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element}
*/ */
export function create_element(tag, namespace, is) { export function create_element(tag, namespace, is) {
if (renderer)
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
renderer.createElement(tag)
);
let options = is ? { is } : undefined; let options = is ? { is } : undefined;
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ ( return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options) document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options)
@ -246,6 +269,7 @@ export function create_element(tag, namespace, is) {
} }
export function create_fragment() { export function create_fragment() {
if (renderer) return /** @type {DocumentFragment} */ (renderer.createFragment());
return document.createDocumentFragment(); return document.createDocumentFragment();
} }
@ -254,6 +278,7 @@ export function create_fragment() {
* @returns * @returns
*/ */
export function create_comment(data = '') { export function create_comment(data = '') {
if (renderer) return /** @type {Comment} */ (renderer.createComment(data));
return document.createComment(data); return document.createComment(data);
} }
@ -264,6 +289,10 @@ export function create_comment(data = '') {
* @returns * @returns
*/ */
export function set_attribute(element, key, value = '') { export function set_attribute(element, key, value = '') {
if (renderer) {
renderer.setAttribute(element, key, value);
return;
}
if (key.startsWith('xlink:')) { if (key.startsWith('xlink:')) {
element.setAttributeNS('http://www.w3.org/1999/xlink', key, value); element.setAttributeNS('http://www.w3.org/1999/xlink', key, value);
return; return;
@ -277,13 +306,16 @@ export function set_attribute(element, key, value = '') {
* @param {Text} text * @param {Text} text
*/ */
export function merge_text_nodes(text) { export function merge_text_nodes(text) {
// if we have a renderer we will not hydrate so we can skip this
if (renderer) return;
if (/** @type {string} */ (text.nodeValue).length < 65536) { if (/** @type {string} */ (text.nodeValue).length < 65536) {
return; return;
} }
let next = text.nextSibling; let next = text.nextSibling;
while (next !== null && next.nodeType === TEXT_NODE) { while (next !== null && node_type(next) === TEXT_NODE) {
next.remove(); next.remove();
/** @type {string} */ (text.nodeValue) += /** @type {string} */ (next.nodeValue); /** @type {string} */ (text.nodeValue) += /** @type {string} */ (next.nodeValue);
@ -291,3 +323,438 @@ export function merge_text_nodes(text) {
next = text.nextSibling; next = text.nextSibling;
} }
} }
/**
* @param {TemplateNode | null} node
* @returns {node is Comment}
*/
function is_comment(node) {
if (renderer) return !!node && node_type(node) === COMMENT_NODE;
return node instanceof Comment;
}
/**
* @param {Node | null | undefined} node
*/
export function node_type(node) {
if (node == null) return undefined;
if (renderer) {
const type = renderer.nodeType(node);
return CUSTOM_RENDERER_NODE_TYPE_MAP[type];
}
return node?.nodeType;
}
/**
* @param {Node | null | undefined} node
*/
export function node_name(node) {
if (node == null) return undefined;
if (renderer) {
return '';
}
return node?.nodeName;
}
/**
* @param {Node} node
* @returns {TemplateNode | null}
*/
export function get_last_child(node) {
if (renderer) return /** @type {TemplateNode | null} */ (renderer.getLastChild(node));
return /** @type {TemplateNode | null} */ (node.lastChild);
}
/**
* @param {Node} node
* @returns {TemplateNode | null}
*/
export function get_parent_node(node) {
if (renderer) return /** @type {TemplateNode | null} */ (renderer.getParent(node));
return /** @type {TemplateNode | null} */ (node.parentNode);
}
/**
* @param {Node} parent
* @param {Node} child
* @returns {Node}
*/
export function append_child(parent, child) {
if (renderer) {
renderer.insert(parent, child, null);
return child;
}
return parent.appendChild(child);
}
/**
* Insert `new_node` before `ref_node` (equivalent to `ref_node.before(new_node)`)
* @param {ChildNode} ref_node
* @param {Node} new_node
*/
export function insert_before(ref_node, new_node) {
if (renderer) {
var parent = renderer.getParent(ref_node);
renderer.insert(parent, new_node, ref_node);
return;
}
ref_node.before(new_node);
}
/**
* Insert `new_node` after `ref_node` (equivalent to `ref_node.after(new_node)`)
* @param {ChildNode} ref_node
* @param {Node} new_node
*/
export function insert_after(ref_node, new_node) {
if (renderer) {
var parent = renderer.getParent(ref_node);
var next = renderer.getNextSibling(ref_node);
renderer.insert(parent, new_node, next);
return;
}
ref_node.after(new_node);
}
/**
* @param {ChildNode} node
*/
export function remove_node(node) {
if (renderer) {
renderer.remove(node);
return;
}
node.remove();
}
/**
* @param {Node} parent
* @param {ChildNode} child
* @returns {ChildNode}
*/
export function remove_child(parent, child) {
if (renderer) {
renderer.remove(child);
return child;
}
return /** @type {ChildNode} */ (parent.removeChild(child));
}
/**
* @param {ChildNode} old_node
* @param {Node} new_node
*/
export function replace_with(old_node, new_node) {
if (renderer) {
var parent = renderer.getParent(old_node);
renderer.insert(parent, new_node, old_node);
renderer.remove(old_node);
return;
}
old_node.replaceWith(new_node);
}
/**
* @param {Node} node
* @param {string} value
*/
export function set_text_content(node, value) {
if (renderer) {
renderer.setText(node, value);
return;
}
node.textContent = value;
}
/**
* @param {Node} node
* @param {string} value
*/
export function set_node_value(node, value) {
if (renderer) {
renderer.setText(node, value);
return;
}
node.nodeValue = value;
}
// --- Helpers for style attribute string manipulation (custom renderer) ---
/**
* @param {string} style_string
* @param {string} property
* @param {string} value
* @param {string} [priority]
* @returns {string}
*/
function set_style_property_in_string(style_string, property, value, priority) {
var declaration = property + ': ' + value + (priority ? ' !' + priority : '');
var parts = style_string.split(';');
var found = false;
for (var i = 0; i < parts.length; i++) {
var colon_index = parts[i].indexOf(':');
if (colon_index !== -1 && parts[i].substring(0, colon_index).trim() === property) {
parts[i] = ' ' + declaration;
found = true;
break;
}
}
if (!found) {
parts.push(' ' + declaration);
}
return parts
.map((p) => p.trim())
.filter(Boolean)
.join('; ');
}
/**
* @param {string} style_string
* @param {string} property
* @returns {string}
*/
function remove_style_property_in_string(style_string, property) {
return style_string
.split(';')
.filter((part) => {
var colon_index = part.indexOf(':');
if (colon_index === -1) return false;
return part.substring(0, colon_index).trim() !== property;
})
.map((p) => p.trim())
.filter(Boolean)
.join('; ');
}
/**
* @param {Node} node
* @returns {string | null}
*/
export function get_node_value(node) {
if (renderer) return renderer.getNodeValue(node);
return node.nodeValue;
}
/**
* @param {Element} element
* @param {string} name
* @returns {string | null}
*/
export function get_attribute(element, name) {
if (renderer) return renderer.getAttribute(element, name);
return element.getAttribute(name);
}
/**
* @param {Element} element
* @param {string} name
*/
export function remove_attribute(element, name) {
if (renderer) {
renderer.removeAttribute(element, name);
return;
}
element.removeAttribute(name);
}
/**
* @param {Element} element
* @param {string} name
* @returns {boolean}
*/
export function has_attribute(element, name) {
// TODO RENDERER: could be worth removing and just using get?
if (renderer) return renderer.hasAttribute(element, name);
return element.hasAttribute(name);
}
/**
* @param {Element} element
* @param {string} value
*/
export function set_inner_html(element, value) {
if (renderer) {
throw new Error('setInnerHTML is not supported with custom renderers');
}
element.innerHTML = value;
}
/**
* @param {Node} node
* @param {boolean} deep
* @returns {Node}
*/
export function clone_node(node, deep) {
if (renderer) return renderer.cloneNode(node, deep);
return node.cloneNode(deep);
}
/**
* @param {Node} node
* @param {boolean} deep
* @returns {Node}
*/
export function import_node(node, deep) {
if (renderer) return renderer.cloneNode(node, deep);
return document.importNode(node, deep);
}
/**
* @param {EventTarget} target
* @param {string} type
* @param {EventListenerOrEventListenerObject} handler
* @param {boolean | AddEventListenerOptions} [options]
*/
export function add_event_listener(target, type, handler, options) {
if (renderer) {
renderer.addEventListener(target, type, handler, options);
return;
}
target.addEventListener(type, handler, options);
}
/**
* @param {EventTarget} target
* @param {string} type
* @param {EventListenerOrEventListenerObject} handler
* @param {boolean | EventListenerOptions} [options]
*/
export function remove_event_listener(target, type, handler, options) {
if (renderer) {
renderer.removeEventListener(target, type, handler, options);
return;
}
target.removeEventListener(type, handler, options);
}
/**
* @param {EventTarget} target
* @param {Event} event
* @returns {boolean}
*/
export function dispatch_event(target, event) {
if (renderer) {
// is only used in SSR which is not a thing for custom renderers
throw new Error('dispatchEvent is not supported with custom renderers');
}
return target.dispatchEvent(event);
}
/**
* @param {HTMLElement} element
* @param {string} property
* @param {string} value
* @param {string} [priority]
*/
export function style_set_property(element, property, value, priority) {
if (renderer) {
var style = renderer.getAttribute(element, 'style') || '';
var updated = set_style_property_in_string(style, property, value, priority);
renderer.setAttribute(element, 'style', updated);
return;
}
element.style.setProperty(property, value, priority);
}
/**
* @param {HTMLElement} element
* @param {string} property
*/
export function style_remove_property(element, property) {
if (renderer) {
var style = renderer.getAttribute(element, 'style') || '';
var updated = remove_style_property_in_string(style, property);
renderer.setAttribute(element, 'style', updated);
return;
}
element.style.removeProperty(property);
}
/**
* @param {HTMLElement} element
* @param {string} value
*/
export function set_css_text(element, value) {
if (renderer) {
renderer.setAttribute(element, 'style', value);
return;
}
element.style.cssText = value;
}
/**
* @param {Element} element
* @param {string} name
* @param {boolean} force
*/
export function class_list_toggle(element, name, force) {
if (renderer) {
const classes = element.getAttribute('class')?.split(/\s+/) ?? [];
const has_class = classes.includes(name);
if (force === has_class) {
return;
}
if (force) {
classes.push(name);
} else {
const index = classes.indexOf(name);
if (index !== -1) {
classes.splice(index, 1);
}
}
element.setAttribute('class', classes.join(' '));
return;
}
element.classList.toggle(name, force);
}
/**
* @param {Element} element
* @param {string} selector
*/
export function query_selector(element, selector) {
if (renderer) {
// TODO RENDERER: just used with css inject which doesn't make sense with custom renderers
throw new Error('querySelector is not supported with custom renderers');
}
return element.querySelector(selector);
}
/**
* @param {Element | DocumentFragment} element
* @param {string} selector
*/
export function query_selector_all(element, selector) {
if (renderer) {
// TODO RENDERER: just used with css inject which doesn't make sense with custom renderers
throw new Error('querySelectorAll is not supported with custom renderers');
}
return element.querySelectorAll(selector);
}
/**
* @param {Node} node
* @returns {Node}
*/
export function get_root_node(node) {
if (renderer) {
// TODO RENDERER: just used with css inject which doesn't make sense with custom renderers
throw new Error('getRootNode is not supported with custom renderers');
}
return node.getRootNode();
}
/**
* @param {HTMLElement} element
*/
export function focus(element) {
if (renderer) {
// doesn't make sense with custom renderers
throw new Error('focus is not supported with custom renderers');
}
element.focus();
}

Loading…
Cancel
Save