From a816f027116220d28b088d545d6053863d335e0d Mon Sep 17 00:00:00 2001 From: adiguba Date: Wed, 12 Oct 2022 22:04:55 +0200 Subject: [PATCH] svelte-display implementation --- package-lock.json | 2 +- src/compiler/compile/compiler_errors.ts | 22 ++- src/compiler/compile/nodes/Element.ts | 30 +++- src/compiler/compile/nodes/InlineComponent.ts | 3 + src/compiler/compile/nodes/SvelteDirective.ts | 22 +++ src/compiler/compile/nodes/interfaces.ts | 2 + .../render_dom/wrappers/Element/index.ts | 143 ++++++++++++------ .../compile/render_ssr/handlers/Element.ts | 5 + src/compiler/interfaces.ts | 3 +- src/compiler/parse/state/tag.ts | 12 +- src/runtime/internal/dom.ts | 4 + 11 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 src/compiler/compile/nodes/SvelteDirective.ts diff --git a/package-lock.json b/package-lock.json index dec2c8ef0e..1f8f623dee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "svelte", - "version": "3.49.0", + "version": "3.51.0", "license": "MIT", "devDependencies": { "@ampproject/remapping": "^0.3.0", diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index c1a7d8bc5c..63c0e691f9 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -281,5 +281,25 @@ export default { invalid_component_style_directive: { code: 'invalid-component-style-directive', message: 'Style directives cannot be used on components' - } + }, + invalid_component_svelte_directive: (name) => ({ + code: 'invalid-component-svelte-directive', + message: `svelte:${name} directives cannot be used on components` + }), + duplicate_directive: (directive) => ({ + code: 'duplicate-directive', + message: `An element can only have one '${directive}' directive` + }), + invalid_directive: (directive) => ({ + code: 'invalid-directive', + message: `'${directive}' is not a valid directive` + }), + invalid_modifier: { + code: 'invalid-modifier', + message: 'No modifier allowed on this directive' + }, + directive_conflict: (directive1, directive2) => ({ + code: 'directive-conflict', + message: `Cannot use ${directive1} and ${directive2} on the same element` + }) }; diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 79f2800437..09435f9b06 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,6 +25,7 @@ import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y'; +import SvelteDirective from './SvelteDirective'; const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attribute_set = new Set(aria_attributes); @@ -217,6 +218,7 @@ export default class Element extends Node { intro?: Transition = null; outro?: Transition = null; animation?: Animation = null; + display? : SvelteDirective = null; children: INode[]; namespace: string; needs_manual_style_scoping: boolean; @@ -355,6 +357,22 @@ export default class Element extends Node { this.animation = new Animation(component, this, scope, node); break; + case 'SvelteDirective': + switch (node.name) { + case 'display': + if (this.display) { + component.error(node, + compiler_errors.duplicate_directive('svelte:' + node.name)); + } else { + this.display = new SvelteDirective(component, this, scope, node); + } + break; + default: + component.error(node, + compiler_errors.invalid_directive('svelte:' + node.name)); + } + break; + default: throw new Error(`Not implemented: ${node.type}`); } @@ -385,7 +403,7 @@ export default class Element extends Node { this.validate_bindings(); this.validate_content(); } - + this.validate_display_directive(); } validate_attributes() { @@ -941,6 +959,16 @@ export default class Element extends Node { }); } + validate_display_directive() { + if (!this.display) { + return; + } + if (this.styles.find(d => d.name === 'display')) { + this.component.error(this.display, + compiler_errors.directive_conflict('svelte:display', 'style:display')); + } + } + is_media_node() { return this.name === 'audio' || this.name === 'video'; } diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 8871c5f306..4853f15e0d 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -77,6 +77,9 @@ export default class InlineComponent extends Node { case 'StyleDirective': return component.error(node, compiler_errors.invalid_component_style_directive); + case 'SvelteDirective': + return component.error(node, compiler_errors.invalid_component_svelte_directive(node.name)); + default: throw new Error(`Not implemented: ${node.type}`); } diff --git a/src/compiler/compile/nodes/SvelteDirective.ts b/src/compiler/compile/nodes/SvelteDirective.ts new file mode 100644 index 0000000000..03d4b0199c --- /dev/null +++ b/src/compiler/compile/nodes/SvelteDirective.ts @@ -0,0 +1,22 @@ +import Node from './shared/Node'; +import Expression from './shared/Expression'; +import Component from '../Component'; +import TemplateScope from './shared/TemplateScope'; +import { TemplateNode } from '../../interfaces'; +import Element from './Element'; +import compiler_errors from '../compiler_errors'; + +export default class SvelteDirective extends Node { + type: 'SvelteDirective'; + expression: Expression; + + constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) { + super(component, parent, scope, info); + + this.expression = new Expression(component, this, scope, info.expression); + + if (info.modifiers && info.modifiers.length) { + component.error(info, compiler_errors.invalid_modifier); + } + } +} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index f023cad25c..0b8046aa0d 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -33,6 +33,7 @@ import ThenBlock from './ThenBlock'; import Title from './Title'; import Transition from './Transition'; import Window from './Window'; +import SvelteDisplayDirective from './SvelteDirective'; // note: to write less types each of types in union below should have type defined as literal // https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions @@ -64,6 +65,7 @@ export type INode = Action | Slot | SlotTemplate | StyleDirective +| SvelteDisplayDirective | Tag | Text | ThenBlock diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 4f0b49729c..23fb74041d 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -163,7 +163,7 @@ export default class ElementWrapper extends Wrapper { ) { super(renderer, block, parent, node); - if (node.is_dynamic_element && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { + if ((node.display || node.is_dynamic_element) && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { this.child_dynamic_element_block = block.child({ comment: create_debugging_comment(node, renderer.component), name: renderer.component.get_unique_name('create_dynamic_element'), @@ -273,16 +273,16 @@ export default class ElementWrapper extends Wrapper { (x`#nodes` as unknown) as Identifier ); - const previous_tag = block.get_unique_name('previous_tag'); + const tag = this.node.tag_expr.manipulate(block); - block.add_variable(previous_tag, tag); - - block.chunks.init.push(b` - ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} - ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} - let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx); - `); + if (this.renderer.options.dev) { + block.chunks.init.push(b` + @validate_dynamic_element(${tag}); + @validate_void_dynamic_element(${tag}); + `); + } + block.chunks.create.push(b` if (${this.var}) ${this.var}.c(); `); @@ -297,45 +297,56 @@ export default class ElementWrapper extends Wrapper { if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); `); - const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); - const has_transitions = !!(this.node.intro || this.node.outro); - const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; - - block.chunks.update.push(b` - if (${tag}) { - if (!${previous_tag}) { - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${has_transitions && b`@transition_in(${this.var})`} - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else if (${not_equal}(${previous_tag}, ${tag})) { - ${this.var}.d(1); - ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} - ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else { - ${this.var}.p(#ctx, #dirty); - } - } else if (${previous_tag}) { - ${ - has_transitions - ? b` - @group_outros(); - @transition_out(${this.var}, 1, 1, () => { + if (this.node.display && tag.type === 'Literal') { + block.chunks.init.push(b`const ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);`); + block.chunks.update.push(b`${this.var}.p(#ctx, #dirty);`); + } else { + const previous_tag = block.get_unique_name('previous_tag'); + block.add_variable(previous_tag, tag); + block.chunks.init.push(b`let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx);`); + + const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); + const has_transitions = !!(this.node.intro || this.node.outro); + const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; + + block.chunks.update.push(b` + if (${tag}) { + if (!${previous_tag}) { + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${this.var}.c(); + ${has_transitions && b`@transition_in(${this.var})`} + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else if (${not_equal}(${previous_tag}, ${tag})) { + ${this.var}.d(1); + ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} + ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${this.var}.c(); + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else { + ${this.var}.p(#ctx, #dirty); + } + } else if (${previous_tag}) { + ${ + has_transitions + ? b` + @group_outros(); + @transition_out(${this.var}, 1, 1, () => { + ${this.var} = null; + }); + @check_outros(); + ` + : b` + ${this.var}.d(1); ${this.var} = null; - }); - @check_outros(); - ` - : b` - ${this.var}.d(1); - ${this.var} = null; - ` + ` + } } - } - ${previous_tag} = ${tag}; - `); + ${previous_tag} = ${tag}; + `); + } + + if (this.child_dynamic_element_block.has_intros) { block.chunks.intro.push(b`@transition_in(${this.var});`); @@ -480,6 +491,7 @@ export default class ElementWrapper extends Wrapper { this.add_animation(block); this.add_classes(block); this.add_styles(block); + this.add_display(block); this.add_manual_style_scoping(block); if (nodes && this.renderer.options.hydratable && !this.void) { @@ -1129,6 +1141,45 @@ export default class ElementWrapper extends Wrapper { }); } + add_display(block: Block) { + const display = this.node.display; + if (display === null) { + return; + } + + const snippet = display.expression.manipulate(block); + const dependencies = display.expression.dynamic_dependencies(); + const has_dependancies = dependencies.length > 0; + + const update_display = b`@set_display(${this.var}, ${snippet})`; + block.chunks.hydrate.push(update_display); + + if (has_dependancies) { + const update_current = (this.node.intro || this.node.outro) + ? x`#current = false` + : null; + + const dirty = block.renderer.dirty(Array.from(dependencies)); + block.chunks.update.push(b` + if (${dirty}) { + if (${snippet}) { + ${update_current} + ${update_display} + @transition_in(this, 1); + } else { + @group_outros(); + @transition_out(this, 1, 0, () => { + ${update_display} + }); + @check_outros(); + } + } + `); + } + } + + + add_manual_style_scoping(block) { if (this.node.needs_manual_style_scoping) { const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`; diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 2af0343b2a..c5821aa8db 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -48,6 +48,11 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio return p`"${name}": ${expression}`; }); + if (node.display) { + const snippet = node.display.expression.node; + style_expression_list.push(p`"display": (${snippet} ? "none !important" : null)`); + } + const style_expression = style_expression_list.length > 0 && x`{ ${style_expression_list} }`; diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 248175da59..73eb5fc397 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -48,7 +48,8 @@ export type DirectiveType = 'Action' | 'EventHandler' | 'Let' | 'Ref' -| 'Transition'; +| 'Transition' +| 'SvelteDirective'; interface BaseDirective extends BaseNode { type: DirectiveType; diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 4be47f25b0..915770261b 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -286,6 +286,15 @@ function read_tag_name(parser: Parser) { return name; } +function use_name_as_expression(type:string, name:string):boolean { + if (type === 'Binding' || type === 'Class') { + return true; + } else if (type === 'SvelteDirective') { + return name === 'display'; + } + return false; +} + function read_attribute(parser: Parser, unique_names: Set) { const start = parser.index; @@ -419,7 +428,7 @@ function read_attribute(parser: Parser, unique_names: Set) { } // Directive name is expression, e.g.

- if (!directive.expression && (type === 'Binding' || type === 'Class')) { + if (!directive.expression && use_name_as_expression(type, directive_name)) { directive.expression = { start: directive.start + colon_index + 1, end: directive.end, @@ -452,6 +461,7 @@ function get_directive_type(name: string): DirectiveType { if (name === 'let') return 'Let'; if (name === 'ref') return 'Ref'; if (name === 'in' || name === 'out' || name === 'transition') return 'Transition'; + if (name === 'svelte') return 'SvelteDirective'; } function read_attribute_value(parser: Parser) { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a1c0e1c0aa..4aa30eb250 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -545,6 +545,10 @@ export function set_style(node, key, value, important) { } } +export function set_display(node, value) { + set_style(node, 'display', value ? null : 'none', 1); +} + export function select_option(select, value) { for (let i = 0; i < select.options.length; i += 1) { const option = select.options[i];