@ -19,10 +19,14 @@ import { INode } from './interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/ ;
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/ ;
const aria_attributes = 'activedescendant atomic autocomplete busy checked colindex controls current describedby 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 rowindex selected setsize sort valuemax valuemin valuenow valuetext' . split ( ' ' ) ;
const aria_attributes = 'activedescendant atomic autocomplete busy checked colindex controls current describedby 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 rowindex selected setsize sort valuemax valuemin valuenow valuetext' . split (
' '
) ;
const aria_attribute_set = new Set ( aria_attributes ) ;
const aria_attribute_set = new Set ( aria_attributes ) ;
const aria_roles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window' . split ( ' ' ) ;
const aria_roles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window' . split (
' '
) ;
const aria_role_set = new Set ( aria_roles ) ;
const aria_role_set = new Set ( aria_roles ) ;
const a11y_required_attributes = {
const a11y_required_attributes = {
@ -35,13 +39,10 @@ const a11y_required_attributes = {
// iframe-has-title
// iframe-has-title
iframe : [ 'title' ] ,
iframe : [ 'title' ] ,
img : [ 'alt' ] ,
img : [ 'alt' ] ,
object : [ 'title' , 'aria-label' , 'aria-labelledby' ]
object : [ 'title' , 'aria-label' , 'aria-labelledby' ] ,
} ;
} ;
const a11y_distracting_elements = new Set ( [
const a11y_distracting_elements = new Set ( [ 'blink' , 'marquee' ] ) ;
'blink' ,
'marquee'
] ) ;
const a11y_required_content = new Set ( [
const a11y_required_content = new Set ( [
// anchor-has-content
// anchor-has-content
@ -53,35 +54,20 @@ const a11y_required_content = new Set([
'h3' ,
'h3' ,
'h4' ,
'h4' ,
'h5' ,
'h5' ,
'h6'
'h6' ,
] ) ;
] ) ;
const invisible_elements = new Set ( [ 'meta' , 'html' , 'script' , 'style' ] ) ;
const invisible_elements = new Set ( [ 'meta' , 'html' , 'script' , 'style' ] ) ;
const valid_modifiers = new Set ( [
const valid_modifiers = new Set ( [ 'preventDefault' , 'stopPropagation' , 'capture' , 'once' , 'passive' , 'self' ] ) ;
'preventDefault' ,
'stopPropagation' ,
'capture' ,
'once' ,
'passive' ,
'self'
] ) ;
const passive_events = new Set ( [
const passive_events = new Set ( [ 'wheel' , 'touchstart' , 'touchmove' , 'touchend' , 'touchcancel' ] ) ;
'wheel' ,
'touchstart' ,
'touchmove' ,
'touchend' ,
'touchcancel'
] ) ;
function get_namespace ( parent : Element , element : Element , explicit_namespace : string ) {
function get_namespace ( parent : Element , element : Element , explicit_namespace : string ) {
const parent_element = parent . find_nearest ( /^Element/ ) ;
const parent_element = parent . find_nearest ( /^Element/ ) ;
if ( ! parent_element ) {
if ( ! parent_element ) {
return explicit_namespace || ( svg . test ( element . name )
return explicit_namespace || ( svg . test ( element . name ) ? namespaces.svg : null ) ;
? namespaces . svg
: null ) ;
}
}
if ( svg . test ( element . name . toLowerCase ( ) ) ) return namespaces . svg ;
if ( svg . test ( element . name . toLowerCase ( ) ) ) return namespaces . svg ;
@ -115,11 +101,11 @@ export default class Element extends Node {
if ( this . name === 'textarea' ) {
if ( this . name === 'textarea' ) {
if ( info . children . length > 0 ) {
if ( info . children . length > 0 ) {
const value_attribute = info . attributes . find ( node = > node . name === 'value' ) ;
const value_attribute = info . attributes . find ( ( node ) = > node . name === 'value' ) ;
if ( value_attribute ) {
if ( value_attribute ) {
component . error ( value_attribute , {
component . error ( value_attribute , {
code : ` textarea-duplicate-value ` ,
code : ` textarea-duplicate-value ` ,
message : ` A <textarea> can have either a value attribute or (equivalently) child content, but not both `
message : ` A <textarea> can have either a value attribute or (equivalently) child content, but not both ` ,
} ) ;
} ) ;
}
}
@ -128,7 +114,7 @@ export default class Element extends Node {
info . attributes . push ( {
info . attributes . push ( {
type : 'Attribute' ,
type : 'Attribute' ,
name : 'value' ,
name : 'value' ,
value : info.children
value : info.children ,
} ) ;
} ) ;
info . children = [ ] ;
info . children = [ ] ;
@ -139,19 +125,19 @@ export default class Element extends Node {
// Special case — treat these the same way:
// Special case — treat these the same way:
// <option>{foo}</option>
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info . attributes . find ( attribute = > attribute . name === 'value' ) ;
const value_attribute = info . attributes . find ( ( attribute ) = > attribute . name === 'value' ) ;
if ( ! value_attribute ) {
if ( ! value_attribute ) {
info . attributes . push ( {
info . attributes . push ( {
type : 'Attribute' ,
type : 'Attribute' ,
name : 'value' ,
name : 'value' ,
value : info.children ,
value : info.children ,
synthetic : true
synthetic : true ,
} ) ;
} ) ;
}
}
}
}
const has_let = info . attributes . some ( node = > node . type === 'Let' ) ;
const has_let = info . attributes . some ( ( node ) = > node . type === 'Let' ) ;
if ( has_let ) {
if ( has_let ) {
scope = scope . child ( ) ;
scope = scope . child ( ) ;
}
}
@ -160,7 +146,7 @@ export default class Element extends Node {
const order = [ 'Binding' ] ; // everything else is -1
const order = [ 'Binding' ] ; // everything else is -1
info . attributes . sort ( ( a , b ) = > order . indexOf ( a . type ) - order . indexOf ( b . type ) ) ;
info . attributes . sort ( ( a , b ) = > order . indexOf ( a . type ) - order . indexOf ( b . type ) ) ;
info . attributes . forEach ( node = > {
info . attributes . forEach ( ( node ) = > {
switch ( node . type ) {
switch ( node . type ) {
case 'Action' :
case 'Action' :
this . actions . push ( new Action ( component , this , scope , node ) ) ;
this . actions . push ( new Action ( component , this , scope , node ) ) ;
@ -191,14 +177,13 @@ export default class Element extends Node {
this . lets . push ( l ) ;
this . lets . push ( l ) ;
const dependencies = new Set ( [ l . name . name ] ) ;
const dependencies = new Set ( [ l . name . name ] ) ;
l . names . forEach ( name = > {
l . names . forEach ( ( name ) = > {
scope . add ( name , dependencies , this ) ;
scope . add ( name , dependencies , this ) ;
} ) ;
} ) ;
break ;
break ;
}
}
case 'Transition' :
case 'Transition' : {
{
const transition = new Transition ( component , this , scope , node ) ;
const transition = new Transition ( component , this , scope , node ) ;
if ( node . intro ) this . intro = transition ;
if ( node . intro ) this . intro = transition ;
if ( node . outro ) this . outro = transition ;
if ( node . outro ) this . outro = transition ;
@ -227,7 +212,7 @@ export default class Element extends Node {
// no-distracting-elements
// no-distracting-elements
this . component . warn ( this , {
this . component . warn ( this , {
code : ` a11y-distracting-elements ` ,
code : ` a11y-distracting-elements ` ,
message : ` A11y: Avoid < ${ this . name } > elements `
message : ` A11y: Avoid < ${ this . name } > elements ` ,
} ) ;
} ) ;
}
}
@ -249,24 +234,24 @@ export default class Element extends Node {
if ( ! is_figure_parent ) {
if ( ! is_figure_parent ) {
this . component . warn ( this , {
this . component . warn ( this , {
code : ` a11y-structure ` ,
code : ` a11y-structure ` ,
message : ` A11y: <figcaption> must be an immediate child of <figure> `
message : ` A11y: <figcaption> must be an immediate child of <figure> ` ,
} ) ;
} ) ;
}
}
}
}
if ( this . name === 'figure' ) {
if ( this . name === 'figure' ) {
const children = this . children . filter ( node = > {
const children = this . children . filter ( ( node ) = > {
if ( node . type === 'Comment' ) return false ;
if ( node . type === 'Comment' ) return false ;
if ( node . type === 'Text' ) return /\S/ . test ( node . data ) ;
if ( node . type === 'Text' ) return /\S/ . test ( node . data ) ;
return true ;
return true ;
} ) ;
} ) ;
const index = children . findIndex ( child = > ( child as Element ) . name === 'figcaption' ) ;
const index = children . findIndex ( ( child ) = > ( child as Element ) . name === 'figcaption' ) ;
if ( index !== - 1 && ( index !== 0 && index !== children . length - 1 ) ) {
if ( index !== - 1 && index !== 0 && index !== children . length - 1 ) {
this . component . warn ( children [ index ] , {
this . component . warn ( children [ index ] , {
code : ` a11y-structure ` ,
code : ` a11y-structure ` ,
message : ` A11y: <figcaption> must be first or last child of <figure> `
message : ` A11y: <figcaption> must be first or last child of <figure> ` ,
} ) ;
} ) ;
}
}
}
}
@ -282,7 +267,7 @@ export default class Element extends Node {
const attribute_map = new Map ( ) ;
const attribute_map = new Map ( ) ;
this . attributes . forEach ( attribute = > {
this . attributes . forEach ( ( attribute ) = > {
if ( attribute . is_spread ) return ;
if ( attribute . is_spread ) return ;
const name = attribute . name . toLowerCase ( ) ;
const name = attribute . name . toLowerCase ( ) ;
@ -293,7 +278,7 @@ export default class Element extends Node {
// aria-unsupported-elements
// aria-unsupported-elements
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-aria-attributes ` ,
code : ` a11y-aria-attributes ` ,
message : ` A11y: < ${ this . name } > should not have aria-* attributes `
message : ` A11y: < ${ this . name } > should not have aria-* attributes ` ,
} ) ;
} ) ;
}
}
@ -305,14 +290,14 @@ export default class Element extends Node {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-unknown-aria-attribute ` ,
code : ` a11y-unknown-aria-attribute ` ,
message
message ,
} ) ;
} ) ;
}
}
if ( name === 'aria-hidden' && /^h[1-6]$/ . test ( this . name ) ) {
if ( name === 'aria-hidden' && /^h[1-6]$/ . test ( this . name ) ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-hidden ` ,
code : ` a11y-hidden ` ,
message : ` A11y: < ${ this . name } > element should not be hidden `
message : ` A11y: < ${ this . name } > element should not be hidden ` ,
} ) ;
} ) ;
}
}
}
}
@ -323,7 +308,7 @@ export default class Element extends Node {
// aria-unsupported-elements
// aria-unsupported-elements
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-misplaced-role ` ,
code : ` a11y-misplaced-role ` ,
message : ` A11y: < ${ this . name } > should not have role attribute `
message : ` A11y: < ${ this . name } > should not have role attribute ` ,
} ) ;
} ) ;
}
}
@ -337,7 +322,7 @@ export default class Element extends Node {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-unknown-role ` ,
code : ` a11y-unknown-role ` ,
message
message ,
} ) ;
} ) ;
}
}
}
}
@ -346,7 +331,7 @@ export default class Element extends Node {
if ( name === 'accesskey' ) {
if ( name === 'accesskey' ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-accesskey ` ,
code : ` a11y-accesskey ` ,
message : ` A11y: Avoid using accesskey `
message : ` A11y: Avoid using accesskey ` ,
} ) ;
} ) ;
}
}
@ -354,7 +339,7 @@ export default class Element extends Node {
if ( name === 'autofocus' ) {
if ( name === 'autofocus' ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-autofocus ` ,
code : ` a11y-autofocus ` ,
message : ` A11y: Avoid using autofocus `
message : ` A11y: Avoid using autofocus ` ,
} ) ;
} ) ;
}
}
@ -362,7 +347,7 @@ export default class Element extends Node {
if ( name === 'scope' && this . name !== 'th' ) {
if ( name === 'scope' && this . name !== 'th' ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-misplaced-scope ` ,
code : ` a11y-misplaced-scope ` ,
message : ` A11y: The scope attribute should only be used with <th> elements `
message : ` A11y: The scope attribute should only be used with <th> elements ` ,
} ) ;
} ) ;
}
}
@ -373,12 +358,11 @@ export default class Element extends Node {
if ( ! isNaN ( value ) && + value > 0 ) {
if ( ! isNaN ( value ) && + value > 0 ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-positive-tabindex ` ,
code : ` a11y-positive-tabindex ` ,
message : ` A11y: avoid tabindex values above zero `
message : ` A11y: avoid tabindex values above zero ` ,
} ) ;
} ) ;
}
}
}
}
if ( /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/ . test ( name ) ) {
if ( /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/ . test ( name ) ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` illegal-attribute ` ,
code : ` illegal-attribute ` ,
@ -390,14 +374,14 @@ export default class Element extends Node {
if ( ! attribute . is_static ) {
if ( ! attribute . is_static ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` invalid-slot-attribute ` ,
code : ` invalid-slot-attribute ` ,
message : ` slot attribute cannot have a dynamic value `
message : ` slot attribute cannot have a dynamic value ` ,
} ) ;
} ) ;
}
}
if ( component . slot_outlets . has ( name ) ) {
if ( component . slot_outlets . has ( name ) ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` duplicate-slot-attribute ` ,
code : ` duplicate-slot-attribute ` ,
message : ` Duplicate ' ${ name } ' slot `
message : ` Duplicate ' ${ name } ' slot ` ,
} ) ;
} ) ;
component . slot_outlets . add ( name ) ;
component . slot_outlets . add ( name ) ;
@ -414,7 +398,7 @@ export default class Element extends Node {
if ( name === 'is' ) {
if ( name === 'is' ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : 'avoid-is' ,
code : 'avoid-is' ,
message : ` The 'is' attribute is not supported cross-browser and should be avoided `
message : ` The 'is' attribute is not supported cross-browser and should be avoided ` ,
} ) ;
} ) ;
}
}
@ -431,21 +415,19 @@ export default class Element extends Node {
if ( value === '' || value === '#' ) {
if ( value === '' || value === '#' ) {
component . warn ( attribute , {
component . warn ( attribute , {
code : ` a11y-invalid-attribute ` ,
code : ` a11y-invalid-attribute ` ,
message : ` A11y: ' ${ value } ' is not a valid ${ attribute . name } attribute `
message : ` A11y: ' ${ value } ' is not a valid ${ attribute . name } attribute ` ,
} ) ;
} ) ;
}
}
} else {
} else {
component . warn ( this , {
component . warn ( this , {
code : ` a11y-missing-attribute ` ,
code : ` a11y-missing-attribute ` ,
message : ` A11y: <a> element should have an href attribute `
message : ` A11y: <a> element should have an href attribute ` ,
} ) ;
} ) ;
}
}
}
} else {
else {
const required_attributes = a11y_required_attributes [ this . name ] ;
const required_attributes = a11y_required_attributes [ this . name ] ;
if ( required_attributes ) {
if ( required_attributes ) {
const has_attribute = required_attributes . some ( name = > attribute_map . has ( name ) ) ;
const has_attribute = required_attributes . some ( ( name ) = > attribute_map . has ( name ) ) ;
if ( ! has_attribute ) {
if ( ! has_attribute ) {
should_have_attribute ( this , required_attributes ) ;
should_have_attribute ( this , required_attributes ) ;
@ -456,7 +438,7 @@ export default class Element extends Node {
const type = attribute_map . get ( 'type' ) ;
const type = attribute_map . get ( 'type' ) ;
if ( type && type . get_static_value ( ) === 'image' ) {
if ( type && type . get_static_value ( ) === 'image' ) {
const required_attributes = [ 'alt' , 'aria-label' , 'aria-labelledby' ] ;
const required_attributes = [ 'alt' , 'aria-label' , 'aria-labelledby' ] ;
const has_attribute = required_attributes . some ( name = > attribute_map . has ( name ) ) ;
const has_attribute = required_attributes . some ( ( name ) = > attribute_map . has ( name ) ) ;
if ( ! has_attribute ) {
if ( ! has_attribute ) {
should_have_attribute ( this , required_attributes , 'input type="image"' ) ;
should_have_attribute ( this , required_attributes , 'input type="image"' ) ;
@ -470,16 +452,14 @@ export default class Element extends Node {
const { component } = this ;
const { component } = this ;
const check_type_attribute = ( ) = > {
const check_type_attribute = ( ) = > {
const attribute = this . attributes . find (
const attribute = this . attributes . find ( ( attribute : Attribute ) = > attribute . name === 'type' ) ;
( attribute : Attribute ) = > attribute . name === 'type'
) ;
if ( ! attribute ) return null ;
if ( ! attribute ) return null ;
if ( ! attribute . is_static ) {
if ( ! attribute . is_static ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` invalid-type ` ,
code : ` invalid-type ` ,
message : ` 'type' attribute cannot be dynamic if input uses two-way binding `
message : ` 'type' attribute cannot be dynamic if input uses two-way binding ` ,
} ) ;
} ) ;
}
}
@ -488,37 +468,31 @@ export default class Element extends Node {
if ( value === true ) {
if ( value === true ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` missing-type ` ,
code : ` missing-type ` ,
message : ` 'type' attribute must be specified `
message : ` 'type' attribute must be specified ` ,
} ) ;
} ) ;
}
}
return value ;
return value ;
} ;
} ;
this . bindings . forEach ( binding = > {
this . bindings . forEach ( ( binding ) = > {
const { name } = binding ;
const { name } = binding ;
if ( name === 'value' ) {
if ( name === 'value' ) {
if (
if ( this . name !== 'input' && this . name !== 'textarea' && this . name !== 'select' ) {
this . name !== 'input' &&
this . name !== 'textarea' &&
this . name !== 'select'
) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` 'value' is not a valid binding on < ${ this . name } > elements `
message : ` 'value' is not a valid binding on < ${ this . name } > elements ` ,
} ) ;
} ) ;
}
}
if ( this . name === 'select' ) {
if ( this . name === 'select' ) {
const attribute = this . attributes . find (
const attribute = this . attributes . find ( ( attribute : Attribute ) = > attribute . name === 'multiple' ) ;
( attribute : Attribute ) = > attribute . name === 'multiple'
) ;
if ( attribute && ! attribute . is_static ) {
if ( attribute && ! attribute . is_static ) {
component . error ( attribute , {
component . error ( attribute , {
code : ` dynamic-multiple-attribute ` ,
code : ` dynamic-multiple-attribute ` ,
message : ` 'multiple' attribute cannot be dynamic if select uses two-way binding `
message : ` 'multiple' attribute cannot be dynamic if select uses two-way binding ` ,
} ) ;
} ) ;
}
}
} else {
} else {
@ -528,7 +502,7 @@ export default class Element extends Node {
if ( this . name !== 'input' ) {
if ( this . name !== 'input' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` ' ${ name } ' is not a valid binding on < ${ this . name } > elements `
message : ` ' ${ name } ' is not a valid binding on < ${ this . name } > elements ` ,
} ) ;
} ) ;
}
}
@ -543,7 +517,7 @@ export default class Element extends Node {
if ( this . name !== 'input' ) {
if ( this . name !== 'input' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` 'group' is not a valid binding on < ${ this . name } > elements `
message : ` 'group' is not a valid binding on < ${ this . name } > elements ` ,
} ) ;
} ) ;
}
}
@ -552,14 +526,14 @@ export default class Element extends Node {
if ( type !== 'checkbox' && type !== 'radio' ) {
if ( type !== 'checkbox' && type !== 'radio' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` 'group' binding can only be used with <input type="checkbox"> or <input type="radio"> `
message : ` 'group' binding can only be used with <input type="checkbox"> or <input type="radio"> ` ,
} ) ;
} ) ;
}
}
} else if ( name === 'files' ) {
} else if ( name === 'files' ) {
if ( this . name !== 'input' ) {
if ( this . name !== 'input' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` 'files' is not a valid binding on < ${ this . name } > elements `
message : ` 'files' is not a valid binding on < ${ this . name } > elements ` ,
} ) ;
} ) ;
}
}
@ -568,15 +542,14 @@ export default class Element extends Node {
if ( type !== 'file' ) {
if ( type !== 'file' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` 'files' binding can only be used with <input type="file"> `
message : ` 'files' binding can only be used with <input type="file"> ` ,
} ) ;
} ) ;
}
}
} else if ( name === 'open' ) {
} else if ( name === 'open' ) {
if ( this . name !== 'details' ) {
if ( this . name !== 'details' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` ' ${ name } ' binding can only be used with <details> `
message : ` ' ${ name } ' binding can only be used with <details> ` ,
} ) ;
} ) ;
}
}
} else if (
} else if (
@ -594,59 +567,54 @@ export default class Element extends Node {
if ( this . name !== 'audio' && this . name !== 'video' ) {
if ( this . name !== 'audio' && this . name !== 'video' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` ' ${ name } ' binding can only be used with <audio> or <video> `
message : ` ' ${ name } ' binding can only be used with <audio> or <video> ` ,
} ) ;
} ) ;
}
}
} else if (
} else if ( name === 'videoHeight' || name === 'videoWidth' ) {
name === 'videoHeight' ||
name === 'videoWidth'
) {
if ( this . name !== 'video' ) {
if ( this . name !== 'video' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` ' ${ name } ' binding can only be used with <video> `
message : ` ' ${ name } ' binding can only be used with <video> ` ,
} ) ;
} ) ;
}
}
} else if ( dimensions . test ( name ) ) {
} else if ( dimensions . test ( name ) ) {
if ( this . name === 'svg' && ( name === 'offsetWidth' || name === 'offsetHeight' ) ) {
if ( this . name === 'svg' && ( name === 'offsetWidth' || name === 'offsetHeight' ) ) {
component . error ( binding , {
component . error ( binding , {
code : 'invalid-binding' ,
code : 'invalid-binding' ,
message : ` ' ${ binding . name } ' is not a valid binding on <svg>. Use ' ${ name . replace ( 'offset' , 'client' ) } ' instead `
message : ` ' ${ binding . name } ' is not a valid binding on <svg>. Use ' ${ name . replace (
'offset' ,
'client'
) } ' instead ` ,
} ) ;
} ) ;
} else if ( svg . test ( this . name ) ) {
} else if ( svg . test ( this . name ) ) {
component . error ( binding , {
component . error ( binding , {
code : 'invalid-binding' ,
code : 'invalid-binding' ,
message : ` ' ${ binding . name } ' is not a valid binding on SVG elements `
message : ` ' ${ binding . name } ' is not a valid binding on SVG elements ` ,
} ) ;
} ) ;
} else if ( is_void ( this . name ) ) {
} else if ( is_void ( this . name ) ) {
component . error ( binding , {
component . error ( binding , {
code : 'invalid-binding' ,
code : 'invalid-binding' ,
message : ` ' ${ binding . name } ' is not a valid binding on void elements like < ${ this . name } >. Use a wrapper element instead `
message : ` ' ${ binding . name } ' is not a valid binding on void elements like < ${ this . name } >. Use a wrapper element instead ` ,
} ) ;
} ) ;
}
}
} else if (
} else if ( name === 'textContent' || name === 'innerHTML' ) {
name === 'textContent' ||
const contenteditable = this . attributes . find ( ( attribute : Attribute ) = > attribute . name === 'contenteditable' ) ;
name === 'innerHTML'
) {
const contenteditable = this . attributes . find (
( attribute : Attribute ) = > attribute . name === 'contenteditable'
) ;
if ( ! contenteditable ) {
if ( ! contenteditable ) {
component . error ( binding , {
component . error ( binding , {
code : ` missing-contenteditable-attribute ` ,
code : ` missing-contenteditable-attribute ` ,
message : ` 'contenteditable' attribute is required for textContent and innerHTML two-way bindings `
message : ` 'contenteditable' attribute is required for textContent and innerHTML two-way bindings ` ,
} ) ;
} ) ;
} else if ( contenteditable && ! contenteditable . is_static ) {
} else if ( contenteditable && ! contenteditable . is_static ) {
component . error ( contenteditable , {
component . error ( contenteditable , {
code : ` dynamic-contenteditable-attribute ` ,
code : ` dynamic-contenteditable-attribute ` ,
message : ` 'contenteditable' attribute cannot be dynamic if element uses two-way binding `
message : ` 'contenteditable' attribute cannot be dynamic if element uses two-way binding ` ,
} ) ;
} ) ;
}
}
} else if ( name !== 'this' ) {
} else if ( name !== 'this' ) {
component . error ( binding , {
component . error ( binding , {
code : ` invalid-binding ` ,
code : ` invalid-binding ` ,
message : ` ' ${ binding . name } ' is not a valid binding `
message : ` ' ${ binding . name } ' is not a valid binding ` ,
} ) ;
} ) ;
}
}
} ) ;
} ) ;
@ -658,7 +626,7 @@ export default class Element extends Node {
if ( this . children . length === 0 ) {
if ( this . children . length === 0 ) {
this . component . warn ( this , {
this . component . warn ( this , {
code : ` a11y-missing-content ` ,
code : ` a11y-missing-content ` ,
message : ` A11y: < ${ this . name } > element should have child content `
message : ` A11y: < ${ this . name } > element should have child content ` ,
} ) ;
} ) ;
}
}
}
}
@ -666,19 +634,19 @@ export default class Element extends Node {
validate_event_handlers() {
validate_event_handlers() {
const { component } = this ;
const { component } = this ;
this . handlers . forEach ( handler = > {
this . handlers . forEach ( ( handler ) = > {
if ( handler . modifiers . has ( 'passive' ) && handler . modifiers . has ( 'preventDefault' ) ) {
if ( handler . modifiers . has ( 'passive' ) && handler . modifiers . has ( 'preventDefault' ) ) {
component . error ( handler , {
component . error ( handler , {
code : 'invalid-event-modifier' ,
code : 'invalid-event-modifier' ,
message : ` The 'passive' and 'preventDefault' modifiers cannot be used together `
message : ` The 'passive' and 'preventDefault' modifiers cannot be used together ` ,
} ) ;
} ) ;
}
}
handler . modifiers . forEach ( modifier = > {
handler . modifiers . forEach ( ( modifier ) = > {
if ( ! valid_modifiers . has ( modifier ) ) {
if ( ! valid_modifiers . has ( modifier ) ) {
component . error ( handler , {
component . error ( handler , {
code : 'invalid-event-modifier' ,
code : 'invalid-event-modifier' ,
message : ` Valid event modifiers are ${ list ( Array . from ( valid_modifiers ) ) } `
message : ` Valid event modifiers are ${ list ( Array . from ( valid_modifiers ) ) } ` ,
} ) ;
} ) ;
}
}
@ -687,25 +655,16 @@ export default class Element extends Node {
if ( handler . can_make_passive ) {
if ( handler . can_make_passive ) {
component . warn ( handler , {
component . warn ( handler , {
code : 'redundant-event-modifier' ,
code : 'redundant-event-modifier' ,
message : ` Touch event handlers that don't use the 'event' object are passive by default `
message : ` Touch event handlers that don't use the 'event' object are passive by default ` ,
} ) ;
} ) ;
}
}
} else {
} else {
component . warn ( handler , {
component . warn ( handler , {
code : 'redundant-event-modifier' ,
code : 'redundant-event-modifier' ,
message : ` The passive modifier only works with wheel and touch events `
message : ` The passive modifier only works with wheel and touch events ` ,
} ) ;
} ) ;
}
}
}
}
if ( component . compile_options . legacy && ( modifier === 'once' || modifier === 'passive' ) ) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
component . error ( handler , {
code : 'invalid-event-modifier' ,
message : ` The ' ${ modifier } ' modifier cannot be used in legacy mode `
} ) ;
}
} ) ;
} ) ;
if ( passive_events . has ( handler . name ) && handler . can_make_passive && ! handler . modifiers . has ( 'preventDefault' ) ) {
if ( passive_events . has ( handler . name ) && handler . can_make_passive && ! handler . modifiers . has ( 'preventDefault' ) ) {
@ -720,14 +679,14 @@ export default class Element extends Node {
}
}
add_css_class() {
add_css_class() {
if ( this . attributes . some ( attr = > attr . is_spread ) ) {
if ( this . attributes . some ( ( attr ) = > attr . is_spread ) ) {
this . needs_manual_style_scoping = true ;
this . needs_manual_style_scoping = true ;
return ;
return ;
}
}
const { id } = this . component . stylesheet ;
const { id } = this . component . stylesheet ;
const class_attribute = this . attributes . find ( a = > a . name === 'class' ) ;
const class_attribute = this . attributes . find ( ( a ) = > a . name === 'class' ) ;
if ( class_attribute && ! class_attribute . is_true ) {
if ( class_attribute && ! class_attribute . is_true ) {
if ( class_attribute . chunks . length === 1 && class_attribute . chunks [ 0 ] . type === 'Text' ) {
if ( class_attribute . chunks . length === 1 && class_attribute . chunks [ 0 ] . type === 'Text' ) {
@ -737,7 +696,7 @@ export default class Element extends Node {
new Text ( this . component , this , this . scope , {
new Text ( this . component , this , this . scope , {
type : 'Text' ,
type : 'Text' ,
data : ` ${ id } ` ,
data : ` ${ id } ` ,
synthetic : true
synthetic : true ,
} )
} )
) ;
) ;
}
}
@ -746,26 +705,23 @@ export default class Element extends Node {
new Attribute ( this . component , this , this . scope , {
new Attribute ( this . component , this , this . scope , {
type : 'Attribute' ,
type : 'Attribute' ,
name : 'class' ,
name : 'class' ,
value : [ { type : 'Text' , data : id , synthetic : true } ]
value : [ { type : 'Text' , data : id , synthetic : true } ] ,
} )
} )
) ;
) ;
}
}
}
}
}
}
function should_have_attribute (
function should_have_attribute ( node , attributes : string [ ] , name = node . name ) {
node ,
attributes : string [ ] ,
name = node . name
) {
const article = /^[aeiou]/ . test ( attributes [ 0 ] ) ? 'an' : 'a' ;
const article = /^[aeiou]/ . test ( attributes [ 0 ] ) ? 'an' : 'a' ;
const sequence = attributes . length > 1 ?
const sequence =
attributes . slice ( 0 , - 1 ) . join ( ', ' ) + ` or ${ attributes [ attributes . length - 1 ] } ` :
attributes . length > 1
attributes [ 0 ] ;
? attributes . slice ( 0 , - 1 ) . join ( ', ' ) + ` or ${ attributes [ attributes . length - 1 ] } `
: attributes [ 0 ] ;
node . component . warn ( node , {
node . component . warn ( node , {
code : ` a11y-missing-attribute ` ,
code : ` a11y-missing-attribute ` ,
message : ` A11y: < ${ name } > element should have ${ article } ${ sequence } attribute `
message : ` A11y: < ${ name } > element should have ${ article } ${ sequence } attribute ` ,
} ) ;
} ) ;
}
}