@ -5,9 +5,15 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get _descriptor , is _extensible } from '../../shared/utils.js' ;
import { active _effect } from '../runtime.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 { NAMESPACE _HTML } from '../../../constants.js' ;
import { current _renderer } from '../custom-renderer/state.js' ;
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -78,6 +84,7 @@ export function init_operations() {
* @ returns { Text }
* /
export function create _text ( value = '' ) {
if ( current _renderer ) return /** @type {Text} */ ( current _renderer . createTextNode ( value ) ) ;
return document . createTextNode ( value ) ;
}
@ -87,6 +94,8 @@ export function create_text(value = '') {
* /
/*@__NO_SIDE_EFFECTS__*/
export function get _first _child ( node ) {
if ( current _renderer )
return /** @type {TemplateNode | null} */ ( current _renderer . getFirstChild ( node ) ) ;
return /** @type {TemplateNode | null} */ ( first _child _getter . call ( node ) ) ;
}
@ -96,6 +105,8 @@ export function get_first_child(node) {
* /
/*@__NO_SIDE_EFFECTS__*/
export function get _next _sibling ( node ) {
if ( current _renderer )
return /** @type {TemplateNode | null} */ ( current _renderer . getNextSibling ( node ) ) ;
return /** @type {TemplateNode | null} */ ( next _sibling _getter . call ( node ) ) ;
}
@ -115,10 +126,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
if ( child === null ) {
child = hydrate _node . appendChild ( create _text ( ) ) ;
} else if ( is _text && child. nodeType !== TEXT _NODE ) {
child = /** @type {TemplateNode} */ ( append _child ( hydrate _node , create _text ( ) ) ) ;
} else if ( is _text && node_type ( child ) !== TEXT _NODE ) {
var text = create _text ( ) ;
child? . before ( text ) ;
insert_before ( child , text ) ;
set _hydrate _node ( text ) ;
return text ;
}
@ -142,7 +153,7 @@ export function first_child(node, is_text = false) {
var first = get _first _child ( node ) ;
// TODO prevent user comments with the empty string when preserveComments is true
if ( first instanceof Comment && first . data === '' ) return get _next _sibling ( first ) ;
if ( is_comment ( first ) && get _node _value ( first ) === '' ) return get _next _sibling ( first ) ;
return first ;
}
@ -150,10 +161,10 @@ export function first_child(node, is_text = false) {
if ( is _text ) {
// if an {expression} is empty during SSR, there might be no
// 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 ( ) ;
hydrate _node ? . before ( text ) ;
if ( hydrate _node ) insert _before ( hydrate _node , text ) ;
set _hydrate _node ( text ) ;
return text ;
}
@ -187,15 +198,15 @@ export function sibling(node, count = 1, is_text = false) {
if ( is _text ) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if ( n ext_sibling ? . nodeType !== TEXT _NODE ) {
if ( n ode_type ( next _sibling ) !== TEXT _NODE ) {
var text = create _text ( ) ;
// 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
// node and insert it after the last sibling
if ( next _sibling === null ) {
last _sibling ? . after ( text ) ;
if ( last _sibling ) insert _after ( last _sibling , text ) ;
} else {
next_sibling . before ( text ) ;
insert_before ( next _sibling , text ) ;
}
set _hydrate _node ( text ) ;
return text ;
@ -214,6 +225,15 @@ export function sibling(node, count = 1, is_text = false) {
* @ returns { void }
* /
export function clear _text _content ( node ) {
if ( current _renderer ) {
var child = current _renderer . getFirstChild ( node ) ;
while ( child !== null ) {
var next = current _renderer . getNextSibling ( child ) ;
current _renderer . remove ( child ) ;
child = next ;
}
return ;
}
node . textContent = '' ;
}
@ -239,6 +259,10 @@ export function should_defer_append() {
* @ returns { T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap [ T ] : Element }
* /
export function create _element ( tag , namespace , is ) {
if ( current _renderer )
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
current _renderer . createElement ( tag )
) ;
let options = is ? { is } : undefined ;
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
document . createElementNS ( namespace ? ? NAMESPACE _HTML , tag , options )
@ -246,6 +270,7 @@ export function create_element(tag, namespace, is) {
}
export function create _fragment ( ) {
if ( current _renderer ) return /** @type {DocumentFragment} */ ( current _renderer . createFragment ( ) ) ;
return document . createDocumentFragment ( ) ;
}
@ -254,6 +279,7 @@ export function create_fragment() {
* @ returns
* /
export function create _comment ( data = '' ) {
if ( current _renderer ) return /** @type {Comment} */ ( current _renderer . createComment ( data ) ) ;
return document . createComment ( data ) ;
}
@ -264,6 +290,10 @@ export function create_comment(data = '') {
* @ returns
* /
export function set _attribute ( element , key , value = '' ) {
if ( current _renderer ) {
current _renderer . setAttribute ( element , key , value ) ;
return ;
}
if ( key . startsWith ( 'xlink:' ) ) {
element . setAttributeNS ( 'http://www.w3.org/1999/xlink' , key , value ) ;
return ;
@ -277,13 +307,16 @@ export function set_attribute(element, key, value = '') {
* @ param { Text } text
* /
export function merge _text _nodes ( text ) {
// if we have a renderer we will not hydrate so we can skip this
if ( current _renderer ) return ;
if ( /** @type {string} */ ( text . nodeValue ) . length < 65536 ) {
return ;
}
let next = text . nextSibling ;
while ( next !== null && n ext. nodeType === TEXT _NODE ) {
while ( next !== null && n ode_type ( next ) === TEXT _NODE ) {
next . remove ( ) ;
/** @type {string} */ ( text . nodeValue ) += /** @type {string} */ ( next . nodeValue ) ;
@ -291,3 +324,473 @@ export function merge_text_nodes(text) {
next = text . nextSibling ;
}
}
/ * *
* @ param { TemplateNode | null } node
* @ returns { node is Comment }
* /
function is _comment ( node ) {
if ( current _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 ( current _renderer ) {
const type = current _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 ( current _renderer ) {
// for custom renderers we don't need to return the node name since all the
// checks that we do on specific node names are meant to be for the HTML
return '' ;
}
return node ? . nodeName ;
}
/ * *
* @ param { Node } node
* @ returns { TemplateNode | null }
* /
export function get _last _child ( node ) {
if ( current _renderer )
return /** @type {TemplateNode | null} */ ( current _renderer . getLastChild ( node ) ) ;
return /** @type {TemplateNode | null} */ ( node . lastChild ) ;
}
/ * *
* @ param { Node } node
* @ returns { TemplateNode | null }
* /
export function get _parent _node ( node ) {
if ( current _renderer )
return /** @type {TemplateNode | null} */ ( current _renderer . getParent ( node ) ) ;
return /** @type {TemplateNode | null} */ ( node . parentNode ) ;
}
/ * *
* @ param { Node } parent
* @ param { Node } child
* @ returns { Node }
* /
export function append _child ( parent , child ) {
if ( current _renderer ) {
current _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 ( current _renderer ) {
var parent = current _renderer . getParent ( ref _node ) ;
current _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 ( current _renderer ) {
var parent = current _renderer . getParent ( ref _node ) ;
var next = current _renderer . getNextSibling ( ref _node ) ;
current _renderer . insert ( parent , new _node , next ) ;
return ;
}
ref _node . after ( new _node ) ;
}
/ * *
* @ param { ChildNode } node
* /
export function remove _node ( node ) {
if ( current _renderer ) {
current _renderer . remove ( node ) ;
return ;
}
node . remove ( ) ;
}
/ * *
* @ param { Node } parent
* @ param { ChildNode } child
* @ returns { ChildNode }
* /
export function remove _child ( parent , child ) {
if ( current _renderer ) {
current _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 ( current _renderer ) {
var parent = current _renderer . getParent ( old _node ) ;
current _renderer . insert ( parent , new _node , old _node ) ;
current _renderer . remove ( old _node ) ;
return ;
}
old _node . replaceWith ( new _node ) ;
}
/ * *
* @ param { Node } node
* @ param { string } value
* /
export function set _text _content ( node , value ) {
if ( current _renderer ) {
current _renderer . setText ( node , value ) ;
return ;
}
node . textContent = value ;
}
/ * *
* @ param { Node } node
* @ param { string } value
* /
export function set _node _value ( node , value ) {
if ( current _renderer ) {
current _renderer . setText ( node , value ) ;
return ;
}
node . nodeValue = value ;
}
// --- Helpers for style attribute string manipulation (custom renderer) ---
// TODO: check if this can be improved?
/ * *
* @ 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 ( current _renderer ) return current _renderer . getNodeValue ( node ) ;
return node . nodeValue ;
}
/ * *
* Sets the ` value ` property on an element . For custom renderers , uses ` setAttribute ` .
* @ param { Element } element
* @ param { any } value
* /
export function set _element _value ( element , value ) {
if ( current _renderer ) {
current _renderer . setAttribute ( element , 'value' , value ? ? '' ) ;
return ;
}
// @ts-expect-error
element . value = value ? ? '' ;
}
/ * *
* Sets the ` checked ` property on an element . For custom renderers , uses ` setAttribute ` .
* @ param { Element } element
* @ param { boolean } checked
* /
export function set _element _checked ( element , checked ) {
if ( current _renderer ) {
if ( checked ) {
current _renderer . setAttribute ( element , 'checked' , '' ) ;
} else {
current _renderer . removeAttribute ( element , 'checked' ) ;
}
return ;
}
// @ts-expect-error
element . checked = checked ;
}
/ * *
* Sets the ` defaultValue ` property on an element without affecting the current ` value ` .
* For custom renderers , uses ` setAttribute ` on ` defaultvalue ` .
* @ param { Element } element
* @ param { string } value
* /
export function set _element _default _value ( element , value ) {
if ( current _renderer ) {
current _renderer . setAttribute ( element , 'defaultValue' , value ) ;
return ;
}
// @ts-expect-error
const existing _value = element . value ;
// @ts-expect-error
element . defaultValue = value ;
// @ts-expect-error
element . value = existing _value ;
}
/ * *
* Sets the ` defaultChecked ` property on an element without affecting the current ` checked ` state .
* For custom renderers , uses ` setAttribute ` on ` defaultchecked ` .
* @ param { Element } element
* @ param { boolean } checked
* /
export function set _element _default _checked ( element , checked ) {
if ( current _renderer ) {
if ( checked ) {
current _renderer . setAttribute ( element , 'defaultChecked' , '' ) ;
} else {
current _renderer . removeAttribute ( element , 'defaultChecked' ) ;
}
return ;
}
// @ts-expect-error
const existing _value = element . checked ;
// @ts-expect-error
element . defaultChecked = checked ;
// @ts-expect-error
element . checked = existing _value ;
}
/ * *
* @ param { Element } element
* @ param { string } name
* @ returns { string | null }
* /
export function get _attribute ( element , name ) {
if ( current _renderer ) return current _renderer . getAttribute ( element , name ) ;
return element . getAttribute ( name ) ;
}
/ * *
* @ param { Element } element
* @ param { string } name
* /
export function remove _attribute ( element , name ) {
if ( current _renderer ) {
current _renderer . removeAttribute ( element , name ) ;
return ;
}
element . removeAttribute ( name ) ;
}
/ * *
* @ param { Element } element
* @ param { string } name
* @ returns { boolean }
* /
export function has _attribute ( element , name ) {
if ( current _renderer ) return current _renderer . hasAttribute ( element , name ) ;
return element . hasAttribute ( name ) ;
}
/ * *
* @ param { Element } element
* @ param { string } value
* /
export function set _inner _html ( element , value ) {
if ( current _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 ( current _renderer ) {
throw new Error ( 'cloneNode is not supported with custom renderers' ) ;
}
return node . cloneNode ( deep ) ;
}
/ * *
* @ param { Node } node
* @ param { boolean } deep
* @ returns { Node }
* /
export function import _node ( node , deep ) {
if ( current _renderer ) {
throw new Error ( 'importNode is not supported with custom renderers' ) ;
}
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 ( current _renderer ) {
current _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 ( current _renderer ) {
current _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 ( current _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 ( current _renderer ) {
var style = current _renderer . getAttribute ( element , 'style' ) || '' ;
var updated = set _style _property _in _string ( style , property , value , priority ) ;
current _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 ( current _renderer ) {
var style = current _renderer . getAttribute ( element , 'style' ) || '' ;
var updated = remove _style _property _in _string ( style , property ) ;
current _renderer . setAttribute ( element , 'style' , updated ) ;
return ;
}
element . style . removeProperty ( property ) ;
}
/ * *
* @ param { HTMLElement } element
* @ param { string } value
* /
export function set _css _text ( element , value ) {
if ( current _renderer ) {
current _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 ( current _renderer ) {
const classes = current _renderer . getAttribute ( element , '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 ) ;
}
}
current _renderer . setAttribute ( element , 'class' , classes . join ( ' ' ) ) ;
return ;
}
element . classList . toggle ( name , force ) ;
}