Skip to content

Commit de021ae

Browse files
authored
move element code (#10778)
* move element code * eslint nonsense --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 84fa18a commit de021ae

File tree

7 files changed

+676
-644
lines changed

7 files changed

+676
-644
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
import { DEV } from 'esm-env';
2+
import { hydrating } from '../../hydration.js';
3+
import { render_effect } from '../../reactivity/effects.js';
4+
import { get_descriptors, object_assign } from '../../utils.js';
5+
import { map_get, map_set } from '../../operations.js';
6+
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
7+
import { delegate } from './events.js';
8+
import { autofocus } from './misc.js';
9+
10+
/**
11+
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
12+
* to remove it upon hydration to avoid a bug when someone resets the form value.
13+
* @param {HTMLInputElement | HTMLSelectElement} dom
14+
* @returns {void}
15+
*/
16+
export function remove_input_attr_defaults(dom) {
17+
if (hydrating) {
18+
attr(dom, 'value', null);
19+
attr(dom, 'checked', null);
20+
}
21+
}
22+
23+
/**
24+
* @param {Element} dom
25+
* @param {string} attribute
26+
* @param {() => string} value
27+
*/
28+
export function attr_effect(dom, attribute, value) {
29+
render_effect(() => {
30+
attr(dom, attribute, value());
31+
});
32+
}
33+
34+
/**
35+
* @param {Element} dom
36+
* @param {string} attribute
37+
* @param {string | null} value
38+
*/
39+
export function attr(dom, attribute, value) {
40+
value = value == null ? null : value + '';
41+
42+
if (DEV) {
43+
check_src_in_dev_hydration(dom, attribute, value);
44+
}
45+
46+
if (
47+
!hydrating ||
48+
(dom.getAttribute(attribute) !== value &&
49+
// If we reset those, they would result in another network request, which we want to avoid.
50+
// We assume they are the same between client and server as checking if they are equal is expensive
51+
// (we can't just compare the strings as they can be different between client and server but result in the
52+
// same url, so we would need to create hidden anchor elements to compare them)
53+
attribute !== 'src' &&
54+
attribute !== 'href' &&
55+
attribute !== 'srcset')
56+
) {
57+
if (value === null) {
58+
dom.removeAttribute(attribute);
59+
} else {
60+
dom.setAttribute(attribute, value);
61+
}
62+
}
63+
}
64+
65+
/**
66+
* @param {Element} dom
67+
* @param {string} attribute
68+
* @param {() => string} value
69+
*/
70+
export function xlink_attr_effect(dom, attribute, value) {
71+
render_effect(() => {
72+
xlink_attr(dom, attribute, value());
73+
});
74+
}
75+
76+
/**
77+
* @param {Element} dom
78+
* @param {string} attribute
79+
* @param {string} value
80+
*/
81+
export function xlink_attr(dom, attribute, value) {
82+
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
83+
}
84+
85+
/**
86+
* @param {any} node
87+
* @param {string} prop
88+
* @param {() => any} value
89+
*/
90+
export function set_custom_element_data_effect(node, prop, value) {
91+
render_effect(() => {
92+
set_custom_element_data(node, prop, value());
93+
});
94+
}
95+
96+
/**
97+
* @param {any} node
98+
* @param {string} prop
99+
* @param {any} value
100+
*/
101+
export function set_custom_element_data(node, prop, value) {
102+
if (prop in node) {
103+
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;
104+
} else {
105+
attr(node, prop, value);
106+
}
107+
}
108+
109+
/**
110+
* Like `spread_attributes` but self-contained
111+
* @param {Element & ElementCSSInlineStyle} dom
112+
* @param {() => Record<string, unknown>[]} attrs
113+
* @param {boolean} lowercase_attributes
114+
* @param {string} css_hash
115+
*/
116+
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
117+
/** @type {Record<string, any> | undefined} */
118+
var current;
119+
120+
render_effect(() => {
121+
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
122+
});
123+
}
124+
125+
/**
126+
* Spreads attributes onto a DOM element, taking into account the currently set attributes
127+
* @param {Element & ElementCSSInlineStyle} dom
128+
* @param {Record<string, unknown> | undefined} prev
129+
* @param {Record<string, unknown>[]} attrs
130+
* @param {boolean} lowercase_attributes
131+
* @param {string} css_hash
132+
* @returns {Record<string, unknown>}
133+
*/
134+
export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) {
135+
var next = object_assign({}, ...attrs);
136+
var has_hash = css_hash.length !== 0;
137+
138+
for (var key in prev) {
139+
if (!(key in next)) {
140+
next[key] = null;
141+
}
142+
}
143+
144+
if (has_hash && !next.class) {
145+
next.class = '';
146+
}
147+
148+
var setters = map_get(setters_cache, dom.nodeName);
149+
if (!setters) map_set(setters_cache, dom.nodeName, (setters = get_setters(dom)));
150+
151+
for (key in next) {
152+
var value = next[key];
153+
if (value === prev?.[key]) continue;
154+
155+
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
156+
if (prefix === '$$') continue;
157+
158+
if (prefix === 'on') {
159+
/** @type {{ capture?: true }} */
160+
var opts = {};
161+
var event_name = key.slice(2);
162+
var delegated = DelegatedEvents.includes(event_name);
163+
164+
if (
165+
event_name.endsWith('capture') &&
166+
event_name !== 'ongotpointercapture' &&
167+
event_name !== 'onlostpointercapture'
168+
) {
169+
event_name = event_name.slice(0, -7);
170+
opts.capture = true;
171+
}
172+
173+
if (!delegated && prev?.[key]) {
174+
dom.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts);
175+
}
176+
177+
if (value != null) {
178+
if (!delegated) {
179+
dom.addEventListener(event_name, value, opts);
180+
} else {
181+
// @ts-ignore
182+
dom[`__${event_name}`] = value;
183+
delegate([event_name]);
184+
}
185+
}
186+
} else if (value == null) {
187+
dom.removeAttribute(key);
188+
} else if (key === 'style') {
189+
dom.style.cssText = value + '';
190+
} else if (key === 'autofocus') {
191+
autofocus(/** @type {HTMLElement} */ (dom), Boolean(value));
192+
} else if (key === '__value' || key === 'value') {
193+
// @ts-ignore
194+
dom.value = dom[key] = dom.__value = value;
195+
} else {
196+
var name = key;
197+
if (lowercase_attributes) {
198+
name = name.toLowerCase();
199+
name = AttributeAliases[name] || name;
200+
}
201+
202+
if (setters.includes(name)) {
203+
if (DEV) {
204+
check_src_in_dev_hydration(dom, name, value);
205+
}
206+
207+
if (
208+
!hydrating ||
209+
// @ts-ignore see attr method for an explanation of src/srcset
210+
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
211+
) {
212+
// @ts-ignore
213+
dom[name] = value;
214+
}
215+
} else if (typeof value !== 'function') {
216+
if (has_hash && name === 'class') {
217+
if (value) value += ' ';
218+
value += css_hash;
219+
}
220+
221+
attr(dom, name, value);
222+
}
223+
}
224+
}
225+
226+
return next;
227+
}
228+
229+
/**
230+
* @param {Element} node
231+
* @param {() => Record<string, unknown>[]} attrs
232+
* @param {string} css_hash
233+
*/
234+
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
235+
/** @type {Record<string, any> | undefined} */
236+
var current;
237+
238+
render_effect(() => {
239+
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
240+
});
241+
}
242+
243+
/**
244+
* @param {Element} node
245+
* @param {Record<string, unknown> | undefined} prev
246+
* @param {Record<string, unknown>[]} attrs
247+
* @param {string} css_hash
248+
*/
249+
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
250+
if (node.tagName.includes('-')) {
251+
var next = object_assign({}, ...attrs);
252+
253+
for (var key in prev) {
254+
if (!(key in next)) {
255+
next[key] = null;
256+
}
257+
}
258+
259+
for (key in next) {
260+
set_custom_element_data(node, key, next[key]);
261+
}
262+
263+
return next;
264+
} else {
265+
return spread_attributes(
266+
/** @type {Element & ElementCSSInlineStyle} */ (node),
267+
prev,
268+
attrs,
269+
node.namespaceURI !== namespace_svg,
270+
css_hash
271+
);
272+
}
273+
}
274+
275+
/**
276+
* List of attributes that should always be set through the attr method,
277+
* because updating them through the property setter doesn't work reliably.
278+
* In the example of `width`/`height`, the problem is that the setter only
279+
* accepts numeric values, but the attribute can also be set to a string like `50%`.
280+
* If this list becomes too big, rethink this approach.
281+
*/
282+
var always_set_through_set_attribute = ['width', 'height'];
283+
284+
/** @type {Map<string, string[]>} */
285+
var setters_cache = new Map();
286+
287+
/** @param {Element} element */
288+
function get_setters(element) {
289+
/** @type {string[]} */
290+
var setters = [];
291+
292+
// @ts-expect-error
293+
var descriptors = get_descriptors(element.__proto__);
294+
295+
for (var key in descriptors) {
296+
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
297+
setters.push(key);
298+
}
299+
}
300+
301+
return setters;
302+
}
303+
304+
/**
305+
* @param {any} dom
306+
* @param {string} attribute
307+
* @param {string | null} value
308+
*/
309+
function check_src_in_dev_hydration(dom, attribute, value) {
310+
if (!hydrating) return;
311+
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
312+
313+
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
314+
if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
315+
316+
// eslint-disable-next-line no-console
317+
console.error(
318+
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
319+
`the ${attribute} value that came from the server will be used. Related element:`,
320+
dom,
321+
' Differing value:',
322+
value
323+
);
324+
}
325+
326+
/**
327+
* @param {string} element_src
328+
* @param {string} url
329+
* @returns {boolean}
330+
*/
331+
function src_url_equal(element_src, url) {
332+
if (element_src === url) return true;
333+
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
334+
}
335+
336+
/** @param {string} srcset */
337+
function split_srcset(srcset) {
338+
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
339+
}
340+
341+
/**
342+
* @param {HTMLSourceElement | HTMLImageElement} element
343+
* @param {string | undefined | null} srcset
344+
* @returns {boolean}
345+
*/
346+
export function srcset_url_equal(element, srcset) {
347+
var element_urls = split_srcset(element.srcset);
348+
var urls = split_srcset(srcset ?? '');
349+
350+
return (
351+
urls.length === element_urls.length &&
352+
urls.every(
353+
([url, width], i) =>
354+
width === element_urls[i][1] &&
355+
// We need to test both ways because Vite will create an a full URL with
356+
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
357+
// relative URLs inside srcset are not automatically resolved to absolute URLs by
358+
// browsers (in contrast to img.src). This means both SSR and DOM code could
359+
// contain relative or absolute URLs.
360+
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
361+
)
362+
);
363+
}

0 commit comments

Comments
 (0)