-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
attributes.js
418 lines (368 loc) · 12.7 KB
/
attributes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '../../constants.js';
import { queue_idle_task, queue_micro_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement} input
* @returns {void}
*/
export function remove_input_defaults(input) {
if (!hydrating) return;
var already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
var remove_defaults = () => {
if (already_removed) return;
already_removed = true;
// Remove the attributes but preserve the values
if (input.hasAttribute('value')) {
var value = input.value;
set_attribute(input, 'value', null);
input.value = value;
}
if (input.hasAttribute('checked')) {
var checked = input.checked;
set_attribute(input, 'checked', null);
input.checked = checked;
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
/**
* @param {Element} element
* @param {any} value
*/
export function set_value(element, value) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
// @ts-expect-error
if (attributes.value === (attributes.value = value) || element.value === value) return;
// @ts-expect-error
element.value = value;
}
/**
* @param {Element} element
* @param {boolean} checked
*/
export function set_checked(element, checked) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
if (attributes.checked === (attributes.checked = checked)) return;
// @ts-expect-error
element.checked = checked;
}
/**
* @param {Element} element
* @param {string} attribute
* @param {string | null} value
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value ?? '');
}
// If we reset these attributes, they would result in another network request, which we want to avoid.
// We assume they are the same between client and server as checking if they are equal is expensive
// (we can't just compare the strings as they can be different between client and server but result in the
// same url, so we would need to create hidden anchor elements to compare them)
return;
}
}
if (attributes[attribute] === (attributes[attribute] = value)) return;
if (attribute === 'loading') {
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = value;
}
if (value == null) {
element.removeAttribute(attribute);
} else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
// @ts-ignore
element[attribute] = value;
} else {
element.setAttribute(attribute, value);
}
}
/**
* @param {Element} dom
* @param {string} attribute
* @param {string} value
*/
export function set_xlink_attribute(dom, attribute, value) {
dom.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
/**
* @param {any} node
* @param {string} prop
* @param {any} value
*/
export function set_custom_element_data(node, prop, value) {
if (get_setters(node).includes(prop)) {
node[prop] = value;
} else {
set_attribute(node, prop, value);
}
}
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
* @param {Record<string, any> | undefined} prev
* @param {Record<string, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [preserve_attribute_case]
* @param {boolean} [is_custom_element]
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
export function set_attributes(
element,
prev,
next,
css_hash,
preserve_attribute_case = false,
is_custom_element = false,
skip_warning = false
) {
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
for (var key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
}
var setters = get_setters(element);
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
/** @type {Array<[string, any, () => void]>} */
var events = [];
// since key is captured we use const
for (const key in next) {
// let instead of var because referenced in a closure
let value = next[key];
// Up here because we want to do this for the initial value, too, even if it's undefined,
// and this wouldn't be reached in case of undefined because of the equality check below
if (is_option_element && key === 'value' && value == null) {
// The <option> element is a special case because removing the value attribute means
// the value is set to the text content of the option element, and setting the value
// to null or undefined means the value is set to the string "null" or "undefined".
// To align with how we handle this case in non-spread-scenarios, this logic is needed.
// There's a super-edge-case bug here that is left in in favor of smaller code size:
// Because of the "set missing props to null" logic above, we can't differentiate
// between a missing value and an explicitly set value of null or undefined. That means
// that once set, the value attribute of an <option> element can't be removed. This is
// a very rare edge case, and removing the attribute altogether isn't possible either
// for the <option value={undefined}> case, so we're not losing any functionality here.
// @ts-ignore
element.value = element.__value = '';
current[key] = value;
continue;
}
var prev_value = current[key];
if (value === prev_value) continue;
current[key] = value;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (prefix === '$$') continue;
if (prefix === 'on') {
/** @type {{ capture?: true }} */
const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2);
var delegated = is_delegated(event_name);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
opts.capture = true;
}
if (!delegated && prev_value) {
// Listening to same event but different handler -> our handle function below takes care of this
// If we were to remove and add listeners in this case, it could happen that the event is "swallowed"
// (the browser seems to not know yet that a new one exists now) and doesn't reach the handler
// https://github.com/sveltejs/svelte/issues/11903
if (value != null) continue;
element.removeEventListener(event_name, current[event_handle_key], opts);
current[event_handle_key] = null;
}
if (value != null) {
if (!delegated) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
if (!prev) {
events.push([
key,
value,
() => (current[event_handle_key] = create_event(event_name, element, handle, opts))
]);
} else {
current[event_handle_key] = create_event(event_name, element, handle, opts);
}
} else {
// @ts-ignore
element[`__${event_name}`] = value;
delegate([event_name]);
}
}
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore
element.value = element[key] = element.__value = value;
} else {
var name = key;
if (!preserve_attribute_case) {
name = normalize_attribute(name);
}
if (value == null && !is_custom_element) {
attributes[key] = null;
element.removeAttribute(key);
} else if (setters.includes(name) && (is_custom_element || typeof value !== 'string')) {
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
} else {
set_attribute(element, name, value);
}
}
}
}
// On the first run, ensure that events are added after bindings so
// that their listeners fire after the binding listeners
if (!prev) {
queue_micro_task(() => {
if (!element.isConnected) return;
for (const [key, value, evt] of events) {
if (current[key] === value) {
evt();
}
}
});
}
return current;
}
/** @type {Map<string, string[]>} */
var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
var setters = setters_cache.get(element.nodeName);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
var descriptors;
var proto = get_prototype_of(element);
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
// Do not use contructor.name here as that's unreliable in some browser environments
while (element_proto !== proto) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set) {
setters.push(key);
}
}
proto = get_prototype_of(proto);
}
return setters;
}
/**
* @param {any} element
* @param {string} attribute
* @param {string} value
*/
function check_src_in_dev_hydration(element, attribute, value) {
if (!DEV) return;
if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value)) return;
w.hydration_attribute_changed(
attribute,
element.outerHTML.replace(element.innerHTML, element.innerHTML && '...'),
String(value)
);
}
/**
* @param {string} element_src
* @param {string} url
* @returns {boolean}
*/
function src_url_equal(element_src, url) {
if (element_src === url) return true;
return new URL(element_src, document.baseURI).href === new URL(url, document.baseURI).href;
}
/** @param {string} srcset */
function split_srcset(srcset) {
return srcset.split(',').map((src) => src.trim().split(' ').filter(Boolean));
}
/**
* @param {HTMLSourceElement | HTMLImageElement} element
* @param {string} srcset
* @returns {boolean}
*/
function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset);
return (
urls.length === element_urls.length &&
urls.every(
([url, width], i) =>
width === element_urls[i][1] &&
// We need to test both ways because Vite will create an a full URL with
// `new URL(asset, import.meta.url).href` for the client when `base: './'`, and the
// relative URLs inside srcset are not automatically resolved to absolute URLs by
// browsers (in contrast to img.src). This means both SSR and DOM code could
// contain relative or absolute URLs.
(src_url_equal(element_urls[i][0], url) || src_url_equal(url, element_urls[i][0]))
)
);
}
/**
* @param {HTMLImageElement} element
* @returns {void}
*/
export function handle_lazy_img(element) {
// If we're using an image that has a lazy loading attribute, we need to apply
// the loading and src after the img element has been appended to the document.
// Otherwise the lazy behaviour will not work due to our cloneNode heuristic for
// templates.
if (!hydrating && element.loading === 'lazy') {
var src = element.src;
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = null;
element.loading = 'eager';
element.removeAttribute('src');
requestAnimationFrame(() => {
// @ts-expect-error
if (element[LOADING_ATTR_SYMBOL] !== 'eager') {
element.loading = 'lazy';
}
element.src = src;
});
}
}