@dooboostore/simple-web-component 1.0.47 → 1.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +109 -11
  2. package/dist/cjs/decorators/addEventListener.js +45 -7
  3. package/dist/cjs/decorators/addEventListener.js.map +2 -2
  4. package/dist/cjs/decorators/elementDefine.js +225 -6
  5. package/dist/cjs/decorators/elementDefine.js.map +3 -3
  6. package/dist/cjs/decorators/index.js +2 -0
  7. package/dist/cjs/decorators/index.js.map +2 -2
  8. package/dist/cjs/decorators/mutationObserver.js +104 -0
  9. package/dist/cjs/decorators/mutationObserver.js.map +7 -0
  10. package/dist/cjs/decorators/query.js +13 -1
  11. package/dist/cjs/decorators/query.js.map +2 -2
  12. package/dist/cjs/decorators/resizeObserver.js +104 -0
  13. package/dist/cjs/decorators/resizeObserver.js.map +7 -0
  14. package/dist/cjs/examples/RxJSOperatorsExample.js +285 -0
  15. package/dist/cjs/examples/RxJSOperatorsExample.js.map +7 -0
  16. package/dist/esm/decorators/addEventListener.js +45 -7
  17. package/dist/esm/decorators/addEventListener.js.map +2 -2
  18. package/dist/esm/decorators/elementDefine.js +225 -6
  19. package/dist/esm/decorators/elementDefine.js.map +3 -3
  20. package/dist/esm/decorators/index.js +2 -0
  21. package/dist/esm/decorators/index.js.map +2 -2
  22. package/dist/esm/decorators/mutationObserver.js +85 -0
  23. package/dist/esm/decorators/mutationObserver.js.map +7 -0
  24. package/dist/esm/decorators/query.js +13 -1
  25. package/dist/esm/decorators/query.js.map +2 -2
  26. package/dist/esm/decorators/resizeObserver.js +85 -0
  27. package/dist/esm/decorators/resizeObserver.js.map +7 -0
  28. package/dist/esm/examples/RxJSOperatorsExample.js +266 -0
  29. package/dist/esm/examples/RxJSOperatorsExample.js.map +7 -0
  30. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js +446 -13
  31. package/dist/esm-bundle/dooboostore-simple-web-component.esm.js.map +4 -4
  32. package/dist/types/decorators/addEventListener.d.ts +24 -5
  33. package/dist/types/decorators/addEventListener.d.ts.map +1 -1
  34. package/dist/types/decorators/elementDefine.d.ts +14 -0
  35. package/dist/types/decorators/elementDefine.d.ts.map +1 -1
  36. package/dist/types/decorators/index.d.ts +2 -0
  37. package/dist/types/decorators/index.d.ts.map +1 -1
  38. package/dist/types/decorators/mutationObserver.d.ts +36 -0
  39. package/dist/types/decorators/mutationObserver.d.ts.map +1 -0
  40. package/dist/types/decorators/query.d.ts +12 -0
  41. package/dist/types/decorators/query.d.ts.map +1 -1
  42. package/dist/types/decorators/resizeObserver.d.ts +37 -0
  43. package/dist/types/decorators/resizeObserver.d.ts.map +1 -0
  44. package/dist/types/examples/RxJSOperatorsExample.d.ts +94 -0
  45. package/dist/types/examples/RxJSOperatorsExample.d.ts.map +1 -0
  46. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js +446 -13
  47. package/dist/umd-bundle/dooboostore-simple-web-component.umd.js.map +4 -4
  48. package/package.json +4 -4
  49. package/src/decorators/addEventListener.ts +52 -9
  50. package/src/decorators/elementDefine.ts +330 -47
  51. package/src/decorators/index.ts +2 -0
  52. package/src/decorators/mutationObserver.ts +128 -0
  53. package/src/decorators/query.ts +23 -0
  54. package/src/decorators/resizeObserver.ts +129 -0
  55. package/src/examples/RxJSOperatorsExample.ts +279 -0
  56. package/dist/cjs/elements/tt/SwcAppMixin.js +0 -1
  57. package/dist/cjs/elements/tt/SwcAppMixin.js.map +0 -7
  58. package/dist/cjs/elements/tt/SwcAsync.js +0 -1
  59. package/dist/cjs/elements/tt/SwcAsync.js.map +0 -7
  60. package/dist/cjs/elements/tt/SwcAsyncTemplate.js +0 -1
  61. package/dist/cjs/elements/tt/SwcAsyncTemplate.js.map +0 -7
  62. package/dist/cjs/elements/tt/SwcChoose.js +0 -1
  63. package/dist/cjs/elements/tt/SwcChoose.js.map +0 -7
  64. package/dist/cjs/elements/tt/SwcChooseTemplate.js +0 -1
  65. package/dist/cjs/elements/tt/SwcChooseTemplate.js.map +0 -7
  66. package/dist/cjs/elements/tt/SwcIf.js +0 -1
  67. package/dist/cjs/elements/tt/SwcIf.js.map +0 -7
  68. package/dist/cjs/elements/tt/SwcIfTemplate.js +0 -1
  69. package/dist/cjs/elements/tt/SwcIfTemplate.js.map +0 -7
  70. package/dist/cjs/elements/tt/SwcLoop.js +0 -1
  71. package/dist/cjs/elements/tt/SwcLoop.js.map +0 -7
  72. package/dist/cjs/elements/tt/SwcLoopTemplate.js +0 -1
  73. package/dist/cjs/elements/tt/SwcLoopTemplate.js.map +0 -7
  74. package/dist/cjs/elements/tt/SwcSubTemplates.js +0 -1
  75. package/dist/cjs/elements/tt/SwcSubTemplates.js.map +0 -7
  76. package/dist/cjs/elements/tt/register.js +0 -1
  77. package/dist/cjs/elements/tt/register.js.map +0 -7
  78. package/dist/esm/elements/tt/SwcAppMixin.js +0 -1
  79. package/dist/esm/elements/tt/SwcAppMixin.js.map +0 -7
  80. package/dist/esm/elements/tt/SwcAsync.js +0 -1
  81. package/dist/esm/elements/tt/SwcAsync.js.map +0 -7
  82. package/dist/esm/elements/tt/SwcAsyncTemplate.js +0 -1
  83. package/dist/esm/elements/tt/SwcAsyncTemplate.js.map +0 -7
  84. package/dist/esm/elements/tt/SwcChoose.js +0 -1
  85. package/dist/esm/elements/tt/SwcChoose.js.map +0 -7
  86. package/dist/esm/elements/tt/SwcChooseTemplate.js +0 -1
  87. package/dist/esm/elements/tt/SwcChooseTemplate.js.map +0 -7
  88. package/dist/esm/elements/tt/SwcIf.js +0 -1
  89. package/dist/esm/elements/tt/SwcIf.js.map +0 -7
  90. package/dist/esm/elements/tt/SwcIfTemplate.js +0 -1
  91. package/dist/esm/elements/tt/SwcIfTemplate.js.map +0 -7
  92. package/dist/esm/elements/tt/SwcLoop.js +0 -1
  93. package/dist/esm/elements/tt/SwcLoop.js.map +0 -7
  94. package/dist/esm/elements/tt/SwcLoopTemplate.js +0 -1
  95. package/dist/esm/elements/tt/SwcLoopTemplate.js.map +0 -7
  96. package/dist/esm/elements/tt/SwcSubTemplates.js +0 -1
  97. package/dist/esm/elements/tt/SwcSubTemplates.js.map +0 -7
  98. package/dist/esm/elements/tt/register.js +0 -1
  99. package/dist/esm/elements/tt/register.js.map +0 -7
  100. package/dist/types/elements/tt/SwcAppMixin.d.ts +0 -1
  101. package/dist/types/elements/tt/SwcAppMixin.d.ts.map +0 -1
  102. package/dist/types/elements/tt/SwcAsync.d.ts +0 -1
  103. package/dist/types/elements/tt/SwcAsync.d.ts.map +0 -1
  104. package/dist/types/elements/tt/SwcAsyncTemplate.d.ts +0 -1
  105. package/dist/types/elements/tt/SwcAsyncTemplate.d.ts.map +0 -1
  106. package/dist/types/elements/tt/SwcChoose.d.ts +0 -1
  107. package/dist/types/elements/tt/SwcChoose.d.ts.map +0 -1
  108. package/dist/types/elements/tt/SwcChooseTemplate.d.ts +0 -1
  109. package/dist/types/elements/tt/SwcChooseTemplate.d.ts.map +0 -1
  110. package/dist/types/elements/tt/SwcIf.d.ts +0 -1
  111. package/dist/types/elements/tt/SwcIf.d.ts.map +0 -1
  112. package/dist/types/elements/tt/SwcIfTemplate.d.ts +0 -1
  113. package/dist/types/elements/tt/SwcIfTemplate.d.ts.map +0 -1
  114. package/dist/types/elements/tt/SwcLoop.d.ts +0 -1
  115. package/dist/types/elements/tt/SwcLoop.d.ts.map +0 -1
  116. package/dist/types/elements/tt/SwcLoopTemplate.d.ts +0 -1
  117. package/dist/types/elements/tt/SwcLoopTemplate.d.ts.map +0 -1
  118. package/dist/types/elements/tt/SwcSubTemplates.d.ts +0 -1
  119. package/dist/types/elements/tt/SwcSubTemplates.d.ts.map +0 -1
  120. package/dist/types/elements/tt/register.d.ts +0 -1
  121. package/dist/types/elements/tt/register.d.ts.map +0 -1
  122. package/src/elements/tt/SwcAppMixin.ts +0 -13
  123. package/src/elements/tt/SwcAsync.ts +0 -139
  124. package/src/elements/tt/SwcAsyncTemplate.ts +0 -126
  125. package/src/elements/tt/SwcChoose.ts +0 -182
  126. package/src/elements/tt/SwcChooseTemplate.ts +0 -229
  127. package/src/elements/tt/SwcIf.ts +0 -145
  128. package/src/elements/tt/SwcIfTemplate.ts +0 -158
  129. package/src/elements/tt/SwcLoop.ts +0 -222
  130. package/src/elements/tt/SwcLoopTemplate.ts +0 -255
  131. package/src/elements/tt/SwcSubTemplates.ts +0 -6
  132. package/src/elements/tt/register.ts +0 -720
@@ -1,6 +1,10 @@
1
1
  import {ActionExpression, FunctionUtils, ReflectUtils, Subject} from '@dooboostore/core';
2
2
  import {debounceTime, distinctUntilChanged, throttleTime} from '@dooboostore/core/message/operators';
3
3
  import {getAddEventListenerMetadata} from './addEventListener';
4
+ import {getMutationObserverMetadata} from './mutationObserver';
5
+ import type {MutationObserverBaseOptions, MutationObserverMetadata} from './mutationObserver';
6
+ import {getResizeObserverMetadata} from './resizeObserver';
7
+ import type {ResizeObserverMetadata} from './resizeObserver';
4
8
  import {findAllLifecycleMetadata, findAllOnConnectedAfterMetadata, findAllOnConnectedBeforeMetadata, findAllOnConnectedMetadata, ON_AFTER_ADOPTED_METADATA_KEY, ON_AFTER_DISCONNECTED_METADATA_KEY, ON_BEFORE_ADOPTED_METADATA_KEY, ON_BEFORE_DISCONNECTED_METADATA_KEY, ON_CONNECTED_COMPLETED_METADATA_KEY, ON_INITIALIZE_METADATA_KEY} from './lifecycles';
5
9
  import {getEmitCustomEventMetadataList} from './emitCustomEvent';
6
10
  import {convertAttributeValue, findAllAttributeChangedMetadata} from './changedAttribute';
@@ -20,6 +24,16 @@ import {findAllPropertyMetadata} from "./applyProperty";
20
24
 
21
25
  export const ELEMENT_CONFIG_KEY = Symbol.for('simple-web-component:element-config');
22
26
 
27
+ /** 바인딩된 이벤트 리스너 한 건 */
28
+ export interface BoundListener {
29
+ target: EventTarget;
30
+ type: string;
31
+ handler: EventListener;
32
+ options: AddEventListenerOptions;
33
+ onRemoves?: Array<{ fn: (target: EventTarget, optionValue: unknown) => void; opts: unknown }>;
34
+ subscription?: { unsubscribe: () => void };
35
+ }
36
+
23
37
  export interface ElementConfig {
24
38
  extends?: string;
25
39
  observedAttributes?: string[];
@@ -42,7 +56,6 @@ export const ensureInit = (inst: any) => { // HTMLElement
42
56
  if (!inst.__swc_initialized) {
43
57
  inst._swcId = inst?.getAttribute?.('swc-use-ssr') ?? 's' + Math.random().toString(36).substring(2, 11).toLowerCase();
44
58
  inst._emitHandlers = new Map();
45
- inst._boundListeners = [];
46
59
  inst.__swc_initialized = true;
47
60
  // const attributeList = findAllQ(inst);
48
61
  // attributeList.forEach(meta => {
@@ -318,8 +331,8 @@ const setupPrototype = (proto: any, win: Window) => {
318
331
  export const elementDefine =
319
332
  (name: string, config: Partial<ElementConfig> = {}): ClassDecorator =>
320
333
  (constructor: any) => {
321
- const {win, doc, builtInTagMap: BUILT_IN_TAG_MAP, domHelpers: SWC_DOM_HELPERS} = buildEnv(config.window);
322
- const metadata: ElementMetadata = {...config, name, window: win};
334
+ const { win, doc, builtInTagMap: BUILT_IN_TAG_MAP, domHelpers: SWC_DOM_HELPERS } = buildEnv(config.window);
335
+ const metadata: ElementMetadata = { ...config, name, window: win };
323
336
 
324
337
  if (!metadata.window) {
325
338
  throw new Error('window is required');
@@ -340,6 +353,8 @@ export const elementDefine =
340
353
  const emitCustomEventList = getEmitCustomEventMetadataList(constructor) || [];
341
354
  const emitHostCustomEventList = emitCustomEventList.filter(meta => meta.selector === '$this' || meta.selector === '');
342
355
  const addEventListenerList = getAddEventListenerMetadata(constructor) || [];
356
+ const mutationObserverList = getMutationObserverMetadata(constructor) || [];
357
+ const resizeObserverList = getResizeObserverMetadata(constructor) || [];
343
358
  const attrChangeMap = findAllAttributeChangedMetadata(constructor);
344
359
  const attributeList = findAllAttributeMetadata(constructor);
345
360
  const applyAttributeMap = findAllAttributeApplyMetadata(constructor);
@@ -361,9 +376,22 @@ export const elementDefine =
361
376
 
362
377
  // 값을 기대하지않는다.
363
378
  let helperHostSet: HelperHostSet | null = null;
379
+ // 이벤트 리스너 / MutationObserver / ResizeObserver 인스턴스 (클로저 변수 — connected/disconnected 공유)
380
+ let boundListeners: BoundListener[] = [];
381
+ let shadowMutationObserver: MutationObserver | null = null;
382
+ let lightMutationObserver: MutationObserver | null = null;
383
+ let resizeObserver: ResizeObserver | null = null;
384
+ // 생성된 모든 track(Mutation)Observer (disconnected 시 정리)
385
+ let resizeObservers: Array<ResizeObserver | MutationObserver> = [];
386
+ // 메타별 removeObserver 콜백 목록 (disconnected 시 호출)
387
+ let removeObserverCallbacks: Array<{ fn: (target: Element, optionValue: unknown) => void; target: any; opts: unknown }> = [];
364
388
  const originalConnected = proto.connectedCallback;
365
389
  proto.connectedCallback = async function () {
366
390
  ensureInit(this);
391
+ // 재연결 시 누적 방지 — 클로저 변수 리셋
392
+ boundListeners = [];
393
+ resizeObservers = [];
394
+ removeObserverCallbacks = [];
367
395
  // connected 됐을시 최신화
368
396
  helperHostSet = SwcUtils.getHelperAndHostSet(win, this as any);
369
397
  const appHost = helperHostSet.$appHost;
@@ -383,7 +411,7 @@ export const elementDefine =
383
411
  const conf = getElementConfig(this);
384
412
  const currentWin = (this as any)._resolveWindow(conf);
385
413
 
386
- const bMethods = findAllOnConnectedBeforeMetadata(this);//.filter(it => useSsr ? !it.options.ssrFirst : true);
414
+ const bMethods = findAllOnConnectedBeforeMetadata(this); //.filter(it => useSsr ? !it.options.ssrFirst : true);
387
415
  // console.log('beforeConnected', bMethods)
388
416
  for (const m of bMethods) await (this as any)._invokeLifecycleMethod(m.propertyKey, helperHostSet);
389
417
  (this as any)._executeSwcScript('swc-on-before-connected', helperHostSet);
@@ -398,19 +426,19 @@ export const elementDefine =
398
426
  const mode = shadowMode === true ? 'open' : shadowMode;
399
427
  console.log('tagName=', this.tagName, 'mode', mode, this.shadowRoot);
400
428
  // if (this.tagName!=='INDEX-ROUTER') {
401
- this.attachShadow({mode: mode as ShadowRootMode});
429
+ this.attachShadow({ mode: mode as ShadowRootMode });
402
430
  // }
403
431
  }
404
432
 
405
- const stateContext: any = {...helperHostSet};
433
+ const stateContext: any = { ...helperHostSet };
406
434
  findAllStateMetadata(this).forEach(it => {
407
- stateContext[it.name] = this[it.propertyKey]
408
- })
435
+ stateContext[it.name] = this[it.propertyKey];
436
+ });
409
437
 
410
438
  const shadowChildren: Node[] = [];
411
439
  const lightChildren: Node[] = [];
412
440
 
413
- console.log('targetConnectList', this.tagName, targetConnectedList.length)
441
+ console.log('targetConnectList', this.tagName, targetConnectedList.length);
414
442
  if (targetConnectedList.length > 0) {
415
443
  // let sContent = '',
416
444
  // lContent = '';
@@ -420,7 +448,7 @@ export const elementDefine =
420
448
  // res = SwcUtils.projectProcessHtml(this, res);
421
449
  if (typeof res === 'string') {
422
450
  const htmlTemplateElement = doc.createElement('template');
423
- htmlTemplateElement.innerHTML = res;//SwcUtils.projectProcessHtml(this._swcId, res, doc);
451
+ htmlTemplateElement.innerHTML = res; //SwcUtils.projectProcessHtml(this._swcId, res, doc);
424
452
  res = htmlTemplateElement.content;
425
453
  }
426
454
 
@@ -428,7 +456,7 @@ export const elementDefine =
428
456
  const nodes = Array.isArray(res) ? res : [res];
429
457
  if (meta.options.useShadow || conf?.useShadow) {
430
458
  shadowChildren.push(...nodes);
431
- }else {
459
+ } else {
432
460
  lightChildren.push(...nodes);
433
461
  }
434
462
  }
@@ -436,23 +464,22 @@ export const elementDefine =
436
464
  try {
437
465
  if (this.shadowRoot) {
438
466
  const applyShadowChildren = SwcUtils.projectProcessHtml(this._swcId, shadowChildren, doc);
439
- this.shadowRoot.replaceChildren(...applyShadowChildren)
467
+ this.shadowRoot.replaceChildren(...applyShadowChildren);
440
468
  }
441
469
  if (lightChildren.length) {
442
470
  const applyLightChildren = SwcUtils.projectProcessHtml(this._swcId, lightChildren, doc);
443
- this.replaceChildren(...applyLightChildren)
471
+ this.replaceChildren(...applyLightChildren);
444
472
  }
445
-
446
473
  } catch (e) {
447
474
  console.error('[ElementDefine] innerHTML setting error:', e);
448
475
  }
449
- new ElementApply(this, {id: this._swcId}).apply({context: stateContext, bind: this});
476
+ new ElementApply(this, { id: this._swcId }).apply({ context: stateContext, bind: this });
450
477
  } else {
451
- new ElementApply(this, {id: this._swcId}).apply({exclude: {html: true, text: true, attribute: true}, context: stateContext, bind: this});
478
+ new ElementApply(this, { id: this._swcId }).apply({ exclude: { html: true, text: true, attribute: true }, context: stateContext, bind: this });
452
479
  }
453
480
 
454
481
  // global delegate event
455
- const root = this.getRootNode()
482
+ const root = this.getRootNode();
456
483
  if (!globalDelegatedRoots.has(root)) {
457
484
  DOM_EVENT_NAMES.forEach(type => {
458
485
  root.addEventListener(type, handleGlobalSwcEvent);
@@ -460,7 +487,6 @@ export const elementDefine =
460
487
  globalDelegatedRoots.add(root);
461
488
  }
462
489
 
463
-
464
490
  // Separate delegate and non-delegate listeners
465
491
  const delegateListeners: typeof addEventListenerList = [];
466
492
  const nonDelegateListeners: typeof addEventListenerList = [];
@@ -488,7 +514,7 @@ export const elementDefine =
488
514
  delegatesByTypeAndRoot.forEach((metaList, typeRootKey) => {
489
515
  const [type, rStr] = typeRootKey.split(':');
490
516
  const r = rStr === 'auto' ? 'auto' : rStr;
491
- const opts = {capture: metaList[0].options.capture, once: metaList[0].options.once, passive: metaList[0].options.passive};
517
+ const opts = { capture: metaList[0].options.capture, once: metaList[0].options.once, passive: metaList[0].options.passive };
492
518
 
493
519
  const bindRoots: (HTMLElement | ShadowRoot)[] = [];
494
520
  if (r === 'auto') bindRoots.push(this.shadowRoot || (this as any));
@@ -508,7 +534,7 @@ export const elementDefine =
508
534
 
509
535
  const unifiedHandler = async (event: Event) => {
510
536
  for (const meta of sortedMetaList) {
511
- const {selector, options} = meta;
537
+ const { selector, options } = meta;
512
538
  const matchedEl = (event.target as HTMLElement)?.closest(selector);
513
539
  if (matchedEl && (br as any).contains(matchedEl)) {
514
540
  // Apply filter if specified
@@ -522,23 +548,30 @@ export const elementDefine =
522
548
  if (options.stopImmediatePropagation) event.stopImmediatePropagation();
523
549
  if (options.preventDefault) event.preventDefault();
524
550
  const currentHostSet = SwcUtils.getHostSet(this as any);
525
- const args = {event, ...currentHostSet, $el: matchedEl, $root: br};
526
- await (this as any)[meta.propertyKey](event, {...currentHostSet, $matchedElement: matchedEl}, args);
551
+ const args = { event, ...currentHostSet, $el: matchedEl, $root: br };
552
+ await (this as any)[meta.propertyKey](event, { ...currentHostSet, $matchedElement: matchedEl }, args);
527
553
  if ((event as any).cancelBubble) break;
528
554
  }
529
555
  }
530
556
  };
531
557
  br.addEventListener(type, unifiedHandler, opts);
532
- (this as any)._boundListeners.push({target: br, type, handler: unifiedHandler, options: opts});
558
+ // 각 메타의 removeListener를 메타 옵션과 함께 보관 (delegate 그룹은 여러 메타 가능)
559
+ const onRemoves: Array<{ fn: (target: Element, optionValue: any) => void; opts: any }> = [];
560
+ for (const m of sortedMetaList) {
561
+ if (m.options.removeListener) {
562
+ onRemoves.push({ fn: m.options.removeListener, opts: m.options });
563
+ }
564
+ }
565
+ boundListeners.push({ target: br, type, handler: unifiedHandler, options: opts, onRemoves });
533
566
  });
534
567
  });
535
568
 
536
569
  nonDelegateListeners.forEach(meta => {
537
- const {selector, type, options} = meta;
538
- const opts = {capture: options.capture, once: options.once, passive: options.passive};
570
+ const { selector, type, options } = meta;
571
+ const opts = { capture: options.capture, once: options.once, passive: options.passive };
539
572
  const bindTargets: EventTarget[] = [];
540
573
  const r = options.root || 'auto';
541
-
574
+
542
575
  // Resolve selector if it's a function
543
576
  let resolvedSelector: string | null = null;
544
577
  if (typeof selector === 'function') {
@@ -556,7 +589,7 @@ export const elementDefine =
556
589
  } else {
557
590
  resolvedSelector = selector;
558
591
  }
559
-
592
+
560
593
  const applyRootOption = (target: any) => {
561
594
  if (!target) return;
562
595
  if (r === 'auto') {
@@ -601,7 +634,7 @@ export const elementDefine =
601
634
  // Apply filter if specified
602
635
  if (options.filter) {
603
636
  const helper = SwcUtils.getHelperAndHostSet(currentWin, t as HTMLElement);
604
- if (!options.filter(event, {currentThis: this, helper})) {
637
+ if (!options.filter(event, { currentThis: this, helper })) {
605
638
  return; // Skip if filter returns false
606
639
  }
607
640
  }
@@ -609,23 +642,23 @@ export const elementDefine =
609
642
  if (options.stopImmediatePropagation) event.stopImmediatePropagation();
610
643
  if (options.preventDefault) event.preventDefault();
611
644
  const currentHostSet = SwcUtils.getHostSet(this as any);
612
- const args = {event, ...currentHostSet, $el: t, $root: t};
613
- await (this as any)[meta.propertyKey](event, {currentHostSet, $matchedElement: event.currentTarget}, args);
645
+ const args = { event, ...currentHostSet, $el: t, $root: t };
646
+ await (this as any)[meta.propertyKey](event, { currentHostSet, $matchedElement: event.currentTarget }, args);
614
647
  };
615
-
648
+
616
649
  // Create event stream with Observable-based operators
617
650
  const eventSubject = new Subject<Event>();
618
651
  let eventStream = eventSubject as any;
619
-
652
+
620
653
  // Apply operators from @dooboostore/core/operators
621
654
  if (options.debounceTime !== undefined && options.debounceTime > 0) {
622
655
  eventStream = eventStream.pipe(debounceTime(options.debounceTime));
623
656
  }
624
-
657
+
625
658
  if (options.throttleTime !== undefined && options.throttleTime > 0) {
626
659
  eventStream = eventStream.pipe(throttleTime(options.throttleTime));
627
660
  }
628
-
661
+
629
662
  if (options.distinctUntilChanged !== undefined && options.distinctUntilChanged !== false) {
630
663
  if (typeof options.distinctUntilChanged === 'function') {
631
664
  eventStream = eventStream.pipe(distinctUntilChanged(options.distinctUntilChanged));
@@ -633,7 +666,7 @@ export const elementDefine =
633
666
  eventStream = eventStream.pipe(distinctUntilChanged());
634
667
  }
635
668
  }
636
-
669
+
637
670
  // Subscribe to the event stream
638
671
  const subscription = eventStream.subscribe({
639
672
  next: (event: Event) => {
@@ -641,18 +674,230 @@ export const elementDefine =
641
674
  },
642
675
  error: (err: any) => console.error('Event stream error:', err)
643
676
  });
644
-
677
+
645
678
  // Create wrapper handler that emits to the subject
646
679
  const wrappedHandler = (event: Event) => {
647
680
  eventSubject.next(event);
648
681
  };
649
-
682
+
650
683
  t.addEventListener(type, wrappedHandler, opts);
651
- (this as any)._boundListeners.push({target: t, type, handler: wrappedHandler, options: opts, subscription});
684
+ const onRemoves: Array<{ fn: (target: Element, optionValue: any) => void; opts: any }> = options.removeListener ? [{ fn: options.removeListener, opts }] : [];
685
+ boundListeners.push({ target: t, type, handler: wrappedHandler, options: opts, subscription, onRemoves });
686
+ });
687
+ });
688
+
689
+ // ─── MutationObserver / ResizeObserver 처리 ───
690
+ // shadow/light 루트별로 각각 observer를 두고, 해당 루트에 데코레이터 메타가 있으면 observer를 생성한다.
691
+ // 두 루트 모두 있으면 observer 2개, 한쪽만 있으면 1개, 없으면 0개.
692
+ shadowMutationObserver = null;
693
+ lightMutationObserver = null;
694
+ resizeObserver = null;
695
+
696
+ // ResizeObserver: @resizeObserver 데코레이터가 하나라도 있으면 observer 하나 생성 (shadow 있으면 shadow 루트, 없으면 light)
697
+ if (resizeObserverList.length > 0) {
698
+ const root: HTMLElement | ShadowRoot = this.shadowRoot || (this as any);
699
+ resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[], obs: ResizeObserver) => {
700
+ for (const m of resizeObserverList) {
701
+ let matchedEls: HTMLElement[] = [];
702
+ if (m.options.delegate && typeof m.selector === 'string') {
703
+ const isThis = m.selector === '$this' || m.selector === '';
704
+ const matchesSel = (node: any): boolean => {
705
+ if (!node || node.nodeType !== 1) return false;
706
+ if (isThis) return true;
707
+ const el = node as HTMLElement;
708
+ return el.matches?.(m.selector as string) || !!el.closest?.(m.selector as string);
709
+ };
710
+ matchedEls = entries.map(e => e.target as HTMLElement).filter(matchesSel);
711
+ } else {
712
+ matchedEls = entries.map(e => e.target as HTMLElement).filter(t => t && t.nodeType === 1);
713
+ }
714
+ if (matchedEls.length === 0) continue;
715
+ if (m.options.filter) {
716
+ const helper = SwcUtils.getHelperAndHostSet(currentWin, this);
717
+ if (!m.options.filter(matchedEls, { currentThis: this, helper })) continue;
718
+ }
719
+ const hostSet = SwcUtils.getHostSet(this as any);
720
+ (this as any)[m.propertyKey](matchedEls, entries, obs, { ...hostSet, $root: root });
721
+ }
722
+ });
723
+ resizeObservers.push(resizeObserver);
724
+ // resizeObserver 메타별 removeObserver 수집
725
+ for (const m of resizeObserverList) {
726
+ if (m.options.removeObserver) {
727
+ removeObserverCallbacks.push({ fn: m.options.removeObserver, target: root, opts: m.options });
728
+ }
729
+ }
730
+ }
731
+ // MutationObserver: shadow/light 루트별 그룹핑
732
+ const shadowMetas = mutationObserverList.filter(m => {
733
+ const r = m.options.root || 'auto';
734
+ return this.shadowRoot && (r === 'shadow' || r === 'all' || r === 'auto');
735
+ });
736
+ const lightMetas = mutationObserverList.filter(m => {
737
+ const r = m.options.root || 'auto';
738
+ return r === 'light' || r === 'all' || (!this.shadowRoot && r === 'auto');
739
+ });
740
+
741
+ const setupMutationObserver = (root: HTMLElement | ShadowRoot, metas: MutationObserverMetadata[], resizeDelegates: ResizeObserverMetadata[] = []): MutationObserver | null => {
742
+ // mutation 메타가 없으면 observer 생성 안 함 (resize delegate는 보조)
743
+ if (metas.length === 0) return null;
744
+
745
+ // 사용자가 입력한 옵션 그대로 (아무것도 안 주면 childList 기본)
746
+ const initFrom = (o: MutationObserverBaseOptions): MutationObserverInit => {
747
+ const init: MutationObserverInit = {
748
+ childList: o.childList,
749
+ attributes: o.attributes,
750
+ characterData: o.characterData,
751
+ subtree: o.subtree,
752
+ attributeFilter: o.attributeFilter,
753
+ attributeOldValue: o.attributeOldValue,
754
+ characterDataOldValue: o.characterDataOldValue
755
+ };
756
+ // MutationObserver는 최소 1개는 true여야 함
757
+ if (!init.childList && !init.attributes && !init.characterData) init.childList = true;
758
+ return init;
759
+ };
760
+
761
+ const observer = new MutationObserver((mutations: MutationRecord[], obs: MutationObserver) => {
762
+ for (const meta of metas) {
763
+ const { selector, options } = meta;
764
+ let matchedEls: HTMLElement[] = [];
765
+
766
+ if (options.delegate && typeof selector === 'string') {
767
+ const isThis = selector === '$this' || selector === '';
768
+ const matchesSel = (node: any): boolean => {
769
+ if (!node || node.nodeType !== 1) return false;
770
+ if (isThis) return true;
771
+ const el = node as HTMLElement;
772
+ return el.matches?.(selector as string) || !!el.closest?.(selector as string);
773
+ };
774
+ matchedEls = mutations.flatMap(m => {
775
+ const els: HTMLElement[] = [];
776
+ if (matchesSel(m.target)) els.push(m.target as HTMLElement);
777
+ Array.from(m.addedNodes || [])
778
+ .filter(matchesSel)
779
+ .forEach(n => els.push(n as HTMLElement));
780
+ Array.from(m.removedNodes || [])
781
+ .filter(matchesSel)
782
+ .forEach(n => els.push(n as HTMLElement));
783
+ return els;
784
+ });
785
+ } else if (typeof selector === 'string') {
786
+ // non-delegate: observe 대상이 매칭 요소들이므로 그들의 mutation 대상 + 추가/제거 노드
787
+ const isThis = selector === '$this' || selector === '';
788
+ const matchesSel = (node: any): boolean => {
789
+ if (!node || node.nodeType !== 1) return false;
790
+ if (isThis) return true;
791
+ return (node as HTMLElement).matches?.(selector as string);
792
+ };
793
+ matchedEls = mutations.flatMap(m => {
794
+ const els: HTMLElement[] = [];
795
+ if (matchesSel(m.target)) els.push(m.target as HTMLElement);
796
+ Array.from(m.addedNodes || [])
797
+ .filter(matchesSel)
798
+ .forEach(n => els.push(n as HTMLElement));
799
+ Array.from(m.removedNodes || [])
800
+ .filter(matchesSel)
801
+ .forEach(n => els.push(n as HTMLElement));
802
+ return els;
803
+ });
804
+ } else {
805
+ matchedEls = mutations.map(m => m.target as HTMLElement).filter(t => t && t.nodeType === 1);
806
+ }
807
+
808
+ if (matchedEls.length === 0) continue;
809
+ if (options.filter) {
810
+ const helper = SwcUtils.getHelperAndHostSet(currentWin, this);
811
+ if (!options.filter(matchedEls, { currentThis: this, helper })) continue;
812
+ }
813
+ const hostSet = SwcUtils.getHostSet(this as any);
814
+ // 1번째: 매치된 element 배열, 2번째: 원본 mutations, 3번째: observer
815
+ (this as any)[meta.propertyKey](matchedEls, mutations, obs, { ...hostSet, $root: root });
816
+ }
817
+
818
+ // ─── resizeObserver delegate 동적 추적 ───
819
+ // 추가/삭제된 요소가 delegate 셀렉터와 매칭되면 resizeObserver에 observe/unobserve
820
+ if (resizeDelegates.length > 0 && resizeObserver) {
821
+ const handle = (n: Node, observe: boolean) => {
822
+ if (n.nodeType !== 1) return;
823
+ const el = n as HTMLElement;
824
+ for (const m of resizeDelegates) {
825
+ if (typeof m.selector !== 'string') continue;
826
+ const isThis = m.selector === '$this' || m.selector === '';
827
+ const matchesSel = (node: any): boolean => {
828
+ if (!node || node.nodeType !== 1) return false;
829
+ if (isThis) return true;
830
+ const e = node as HTMLElement;
831
+ return e.matches?.(m.selector as string) || !!e.closest?.(m.selector as string);
832
+ };
833
+ if (matchesSel(el)) {
834
+ if (observe) resizeObserver.observe(el, m.options.box ? { box: m.options.box } : undefined);
835
+ else resizeObserver.unobserve(el);
836
+ } else if (!isThis) {
837
+ el.querySelectorAll(m.selector).forEach(sub => {
838
+ if (observe) resizeObserver.observe(sub as HTMLElement, m.options.box ? { box: m.options.box } : undefined);
839
+ else resizeObserver.unobserve(sub as HTMLElement);
840
+ });
841
+ }
842
+ }
843
+ };
844
+ for (const m of mutations) {
845
+ Array.from(m.addedNodes || []).forEach(n => handle(n, true));
846
+ Array.from(m.removedNodes || []).forEach(n => handle(n, false));
847
+ }
848
+ }
652
849
  });
850
+
851
+ // delegate 메타(mutation 또는 resize)가 하나라도 있으면 → 루트에 subtree observe 1개로 전부 감지 (콜백에서 메타별 매칭 처리)
852
+ const hasDelegate = metas.some(m => m.options.delegate) || resizeDelegates.some(m => m.options.delegate);
853
+ if (hasDelegate) {
854
+ observer.observe(root, { childList: true, subtree: true });
855
+ }
856
+
857
+ // non-delegate 메타: 셀렉터 매칭 요소들을 사용자 옵션 그대로 observe
858
+ const nonDelegateMetas = metas.filter(m => !m.options.delegate);
859
+ for (const m of nonDelegateMetas) {
860
+ const { selector, options } = m;
861
+ const mInit = initFrom(options);
862
+ if (typeof selector === 'string') {
863
+ if (selector === '$this' || selector === '') {
864
+ observer.observe(root, mInit);
865
+ } else {
866
+ root.querySelectorAll(selector).forEach(el => observer.observe(el as HTMLElement, mInit));
867
+ }
868
+ } else if (typeof selector === 'function') {
869
+ // 함수 셀렉터는 루트 관찰로 대체 (매칭 평가 불가)
870
+ observer.observe(root, mInit);
871
+ }
872
+ }
873
+
874
+ return observer;
875
+ };
876
+
877
+ // resizeObserver delegate 메타도 루트별로 필터해서 넘김
878
+ const shadowResizeDelegateMetas = resizeObserverList.filter(m => {
879
+ if (!m.options.delegate) return false;
880
+ const r = m.options.root || 'auto';
881
+ return this.shadowRoot && (r === 'shadow' || r === 'all' || r === 'auto');
882
+ });
883
+ const lightResizeDelegateMetas = resizeObserverList.filter(m => {
884
+ if (!m.options.delegate) return false;
885
+ const r = m.options.root || 'auto';
886
+ return r === 'light' || r === 'all' || (!this.shadowRoot && r === 'auto');
653
887
  });
654
888
 
655
- if (originalConnected) await (originalConnected.apply(this));
889
+ if (this.shadowRoot) shadowMutationObserver = setupMutationObserver(this.shadowRoot, shadowMetas, shadowResizeDelegateMetas);
890
+ lightMutationObserver = setupMutationObserver(this as any, lightMetas, lightResizeDelegateMetas);
891
+
892
+ // mutationObserver 메타별 removeObserver 수집
893
+ for (const m of mutationObserverList) {
894
+ if (m.options.removeObserver) {
895
+ const rootForM = this.shadowRoot && (m.options.root === 'shadow' || m.options.root === 'all' || (m.options.root === 'auto' && this.shadowRoot)) ? this.shadowRoot : (this as any);
896
+ removeObserverCallbacks.push({ fn: m.options.removeObserver, target: rootForM, opts: m.options });
897
+ }
898
+ }
899
+
900
+ if (originalConnected) await originalConnected.apply(this);
656
901
 
657
902
  const aMethods = findAllOnConnectedAfterMetadata(this); //.filter(it => { return useSsr ? !it.options.ssrFirst : true; });
658
903
  // console.log('afterConnected', this.tagName, aMethods, getOnConnectedAfterMetadata(this))
@@ -665,9 +910,9 @@ export const elementDefine =
665
910
  for (let [name, metaList] of Array.from(attrChangeMap)) {
666
911
  for (const meta of metaList) {
667
912
  if (meta.options.while === 'connected') {
668
- const val = getAttributeValue(this, name, {type: meta.options.type});
913
+ const val = getAttributeValue(this, name, { type: meta.options.type });
669
914
  if (val !== null) {
670
- await ((this as any)[meta.propertyKey](val, null, name, helperHostSet));
915
+ await (this as any)[meta.propertyKey](val, null, name, helperHostSet);
671
916
  }
672
917
  }
673
918
  }
@@ -682,6 +927,9 @@ export const elementDefine =
682
927
  }
683
928
  };
684
929
 
930
+ /////////////////////////////////////////////////
931
+ // disconnectedCallback
932
+ ////////////////////////////////////////////////
685
933
  const originalDisconnected = proto.disconnectedCallback;
686
934
  proto.disconnectedCallback = function () {
687
935
  // const helperHostSet = SwcUtils.getHelperAndHostSet(win, this as any);
@@ -695,19 +943,54 @@ export const elementDefine =
695
943
  (this as any)._executeSwcScript('swc-on-before-disconnected', helperHostSet);
696
944
  const bMethods = findAllLifecycleMetadata(this, ON_BEFORE_DISCONNECTED_METADATA_KEY);
697
945
  for (const m of bMethods) (this as any)._invokeLifecycleMethod(m.propertyKey, helperHostSet);
698
-
699
- if ((this as any)._boundListeners) {
700
- (this as any)._boundListeners.forEach((l: any) => {
946
+ // event listener 정리
947
+ if (boundListeners.length > 0) {
948
+ boundListeners.forEach((l: BoundListener) => {
701
949
  l.target.removeEventListener(l.type, l.handler, l.options);
702
950
  // Cleanup Observable subscription if present
703
951
  if (l.subscription && typeof l.subscription.unsubscribe === 'function') {
704
952
  l.subscription.unsubscribe();
705
953
  }
954
+ // 사용자 정의 정리 콜백 호출
955
+ if (Array.isArray(l.onRemoves)) {
956
+ for (const r of l.onRemoves) {
957
+ try {
958
+ r.fn(l.target, r.opts);
959
+ } catch (e) {
960
+ console.error('[SWC] removeListener error:', e);
961
+ }
962
+ }
963
+ }
706
964
  });
707
- (this as any)._boundListeners = [];
965
+ boundListeners = [];
966
+ }
967
+
968
+ // MutationObserver / ResizeObserver 정리
969
+ const _obs: Array<MutationObserver | ResizeObserver | null> = [shadowMutationObserver, lightMutationObserver, resizeObserver, ...resizeObservers];
970
+ for (const o of _obs) {
971
+ try {
972
+ (o as any)?.disconnect?.();
973
+ } catch (e) {
974
+ console.error('[SWC] observer disconnect error:', e);
975
+ }
976
+ }
977
+ shadowMutationObserver = null;
978
+ lightMutationObserver = null;
979
+ resizeObserver = null;
980
+ resizeObservers = [];
981
+
982
+ // 메타별 removeObserver 콜백 호출
983
+ for (const cb of removeObserverCallbacks) {
984
+ try {
985
+ cb.fn(cb.target, cb.opts);
986
+ } catch (e) {
987
+ console.error('[SWC] removeObserver error:', e);
988
+ }
708
989
  }
990
+ removeObserverCallbacks = [];
709
991
 
710
- new ElementApply(this, {id: this._swcId}).removeAllEventListener();
992
+ // elementApply event listener 정리
993
+ new ElementApply(this, { id: this._swcId }).removeAllEventListener();
711
994
 
712
995
  // globalDelegatedRoots remove
713
996
  // const roots = [this, this.getRootNode()];
@@ -807,7 +1090,7 @@ export const elementDefine =
807
1090
  ReflectUtils.defineMetadata(ELEMENT_CONFIG_KEY, metadata, constructor);
808
1091
  const registry = metadata.customElementRegistry || (win as any)?.customElements;
809
1092
  if (registry && !registry.get(metadata.name)) {
810
- registry.define(metadata.name, constructor as any, metadata.extends ? {extends: metadata.extends} : undefined);
1093
+ registry.define(metadata.name, constructor as any, metadata.extends ? { extends: metadata.extends } : undefined);
811
1094
  }
812
1095
  return constructor;
813
1096
  };
@@ -1,4 +1,6 @@
1
1
  export * from './addEventListener';
2
+ export * from './mutationObserver';
3
+ export * from './resizeObserver';
2
4
  export * from './applyAttribute';
3
5
  export * from './elementDefine';
4
6
  export * from './changedAttribute';