import Renderer from '../../Renderer'; import Element from '../../../nodes/Element'; import Wrapper from '../shared/Wrapper'; import Block from '../../Block'; import { is_void } from '../../../../../shared/utils/names'; import FragmentWrapper from '../Fragment'; import { escape_html, string_literal } from '../../../utils/stringify'; import TextWrapper from '../Text'; import fix_attribute_casing from './fix_attribute_casing'; import { b, x, p } from 'code-red'; import { namespaces } from '../../../../utils/namespaces'; import AttributeWrapper from './Attribute'; import StyleAttributeWrapper from './StyleAttribute'; import SpreadAttributeWrapper from './SpreadAttribute'; import { regex_dimensions, regex_starts_with_newline, regex_backslashes, regex_border_box_size, regex_content_box_size, regex_device_pixel_content_box_size, regex_content_rect } from '../../../../utils/patterns'; import Binding from './Binding'; import add_to_set from '../../../utils/add_to_set'; import { add_event_handler } from '../shared/add_event_handlers'; import { add_action } from '../shared/add_actions'; import bind_this from '../shared/bind_this'; import { is_head } from '../shared/is_head'; import { Identifier, ExpressionStatement, CallExpression, Node } from 'estree'; import EventHandler from './EventHandler'; import { extract_names } from 'periscopic'; import Action from '../../../nodes/Action'; import MustacheTagWrapper from '../MustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag'; import is_dynamic from '../shared/is_dynamic'; import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable'; import create_debugging_comment from '../shared/create_debugging_comment'; import { push_array } from '../../../../utils/push_array'; import CommentWrapper from '../Comment'; interface BindingGroup { events: string[]; bindings: Binding[]; } const regex_contains_radio_or_checkbox_or_file = /radio|checkbox|file/; const regex_contains_radio_or_checkbox_or_range_or_file = /radio|checkbox|range|file/; const events = [ { event_names: ['input'], filter: (node: Element, _name: string) => node.name === 'textarea' || (node.name === 'input' && !regex_contains_radio_or_checkbox_or_range_or_file.test( node.get_static_attribute_value('type') as string )) }, { event_names: ['input'], filter: (node: Element, name: string) => is_name_contenteditable(name) && has_contenteditable_attr(node) }, { event_names: ['change'], filter: (node: Element, _name: string) => node.name === 'select' || (node.name === 'input' && regex_contains_radio_or_checkbox_or_file.test( node.get_static_attribute_value('type') as string )) }, { event_names: ['change', 'input'], filter: (node: Element, _name: string) => node.name === 'input' && node.get_static_attribute_value('type') === 'range' }, // resize events { event_names: ['elementresize'], filter: (_node: Element, name: string) => regex_dimensions.test(name) }, { event_names: ['elementresizecontentbox'], filter: (_node: Element, name: string) => regex_content_rect.test(name) ?? regex_content_box_size.test(name) }, { event_names: ['elementresizeborderbox'], filter: (_node: Element, name: string) => regex_border_box_size.test(name) }, { event_names: ['elementresizedevicepixelcontentbox'], filter: (_node: Element, name: string) => regex_device_pixel_content_box_size.test(name) }, // media events { event_names: ['timeupdate'], filter: (node: Element, name: string) => node.is_media_node() && (name === 'currentTime' || name === 'played' || name === 'ended') }, { event_names: ['durationchange'], filter: (node: Element, name: string) => node.is_media_node() && name === 'duration' }, { event_names: ['play', 'pause'], filter: (node: Element, name: string) => node.is_media_node() && name === 'paused' }, { event_names: ['progress'], filter: (node: Element, name: string) => node.is_media_node() && name === 'buffered' }, { event_names: ['loadedmetadata'], filter: (node: Element, name: string) => node.is_media_node() && (name === 'buffered' || name === 'seekable') }, { event_names: ['volumechange'], filter: (node: Element, name: string) => node.is_media_node() && (name === 'volume' || name === 'muted') }, { event_names: ['ratechange'], filter: (node: Element, name: string) => node.is_media_node() && name === 'playbackRate' }, { event_names: ['seeking', 'seeked'], filter: (node: Element, name: string) => node.is_media_node() && name === 'seeking' }, { event_names: ['ended'], filter: (node: Element, name: string) => node.is_media_node() && name === 'ended' }, { event_names: ['resize'], filter: (node: Element, name: string) => node.is_media_node() && (name === 'videoHeight' || name === 'videoWidth') }, { // from https://html.spec.whatwg.org/multipage/media.html#ready-states // and https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource event_names: [ 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied' ], filter: (node: Element, name: string) => node.is_media_node() && name === 'readyState' }, // details event { event_names: ['toggle'], filter: (node: Element, _name: string) => node.name === 'details' }, { event_names: ['load'], filter: (_: Element, name: string) => name === 'naturalHeight' || name === 'naturalWidth' } ]; const CHILD_DYNAMIC_ELEMENT_BLOCK = 'child_dynamic_element'; const regex_invalid_variable_identifier_characters = /[^a-zA-Z0-9_$]/g; const regex_minus_signs = /-/g; export default class ElementWrapper extends Wrapper { node: Element; fragment: FragmentWrapper; attributes: Array; bindings: Binding[]; event_handlers: EventHandler[]; class_dependencies: string[]; dynamic_style_dependencies: Set; has_dynamic_attribute: boolean; select_binding_dependencies?: Set; has_dynamic_value: boolean; dynamic_value_condition: any; var: any; void: boolean; child_dynamic_element_block?: Block = null; child_dynamic_element?: ElementWrapper = null; element_data_name = null; constructor( renderer: Renderer, block: Block, parent: Wrapper, node: Element, strip_whitespace: boolean, next_sibling: Wrapper ) { super(renderer, block, parent, node); this.var = { type: 'Identifier', name: node.name.replace(regex_invalid_variable_identifier_characters, '_') }; this.void = is_void(node.name); this.class_dependencies = []; if (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'), type: CHILD_DYNAMIC_ELEMENT_BLOCK }); renderer.blocks.push(this.child_dynamic_element_block); this.child_dynamic_element = new ElementWrapper( renderer, this.child_dynamic_element_block, parent, node, strip_whitespace, next_sibling ); // the original svelte:element is never used for rendering, because // it gets assigned a child_dynamic_element which is used in all rendering logic. // so doing all of this on the original svelte:element will just cause double // code, because it will be done again on the child_dynamic_element. return; } this.dynamic_style_dependencies = new Set(); if (this.node.children.length) { this.node.lets.forEach((l) => { extract_names(l.value || l.name).forEach((name) => { renderer.add_to_context(name, true); }); }); } this.attributes = this.node.attributes.map((attribute) => { if (attribute.name === 'style') { return new StyleAttributeWrapper(this, block, attribute); } if (attribute.type === 'Spread') { return new SpreadAttributeWrapper(this, block, attribute); } return new AttributeWrapper(this, block, attribute); }); this.has_dynamic_attribute = !!this.attributes.find( (attr) => attr.node.get_dependencies().length > 0 ); // ordinarily, there'll only be one... but we need to handle // the rare case where an element can have multiple bindings, // e.g.