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 +40 -0
- package/dokodemo-ui.d.ts +19 -0
- package/dokodemo-ui.js +384 -33
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
379
|
-
|
|
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.
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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);
|