set_class with class: directives

pull/15352/head
adiguba 2 years ago
parent 9873443337
commit 55761143ca

@ -767,66 +767,39 @@ export function analyze_component(root, source, options) {
if (!should_ignore_unused) {
warn_unused(analysis.css.ast);
}
}
outer: for (const node of analysis.elements) {
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {AST.Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;
for (const node of analysis.elements) {
if (node.metadata.scoped && is_custom_element_node(node)) {
mark_subtree_dynamic(node.metadata.path);
}
class_attribute = attribute;
}
let has_class = false;
let has_spread = false;
let has_class_directive = false;
for (const attribute of node.attributes) {
// The spread method appends the hash to the end of the class attribute on its own
if (attribute.type === 'SpreadAttribute') {
has_spread = true;
break;
}
has_class_directive ||= attribute.type === 'ClassDirective';
has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class';
}
if (class_attribute && class_attribute.value !== true) {
if (is_text_attribute(class_attribute)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`;
} else {
/** @type {AST.Text} */
const css_text = {
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
start: -1,
end: -1
};
if (Array.isArray(class_attribute.value)) {
class_attribute.value.push(css_text);
} else {
class_attribute.value = [class_attribute.value, css_text];
}
// We need an empty class to generate the set_class() or class="" correctly
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: '',
raw: '',
start: -1,
end: -1
}
} else {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
start: -1,
end: -1
}
])
);
if (is_custom_element_node(node) && node.attributes.length === 1) {
mark_subtree_dynamic(node.metadata.path);
}
}
}
])
);
}
}

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -14,15 +14,15 @@ import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { is_custom_element_node } from '../../../nodes.js';
import { create_expression_metadata, is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_style_directives,
build_set_attributes
build_set_attributes,
build_set_class
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
@ -223,6 +223,8 @@ export function RegularElement(node, context) {
build_set_attributes(
attributes,
class_directives,
style_directives,
context,
node,
node_id,
@ -270,13 +272,22 @@ export function RegularElement(node, context) {
continue;
}
const name = get_attribute_name(node, attribute);
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
(attribute.value === true || is_text_attribute(attribute))
(attribute.value === true || is_text_attribute(attribute)) &&
(name !== 'class' || class_directives.length === 0)
) {
const name = get_attribute_name(node, attribute);
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) {
if (value === true || value === '') {
value = context.state.analysis.css.hash;
} else {
value += ' ' + context.state.analysis.css.hash;
}
}
if (name !== 'class' || value) {
context.state.template.push(
@ -290,15 +301,23 @@ export function RegularElement(node, context) {
continue;
}
const is = is_custom_element
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
const is =
is_custom_element && name !== 'class'
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(
node,
node_id,
attribute,
attributes,
class_directives,
style_directives,
context
);
if (is) is_attributes_reactive = true;
}
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
// style directives must be applied last since they could override class/style attributes
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
if (
@ -492,6 +511,23 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @return {ObjectExpression}
*/
export function build_class_directives_object(class_directives, context) {
let properties = [];
for (const d of class_directives) {
let expression = /** @type Expression */ (context.visit(d.expression));
if (d.metadata.expression.has_call) {
expression = get_expression_id(context.state, expression);
}
properties.push(b.init(d.name, expression));
}
return b.object(properties);
}
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@ -518,6 +554,8 @@ function setup_select_synchronization(value_binding, context) {
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @returns {boolean}
*/
@ -526,6 +564,8 @@ function build_element_attribute_update_assignment(
node_id,
attribute,
attributes,
class_directives,
style_directives,
context
) {
const state = context.state;
@ -564,19 +604,15 @@ function build_element_attribute_update_assignment(
let update;
if (name === 'class') {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
update = b.stmt(
b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id,
value,
attribute.metadata.needs_clsx && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: undefined
)
return build_set_class(
element,
node_id,
attribute,
value,
has_state,
class_directives,
context,
!is_svg && !is_mathml
);
} else if (name === 'value') {
update = b.stmt(b.call('$.set_value', node_id, value));
@ -640,14 +676,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let { value, has_state } = build_attribute_value(attribute.value, context);
// We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
if (name === 'class' && attribute.metadata.needs_clsx) {
if (context.state.analysis.css.hash) {
value = b.array([value, b.literal(context.state.analysis.css.hash)]);
}
value = b.call('$.clsx', value);
}
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
if (has_state) {

@ -7,11 +7,11 @@ import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
build_class_directives,
build_set_attributes,
build_set_class,
build_style_directives
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, get_expression_id } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -80,19 +80,31 @@ export function SvelteElement(node, context) {
// Then do attributes
let is_attributes_reactive = false;
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
inner_context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
} else {
if (
attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
attributes[0].name.toLowerCase() === 'class'
) {
// special case when there only a class attribute
build_set_class(
node,
element_id,
attributes[0],
b.null,
false,
class_directives,
inner_context,
false
);
} else if (attributes.length) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
is_attributes_reactive = build_set_attributes(
attributes,
class_directives,
style_directives,
inner_context,
node,
element_id,
@ -103,8 +115,7 @@ export function SvelteElement(node, context) {
);
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
// style directives must be applied last since they could override class/style attributes
build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));

@ -6,10 +6,13 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter } from '../../utils.js';
import { build_class_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
@ -20,6 +23,8 @@ import { build_template_chunk, get_expression_id } from './utils.js';
*/
export function build_set_attributes(
attributes,
class_directives,
style_directives,
context,
element,
element_id,
@ -68,6 +73,18 @@ export function build_set_attributes(
}
}
if (class_directives.length) {
values.push(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context)
)
);
is_dynamic ||=
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
}
const call = b.call(
'$.set_attributes',
element_id,
@ -134,39 +151,6 @@ export function build_style_directives(
}
}
/**
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {AST.ClassDirective[]} class_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
export function build_class_directives(
class_directives,
element_id,
context,
is_attributes_reactive
) {
const state = context.state;
for (const directive of class_directives) {
const { has_state, has_call } = directive.metadata.expression;
let value = /** @type {Expression} */ (context.visit(directive.expression));
if (has_call) {
value = get_expression_id(state, value);
}
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
if (is_attributes_reactive || has_state) {
state.update.push(update);
} else {
state.init.push(update);
}
}
}
/**
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
@ -207,3 +191,85 @@ export function get_attribute_name(element, attribute) {
return attribute.name;
}
/**
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} node_id
* @param {AST.Attribute | null} attribute
* @param {Expression} value
* @param {boolean} has_state
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {boolean} is_html
* @returns {boolean}
*/
export function build_set_class(
element,
node_id,
attribute,
value,
has_state,
class_directives,
context,
is_html
) {
if (attribute && attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
/** @type {Identifier | undefined} */
let previous_id;
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ObjectExpression | undefined} */
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state = has_state || !!class_directives.find((d) => d.metadata.expression.has_state);
if (has_state) {
previous_id = b.id(context.state.scope.generate('classes'));
context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
prev = previous_id;
} else {
prev = b.object([]);
}
}
/** @type {Expression | undefined} */
let css_hash;
if (element.metadata.scoped && context.state.analysis.css.hash) {
if (value.type === 'Literal' && (value.value === '' || value.value === null)) {
value = b.literal(context.state.analysis.css.hash);
} else {
css_hash = b.literal(context.state.analysis.css.hash);
}
}
if (!css_hash && next) {
css_hash = b.null;
}
/** @type {Expression} */
let set_class = b.call(
'$.set_class',
node_id,
is_html ? b.literal(1) : b.literal(0),
value,
css_hash,
prev,
next
);
if (previous_id) {
set_class = b.assignment('=', previous_id, set_class);
}
const update = b.stmt(set_class);
if (has_state) {
context.state.update.push(update);
return true;
} else {
context.state.init.push(update);
return false;
}
}

@ -1,4 +1,4 @@
/** @import { Expression, Literal } from 'estree' */
/** @import { Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import {
@ -24,6 +24,7 @@ import {
is_content_editable_binding,
is_load_error_element
} from '../../../../../../utils.js';
import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
@ -86,23 +87,15 @@ export function build_element_attributes(node, context) {
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
class_index = attributes.length;
if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash
? b.binary(
'+',
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
)
: clsx_value
expression: b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
)
}
});
} else {
@ -219,8 +212,9 @@ export function build_element_attributes(node, context) {
}
}
if (class_directives.length > 0 && !has_spread) {
const class_attribute = build_class_directives(
if ((node.metadata.scoped || class_directives.length) && !has_spread) {
const class_attribute = build_to_class(
node.metadata.scoped ? context.state.analysis.css.hash : null,
class_directives,
/** @type {AST.Attribute | null} */ (attributes[class_index] ?? null)
);
@ -274,9 +268,14 @@ export function build_element_attributes(node, context) {
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
// pre-escape and inline literal attributes :
if (value.type === 'Literal' && typeof value.value === 'string') {
context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
}
}
@ -322,7 +321,7 @@ function build_element_spread_attributes(
let styles;
let flags = 0;
if (class_directives.length > 0 || context.state.analysis.css.hash) {
if (class_directives.length) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
@ -331,11 +330,6 @@ function build_element_spread_attributes(
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
if (context.state.analysis.css.hash) {
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
}
classes = b.object(properties);
}
@ -374,55 +368,74 @@ function build_element_spread_attributes(
})
);
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
const css_hash = context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: b.null;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
}
/**
*
* @param {string|null} hash
* @param {AST.ClassDirective[]} class_directives
* @param {AST.Attribute | null} class_attribute
* @returns
*/
function build_class_directives(class_directives, class_attribute) {
const expressions = class_directives.map((directive) =>
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
);
function build_to_class(hash, class_directives, class_attribute) {
if (class_attribute === null) {
class_attribute = create_attribute('class', -1, -1, []);
}
const chunks = get_attribute_chunks(class_attribute.value);
const last = chunks.at(-1);
if (last?.type === 'Text') {
last.data += ' ';
last.raw += ' ';
} else if (last) {
chunks.push({
type: 'Text',
start: -1,
end: -1,
data: ' ',
raw: ' '
});
/** @type {ObjectExpression | undefined} */
let classes;
if (class_directives.length) {
classes = b.object(
class_directives.map((directive) =>
b.prop('init', b.literal(directive.name), directive.expression)
)
);
}
/** @type {Expression} */
let class_name;
if (class_attribute.value === true) {
class_name = b.literal('');
} else if (Array.isArray(class_attribute.value)) {
if (class_attribute.value.length === 0) {
class_name = b.null;
} else {
class_name = class_attribute.value
.map((val) => (val.type === 'Text' ? b.literal(val.data) : val.expression))
.reduce((left, right) => b.binary('+', left, right));
}
} else {
class_name = class_attribute.value.expression;
}
chunks.push({
/** @type {Expression} */
let expression;
if (
hash &&
!classes &&
class_name.type === 'Literal' &&
(class_name.value === null || class_name.value === '')
) {
expression = b.literal(hash);
} else {
expression = b.call('$.to_class', class_name, b.literal(hash), classes);
}
class_attribute.value = {
type: 'ExpressionTag',
start: -1,
end: -1,
expression: b.call(
b.member(b.call(b.member(b.array(expressions), 'filter'), b.id('Boolean')), b.id('join')),
b.literal(' ')
),
expression: expression,
metadata: {
expression: create_expression_metadata()
}
});
class_attribute.value = chunks;
};
return class_attribute;
}

@ -14,6 +14,10 @@ import {
set_active_reaction
} from '../../runtime.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -254,8 +258,8 @@ export function set_custom_element_data(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 {Record<string|symbol, any> | undefined} prev
* @param {Record<string|symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [preserve_attribute_case]
* @param {boolean} [is_custom_element]
@ -289,10 +293,8 @@ export function set_attributes(
if (next.class) {
next.class = clsx(next.class);
}
if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
} else if (css_hash || next[CLASS]) {
next.class = null; /* force call to set_class() */
}
var setters = get_setters(element);
@ -325,7 +327,9 @@ export function set_attributes(
}
var prev_value = current[key];
if (value === prev_value) continue;
if (value === prev_value && key !== 'class') {
continue;
}
current[key] = value;
@ -375,6 +379,9 @@ export function set_attributes(
// @ts-ignore
element[`__${event_name}`] = undefined;
}
} else if (key === 'class') {
var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]);
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
} else if (key === 'autofocus') {

@ -1,120 +1,49 @@
import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {SVGElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_svg_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {MathMLElement} dom
* @param {string} value
* @param {Element} dom
* @param {boolean|number} is_html
* @param {string|null} value
* @param {string} [hash]
* @returns {void}
* @param {Record<string,boolean>} [prev_classes]
* @param {Record<string,boolean>} [next_classes]
* @returns {Record<string,boolean>|undefined}
*/
export function set_mathml_class(dom, value, hash) {
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
var prev = dom.__className;
if (hydrating || prev !== value) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
} else if (is_html) {
dom.className = next_class_name;
} else {
dom.setAttribute('class', next_class_name);
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {HTMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name)
) {
// Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null && !hash) {
dom.removeAttribute('class');
} else {
dom.className = next_class_name;
dom.__className = value;
} else if (next_classes) {
prev_classes = prev_classes ?? {};
for (const key in next_classes) {
const is_present = !!next_classes[key];
if (is_present !== !!prev_classes[key]) {
// TODO : use dom.classList.toggle instead ?
if (is_present) {
dom.classList.add(key);
} else {
dom.classList.remove(key);
}
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @template V
* @param {V} value
* @param {string} [hash]
* @returns {string | V}
*/
function to_class(value, hash) {
return (value == null ? '' : value) + (hash ? ' ' + hash : '');
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {boolean} value
* @returns {void}
*/
export function toggle_class(dom, class_name, value) {
if (value) {
if (dom.classList.contains(class_name)) return;
dom.classList.add(class_name);
} else {
if (!dom.classList.contains(class_name)) return;
dom.classList.remove(class_name);
}
return next_classes;
}

@ -44,7 +44,7 @@ export function init_operations() {
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = '';
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error

@ -39,9 +39,11 @@ export {
set_checked,
set_selected,
set_default_checked,
set_default_value
set_default_value,
CLASS,
STYLE
} from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js';

@ -2,7 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx } from '../shared/attributes.js';
import { attr, clsx, to_class } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@ -198,12 +198,13 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
/**
* @param {Record<string, unknown>} attrs
* @param {Record<string, string>} [classes]
* @param {string|null} css_hash
* @param {Record<string, boolean>} [classes]
* @param {Record<string, string>} [styles]
* @param {number} [flags]
* @returns {string}
*/
export function spread_attributes(attrs, classes, styles, flags = 0) {
export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
attrs.style = attrs.style
? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
@ -213,17 +214,8 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
if (attrs.class) {
attrs.class = clsx(attrs.class);
}
if (classes) {
const classlist = attrs.class ? [attrs.class] : [];
for (const key in classes) {
if (classes[key]) {
classlist.push(key);
}
}
attrs.class = classlist.join(' ');
if (css_hash || classes) {
attrs.class = to_class(attrs.class, css_hash, classes);
}
let attr_str = '';
@ -552,7 +544,7 @@ export function props_id(payload) {
return uid;
}
export { attr, clsx };
export { attr, clsx, to_class };
export { html } from './blocks/html.js';

@ -40,3 +40,46 @@ export function clsx(value) {
return value ?? '';
}
}
/**
* @param {any} clazz
* @param {string|null} [hash]
* @param {Record<string,boolean>} [classes]
* @returns {string|null}
*/
export function to_class(clazz, hash, classes) {
let class_name = clazz == null ? '' : '' + clazz;
if (hash) {
class_name = class_name ? class_name + ' ' + hash : hash;
}
if (classes) {
const white_spaces = ' \t\n\r\f\u00a0\u000b\ufeff';
for (const key in classes) {
if (classes[key]) {
class_name = class_name ? class_name + ' ' + key : key;
} else if (class_name.length) {
const len = key.length;
let start = 0;
while ((start = class_name.indexOf(key, start)) >= 0) {
let stop = start + len;
if (
white_spaces.indexOf(class_name[start - 1] ?? ' ') >= 0 &&
white_spaces.indexOf(class_name[stop] ?? ' ') >= 0
) {
class_name = (
class_name.substring(0, start).trim() +
' ' +
class_name.substring(stop).trim()
).trim();
} else {
start = stop;
}
}
}
}
}
if (class_name === '') {
return null;
}
return class_name;
}

Loading…
Cancel
Save