dokodemo-ui 1.0.0 → 1.1.0

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.
package/README.md CHANGED
@@ -15,6 +15,7 @@ https://tamoco-mocomoco.github.io/dokodemo-ui/
15
15
  - 位置プリセット(top-left, top-right, bottom-left, bottom-right, center)
16
16
  - ウィンドウリサイズ追従
17
17
  - マウス・タッチ対応
18
+ - 外部モード(サードパーティウィジェット対応)
18
19
 
19
20
  ## インストール
20
21
 
@@ -86,6 +87,41 @@ import 'dokodemo-ui';
86
87
  </dokodemo-ui>
87
88
  ```
88
89
 
90
+ ### 外部モード(サードパーティウィジェット対応)
91
+
92
+ Channel Talkなどのサードパーティウィジェットは、DOMを移動すると壊れることがあります。外部モードを使うと、DOMを移動せずに位置を制御できます。
93
+
94
+ ```html
95
+ <!-- サードパーティのウィジェット(そのまま) -->
96
+ <div id="ch-plugin" style="position: fixed; right: 20px; bottom: 20px;">
97
+ ...
98
+ </div>
99
+
100
+ <!-- オーバーレイで制御 -->
101
+ <dokodemo-ui target="#ch-plugin" mode="external" closable></dokodemo-ui>
102
+ ```
103
+
104
+ ### 属性の自動転送
105
+
106
+ 外部モードでは、dokodemo-ui固有の属性以外がターゲット要素に自動転送されます。
107
+
108
+ ```html
109
+ <dokodemo-ui
110
+ target="#my-widget"
111
+ mode="external"
112
+ closable
113
+ api-key="xxx"
114
+ theme="dark"
115
+ lang="ja"
116
+ ></dokodemo-ui>
117
+ ```
118
+
119
+ 上記の例では:
120
+ - `target`, `mode`, `closable` → dokodemo-ui が使用(転送されない)
121
+ - `api-key`, `theme`, `lang` → ターゲット要素に転送される
122
+
123
+ これにより、サードパーティウィジェットの設定をdokodemo-ui経由で行えます。
124
+
89
125
  ## 属性
90
126
 
91
127
  | 属性 | 説明 | デフォルト |
@@ -99,6 +135,10 @@ import 'dokodemo-ui';
99
135
  | `padding` | 角からの距離(px) | `20` |
100
136
  | `x` | X座標を直接指定(px) | - |
101
137
  | `y` | Y座標を直接指定(px) | - |
138
+ | `target` | 外部モードのターゲット要素(CSSセレクタ) | - |
139
+ | `mode` | モード(`internal` / `external`) | `internal` |
140
+
141
+ > **Note**: 属性は初期化時に自動的に`dokodemo-*`プレフィックス付きに正規化されます。これにより外部モードでの属性転送時に名前衝突を防ぎます。
102
142
 
103
143
  ## イベント
104
144
 
package/dokodemo-ui.d.ts CHANGED
@@ -30,7 +30,13 @@ declare class DokodemoUI extends HTMLElement {
30
30
  private positionInitialized;
31
31
  private iframePointerEvents;
32
32
  private edgeSize;
33
+ private targetElement;
34
+ private externalObserver;
35
+ private externalResizeObserver;
36
+ private attributeForwardObserver;
33
37
  static get observedAttributes(): string[];
38
+ private getDokodemoAttr;
39
+ private hasDokodemoAttr;
34
40
  get closable(): boolean;
35
41
  set closable(value: boolean);
36
42
  get position(): Position | null;
@@ -49,8 +55,21 @@ declare class DokodemoUI extends HTMLElement {
49
55
  set x(value: number | null);
50
56
  get y(): number | null;
51
57
  set y(value: number | null);
58
+ get target(): string | null;
59
+ set target(value: string | null);
60
+ get mode(): "internal" | "external";
61
+ set mode(value: "internal" | "external");
62
+ get isExternalMode(): boolean;
52
63
  constructor();
53
64
  connectedCallback(): void;
65
+ private normalizeAttributes;
66
+ private setupExternalMode;
67
+ private findTargetElement;
68
+ private initExternalOverlay;
69
+ private forwardAllAttributes;
70
+ private forwardAttribute;
71
+ private renderExternalOverlay;
72
+ private syncWithTarget;
54
73
  attributeChangedCallback(): void;
55
74
  private render;
56
75
  private initializePosition;
package/dokodemo-ui.js CHANGED
@@ -16,93 +16,167 @@
16
16
  * - x, y: 初期位置を直接指定(単位はpx)※positionより優先
17
17
  * <dokodemo-ui x="100" y="200">...</dokodemo-ui>
18
18
  */
19
+ // dokodemo-ui 固有の属性名(プレフィックスなし)
20
+ const DOKODEMO_ATTRIBUTE_NAMES = new Set([
21
+ "closable",
22
+ "close-style",
23
+ "close-position",
24
+ "close-color",
25
+ "resizable",
26
+ "position",
27
+ "padding",
28
+ "x",
29
+ "y",
30
+ "target",
31
+ "mode",
32
+ ]);
33
+ // 転送しない属性(HTML標準属性)
34
+ const NON_FORWARDED_ATTRIBUTES = new Set([
35
+ "hidden",
36
+ "style",
37
+ "class",
38
+ "id",
39
+ "slot",
40
+ "tabindex",
41
+ ]);
42
+ // dokodemo-ui 固有の属性かどうか判定
43
+ function isDokodemoAttribute(name) {
44
+ // dokodemo-* プレフィックス付き
45
+ if (name.startsWith("dokodemo-"))
46
+ return true;
47
+ // プレフィックスなしでも既知の属性名ならtrue
48
+ return DOKODEMO_ATTRIBUTE_NAMES.has(name);
49
+ }
50
+ // 転送すべき属性かどうか判定
51
+ function shouldForwardAttribute(name) {
52
+ // dokodemo-ui 固有の属性は転送しない
53
+ if (isDokodemoAttribute(name))
54
+ return false;
55
+ // HTML標準属性は転送しない
56
+ if (NON_FORWARDED_ATTRIBUTES.has(name))
57
+ return false;
58
+ return true;
59
+ }
19
60
  class DokodemoUI extends HTMLElement {
20
61
  static get observedAttributes() {
21
62
  return ["closable"];
22
63
  }
64
+ // ヘルパー: dokodemo-* プレフィックス付きまたはレガシー属性を取得
65
+ getDokodemoAttr(name) {
66
+ return this.getAttribute(`dokodemo-${name}`) ?? this.getAttribute(name);
67
+ }
68
+ hasDokodemoAttr(name) {
69
+ return this.hasAttribute(`dokodemo-${name}`) || this.hasAttribute(name);
70
+ }
23
71
  get closable() {
24
- return this.hasAttribute("closable");
72
+ return this.hasDokodemoAttr("closable");
25
73
  }
26
74
  set closable(value) {
27
75
  if (value) {
28
- this.setAttribute("closable", "");
76
+ this.setAttribute("dokodemo-closable", "");
29
77
  }
30
78
  else {
79
+ this.removeAttribute("dokodemo-closable");
31
80
  this.removeAttribute("closable");
32
81
  }
33
82
  }
34
83
  get position() {
35
- return this.getAttribute("position");
84
+ return this.getDokodemoAttr("position");
36
85
  }
37
86
  set position(value) {
38
87
  if (value) {
39
- this.setAttribute("position", value);
88
+ this.setAttribute("dokodemo-position", value);
40
89
  }
41
90
  else {
91
+ this.removeAttribute("dokodemo-position");
42
92
  this.removeAttribute("position");
43
93
  }
44
94
  }
45
95
  get padding() {
46
- return parseInt(this.getAttribute("padding") || "20", 10);
96
+ return parseInt(this.getDokodemoAttr("padding") || "20", 10);
47
97
  }
48
98
  set padding(value) {
49
- this.setAttribute("padding", String(value));
99
+ this.setAttribute("dokodemo-padding", String(value));
50
100
  }
51
101
  get closeColor() {
52
- return this.getAttribute("close-color") || "#ff5f57";
102
+ return this.getDokodemoAttr("close-color") || "#ff5f57";
53
103
  }
54
104
  set closeColor(value) {
55
- this.setAttribute("close-color", value);
105
+ this.setAttribute("dokodemo-close-color", value);
56
106
  }
57
107
  get closeStyle() {
58
- return this.getAttribute("close-style") || "circle";
108
+ return this.getDokodemoAttr("close-style") || "circle";
59
109
  }
60
110
  set closeStyle(value) {
61
- this.setAttribute("close-style", value);
111
+ this.setAttribute("dokodemo-close-style", value);
62
112
  }
63
113
  get closePosition() {
64
- return this.getAttribute("close-position") || "inside";
114
+ return this.getDokodemoAttr("close-position") || "inside";
65
115
  }
66
116
  set closePosition(value) {
67
- this.setAttribute("close-position", value);
117
+ this.setAttribute("dokodemo-close-position", value);
68
118
  }
69
119
  get resizable() {
70
- return this.hasAttribute("resizable");
120
+ return this.hasDokodemoAttr("resizable");
71
121
  }
72
122
  set resizable(value) {
73
123
  if (value) {
74
- this.setAttribute("resizable", "");
124
+ this.setAttribute("dokodemo-resizable", "");
75
125
  }
76
126
  else {
127
+ this.removeAttribute("dokodemo-resizable");
77
128
  this.removeAttribute("resizable");
78
129
  }
79
130
  }
80
131
  get x() {
81
- const attr = this.getAttribute("x");
132
+ const attr = this.getDokodemoAttr("x");
82
133
  return attr ? parseInt(attr, 10) : null;
83
134
  }
84
135
  set x(value) {
85
136
  if (value !== null) {
86
- this.setAttribute("x", String(value));
137
+ this.setAttribute("dokodemo-x", String(value));
87
138
  this.style.left = `${value}px`;
88
139
  }
89
140
  else {
141
+ this.removeAttribute("dokodemo-x");
90
142
  this.removeAttribute("x");
91
143
  }
92
144
  }
93
145
  get y() {
94
- const attr = this.getAttribute("y");
146
+ const attr = this.getDokodemoAttr("y");
95
147
  return attr ? parseInt(attr, 10) : null;
96
148
  }
97
149
  set y(value) {
98
150
  if (value !== null) {
99
- this.setAttribute("y", String(value));
151
+ this.setAttribute("dokodemo-y", String(value));
100
152
  this.style.top = `${value}px`;
101
153
  }
102
154
  else {
155
+ this.removeAttribute("dokodemo-y");
103
156
  this.removeAttribute("y");
104
157
  }
105
158
  }
159
+ get target() {
160
+ return this.getDokodemoAttr("target");
161
+ }
162
+ set target(value) {
163
+ if (value) {
164
+ this.setAttribute("dokodemo-target", value);
165
+ }
166
+ else {
167
+ this.removeAttribute("dokodemo-target");
168
+ this.removeAttribute("target");
169
+ }
170
+ }
171
+ get mode() {
172
+ return this.getDokodemoAttr("mode") || "internal";
173
+ }
174
+ set mode(value) {
175
+ this.setAttribute("dokodemo-mode", value);
176
+ }
177
+ get isExternalMode() {
178
+ return this.mode === "external" && this.target !== null;
179
+ }
106
180
  constructor() {
107
181
  super();
108
182
  this.isDragging = false;
@@ -117,6 +191,19 @@ class DokodemoUI extends HTMLElement {
117
191
  this.positionInitialized = false;
118
192
  this.iframePointerEvents = new Map();
119
193
  this.edgeSize = 8; // 端からの距離(px)
194
+ this.targetElement = null;
195
+ this.externalObserver = null;
196
+ this.externalResizeObserver = null;
197
+ this.attributeForwardObserver = null;
198
+ this.syncWithTarget = () => {
199
+ if (!this.targetElement)
200
+ return;
201
+ const rect = this.targetElement.getBoundingClientRect();
202
+ this.style.left = `${rect.left}px`;
203
+ this.style.top = `${rect.top}px`;
204
+ this.style.width = `${rect.width}px`;
205
+ this.style.height = `${rect.height}px`;
206
+ };
120
207
  this.onMouseMoveForCursor = (e) => {
121
208
  if (this.isDragging || this.isResizing)
122
209
  return;
@@ -168,7 +255,9 @@ class DokodemoUI extends HTMLElement {
168
255
  if (!isEdgeOverlay && !this.isNearEdge(touch.clientX, touch.clientY))
169
256
  return;
170
257
  this.startDrag(touch.clientX, touch.clientY);
171
- document.addEventListener("touchmove", this.onTouchMove, { passive: false });
258
+ document.addEventListener("touchmove", this.onTouchMove, {
259
+ passive: false,
260
+ });
172
261
  document.addEventListener("touchend", this.onTouchEnd);
173
262
  };
174
263
  this.onMouseMove = (e) => {
@@ -206,7 +295,9 @@ class DokodemoUI extends HTMLElement {
206
295
  e.stopPropagation();
207
296
  const touch = e.touches[0];
208
297
  this.startResize(touch.clientX, touch.clientY);
209
- document.addEventListener("touchmove", this.onResizeTouchMove, { passive: false });
298
+ document.addEventListener("touchmove", this.onResizeTouchMove, {
299
+ passive: false,
300
+ });
210
301
  document.addEventListener("touchend", this.onResizeTouchEnd);
211
302
  };
212
303
  this.onResizeMouseMove = (e) => {
@@ -235,10 +326,217 @@ class DokodemoUI extends HTMLElement {
235
326
  this.attachShadow({ mode: "open" });
236
327
  }
237
328
  connectedCallback() {
238
- this.render();
329
+ // 既知の属性にプレフィックスを自動付与
330
+ this.normalizeAttributes();
331
+ if (this.isExternalMode) {
332
+ this.setupExternalMode();
333
+ }
334
+ else {
335
+ this.render();
336
+ this.setupDrag();
337
+ this.setupCloseButton();
338
+ this.setupResize();
339
+ }
340
+ }
341
+ // 既知の属性を dokodemo-* プレフィックス付きに正規化(元の属性は削除)
342
+ normalizeAttributes() {
343
+ for (const name of DOKODEMO_ATTRIBUTE_NAMES) {
344
+ if (this.hasAttribute(name)) {
345
+ const value = this.getAttribute(name);
346
+ if (!this.hasAttribute(`dokodemo-${name}`)) {
347
+ this.setAttribute(`dokodemo-${name}`, value || "");
348
+ }
349
+ this.removeAttribute(name);
350
+ }
351
+ }
352
+ }
353
+ setupExternalMode() {
354
+ // ターゲット要素を取得(まだなければ待機)
355
+ this.findTargetElement();
356
+ if (!this.targetElement) {
357
+ // 要素がまだ存在しない場合、MutationObserverで監視
358
+ this.externalObserver = new MutationObserver(() => {
359
+ if (this.findTargetElement()) {
360
+ this.externalObserver?.disconnect();
361
+ this.initExternalOverlay();
362
+ }
363
+ });
364
+ this.externalObserver.observe(document.body, {
365
+ childList: true,
366
+ subtree: true,
367
+ });
368
+ }
369
+ else {
370
+ this.initExternalOverlay();
371
+ }
372
+ }
373
+ findTargetElement() {
374
+ if (!this.target)
375
+ return false;
376
+ this.targetElement = document.querySelector(this.target);
377
+ return this.targetElement !== null;
378
+ }
379
+ initExternalOverlay() {
380
+ if (!this.targetElement || !this.shadowRoot)
381
+ return;
382
+ // オーバーレイとしてレンダリング
383
+ this.renderExternalOverlay();
384
+ this.syncWithTarget();
385
+ // ターゲット要素のサイズ変化を監視
386
+ this.externalResizeObserver = new ResizeObserver(() => {
387
+ this.syncWithTarget();
388
+ });
389
+ this.externalResizeObserver.observe(this.targetElement);
390
+ // ウィンドウリサイズにも対応
391
+ window.addEventListener("resize", this.syncWithTarget);
392
+ // 属性転送: 初期属性を転送
393
+ this.forwardAllAttributes();
394
+ // 属性転送: 属性変更を監視して転送
395
+ this.attributeForwardObserver = new MutationObserver((mutations) => {
396
+ for (const mutation of mutations) {
397
+ if (mutation.type === "attributes" && mutation.attributeName) {
398
+ this.forwardAttribute(mutation.attributeName);
399
+ }
400
+ }
401
+ });
402
+ this.attributeForwardObserver.observe(this, { attributes: true });
239
403
  this.setupDrag();
240
404
  this.setupCloseButton();
241
- this.setupResize();
405
+ }
406
+ // 全属性を転送
407
+ forwardAllAttributes() {
408
+ if (!this.targetElement)
409
+ return;
410
+ for (const attr of Array.from(this.attributes)) {
411
+ this.forwardAttribute(attr.name);
412
+ }
413
+ }
414
+ // 単一属性を転送
415
+ forwardAttribute(name) {
416
+ if (!this.targetElement)
417
+ return;
418
+ if (!shouldForwardAttribute(name))
419
+ return;
420
+ const value = this.getAttribute(name);
421
+ if (value !== null) {
422
+ this.targetElement.setAttribute(name, value);
423
+ }
424
+ else {
425
+ this.targetElement.removeAttribute(name);
426
+ }
427
+ }
428
+ renderExternalOverlay() {
429
+ if (!this.shadowRoot)
430
+ return;
431
+ this.shadowRoot.innerHTML = `
432
+ <style>
433
+ :host {
434
+ position: fixed;
435
+ user-select: none;
436
+ z-index: 10000;
437
+ pointer-events: none;
438
+ }
439
+ :host(.dragging) {
440
+ cursor: grabbing !important;
441
+ }
442
+ :host([hidden]) {
443
+ display: none !important;
444
+ }
445
+ .container {
446
+ position: relative;
447
+ width: 100%;
448
+ height: 100%;
449
+ }
450
+ .edge-overlay {
451
+ position: absolute;
452
+ pointer-events: auto;
453
+ cursor: grab;
454
+ }
455
+ .edge-overlay:active {
456
+ cursor: grabbing;
457
+ }
458
+ .edge-top {
459
+ top: 0;
460
+ left: 0;
461
+ right: 0;
462
+ height: 8px;
463
+ }
464
+ .edge-bottom {
465
+ bottom: 0;
466
+ left: 0;
467
+ right: 0;
468
+ height: 8px;
469
+ }
470
+ .edge-left {
471
+ top: 0;
472
+ bottom: 0;
473
+ left: 0;
474
+ width: 8px;
475
+ }
476
+ .edge-right {
477
+ top: 0;
478
+ bottom: 0;
479
+ right: 0;
480
+ width: 8px;
481
+ }
482
+ .close-button {
483
+ position: absolute;
484
+ top: -8px;
485
+ right: -8px;
486
+ border: none;
487
+ cursor: pointer;
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ transition: transform 0.1s, opacity 0.1s;
492
+ pointer-events: auto;
493
+ }
494
+ .close-button.circle {
495
+ width: 20px;
496
+ height: 20px;
497
+ border-radius: 50%;
498
+ background: ${this.closeColor};
499
+ color: white;
500
+ font-size: 14px;
501
+ line-height: 1;
502
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
503
+ }
504
+ .close-button.circle:hover {
505
+ filter: brightness(0.85);
506
+ transform: scale(1.1);
507
+ }
508
+ .close-button.simple {
509
+ width: 20px;
510
+ height: 20px;
511
+ top: 4px;
512
+ right: 4px;
513
+ background: transparent;
514
+ color: ${this.closeColor};
515
+ font-size: 18px;
516
+ font-weight: bold;
517
+ line-height: 1;
518
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3);
519
+ border-radius: 4px;
520
+ }
521
+ .close-button.simple.outside {
522
+ top: -20px;
523
+ right: -20px;
524
+ }
525
+ .close-button.simple:hover {
526
+ opacity: 0.7;
527
+ transform: scale(1.1);
528
+ }
529
+ </style>
530
+ <div class="container">
531
+ <div class="edge-overlay edge-top"></div>
532
+ <div class="edge-overlay edge-bottom"></div>
533
+ <div class="edge-overlay edge-left"></div>
534
+ <div class="edge-overlay edge-right"></div>
535
+ ${this.closable
536
+ ? `<button class="close-button ${this.closeStyle}${this.closePosition === "outside" ? " outside" : ""}" aria-label="閉じる">×</button>`
537
+ : ""}
538
+ </div>
539
+ `;
242
540
  }
243
541
  attributeChangedCallback() {
244
542
  this.render();
@@ -375,8 +673,10 @@ class DokodemoUI extends HTMLElement {
375
673
  <div class="edge-overlay edge-bottom"></div>
376
674
  <div class="edge-overlay edge-left"></div>
377
675
  <div class="edge-overlay edge-right"></div>
378
- ${this.closable ? `<button class="close-button ${this.closeStyle}${this.closePosition === 'outside' ? ' outside' : ''}" aria-label="閉じる">×</button>` : ''}
379
- ${this.resizable ? '<div class="resize-handle"></div>' : ''}
676
+ ${this.closable
677
+ ? `<button class="close-button ${this.closeStyle}${this.closePosition === "outside" ? " outside" : ""}" aria-label="閉じる">×</button>`
678
+ : ""}
679
+ ${this.resizable ? '<div class="resize-handle"></div>' : ""}
380
680
  </div>
381
681
  `;
382
682
  // 初期位置を設定
@@ -487,16 +787,31 @@ class DokodemoUI extends HTMLElement {
487
787
  closeButton.addEventListener("click", (e) => {
488
788
  e.stopPropagation();
489
789
  e.preventDefault();
490
- this.hidden = true;
790
+ if (this.isExternalMode && this.targetElement) {
791
+ // 外部モード: ターゲット要素を非表示
792
+ this.targetElement.style.setProperty("display", "none", "important");
793
+ this.hidden = true;
794
+ }
795
+ else {
796
+ this.hidden = true;
797
+ }
491
798
  this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
492
799
  });
493
800
  }
494
801
  }
495
802
  show() {
496
803
  this.hidden = false;
804
+ if (this.isExternalMode && this.targetElement) {
805
+ this.targetElement.style.removeProperty("display");
806
+ // 位置を同期
807
+ requestAnimationFrame(() => this.syncWithTarget());
808
+ }
497
809
  }
498
810
  hide() {
499
811
  this.hidden = true;
812
+ if (this.isExternalMode && this.targetElement) {
813
+ this.targetElement.style.setProperty("display", "none", "important");
814
+ }
500
815
  }
501
816
  startDrag(clientX, clientY) {
502
817
  this.isDragging = true;
@@ -523,16 +838,38 @@ class DokodemoUI extends HTMLElement {
523
838
  }
524
839
  updatePosition(clientX, clientY) {
525
840
  this.hasMoved = true;
526
- // ドラッグ中は left/top を使用(right/bottom をクリア)
527
- this.style.right = "";
528
- this.style.bottom = "";
529
841
  const newX = clientX - this.dragOffsetX;
530
842
  const newY = clientY - this.dragOffsetY;
531
- // 画面外に出ないように制限
532
- const maxX = window.innerWidth - this.offsetWidth;
533
- const maxY = window.innerHeight - this.offsetHeight;
534
- this.style.left = `${Math.max(0, Math.min(newX, maxX))}px`;
535
- this.style.top = `${Math.max(0, Math.min(newY, maxY))}px`;
843
+ if (this.isExternalMode && this.targetElement) {
844
+ // 外部モード: ターゲット要素の位置を直接更新
845
+ const targetWidth = this.targetElement.offsetWidth;
846
+ const targetHeight = this.targetElement.offsetHeight;
847
+ // 画面外に出ないように制限
848
+ const maxX = window.innerWidth - targetWidth;
849
+ const maxY = window.innerHeight - targetHeight;
850
+ const clampedX = Math.max(0, Math.min(newX, maxX));
851
+ const clampedY = Math.max(0, Math.min(newY, maxY));
852
+ // right/bottom で位置を更新(!important で上書き)
853
+ const newRight = window.innerWidth - clampedX - targetWidth;
854
+ const newBottom = window.innerHeight - clampedY - targetHeight;
855
+ this.targetElement.style.setProperty("right", `${newRight}px`, "important");
856
+ this.targetElement.style.setProperty("bottom", `${newBottom}px`, "important");
857
+ this.targetElement.style.setProperty("left", "auto", "important");
858
+ this.targetElement.style.setProperty("top", "auto", "important");
859
+ // オーバーレイも追従
860
+ this.style.left = `${clampedX}px`;
861
+ this.style.top = `${clampedY}px`;
862
+ }
863
+ else {
864
+ // 通常モード: 自身の位置を更新
865
+ this.style.right = "";
866
+ this.style.bottom = "";
867
+ // 画面外に出ないように制限
868
+ const maxX = window.innerWidth - this.offsetWidth;
869
+ const maxY = window.innerHeight - this.offsetHeight;
870
+ this.style.left = `${Math.max(0, Math.min(newX, maxX))}px`;
871
+ this.style.top = `${Math.max(0, Math.min(newY, maxY))}px`;
872
+ }
536
873
  }
537
874
  endDrag() {
538
875
  this.isDragging = false;
@@ -573,6 +910,20 @@ class DokodemoUI extends HTMLElement {
573
910
  this.removeEventListener("touchstart", this.onTouchStart);
574
911
  this.removeEventListener("mousemove", this.onMouseMoveForCursor);
575
912
  this.removeEventListener("mouseleave", this.onMouseLeaveForCursor);
913
+ // 外部モードのクリーンアップ
914
+ if (this.externalObserver) {
915
+ this.externalObserver.disconnect();
916
+ this.externalObserver = null;
917
+ }
918
+ if (this.externalResizeObserver) {
919
+ this.externalResizeObserver.disconnect();
920
+ this.externalResizeObserver = null;
921
+ }
922
+ if (this.attributeForwardObserver) {
923
+ this.attributeForwardObserver.disconnect();
924
+ this.attributeForwardObserver = null;
925
+ }
926
+ window.removeEventListener("resize", this.syncWithTarget);
576
927
  }
577
928
  }
578
929
  customElements.define("dokodemo-ui", DokodemoUI);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dokodemo-ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "ドラッグで自由に移動できるWeb Component",
5
5
  "main": "dokodemo-ui.js",
6
6
  "types": "dokodemo-ui.d.ts",