11import { DATASTAR_FETCH_EVENT , DSP , DSS } from '@engine/consts'
2- import { snake } from '@utils/text'
32import { root } from '@engine/signals'
43import type {
5- ActionPlugin ,
64 ActionContext ,
5+ ActionPlugin ,
76 AttributeContext ,
87 AttributePlugin ,
98 DatastarFetchEvent ,
109 HTMLOrSVG ,
10+ Modifiers ,
1111 Requirement ,
1212 WatcherPlugin ,
1313} from '@engine/types'
1414import { isHTMLOrSVG } from '@utils/dom'
15- import { aliasify } from '@utils/text'
15+ import { aliasify , snake } from '@utils/text'
1616
1717const url = 'https://data-star.dev/errors'
1818
@@ -50,11 +50,12 @@ export const actions: Record<
5050 } ,
5151)
5252
53- // Map of cleanups keyed by element and attribute name
54- const removals = new Map < HTMLOrSVG , Map < string , ( ) => void > > ( )
53+ // Map of cleanups keyed by element, attribute name, and cleanup name
54+ const removals = new Map < HTMLOrSVG , Map < string , Map < string , ( ) => void > > > ( )
5555
5656const queuedAttributes : AttributePlugin [ ] = [ ]
5757const queuedAttributeNames = new Set < string > ( )
58+ const observedRoots = new WeakSet < Node > ( )
5859export const attribute = < R extends Requirement , B extends boolean > (
5960 plugin : AttributePlugin < R , B > ,
6061) : void => {
@@ -103,13 +104,13 @@ export const watcher = (plugin: WatcherPlugin): void => {
103104
104105const cleanupEls = ( els : Iterable < HTMLOrSVG > ) : void => {
105106 for ( const el of els ) {
106- const cleanups = removals . get ( el )
107- // If removals has el, delete it and run all cleanup functions
108- if ( removals . delete ( el ) ) {
109- for ( const cleanup of cleanups ! . values ( ) ) {
110- cleanup ( )
107+ const elCleanups = removals . get ( el )
108+ if ( elCleanups && removals . delete ( el ) ) {
109+ for ( const attrCleanups of elCleanups . values ( ) ) {
110+ for ( const cleanup of attrCleanups . values ( ) ) {
111+ cleanup ( )
112+ }
111113 }
112- cleanups ! . clear ( )
113114 }
114115 }
115116}
@@ -166,10 +167,15 @@ const observe = (mutations: MutationRecord[]) => {
166167 const key = attributeName ! . slice ( 5 )
167168 const value = target . getAttribute ( attributeName ! )
168169 if ( value === null ) {
169- const cleanups = removals . get ( target )
170- if ( cleanups ) {
171- cleanups . get ( key ) ?.( )
172- cleanups . delete ( key )
170+ const elCleanups = removals . get ( target )
171+ if ( elCleanups ) {
172+ const attrCleanups = elCleanups . get ( key )
173+ if ( attrCleanups ) {
174+ for ( const cleanup of attrCleanups . values ( ) ) {
175+ cleanup ( )
176+ }
177+ elCleanups . delete ( key )
178+ }
173179 }
174180 } else {
175181 applyAttributePlugin ( target , key , value )
@@ -181,19 +187,45 @@ const observe = (mutations: MutationRecord[]) => {
181187// TODO: mutation observer per root so applying to web component doesnt overwrite main observer
182188const mutationObserver = new MutationObserver ( observe )
183189
190+ export const parseAttributeKey = (
191+ rawKey : string ,
192+ ) : {
193+ pluginName : string
194+ key : string | undefined
195+ mods : Modifiers
196+ } => {
197+ const [ namePart , ...rawModifiers ] = rawKey . split ( '__' )
198+ const [ pluginName , key ] = namePart . split ( / : ( .+ ) / )
199+ const mods : Modifiers = new Map ( )
200+
201+ for ( const rawMod of rawModifiers ) {
202+ const [ label , ...mod ] = rawMod . split ( '.' )
203+ mods . set ( label , new Set ( mod ) )
204+ }
205+
206+ return { pluginName, key, mods }
207+ }
208+
209+ export const isDocumentObserverActive = ( ) =>
210+ observedRoots . has ( document . documentElement )
211+
184212export const apply = (
185213 root : HTMLOrSVG | ShadowRoot = document . documentElement ,
214+ observeRoot = true ,
186215) : void => {
187216 if ( isHTMLOrSVG ( root ) ) {
188217 applyEls ( [ root ] , true )
189218 }
190219 applyEls ( root . querySelectorAll < HTMLOrSVG > ( '*' ) , true )
191220
192- mutationObserver . observe ( root , {
193- subtree : true ,
194- childList : true ,
195- attributes : true ,
196- } )
221+ if ( observeRoot ) {
222+ mutationObserver . observe ( root , {
223+ subtree : true ,
224+ childList : true ,
225+ attributes : true ,
226+ } )
227+ observedRoots . add ( root )
228+ }
197229}
198230
199231const applyAttributePlugin = (
@@ -204,21 +236,24 @@ const applyAttributePlugin = (
204236) : void => {
205237 if ( ! ALIAS || attrKey . startsWith ( `${ ALIAS } -` ) ) {
206238 const rawKey = ALIAS ? attrKey . slice ( ALIAS . length + 1 ) : attrKey
207- const [ namePart , ...rawModifiers ] = rawKey . split ( '__' )
208- const [ pluginName , key ] = namePart . split ( / : ( .+ ) / )
239+ const { pluginName, key, mods } = parseAttributeKey ( rawKey )
209240 const plugin = attributePlugins . get ( pluginName )
210241 if ( ( ! onlyNew || queuedAttributeNames . has ( pluginName ) ) && plugin ) {
211242 const ctx = {
212243 el,
213244 rawKey,
214- mods : new Map ( ) ,
245+ mods,
215246 error : error . bind ( 0 , {
216247 plugin : { type : 'attribute' , name : plugin . name } ,
217248 element : { id : el . id , tag : el . tagName } ,
218249 expression : { rawKey, key, value } ,
219250 } ) ,
220251 key,
221252 value,
253+ loadedPluginNames : {
254+ actions : new Set ( actionPlugins . keys ( ) ) ,
255+ attributes : new Set ( attributePlugins . keys ( ) ) ,
256+ } ,
222257 rx : undefined ,
223258 } as AttributeContext
224259
@@ -235,15 +270,19 @@ const applyAttributePlugin = (
235270 : plugin . requirement . value ) ) ||
236271 'allowed'
237272
238- if ( key ) {
273+ const keyProvided = key !== undefined && key !== null && key !== ''
274+ const valueProvided =
275+ value !== undefined && value !== null && value !== ''
276+
277+ if ( keyProvided ) {
239278 if ( keyReq === 'denied' ) {
240279 throw ctx . error ( 'KeyNotAllowed' )
241280 }
242281 } else if ( keyReq === 'must' ) {
243282 throw ctx . error ( 'KeyRequired' )
244283 }
245284
246- if ( value ) {
285+ if ( valueProvided ) {
247286 if ( valueReq === 'denied' ) {
248287 throw ctx . error ( 'ValueNotAllowed' )
249288 }
@@ -252,57 +291,66 @@ const applyAttributePlugin = (
252291 }
253292
254293 if ( keyReq === 'exclusive' || valueReq === 'exclusive' ) {
255- if ( key && value ) {
294+ if ( keyProvided && valueProvided ) {
256295 throw ctx . error ( 'KeyAndValueProvided' )
257296 }
258- if ( ! key && ! value ) {
297+ if ( ! keyProvided && ! valueProvided ) {
259298 throw ctx . error ( 'KeyOrValueRequired' )
260299 }
261300 }
262301
263- if ( value ) {
302+ const cleanups = new Map < string , ( ) => void > ( )
303+ if ( valueProvided ) {
264304 let cachedRx : GenRxFn
265305 ctx . rx = ( ...args : any [ ] ) => {
266306 if ( ! cachedRx ) {
267307 cachedRx = genRx ( value , {
268308 returnsValue : plugin . returnsValue ,
269309 argNames : plugin . argNames ,
310+ cleanups,
270311 } )
271312 }
272313 return cachedRx ( el , ...args )
273314 }
274315 }
275316
276- for ( const rawMod of rawModifiers ) {
277- const [ label , ...mod ] = rawMod . split ( '.' )
278- ctx . mods . set ( label , new Set ( mod ) )
279- }
280-
281317 const cleanup = plugin . apply ( ctx )
282318 if ( cleanup ) {
283- let cleanups = removals . get ( el )
284- if ( cleanups ) {
285- cleanups . get ( rawKey ) ?.( )
286- } else {
287- cleanups = new Map ( )
288- removals . set ( el , cleanups )
319+ cleanups . set ( 'attribute' , cleanup )
320+ }
321+
322+ let elCleanups = removals . get ( el )
323+ if ( elCleanups ) {
324+ const attrCleanups = elCleanups . get ( rawKey )
325+ if ( attrCleanups ) {
326+ for ( const oldCleanup of attrCleanups . values ( ) ) {
327+ oldCleanup ( )
328+ }
289329 }
290- cleanups . set ( rawKey , cleanup )
330+ } else {
331+ elCleanups = new Map ( )
332+ removals . set ( el , elCleanups )
291333 }
334+ elCleanups . set ( rawKey , cleanups )
292335 }
293336 }
294337}
295338
296339type GenRxOptions = {
297340 returnsValue ?: boolean
298341 argNames ?: string [ ]
342+ cleanups ?: Map < string , ( ) => void >
299343}
300344
301345type GenRxFn = < T > ( el : HTMLOrSVG , ...args : any [ ] ) => T
302346
303347const genRx = (
304348 value : string ,
305- { returnsValue = false , argNames = [ ] } : GenRxOptions = { } ,
349+ {
350+ returnsValue = false ,
351+ argNames = [ ] ,
352+ cleanups = new Map ( ) ,
353+ } : GenRxOptions = { } ,
306354) : GenRxFn => {
307355 let expr = ''
308356 if ( returnsValue ) {
@@ -376,13 +424,8 @@ const genRx = (
376424 . split ( '.' )
377425 . reduce ( ( acc : string , part : string ) => `${ acc } ['${ part } ']` , '$' ) ,
378426 )
379- // [$x] -> [$['x']] ($ inside brackets)
380- . replace (
381- / \[ ( \$ [ a - z A - Z _ \d ] \w * ) \] / g,
382- ( _ , varName ) => `[$['${ varName . slice ( 1 ) } ']]` ,
383- )
384427
385- expr = expr . replaceAll ( / @ ( \w + ) \( / g, '__action("$1",evt,' )
428+ expr = expr . replaceAll ( / @ ( [ A - Z a - z _ $ ] [ \w $ ] * ) \( / g, '__action("$1",evt,' )
386429
387430 // Replace any escaped values
388431 for ( const [ k , v ] of escaped ) {
@@ -408,6 +451,7 @@ const genRx = (
408451 el,
409452 evt,
410453 error : err ,
454+ cleanups,
411455 } ,
412456 ...args ,
413457 )
0 commit comments