Convert src/compiler/compile/render_dom/wrappers/Element/Attribute.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent 91fb7d3b1d
commit 51d1fbefcb

@ -1,6 +1,6 @@
module.exports = { module.exports = {
spec: [ spec: [
'src/**/__test__.ts', 'src/**/__test__.js',
], ],
require: [ require: [
'sucrase/register' 'sucrase/register'

@ -1,423 +1,416 @@
import Attribute from '../../../nodes/Attribute'; import fix_attribute_casing from './fix_attribute_casing.js';
import Block from '../../Block'; import { string_literal } from '../../../utils/stringify.js';
import fix_attribute_casing from './fix_attribute_casing';
import ElementWrapper from './index';
import { string_literal } from '../../../utils/stringify';
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression'; import handle_select_value_binding from './handle_select_value_binding.js';
import Text from '../../../nodes/Text'; import { namespaces } from '../../../../utils/namespaces.js';
import handle_select_value_binding from './handle_select_value_binding'; import { boolean_attributes } from '../../../../../shared/boolean_attributes.js';
import { Identifier, Node } from 'estree'; import { regex_double_quotes } from '../../../../utils/patterns.js';
import { namespaces } from '../../../../utils/namespaces';
import { BooleanAttributes, boolean_attributes } from '../../../../../shared/boolean_attributes';
import { regex_double_quotes } from '../../../../utils/patterns';
const non_textlike_input_types = new Set([ const non_textlike_input_types = new Set([
'button', 'button',
'checkbox', 'checkbox',
'color', 'color',
'date', 'date',
'datetime-local', 'datetime-local',
'file', 'file',
'hidden', 'hidden',
'image', 'image',
'radio', 'radio',
'range', 'range',
'reset', 'reset',
'submit' 'submit'
]); ]);
/** */
export class BaseAttributeWrapper { export class BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
block.add_dependencies(node.dependencies);
}
}
render(_block: Block) {} /** @type {import('../../../nodes/Attribute.js').default} */
node;
/** @type {import('./index.js').default} */
parent;
constructor(parent, block, node) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
block.add_dependencies(node.dependencies);
}
}
/** @param {import('../../Block.js').default} _block */
render(_block) { }
} }
const regex_minus_sign = /-/; const regex_minus_sign = /-/;
const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g; const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;
/** @extends BaseAttributeWrapper */
export default class AttributeWrapper extends BaseAttributeWrapper { export default class AttributeWrapper extends BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
metadata: any;
name: string;
property_name: string;
is_indirectly_bound_value: boolean;
is_src: boolean;
is_select_value_attribute: boolean;
is_input_value: boolean;
should_cache: boolean;
last: Identifier;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
super(parent, block, node);
if (node.dependencies.size > 0) {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
}
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach((prop) => {
this.node.dependencies.forEach((dependency: string) => {
this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency);
});
});
}
}
if (node.name === 'value') {
handle_select_value_binding(this, node.dependencies);
this.parent.has_dynamic_value = true;
}
}
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = false;
this.property_name = null;
this.is_select_value_attribute = false;
this.is_input_value = false;
} else {
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
// TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_src =
this.name === 'src' &&
(!this.parent.node.namespace || this.parent.node.namespace === namespaces.html);
this.should_cache = should_cache(this);
}
render(block: Block) {
const element = this.parent;
const { name, property_name, should_cache, is_indirectly_bound_value } = this;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = regex_minus_sign.test(element.node.name)
? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
: '@attr';
const is_legacy_input_type =
element.renderer.component.compile_options.legacy &&
name === 'type' &&
this.parent.node.name === 'input';
const dependencies = this.get_dependencies(); /** @type {import('../../../nodes/Attribute.js').default} */
const value = this.get_value(block); node;
let updater: Node[]; /** @type {import('./index.js').default} */
const init = this.get_init(block, value); parent;
if (is_legacy_input_type) { /** @type {any} */
block.chunks.hydrate.push(b`@set_input_type(${element.var}, ${init});`); metadata;
updater = b`@set_input_type(${element.var}, ${should_cache ? this.last : value});`;
} else if (this.is_select_value_attribute) { /** @type {string} */
// annoying special case name;
const is_multiple_select = element.node.get_static_attribute_value('multiple');
/** @type {string} */
if (is_multiple_select) { property_name;
updater = b`@select_options(${element.var}, ${value});`;
} else { /** @type {boolean} */
updater = b`@select_option(${element.var}, ${value});`; is_indirectly_bound_value;
}
/** @type {boolean} */
block.chunks.mount.push(b` is_src;
/** @type {boolean} */
is_select_value_attribute;
/** @type {boolean} */
is_input_value;
/** @type {boolean} */
should_cache;
/** @type {import('estree').Identifier} */
last;
constructor(parent, block, node) {
super(parent, block, node);
if (node.dependencies.size > 0) {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
}
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach((prop) => {
this.node.dependencies.forEach((dependency) => {
this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency);
});
});
}
}
if (node.name === 'value') {
handle_select_value_binding(this, node.dependencies);
this.parent.has_dynamic_value = true;
}
}
if (this.parent.node.namespace == namespaces.foreign) {
// leave attribute case alone for elements in the "foreign" namespace
this.name = this.node.name;
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = false;
this.property_name = null;
this.is_select_value_attribute = false;
this.is_input_value = false;
}
else {
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
}
// TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_src =
this.name === 'src' &&
(!this.parent.node.namespace || this.parent.node.namespace === namespaces.html);
this.should_cache = should_cache(this);
}
/** @param {import('../../Block.js').default} block */
render(block) {
const element = this.parent;
const { name, property_name, should_cache, is_indirectly_bound_value } = this;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = regex_minus_sign.test(element.node.name)
? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
: '@attr';
const is_legacy_input_type = element.renderer.component.compile_options.legacy &&
name === 'type' &&
this.parent.node.name === 'input';
const dependencies = this.get_dependencies();
const value = this.get_value(block);
/** @type {Node[]} */
let updater;
const init = this.get_init(block, value);
if (is_legacy_input_type) {
block.chunks.hydrate.push(b `@set_input_type(${element.var}, ${init});`);
updater = b `@set_input_type(${element.var}, ${should_cache ? this.last : value});`;
}
else if (this.is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
if (is_multiple_select) {
updater = b `@select_options(${element.var}, ${value});`;
}
else {
updater = b `@select_option(${element.var}, ${value});`;
}
block.chunks.mount.push(b `
${updater} ${updater}
`); `);
} else if (this.is_src) { }
block.chunks.hydrate.push( else if (this.is_src) {
b`if (!@src_url_equal(${element.var}.src, ${init})) ${method}(${element.var}, "${name}", ${this.last});` block.chunks.hydrate.push(b `if (!@src_url_equal(${element.var}.src, ${init})) ${method}(${element.var}, "${name}", ${this.last});`);
); updater = b `${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`; }
} else if (property_name) { else if (property_name) {
block.chunks.hydrate.push(b`${element.var}.${property_name} = ${init};`); block.chunks.hydrate.push(b `${element.var}.${property_name} = ${init};`);
updater = block.renderer.options.dev updater = block.renderer.options.dev
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? this.last : value});` ? b `@prop_dev(${element.var}, "${property_name}", ${should_cache ? this.last : value});`
: b`${element.var}.${property_name} = ${should_cache ? this.last : value};`; : b `${element.var}.${property_name} = ${should_cache ? this.last : value};`;
} else { }
block.chunks.hydrate.push(b`${method}(${element.var}, "${name}", ${init});`); else {
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`; block.chunks.hydrate.push(b `${method}(${element.var}, "${name}", ${init});`);
} updater = b `${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
}
if (is_indirectly_bound_value) { if (is_indirectly_bound_value) {
const update_value = b`@set_input_value(${element.var}, ${element.var}.__value);`; const update_value = b `@set_input_value(${element.var}, ${element.var}.__value);`;
block.chunks.hydrate.push(update_value); block.chunks.hydrate.push(update_value);
updater = b `
updater = b`
${updater} ${updater}
${update_value}; ${update_value};
`; `;
} }
if (this.node.name === 'value' && dependencies.length > 0) {
if (this.node.name === 'value' && dependencies.length > 0) { if (this.parent.bindings.some((binding) => binding.node.name === 'group')) {
if (this.parent.bindings.some((binding) => binding.node.name === 'group')) { this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed');
this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed'); block.add_variable(this.parent.dynamic_value_condition, x `false`);
block.add_variable(this.parent.dynamic_value_condition, x`false`); updater = b `
updater = b`
${updater} ${updater}
${this.parent.dynamic_value_condition} = true; ${this.parent.dynamic_value_condition} = true;
`; `;
} }
} }
if (dependencies.length > 0) {
if (dependencies.length > 0) { const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies)); block.chunks.update.push(b `
block.chunks.update.push(b`
if (${condition}) { if (${condition}) {
${updater} ${updater}
}`); }`);
} }
// special case autofocus. has to be handled in a bit of a weird way
// special case autofocus. has to be handled in a bit of a weird way if (name === 'autofocus') {
if (name === 'autofocus') { block.autofocus = {
block.autofocus = { element_var: element.var,
element_var: element.var, condition_expression: this.node.is_true ? undefined : value
condition_expression: this.node.is_true ? undefined : value };
}; }
} }
}
/**
get_init(block: Block, value) { * @param {import('../../Block.js').default} block
this.last = * @param {any} value
this.should_cache && */
block.get_unique_name( get_init(block, value) {
`${this.parent.var.name}_${this.name.replace( this.last =
regex_invalid_variable_identifier_characters, this.should_cache &&
'_' block.get_unique_name(`${this.parent.var.name}_${this.name.replace(regex_invalid_variable_identifier_characters, '_')}_value`);
)}_value` if (this.should_cache)
); block.add_variable(this.last);
return this.should_cache ? x `${this.last} = ${value}` : value;
if (this.should_cache) block.add_variable(this.last); }
return this.should_cache ? x`${this.last} = ${value}` : value; /**
} * @param {import('../../Block.js').default} block
* @param {import('estree').Node} dependency_condition
get_dom_update_conditions(block: Block, dependency_condition: Node) { */
const { property_name, should_cache, last } = this; get_dom_update_conditions(block, dependency_condition) {
const element = this.parent; const { property_name, should_cache, last } = this;
const value = this.get_value(block); const element = this.parent;
const value = this.get_value(block);
let condition = dependency_condition; let condition = dependency_condition;
if (should_cache) {
if (should_cache) { condition = this.is_src
condition = this.is_src ? x `${condition} && (!@src_url_equal(${element.var}.src, (${last} = ${value})))`
? x`${condition} && (!@src_url_equal(${element.var}.src, (${last} = ${value})))` : x `${condition} && (${last} !== (${last} = ${value}))`;
: x`${condition} && (${last} !== (${last} = ${value}))`; }
} if (this.is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (this.is_input_value) { if (type !== true && !non_textlike_input_types.has(type)) {
const type = element.node.get_static_attribute_value('type'); condition = x `${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
if (type !== true && !non_textlike_input_types.has(type)) { }
condition = x`${condition} && ${element.var}.${property_name} !== ${ }
should_cache ? last : value if (block.has_outros) {
}`; condition = x `!#current || ${condition}`;
} }
} return condition;
}
if (block.has_outros) { get_dependencies() {
condition = x`!#current || ${condition}`; const node_dependencies = this.node.get_dependencies();
} const dependencies = new Set(node_dependencies);
node_dependencies.forEach((prop) => {
return condition; const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop);
} if (indirect_dependencies) {
indirect_dependencies.forEach((indirect_dependency) => {
get_dependencies() { dependencies.add(indirect_dependency);
const node_dependencies = this.node.get_dependencies(); });
const dependencies = new Set(node_dependencies); }
});
node_dependencies.forEach((prop: string) => { return Array.from(dependencies);
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop); }
if (indirect_dependencies) { get_metadata() {
indirect_dependencies.forEach((indirect_dependency) => { if (this.parent.node.namespace)
dependencies.add(indirect_dependency); return null;
}); const metadata = attribute_lookup[this.name];
} if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name))
}); return null;
return metadata;
return Array.from(dependencies); }
}
/** @param {import('../../Block.js').default} block */
get_metadata() { get_value(block) {
if (this.parent.node.namespace) return null; if (this.node.is_true) {
const metadata = attribute_lookup[this.name]; if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) {
if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return x `true`;
return null; }
return metadata; return x `""`;
} }
if (this.node.chunks.length === 0)
get_value(block: Block) { return x `""`;
if (this.node.is_true) { // TODO some of this code is repeated in Tag.ts — would be good to
if (this.metadata && boolean_attributes.has(this.metadata.property_name.toLowerCase())) { // DRY it out if that's possible without introducing crazy indirection
return x`true`; if (this.node.chunks.length === 1) {
} return this.node.chunks[0].type === 'Text'
return x`""`; ? string_literal(( /** @type {import('../../../nodes/Text.js').default} */(this.node.chunks[0])).data)
} : ( /** @type {import('../../../nodes/shared/Expression.js').default} */(this.node.chunks[0])).manipulate(block);
if (this.node.chunks.length === 0) return x`""`; }
let value = this.node.name === 'class'
// TODO some of this code is repeated in Tag.ts — would be good to ? this.get_class_name_text(block)
// DRY it out if that's possible without introducing crazy indirection : this.render_chunks(block).reduce((lhs, rhs) => x `${lhs} + ${rhs}`);
if (this.node.chunks.length === 1) { // '{foo} {bar}' — treat as string concatenation
return this.node.chunks[0].type === 'Text' if (this.node.chunks[0].type !== 'Text') {
? string_literal((this.node.chunks[0] as Text).data) value = x `"" + ${value}`;
: (this.node.chunks[0] as Expression).manipulate(block); }
} return value;
}
let value =
this.node.name === 'class' /** @param {import('../../Block.js').default} block */
? this.get_class_name_text(block) get_class_name_text(block) {
: this.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`); const scoped_css = this.node.chunks.some((chunk) => chunk.synthetic);
const rendered = this.render_chunks(block);
// '{foo} {bar}' — treat as string concatenation if (scoped_css && rendered.length === 2) {
if (this.node.chunks[0].type !== 'Text') { // we have a situation like class={possiblyUndefined}
value = x`"" + ${value}`; rendered[0] = x `@null_to_empty(${rendered[0]})`;
} }
return rendered.reduce((lhs, rhs) => x `${lhs} + ${rhs}`);
return value; }
}
/** @param {import('../../Block.js').default} block */
get_class_name_text(block: Block) { render_chunks(block) {
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic); return this.node.chunks.map((chunk) => {
const rendered = this.render_chunks(block); if (chunk.type === 'Text') {
return string_literal(chunk.data);
if (scoped_css && rendered.length === 2) { }
// we have a situation like class={possiblyUndefined} return chunk.manipulate(block);
rendered[0] = x`@null_to_empty(${rendered[0]})`; });
} }
stringify() {
return rendered.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); if (this.node.is_true)
} return '';
const value = this.node.chunks;
render_chunks(block: Block) { if (value.length === 0)
return this.node.chunks.map((chunk) => { return '=""';
if (chunk.type === 'Text') { return `="${value
return string_literal(chunk.data); .map((chunk) => {
} return chunk.type === 'Text'
? chunk.data.replace(regex_double_quotes, '\\"')
return chunk.manipulate(block); : `\${${chunk.manipulate()}}`;
}); })
} .join('')}"`;
}
stringify() {
if (this.node.is_true) return '';
const value = this.node.chunks;
if (value.length === 0) return '=""';
return `="${value
.map((chunk) => {
return chunk.type === 'Text'
? chunk.data.replace(regex_double_quotes, '\\"')
: `\${${chunk.manipulate()}}`;
})
.join('')}"`;
}
} }
/**
// source: https://html.spec.whatwg.org/multipage/indices.html * @type {{ [key in BooleanAttributes]: AttributeMetadata } & {
type AttributeMetadata = { property_name?: string; applies_to?: string[] }; * [key in string]: AttributeMetadata;
const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { * }}
[key in string]: AttributeMetadata; */
} = { const attribute_lookup = {
allowfullscreen: { property_name: 'allowFullscreen', applies_to: ['iframe'] }, allowfullscreen: { property_name: 'allowFullscreen', applies_to: ['iframe'] },
allowpaymentrequest: { property_name: 'allowPaymentRequest', applies_to: ['iframe'] }, allowpaymentrequest: { property_name: 'allowPaymentRequest', applies_to: ['iframe'] },
async: { applies_to: ['script'] }, async: { applies_to: ['script'] },
autofocus: { applies_to: ['button', 'input', 'keygen', 'select', 'textarea'] }, autofocus: { applies_to: ['button', 'input', 'keygen', 'select', 'textarea'] },
autoplay: { applies_to: ['audio', 'video'] }, autoplay: { applies_to: ['audio', 'video'] },
checked: { applies_to: ['input'] }, checked: { applies_to: ['input'] },
controls: { applies_to: ['audio', 'video'] }, controls: { applies_to: ['audio', 'video'] },
default: { applies_to: ['track'] }, default: { applies_to: ['track'] },
defer: { applies_to: ['script'] }, defer: { applies_to: ['script'] },
disabled: { disabled: {
applies_to: [ applies_to: [
'button', 'button',
'fieldset', 'fieldset',
'input', 'input',
'keygen', 'keygen',
'optgroup', 'optgroup',
'option', 'option',
'select', 'select',
'textarea' 'textarea'
] ]
}, },
formnovalidate: { property_name: 'formNoValidate', applies_to: ['button', 'input'] }, formnovalidate: { property_name: 'formNoValidate', applies_to: ['button', 'input'] },
hidden: {}, hidden: {},
indeterminate: { applies_to: ['input'] }, indeterminate: { applies_to: ['input'] },
inert: {}, inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] }, ismap: { property_name: 'isMap', applies_to: ['img'] },
loop: { applies_to: ['audio', 'bgsound', 'video'] }, loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] }, multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] }, muted: { applies_to: ['audio', 'video'] },
nomodule: { property_name: 'noModule', applies_to: ['script'] }, nomodule: { property_name: 'noModule', applies_to: ['script'] },
novalidate: { property_name: 'noValidate', applies_to: ['form'] }, novalidate: { property_name: 'noValidate', applies_to: ['form'] },
open: { applies_to: ['details', 'dialog'] }, open: { applies_to: ['details', 'dialog'] },
playsinline: { property_name: 'playsInline', applies_to: ['video'] }, playsinline: { property_name: 'playsInline', applies_to: ['video'] },
readonly: { property_name: 'readOnly', applies_to: ['input', 'textarea'] }, readonly: { property_name: 'readOnly', applies_to: ['input', 'textarea'] },
required: { applies_to: ['input', 'select', 'textarea'] }, required: { applies_to: ['input', 'select', 'textarea'] },
reversed: { applies_to: ['ol'] }, reversed: { applies_to: ['ol'] },
selected: { applies_to: ['option'] }, selected: { applies_to: ['option'] },
value: { value: {
applies_to: [ applies_to: [
'button', 'button',
'option', 'option',
'input', 'input',
'li', 'li',
'meter', 'meter',
'progress', 'progress',
'param', 'param',
'select', 'select',
'textarea' 'textarea'
] ]
} }
}; };
Object.keys(attribute_lookup).forEach((name) => { Object.keys(attribute_lookup).forEach((name) => {
const metadata = attribute_lookup[name]; const metadata = attribute_lookup[name];
if (!metadata.property_name) metadata.property_name = name; if (!metadata.property_name)
metadata.property_name = name;
}); });
function should_cache(attribute: AttributeWrapper) { /** @param {AttributeWrapper} attribute */
return attribute.is_src || attribute.node.should_cache(); function should_cache(attribute) {
return attribute.is_src || attribute.node.should_cache();
} }
const regex_contains_checked_or_group = /checked|group/; const regex_contains_checked_or_group = /checked|group/;
function is_indirectly_bound_value(attribute: AttributeWrapper) { /** @param {AttributeWrapper} attribute */
const element = attribute.parent; function is_indirectly_bound_value(attribute) {
return ( const element = attribute.parent;
attribute.name === 'value' && return (attribute.name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound (element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' && (element.node.name === 'input' &&
element.node.bindings.some((binding) => element.node.bindings.some((binding) => regex_contains_checked_or_group.test(binding.name)))));
regex_contains_checked_or_group.test(binding.name)
)))
);
} }
/** @typedef {{ property_name?: string; applies_to?: string[] }} AttributeMetadata */

Loading…
Cancel
Save