@ablogcms/modal 3.2.28-beta.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 +25 -0
- package/dist/acms.d.ts +6 -0
- package/dist/acms.mjs +1 -0
- package/dist/acms.mjs.map +1 -0
- package/dist/chunk-M67WUT4A.mjs +61 -0
- package/dist/chunk-M67WUT4A.mjs.map +1 -0
- package/dist/chunk-SOMGEYKM.mjs +62 -0
- package/dist/chunk-SOMGEYKM.mjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +404 -0
- package/dist/index.mjs.map +1 -0
- package/dist/modal-controller.d.ts +9 -0
- package/dist/store/content-fetcher.d.ts +12 -0
- package/dist/store/content-fetcher.mjs +11 -0
- package/dist/store/content-fetcher.mjs.map +1 -0
- package/dist/store/modal-manager.d.ts +64 -0
- package/dist/store/security-manager.d.ts +35 -0
- package/dist/store/security-manager.mjs +11 -0
- package/dist/store/security-manager.mjs.map +1 -0
- package/dist/store/use-modal-manager.d.ts +7 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @ablogcms/modal
|
|
2
|
+
|
|
3
|
+
`data-modal-*` 属性を持つトリガー要素のクリックを監視し、URL/セレクタからのコンテンツ取得や
|
|
4
|
+
フォーム submit と組み合わせてモーダルを開閉するパッケージ。
|
|
5
|
+
|
|
6
|
+
## 使い方
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { ModalController } from '@ablogcms/modal';
|
|
10
|
+
|
|
11
|
+
ModalController.openTrigger = '.js-acms-modal-open';
|
|
12
|
+
ModalController.closeTrigger = '.js-acms-modal-close';
|
|
13
|
+
render(<ModalController />, element);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<a class="js-acms-modal-open" data-url="/path/to/content" data-modal-size="large">開く</a>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `ModalController`: `openTrigger`/`closeTrigger`(セレクタ文字列)を静的プロパティに持つ
|
|
21
|
+
コンポーネント。1 度だけマウントすればページ全体のトリガーを監視する。
|
|
22
|
+
- `ModalManager`: `ModalController` が内部で使う状態管理クラス。
|
|
23
|
+
|
|
24
|
+
`./content-fetcher` / `./security-manager` は `@ablogcms/drawer` が同じロジックを再利用するための
|
|
25
|
+
sub-path で、通常の利用ではメインエントリだけで足りる。
|
package/dist/acms.d.ts
ADDED
package/dist/acms.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=acms.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/store/security-manager.ts
|
|
2
|
+
var defaultOptions = {
|
|
3
|
+
allowSameOrigin: true,
|
|
4
|
+
allowRelativeUrls: true,
|
|
5
|
+
allowedDomains: []
|
|
6
|
+
};
|
|
7
|
+
function validateUrl(url, config = defaultOptions) {
|
|
8
|
+
try {
|
|
9
|
+
const urlObj = new URL(url, window.location.href);
|
|
10
|
+
if (urlObj.protocol === "file:" && !config.allowRelativeUrls) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (urlObj.origin === window.location.origin) {
|
|
14
|
+
return config.allowSameOrigin;
|
|
15
|
+
}
|
|
16
|
+
if (config.allowedDomains && config.allowedDomains.length > 0) {
|
|
17
|
+
return config.allowedDomains.some((domain) => {
|
|
18
|
+
if (domain.startsWith("*.")) {
|
|
19
|
+
const domainSuffix = domain.slice(2);
|
|
20
|
+
return urlObj.hostname.endsWith(domainSuffix);
|
|
21
|
+
}
|
|
22
|
+
return urlObj.hostname === domain;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Invalid URL:", error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
var SecurityManager = class {
|
|
32
|
+
#config = { ...defaultOptions };
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.#config = { ...defaultOptions, ...options };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* セキュリティ設定を更新
|
|
38
|
+
*/
|
|
39
|
+
setConfig(newConfig) {
|
|
40
|
+
this.#config = { ...this.#config, ...newConfig };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 現在の設定を取得
|
|
44
|
+
*/
|
|
45
|
+
get config() {
|
|
46
|
+
return this.#config;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* URLを検証
|
|
50
|
+
*/
|
|
51
|
+
validateUrl(url) {
|
|
52
|
+
return validateUrl(url, this.#config);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
defaultOptions,
|
|
58
|
+
validateUrl,
|
|
59
|
+
SecurityManager
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=chunk-M67WUT4A.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/security-manager.ts"],"sourcesContent":["/**\n * URL検証の設定\n */\nexport interface SecurityConfig {\n allowedDomains?: string[];\n allowSameOrigin: boolean;\n allowRelativeUrls: boolean;\n}\n\n/**\n * デフォルトのセキュリティ設定\n */\nexport const defaultOptions: SecurityConfig = {\n allowSameOrigin: true,\n allowRelativeUrls: true,\n allowedDomains: [],\n};\n\n/**\n * URLが安全かどうかを検証する関数\n */\nexport function validateUrl(url: string, config: SecurityConfig = defaultOptions): boolean {\n try {\n const urlObj = new URL(url, window.location.href);\n\n // 相対URLの許可チェック\n if (urlObj.protocol === 'file:' && !config.allowRelativeUrls) {\n return false;\n }\n\n // 同一オリジンの許可チェック\n if (urlObj.origin === window.location.origin) {\n return config.allowSameOrigin;\n }\n\n // 許可されたドメインのチェック\n if (config.allowedDomains && config.allowedDomains.length > 0) {\n return config.allowedDomains.some((domain) => {\n // ワイルドカードサポート(例: *.example.com)\n if (domain.startsWith('*.')) {\n const domainSuffix = domain.slice(2);\n return urlObj.hostname.endsWith(domainSuffix);\n }\n return urlObj.hostname === domain;\n });\n }\n\n // 許可されたドメインが設定されていない場合は拒否\n return false;\n } catch (error) {\n console.error('Invalid URL:', error);\n return false;\n }\n}\n\n/**\n * セキュリティ設定を管理するクラス\n */\nexport default class SecurityManager {\n #config: SecurityConfig = { ...defaultOptions };\n\n constructor(options: Partial<SecurityConfig> = {}) {\n this.#config = { ...defaultOptions, ...options };\n }\n\n /**\n * セキュリティ設定を更新\n */\n setConfig(newConfig: Partial<SecurityConfig>) {\n this.#config = { ...this.#config, ...newConfig };\n }\n\n /**\n * 現在の設定を取得\n */\n public get config(): SecurityConfig {\n return this.#config;\n }\n\n /**\n * URLを検証\n */\n public validateUrl(url: string): boolean {\n return validateUrl(url, this.#config);\n }\n}\n"],"mappings":";AAYO,IAAM,iBAAiC;AAAA,EAC5C,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,gBAAgB,CAAC;AACnB;AAKO,SAAS,YAAY,KAAa,SAAyB,gBAAyB;AACzF,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK,OAAO,SAAS,IAAI;AAGhD,QAAI,OAAO,aAAa,WAAW,CAAC,OAAO,mBAAmB;AAC5D,aAAO;AAAA,IACT;AAGA,QAAI,OAAO,WAAW,OAAO,SAAS,QAAQ;AAC5C,aAAO,OAAO;AAAA,IAChB;AAGA,QAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,aAAO,OAAO,eAAe,KAAK,CAAC,WAAW;AAE5C,YAAI,OAAO,WAAW,IAAI,GAAG;AAC3B,gBAAM,eAAe,OAAO,MAAM,CAAC;AACnC,iBAAO,OAAO,SAAS,SAAS,YAAY;AAAA,QAC9C;AACA,eAAO,OAAO,aAAa;AAAA,MAC7B,CAAC;AAAA,IACH;AAGA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,gBAAgB,KAAK;AACnC,WAAO;AAAA,EACT;AACF;AAKA,IAAqB,kBAArB,MAAqC;AAAA,EACnC,UAA0B,EAAE,GAAG,eAAe;AAAA,EAE9C,YAAY,UAAmC,CAAC,GAAG;AACjD,SAAK,UAAU,EAAE,GAAG,gBAAgB,GAAG,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,WAAoC;AAC5C,SAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,UAAU;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,IAAW,SAAyB;AAClC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKO,YAAY,KAAsB;AACvC,WAAO,YAAY,KAAK,KAAK,OAAO;AAAA,EACtC;AACF;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/store/content-fetcher.ts
|
|
2
|
+
function patchFormActions(html, baseUrl) {
|
|
3
|
+
try {
|
|
4
|
+
const parser = new DOMParser();
|
|
5
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
6
|
+
doc.querySelectorAll("form").forEach((form) => {
|
|
7
|
+
form.setAttribute("action", baseUrl);
|
|
8
|
+
});
|
|
9
|
+
return doc.body.innerHTML;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error("Failed to patch form actions:", error);
|
|
12
|
+
return html;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function fetchContentFromUrl(url) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "text/html",
|
|
20
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
21
|
+
"X-Csrf-Token": window.csrfToken || ""
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
26
|
+
}
|
|
27
|
+
const contentType = response.headers.get("content-type");
|
|
28
|
+
if (contentType && !contentType.includes("text/html")) {
|
|
29
|
+
throw new Error(`Invalid content type: ${contentType}. Expected text/html`);
|
|
30
|
+
}
|
|
31
|
+
const html = await response.text();
|
|
32
|
+
return patchFormActions(html, url);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to fetch content from URL:", error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getContentFromSelector(selector) {
|
|
39
|
+
try {
|
|
40
|
+
const template = document.querySelector(selector);
|
|
41
|
+
if (!template) {
|
|
42
|
+
throw new Error(`Element not found: ${selector}`);
|
|
43
|
+
}
|
|
44
|
+
if (!(template instanceof HTMLTemplateElement)) {
|
|
45
|
+
throw new Error(`Element is not a template: ${selector}`);
|
|
46
|
+
}
|
|
47
|
+
const clone = template.content.cloneNode(true);
|
|
48
|
+
const div = document.createElement("div");
|
|
49
|
+
div.appendChild(clone);
|
|
50
|
+
return div.innerHTML;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Failed to get content from selector:", error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
patchFormActions,
|
|
59
|
+
fetchContentFromUrl,
|
|
60
|
+
getContentFromSelector
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=chunk-SOMGEYKM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/content-fetcher.ts"],"sourcesContent":["/**\n * フォームアクション補完機能\n */\nexport function patchFormActions(html: string, baseUrl: string): string {\n try {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n\n // formのactionを補完\n doc.querySelectorAll('form').forEach((form) => {\n form.setAttribute('action', baseUrl);\n });\n\n return doc.body.innerHTML;\n } catch (error) {\n console.error('Failed to patch form actions:', error);\n return html;\n }\n}\n\n/**\n * URL からのコンテンツ取得\n */\nexport async function fetchContentFromUrl(url: string): Promise<string> {\n try {\n const response = await fetch(url, {\n headers: {\n 'Content-Type': 'text/html',\n 'X-Requested-With': 'XMLHttpRequest',\n 'X-Csrf-Token': window.csrfToken || '',\n },\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n // Content-Typeの検証\n const contentType = response.headers.get('content-type');\n if (contentType && !contentType.includes('text/html')) {\n throw new Error(`Invalid content type: ${contentType}. Expected text/html`);\n }\n\n const html = await response.text();\n // フォームアクションの補完\n return patchFormActions(html, url);\n } catch (error) {\n console.error('Failed to fetch content from URL:', error);\n throw error;\n }\n}\n\n/**\n * セレクタからのコンテンツ取得\n */\nexport function getContentFromSelector(selector: string): string {\n try {\n const template = document.querySelector(selector);\n\n if (!template) {\n throw new Error(`Element not found: ${selector}`);\n }\n\n if (!(template instanceof HTMLTemplateElement)) {\n throw new Error(`Element is not a template: ${selector}`);\n }\n\n const clone = template.content.cloneNode(true) as DocumentFragment;\n const div = document.createElement('div');\n div.appendChild(clone);\n return div.innerHTML;\n } catch (error) {\n console.error('Failed to get content from selector:', error);\n throw error;\n }\n}\n"],"mappings":";AAGO,SAAS,iBAAiB,MAAc,SAAyB;AACtE,MAAI;AACF,UAAM,SAAS,IAAI,UAAU;AAC7B,UAAM,MAAM,OAAO,gBAAgB,MAAM,WAAW;AAGpD,QAAI,iBAAiB,MAAM,EAAE,QAAQ,CAAC,SAAS;AAC7C,WAAK,aAAa,UAAU,OAAO;AAAA,IACrC,CAAC;AAED,WAAO,IAAI,KAAK;AAAA,EAClB,SAAS,OAAO;AACd,YAAQ,MAAM,iCAAiC,KAAK;AACpD,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,oBAAoB,KAA8B;AACtE,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,oBAAoB;AAAA,QACpB,gBAAgB,OAAO,aAAa;AAAA,MACtC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,uBAAuB,SAAS,MAAM,EAAE;AAAA,IAC1D;AAGA,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;AACvD,QAAI,eAAe,CAAC,YAAY,SAAS,WAAW,GAAG;AACrD,YAAM,IAAI,MAAM,yBAAyB,WAAW,sBAAsB;AAAA,IAC5E;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,WAAO,iBAAiB,MAAM,GAAG;AAAA,EACnC,SAAS,OAAO;AACd,YAAQ,MAAM,qCAAqC,KAAK;AACxD,UAAM;AAAA,EACR;AACF;AAKO,SAAS,uBAAuB,UAA0B;AAC/D,MAAI;AACF,UAAM,WAAW,SAAS,cAAc,QAAQ;AAEhD,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,sBAAsB,QAAQ,EAAE;AAAA,IAClD;AAEA,QAAI,EAAE,oBAAoB,sBAAsB;AAC9C,YAAM,IAAI,MAAM,8BAA8B,QAAQ,EAAE;AAAA,IAC1D;AAEA,UAAM,QAAQ,SAAS,QAAQ,UAAU,IAAI;AAC7C,UAAM,MAAM,SAAS,cAAc,KAAK;AACxC,QAAI,YAAY,KAAK;AACrB,WAAO,IAAI;AAAA,EACb,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC,KAAK;AAC3D,UAAM;AAAA,EACR;AACF;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as ModalController } from './modal-controller.js';
|
|
2
|
+
export type { ModalControllerProps } from './modal-controller.js';
|
|
3
|
+
export { default as ModalManager } from './store/modal-manager.js';
|
|
4
|
+
export type { ModalManagerOptions } from './store/modal-manager.js';
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchContentFromUrl,
|
|
3
|
+
getContentFromSelector,
|
|
4
|
+
patchFormActions
|
|
5
|
+
} from "./chunk-SOMGEYKM.mjs";
|
|
6
|
+
import {
|
|
7
|
+
SecurityManager
|
|
8
|
+
} from "./chunk-M67WUT4A.mjs";
|
|
9
|
+
|
|
10
|
+
// src/modal-controller.tsx
|
|
11
|
+
import { useCallback, useEffect, useRef as useRef2 } from "react";
|
|
12
|
+
import { emit } from "@ablogcms/events";
|
|
13
|
+
import useUpdateEffect from "@ablogcms/react-hooks/use-update-effect";
|
|
14
|
+
import { datasetToProps } from "@ablogcms/utils/react";
|
|
15
|
+
import Modal, { ModalBody, ModalFooter, ModalHeader } from "@ablogcms/components/modal";
|
|
16
|
+
|
|
17
|
+
// src/store/use-modal-manager.ts
|
|
18
|
+
import { useRef, useState, useSyncExternalStore } from "react";
|
|
19
|
+
|
|
20
|
+
// src/store/modal-manager.tsx
|
|
21
|
+
import { pending } from "@ablogcms/pending";
|
|
22
|
+
var defaultOptions = {
|
|
23
|
+
header: '[data-modal-section="header"]',
|
|
24
|
+
body: '[data-modal-section="body"]',
|
|
25
|
+
footer: '[data-modal-section="footer"]',
|
|
26
|
+
security: {}
|
|
27
|
+
};
|
|
28
|
+
var ModalManager = class {
|
|
29
|
+
listeners = /* @__PURE__ */ new Set();
|
|
30
|
+
state = {
|
|
31
|
+
content: {
|
|
32
|
+
raw: ""
|
|
33
|
+
},
|
|
34
|
+
modalProps: {
|
|
35
|
+
isOpen: false
|
|
36
|
+
},
|
|
37
|
+
modalHeaderProps: {},
|
|
38
|
+
modalBodyProps: {},
|
|
39
|
+
modalFooterProps: {}
|
|
40
|
+
};
|
|
41
|
+
#container = null;
|
|
42
|
+
config = defaultOptions;
|
|
43
|
+
securityManager;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.config = { ...defaultOptions, ...options.current };
|
|
46
|
+
this.securityManager = new SecurityManager(this.config.security);
|
|
47
|
+
this.handleSubmit = this.handleSubmit.bind(this);
|
|
48
|
+
this.open = this.open.bind(this);
|
|
49
|
+
this.close = this.close.bind(this);
|
|
50
|
+
this.reset = this.reset.bind(this);
|
|
51
|
+
this.init();
|
|
52
|
+
}
|
|
53
|
+
subscribe = (callback) => {
|
|
54
|
+
this.listeners.add(callback);
|
|
55
|
+
return () => this.listeners.delete(callback);
|
|
56
|
+
};
|
|
57
|
+
getSnapshot = () => this.state;
|
|
58
|
+
setState(state) {
|
|
59
|
+
if (typeof state === "function") {
|
|
60
|
+
this.state = state(this.state);
|
|
61
|
+
} else {
|
|
62
|
+
this.state = state;
|
|
63
|
+
}
|
|
64
|
+
this.emit();
|
|
65
|
+
}
|
|
66
|
+
get content() {
|
|
67
|
+
return this.state.content;
|
|
68
|
+
}
|
|
69
|
+
get modalProps() {
|
|
70
|
+
return this.state.modalProps;
|
|
71
|
+
}
|
|
72
|
+
get modalHeaderProps() {
|
|
73
|
+
return this.state.modalHeaderProps;
|
|
74
|
+
}
|
|
75
|
+
get modalBodyProps() {
|
|
76
|
+
return this.state.modalBodyProps;
|
|
77
|
+
}
|
|
78
|
+
get modalFooterProps() {
|
|
79
|
+
return this.state.modalFooterProps;
|
|
80
|
+
}
|
|
81
|
+
get container() {
|
|
82
|
+
return this.#container;
|
|
83
|
+
}
|
|
84
|
+
init() {
|
|
85
|
+
const container = document.getElementById("acms-modal-manager-container");
|
|
86
|
+
if (container !== null) {
|
|
87
|
+
this.#container = container;
|
|
88
|
+
} else {
|
|
89
|
+
this.#container = document.createElement("div");
|
|
90
|
+
this.#container.id = "acms-modal-manager-container";
|
|
91
|
+
document.body.appendChild(this.#container);
|
|
92
|
+
}
|
|
93
|
+
this.registerEventListeners();
|
|
94
|
+
}
|
|
95
|
+
async open(options = {}) {
|
|
96
|
+
const removeSplash = pending.splash(ACMS.i18n("splash.default"));
|
|
97
|
+
try {
|
|
98
|
+
const element = await this.fetchContent(options);
|
|
99
|
+
const content = this.parseContent(element);
|
|
100
|
+
this.setState({
|
|
101
|
+
content,
|
|
102
|
+
modalProps: {
|
|
103
|
+
...options.modalProps,
|
|
104
|
+
isOpen: true
|
|
105
|
+
},
|
|
106
|
+
modalHeaderProps: {
|
|
107
|
+
...options.modalHeaderProps
|
|
108
|
+
},
|
|
109
|
+
modalBodyProps: {
|
|
110
|
+
...options.modalBodyProps
|
|
111
|
+
},
|
|
112
|
+
modalFooterProps: {
|
|
113
|
+
...options.modalFooterProps
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Failed to open modal:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
} finally {
|
|
120
|
+
removeSplash();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
close() {
|
|
124
|
+
this.setState((prev) => ({
|
|
125
|
+
...prev,
|
|
126
|
+
modalProps: {
|
|
127
|
+
...prev.modalProps,
|
|
128
|
+
isOpen: false
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
reset() {
|
|
133
|
+
this.setState({
|
|
134
|
+
content: {
|
|
135
|
+
raw: ""
|
|
136
|
+
},
|
|
137
|
+
modalProps: {
|
|
138
|
+
isOpen: false
|
|
139
|
+
},
|
|
140
|
+
modalHeaderProps: {},
|
|
141
|
+
modalBodyProps: {},
|
|
142
|
+
modalFooterProps: {}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
emit = () => {
|
|
146
|
+
this.listeners.forEach((cb) => cb());
|
|
147
|
+
};
|
|
148
|
+
// クリーンアップ用メソッド
|
|
149
|
+
destroy() {
|
|
150
|
+
this.unregisterEventListeners();
|
|
151
|
+
if (this.#container) {
|
|
152
|
+
this.#container.remove();
|
|
153
|
+
this.#container = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async handleSubmit(event) {
|
|
157
|
+
if (event.defaultPrevented) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
const form = event.target;
|
|
162
|
+
const { submitter } = event;
|
|
163
|
+
const formData = new FormData(form, submitter);
|
|
164
|
+
let isSuccess = false;
|
|
165
|
+
const removeSplash = pending.splash(ACMS.i18n("splash.save"));
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(form.action, {
|
|
168
|
+
method: form.method,
|
|
169
|
+
body: formData,
|
|
170
|
+
headers: {
|
|
171
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
172
|
+
"X-Csrf-Token": window.csrfToken || ""
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
177
|
+
}
|
|
178
|
+
const html = await response.text();
|
|
179
|
+
isSuccess = this.checkSuccessFromContent(html);
|
|
180
|
+
const content = this.parseContent(patchFormActions(html, form.action));
|
|
181
|
+
this.setState((prev) => ({
|
|
182
|
+
...prev,
|
|
183
|
+
content
|
|
184
|
+
}));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("Failed to submit form:", error);
|
|
187
|
+
} finally {
|
|
188
|
+
removeSplash();
|
|
189
|
+
if (isSuccess) {
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
if (confirm(ACMS.i18n("modal.reload"))) {
|
|
192
|
+
window.location.reload();
|
|
193
|
+
}
|
|
194
|
+
}, 500);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
checkSuccessFromContent(html) {
|
|
199
|
+
const parser = new DOMParser();
|
|
200
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
201
|
+
const successSelectors = [
|
|
202
|
+
".acms-admin-alert-info",
|
|
203
|
+
// 成功アラート
|
|
204
|
+
".acms-admin-alert-success",
|
|
205
|
+
// 成功アラート
|
|
206
|
+
'[data-status="success"]',
|
|
207
|
+
// 成功ステータス
|
|
208
|
+
".success-message"
|
|
209
|
+
// 成功メッセージ
|
|
210
|
+
];
|
|
211
|
+
const errorSelectors = [
|
|
212
|
+
".acms-admin-alert-danger",
|
|
213
|
+
// エラーアラート
|
|
214
|
+
".acms-admin-alert-error",
|
|
215
|
+
// エラーアラート
|
|
216
|
+
'[data-status="error"]',
|
|
217
|
+
// エラーステータス
|
|
218
|
+
".error-message"
|
|
219
|
+
// エラーメッセージ
|
|
220
|
+
];
|
|
221
|
+
for (const selector of errorSelectors) {
|
|
222
|
+
if (doc.querySelector(selector)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const selector of successSelectors) {
|
|
227
|
+
if (doc.querySelector(selector)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
registerEventListeners() {
|
|
234
|
+
this.#container?.addEventListener("submit", this.handleSubmit);
|
|
235
|
+
}
|
|
236
|
+
unregisterEventListeners() {
|
|
237
|
+
this.#container?.removeEventListener("submit", this.handleSubmit);
|
|
238
|
+
}
|
|
239
|
+
parseContent(htmlContent) {
|
|
240
|
+
const { header, body, footer } = this.config;
|
|
241
|
+
const result = {
|
|
242
|
+
raw: htmlContent
|
|
243
|
+
};
|
|
244
|
+
if (!header && !body && !footer) {
|
|
245
|
+
result.body = htmlContent;
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
const div = document.createElement("div");
|
|
249
|
+
div.innerHTML = htmlContent;
|
|
250
|
+
if (header) {
|
|
251
|
+
const headerElement = div.querySelector(header);
|
|
252
|
+
if (headerElement) {
|
|
253
|
+
result.header = headerElement.innerHTML;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (body) {
|
|
257
|
+
const bodyElement = div.querySelector(body);
|
|
258
|
+
if (bodyElement) {
|
|
259
|
+
result.body = bodyElement.innerHTML;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (footer) {
|
|
263
|
+
const footerElement = div.querySelector(footer);
|
|
264
|
+
if (footerElement) {
|
|
265
|
+
result.footer = footerElement.innerHTML;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!result.header && !result.body && !result.footer) {
|
|
269
|
+
result.body = htmlContent;
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
fetchContent(options) {
|
|
274
|
+
const { selector, url } = options;
|
|
275
|
+
if (selector) {
|
|
276
|
+
return getContentFromSelector(selector);
|
|
277
|
+
}
|
|
278
|
+
if (url) {
|
|
279
|
+
if (!this.securityManager.validateUrl(url)) {
|
|
280
|
+
throw new Error(`URL is not allowed: ${url}`);
|
|
281
|
+
}
|
|
282
|
+
return fetchContentFromUrl(url);
|
|
283
|
+
}
|
|
284
|
+
throw new Error("No selector or URL provided");
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var modal_manager_default = ModalManager;
|
|
288
|
+
|
|
289
|
+
// src/store/use-modal-manager.ts
|
|
290
|
+
function useModalManager(options = {}) {
|
|
291
|
+
const mostRecentOptions = useRef(options);
|
|
292
|
+
mostRecentOptions.current = options;
|
|
293
|
+
const [manager] = useState(() => new modal_manager_default(mostRecentOptions));
|
|
294
|
+
const state = useSyncExternalStore(
|
|
295
|
+
manager.subscribe,
|
|
296
|
+
() => manager.getSnapshot(),
|
|
297
|
+
() => manager.getSnapshot()
|
|
298
|
+
);
|
|
299
|
+
ACMS.Library.modal = manager;
|
|
300
|
+
return {
|
|
301
|
+
state,
|
|
302
|
+
manager
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/modal-controller.tsx
|
|
307
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
308
|
+
function parseProps(element, type) {
|
|
309
|
+
const props = datasetToProps(element.dataset, type);
|
|
310
|
+
return props;
|
|
311
|
+
}
|
|
312
|
+
function ModalController({ ...props }) {
|
|
313
|
+
const { manager, state } = useModalManager(props);
|
|
314
|
+
const openStateRef = useRef2(state.modalProps.isOpen);
|
|
315
|
+
const hydrateModalContent = useCallback(() => {
|
|
316
|
+
if (manager.container) {
|
|
317
|
+
ACMS.Dispatch(manager.container);
|
|
318
|
+
void emit("dialog.open.after", { element: manager.container });
|
|
319
|
+
ACMS.dispatchEvent("acmsDialogOpened", manager.container, {
|
|
320
|
+
item: manager.container
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}, [manager.container]);
|
|
324
|
+
const handleAfterOpen = useCallback(() => {
|
|
325
|
+
openStateRef.current = true;
|
|
326
|
+
hydrateModalContent();
|
|
327
|
+
}, [hydrateModalContent]);
|
|
328
|
+
const handleAfterClose = useCallback(() => {
|
|
329
|
+
openStateRef.current = false;
|
|
330
|
+
manager.reset();
|
|
331
|
+
}, [manager]);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
const handleOpen = (event) => {
|
|
334
|
+
if (!(event.target instanceof HTMLElement)) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const element = event.target.closest(ModalController.openTrigger);
|
|
338
|
+
if (!element) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const { target: selector, url } = element.dataset;
|
|
342
|
+
const modalProps = parseProps(element, "modal");
|
|
343
|
+
const modalHeaderProps = parseProps(element, "modal-header");
|
|
344
|
+
const modalBodyProps = parseProps(element, "modal-body");
|
|
345
|
+
const modalFooterProps = parseProps(element, "modal-footer");
|
|
346
|
+
manager.open({
|
|
347
|
+
selector,
|
|
348
|
+
url,
|
|
349
|
+
modalProps,
|
|
350
|
+
modalHeaderProps,
|
|
351
|
+
modalBodyProps,
|
|
352
|
+
modalFooterProps
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
const handleClose = (event) => {
|
|
356
|
+
if (!(event.target instanceof HTMLElement)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const element = event.target.closest(ModalController.closeTrigger);
|
|
360
|
+
if (!element) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
manager.close();
|
|
364
|
+
};
|
|
365
|
+
document.addEventListener("click", handleOpen);
|
|
366
|
+
document.addEventListener("click", handleClose);
|
|
367
|
+
return () => {
|
|
368
|
+
document.removeEventListener("click", handleOpen);
|
|
369
|
+
document.removeEventListener("click", handleClose);
|
|
370
|
+
};
|
|
371
|
+
}, [manager]);
|
|
372
|
+
useUpdateEffect(() => {
|
|
373
|
+
if (!openStateRef.current) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
hydrateModalContent();
|
|
377
|
+
}, [state.content.raw]);
|
|
378
|
+
if (!manager.container) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return /* @__PURE__ */ jsxs(
|
|
382
|
+
Modal,
|
|
383
|
+
{
|
|
384
|
+
...state.modalProps,
|
|
385
|
+
onClose: manager.close,
|
|
386
|
+
container: manager.container,
|
|
387
|
+
onAfterOpen: handleAfterOpen,
|
|
388
|
+
onAfterClose: handleAfterClose,
|
|
389
|
+
children: [
|
|
390
|
+
state.content.header && /* @__PURE__ */ jsx(ModalHeader, { ...state.modalHeaderProps, children: /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: state.content.header } }) }),
|
|
391
|
+
/* @__PURE__ */ jsx(ModalBody, { ...state.modalBodyProps, children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: state.content.body || state.content.raw } }) }),
|
|
392
|
+
state.content.footer && /* @__PURE__ */ jsx(ModalFooter, { ...state.modalFooterProps, children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: state.content.footer } }) })
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
ModalController.openTrigger = ".js-acms-modal-open";
|
|
398
|
+
ModalController.closeTrigger = ".js-acms-modal-close";
|
|
399
|
+
var modal_controller_default = ModalController;
|
|
400
|
+
export {
|
|
401
|
+
modal_controller_default as ModalController,
|
|
402
|
+
modal_manager_default as ModalManager
|
|
403
|
+
};
|
|
404
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/modal-controller.tsx","../src/store/use-modal-manager.ts","../src/store/modal-manager.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport { emit } from '@ablogcms/events';\nimport useUpdateEffect from '@ablogcms/react-hooks/use-update-effect';\nimport { datasetToProps } from '@ablogcms/utils/react';\nimport Modal, { ModalBody, ModalFooter, ModalHeader } from '@ablogcms/components/modal';\nimport useModalManager, { UseModalManagerOptions } from './store/use-modal-manager';\n\ntype ModalProps = React.ComponentProps<typeof Modal>;\ntype ModalHeaderProps = React.ComponentProps<typeof ModalHeader>;\ntype ModalBodyProps = React.ComponentProps<typeof ModalBody>;\ntype ModalFooterProps = React.ComponentProps<typeof ModalFooter>;\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ModalControllerProps extends UseModalManagerOptions {}\n\n/**\n * data-modal-*属性を動的に取得する関数\n */\nfunction parseProps<T extends 'modal' | 'modal-header' | 'modal-body' | 'modal-footer'>(\n element: HTMLElement,\n type: T\n): Partial<\n T extends 'modal'\n ? ModalProps\n : T extends 'modal-header'\n ? ModalHeaderProps\n : T extends 'modal-body'\n ? ModalBodyProps\n : ModalFooterProps\n> {\n const props = datasetToProps(element.dataset, type);\n return props as Partial<\n T extends 'modal'\n ? ModalProps\n : T extends 'modal-header'\n ? ModalHeaderProps\n : T extends 'modal-body'\n ? ModalBodyProps\n : ModalFooterProps\n >;\n}\n\nfunction ModalController({ ...props }: ModalControllerProps) {\n const { manager, state } = useModalManager(props);\n const openStateRef = useRef<boolean>(state.modalProps.isOpen);\n\n const hydrateModalContent = useCallback(() => {\n if (manager.container) {\n ACMS.Dispatch(manager.container);\n void emit('dialog.open.after', { element: manager.container });\n ACMS.dispatchEvent('acmsDialogOpened', manager.container, {\n item: manager.container,\n });\n }\n }, [manager.container]);\n\n const handleAfterOpen = useCallback(() => {\n openStateRef.current = true;\n hydrateModalContent();\n }, [hydrateModalContent]);\n\n const handleAfterClose = useCallback(() => {\n openStateRef.current = false;\n // 閉じるトランジション完了後に state をクリアする\n manager.reset();\n }, [manager]);\n\n useEffect(() => {\n const handleOpen = (event: Event) => {\n if (!(event.target instanceof HTMLElement)) {\n return;\n }\n\n const element = event.target.closest<HTMLElement>(ModalController.openTrigger);\n\n if (!element) {\n return;\n }\n\n const { target: selector, url } = element.dataset;\n\n // Modal用 props とセレクタ情報を data-* から取得\n const modalProps = parseProps(element, 'modal');\n const modalHeaderProps = parseProps(element, 'modal-header');\n const modalBodyProps = parseProps(element, 'modal-body');\n const modalFooterProps = parseProps(element, 'modal-footer');\n\n // ModalManagerでモーダルを開く\n manager.open({\n selector,\n url,\n modalProps,\n modalHeaderProps,\n modalBodyProps,\n modalFooterProps,\n });\n };\n\n const handleClose = (event: Event) => {\n if (!(event.target instanceof HTMLElement)) {\n return;\n }\n\n const element = event.target.closest<HTMLElement>(ModalController.closeTrigger);\n\n if (!element) {\n return;\n }\n\n manager.close();\n };\n document.addEventListener('click', handleOpen);\n document.addEventListener('click', handleClose);\n\n return () => {\n document.removeEventListener('click', handleOpen);\n document.removeEventListener('click', handleClose);\n };\n }, [manager]);\n\n useUpdateEffect(() => {\n if (!openStateRef.current) {\n // モーダルが開いた初回はonAfterOpenでhydrateするためスキップ\n return;\n }\n\n hydrateModalContent();\n // コンテンツが変更されたら組み込みJSを実行\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state.content.raw]);\n\n if (!manager.container) {\n return null;\n }\n\n return (\n <Modal\n {...state.modalProps}\n onClose={manager.close}\n container={manager.container}\n onAfterOpen={handleAfterOpen}\n onAfterClose={handleAfterClose}\n >\n {state.content.header && (\n <ModalHeader {...state.modalHeaderProps}>\n {}\n <span dangerouslySetInnerHTML={{ __html: state.content.header }} />\n </ModalHeader>\n )}\n <ModalBody {...state.modalBodyProps}>\n {}\n <div dangerouslySetInnerHTML={{ __html: state.content.body || state.content.raw }} />\n </ModalBody>\n {state.content.footer && (\n <ModalFooter {...state.modalFooterProps}>\n {}\n <div dangerouslySetInnerHTML={{ __html: state.content.footer }} />\n </ModalFooter>\n )}\n </Modal>\n );\n}\n\nModalController.openTrigger = '.js-acms-modal-open';\nModalController.closeTrigger = '.js-acms-modal-close';\n\nexport default ModalController;\n","import { useRef, useState, useSyncExternalStore } from 'react';\nimport ModalManager, { ModalManagerOptions } from './modal-manager';\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface UseModalManagerOptions extends Partial<ModalManagerOptions> {}\n\nexport default function useModalManager(options: UseModalManagerOptions = {}) {\n const mostRecentOptions = useRef(options);\n\n mostRecentOptions.current = options;\n const [manager] = useState(() => new ModalManager(mostRecentOptions));\n\n const state = useSyncExternalStore(\n manager.subscribe,\n () => manager.getSnapshot(),\n () => manager.getSnapshot()\n );\n\n ACMS.Library.modal = manager;\n\n return {\n state,\n manager,\n };\n}\n","import { pending } from '@ablogcms/pending';\nimport type Modal from '@ablogcms/components/modal';\nimport type { ModalBody, ModalFooter, ModalHeader } from '@ablogcms/components/modal';\nimport { fetchContentFromUrl, getContentFromSelector, patchFormActions } from './content-fetcher';\nimport SecurityManager, { SecurityConfig } from './security-manager';\n\ntype ModalProps = Omit<React.ComponentProps<typeof Modal>, 'onClose'>;\ntype ModalHeaderProps = Omit<React.ComponentProps<typeof ModalHeader>, 'children'>;\ntype ModalBodyProps = Omit<React.ComponentProps<typeof ModalBody>, 'children'>;\ntype ModalFooterProps = Omit<React.ComponentProps<typeof ModalFooter>, 'children'>;\n\nexport interface ModalManagerState {\n content: Content;\n modalProps: ModalProps;\n modalHeaderProps: ModalHeaderProps;\n modalBodyProps: ModalBodyProps;\n modalFooterProps: ModalFooterProps;\n}\n\nexport interface ModalManagerOptions {\n header: string;\n body: string;\n footer: string;\n security: Partial<SecurityConfig>;\n}\n\ninterface Content {\n header?: string;\n body?: string;\n footer?: string;\n raw: string;\n}\n\ninterface ModalManagerOpenOptions {\n selector?: string;\n url?: string;\n modalProps?: Partial<ModalProps>;\n modalHeaderProps?: Partial<ModalHeaderProps>;\n modalBodyProps?: Partial<ModalBodyProps>;\n modalFooterProps?: Partial<ModalFooterProps>;\n}\n\nconst defaultOptions: ModalManagerOptions = {\n header: '[data-modal-section=\"header\"]',\n body: '[data-modal-section=\"body\"]',\n footer: '[data-modal-section=\"footer\"]',\n security: {},\n};\n\nclass ModalManager {\n private listeners = new Set<() => void>();\n\n private state: ModalManagerState = {\n content: {\n raw: '',\n },\n modalProps: {\n isOpen: false,\n },\n modalHeaderProps: {},\n modalBodyProps: {},\n modalFooterProps: {},\n };\n\n #container: HTMLElement | null = null;\n\n private config: ModalManagerOptions = defaultOptions;\n\n private securityManager: SecurityManager;\n\n constructor(options: React.MutableRefObject<Partial<ModalManagerOptions>>) {\n this.config = { ...defaultOptions, ...options.current };\n this.securityManager = new SecurityManager(this.config.security);\n this.handleSubmit = this.handleSubmit.bind(this);\n this.open = this.open.bind(this);\n this.close = this.close.bind(this);\n this.reset = this.reset.bind(this);\n this.init();\n }\n\n subscribe = (callback: () => void) => {\n this.listeners.add(callback);\n return () => this.listeners.delete(callback);\n };\n\n getSnapshot = () => this.state;\n\n public setState(state: ModalManagerState | ((state: ModalManagerState) => ModalManagerState)) {\n if (typeof state === 'function') {\n this.state = state(this.state);\n } else {\n this.state = state;\n }\n this.emit();\n }\n\n public get content() {\n return this.state.content;\n }\n\n public get modalProps() {\n return this.state.modalProps;\n }\n\n public get modalHeaderProps() {\n return this.state.modalHeaderProps;\n }\n\n public get modalBodyProps() {\n return this.state.modalBodyProps;\n }\n\n public get modalFooterProps() {\n return this.state.modalFooterProps;\n }\n\n public get container() {\n return this.#container;\n }\n\n private init() {\n // モーダル用のコンテナ要素を作成\n const container = document.getElementById('acms-modal-manager-container');\n if (container !== null) {\n this.#container = container;\n } else {\n this.#container = document.createElement('div');\n this.#container.id = 'acms-modal-manager-container';\n document.body.appendChild(this.#container);\n }\n this.registerEventListeners();\n }\n\n async open(options: ModalManagerOpenOptions = {}) {\n const removeSplash = pending.splash(ACMS.i18n('splash.default'));\n try {\n // コンテンツ取得ユーティリティを使用\n const element = await this.fetchContent(options);\n\n // コンテンツをパース\n const content = this.parseContent(element);\n\n // 状態を更新\n this.setState({\n content,\n modalProps: {\n ...options.modalProps,\n isOpen: true,\n },\n modalHeaderProps: {\n ...options.modalHeaderProps,\n },\n modalBodyProps: {\n ...options.modalBodyProps,\n },\n modalFooterProps: {\n ...options.modalFooterProps,\n },\n });\n } catch (error) {\n console.error('Failed to open modal:', error);\n // エラーを再スローして呼び出し元でハンドリングできるようにする\n throw error;\n } finally {\n removeSplash();\n }\n }\n\n close() {\n // 閉じるトランジション中は size などの modalProps を保持する必要があるため、\n // isOpen のみを切り替える。完全な初期化は reset() を onAfterClose 経由で呼ぶ\n this.setState((prev) => ({\n ...prev,\n modalProps: {\n ...prev.modalProps,\n isOpen: false,\n },\n }));\n }\n\n reset() {\n // 閉じるトランジション完了後に呼ばれることを想定し、コンテンツと props をクリアする\n this.setState({\n content: {\n raw: '',\n },\n modalProps: {\n isOpen: false,\n },\n modalHeaderProps: {},\n modalBodyProps: {},\n modalFooterProps: {},\n });\n }\n\n private emit = () => {\n this.listeners.forEach((cb) => cb());\n };\n\n // クリーンアップ用メソッド\n destroy() {\n this.unregisterEventListeners();\n if (this.#container) {\n this.#container.remove();\n this.#container = null;\n }\n }\n\n private async handleSubmit(event: SubmitEvent) {\n if (event.defaultPrevented) {\n // バリデーターがフォームの送信をキャンセルしている場合は、何もしない\n return;\n }\n event.preventDefault();\n const form = event.target as HTMLFormElement;\n const { submitter } = event;\n const formData = new FormData(form, submitter);\n\n let isSuccess = false;\n\n const removeSplash = pending.splash(ACMS.i18n('splash.save'));\n try {\n const response = await fetch(form.action, {\n method: form.method,\n body: formData,\n headers: {\n 'X-Requested-With': 'XMLHttpRequest',\n 'X-Csrf-Token': window.csrfToken || '',\n },\n });\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n const html = await response.text();\n isSuccess = this.checkSuccessFromContent(html);\n const content = this.parseContent(patchFormActions(html, form.action));\n this.setState((prev) => ({\n ...prev,\n content,\n }));\n } catch (error) {\n console.error('Failed to submit form:', error);\n } finally {\n removeSplash();\n if (isSuccess) {\n setTimeout(() => {\n // 成功メッセージの表示が完了してから確認ダイアログを表示\n if (confirm(ACMS.i18n('modal.reload'))) {\n window.location.reload();\n }\n }, 500);\n }\n }\n }\n\n private checkSuccessFromContent(html: string): boolean {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n\n // 成功メッセージのセレクタを確認\n const successSelectors = [\n '.acms-admin-alert-info', // 成功アラート\n '.acms-admin-alert-success', // 成功アラート\n '[data-status=\"success\"]', // 成功ステータス\n '.success-message', // 成功メッセージ\n ];\n\n // エラーメッセージのセレクタを確認\n const errorSelectors = [\n '.acms-admin-alert-danger', // エラーアラート\n '.acms-admin-alert-error', // エラーアラート\n '[data-status=\"error\"]', // エラーステータス\n '.error-message', // エラーメッセージ\n ];\n\n // エラーメッセージが存在する場合は失敗\n for (const selector of errorSelectors) {\n if (doc.querySelector(selector)) {\n return false;\n }\n }\n\n // 成功メッセージが存在する場合は成功\n for (const selector of successSelectors) {\n if (doc.querySelector(selector)) {\n return true;\n }\n }\n\n // デフォルトは成功とみなす(エラーがない場合)\n return true;\n }\n\n private registerEventListeners() {\n this.#container?.addEventListener('submit', this.handleSubmit);\n }\n\n private unregisterEventListeners() {\n this.#container?.removeEventListener('submit', this.handleSubmit);\n }\n\n private parseContent(htmlContent: string) {\n const { header, body, footer } = this.config;\n const result: Content = {\n raw: htmlContent,\n };\n\n if (!header && !body && !footer) {\n // セレクタが指定されていない場合は、全体をボディとして扱う\n result.body = htmlContent;\n return result;\n }\n\n // 一時的なDOM要素を作成してHTMLを解析\n const div = document.createElement('div');\n div.innerHTML = htmlContent;\n\n // ヘッダーセクションの抽出\n if (header) {\n const headerElement = div.querySelector(header);\n if (headerElement) {\n result.header = headerElement.innerHTML;\n }\n }\n\n // ボディセクションの抽出\n if (body) {\n const bodyElement = div.querySelector(body);\n if (bodyElement) {\n result.body = bodyElement.innerHTML;\n }\n }\n\n // フッターセクションの抽出\n if (footer) {\n const footerElement = div.querySelector(footer);\n if (footerElement) {\n result.footer = footerElement.innerHTML;\n }\n }\n\n // セレクタが指定されていない場合は、対応するセクションに全体のコンテンツを設定\n if (!result.header && !result.body && !result.footer) {\n result.body = htmlContent;\n }\n\n return result;\n }\n\n private fetchContent(options: ModalManagerOpenOptions) {\n const { selector, url } = options;\n\n if (selector) {\n return getContentFromSelector(selector);\n }\n\n if (url) {\n if (!this.securityManager.validateUrl(url)) {\n throw new Error(`URL is not allowed: ${url}`);\n }\n return fetchContentFromUrl(url);\n }\n\n throw new Error('No selector or URL provided');\n }\n}\n\nexport default ModalManager;\n"],"mappings":"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","names":["useRef","useRef"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { UseModalManagerOptions } from './store/use-modal-manager.js';
|
|
2
|
+
export interface ModalControllerProps extends UseModalManagerOptions {
|
|
3
|
+
}
|
|
4
|
+
declare function ModalController({ ...props }: ModalControllerProps): import("react").JSX.Element | null;
|
|
5
|
+
declare namespace ModalController {
|
|
6
|
+
var openTrigger: string;
|
|
7
|
+
var closeTrigger: string;
|
|
8
|
+
}
|
|
9
|
+
export default ModalController;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* フォームアクション補完機能
|
|
3
|
+
*/
|
|
4
|
+
export declare function patchFormActions(html: string, baseUrl: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* URL からのコンテンツ取得
|
|
7
|
+
*/
|
|
8
|
+
export declare function fetchContentFromUrl(url: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* セレクタからのコンテンツ取得
|
|
11
|
+
*/
|
|
12
|
+
export declare function getContentFromSelector(selector: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type Modal from '@ablogcms/components/modal';
|
|
2
|
+
import type { ModalBody, ModalFooter, ModalHeader } from '@ablogcms/components/modal';
|
|
3
|
+
import { SecurityConfig } from './security-manager.js';
|
|
4
|
+
type ModalProps = Omit<React.ComponentProps<typeof Modal>, 'onClose'>;
|
|
5
|
+
type ModalHeaderProps = Omit<React.ComponentProps<typeof ModalHeader>, 'children'>;
|
|
6
|
+
type ModalBodyProps = Omit<React.ComponentProps<typeof ModalBody>, 'children'>;
|
|
7
|
+
type ModalFooterProps = Omit<React.ComponentProps<typeof ModalFooter>, 'children'>;
|
|
8
|
+
export interface ModalManagerState {
|
|
9
|
+
content: Content;
|
|
10
|
+
modalProps: ModalProps;
|
|
11
|
+
modalHeaderProps: ModalHeaderProps;
|
|
12
|
+
modalBodyProps: ModalBodyProps;
|
|
13
|
+
modalFooterProps: ModalFooterProps;
|
|
14
|
+
}
|
|
15
|
+
export interface ModalManagerOptions {
|
|
16
|
+
header: string;
|
|
17
|
+
body: string;
|
|
18
|
+
footer: string;
|
|
19
|
+
security: Partial<SecurityConfig>;
|
|
20
|
+
}
|
|
21
|
+
interface Content {
|
|
22
|
+
header?: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
footer?: string;
|
|
25
|
+
raw: string;
|
|
26
|
+
}
|
|
27
|
+
interface ModalManagerOpenOptions {
|
|
28
|
+
selector?: string;
|
|
29
|
+
url?: string;
|
|
30
|
+
modalProps?: Partial<ModalProps>;
|
|
31
|
+
modalHeaderProps?: Partial<ModalHeaderProps>;
|
|
32
|
+
modalBodyProps?: Partial<ModalBodyProps>;
|
|
33
|
+
modalFooterProps?: Partial<ModalFooterProps>;
|
|
34
|
+
}
|
|
35
|
+
declare class ModalManager {
|
|
36
|
+
#private;
|
|
37
|
+
private listeners;
|
|
38
|
+
private state;
|
|
39
|
+
private config;
|
|
40
|
+
private securityManager;
|
|
41
|
+
constructor(options: React.MutableRefObject<Partial<ModalManagerOptions>>);
|
|
42
|
+
subscribe: (callback: () => void) => () => boolean;
|
|
43
|
+
getSnapshot: () => ModalManagerState;
|
|
44
|
+
setState(state: ModalManagerState | ((state: ModalManagerState) => ModalManagerState)): void;
|
|
45
|
+
get content(): Content;
|
|
46
|
+
get modalProps(): ModalProps;
|
|
47
|
+
get modalHeaderProps(): ModalHeaderProps;
|
|
48
|
+
get modalBodyProps(): ModalBodyProps;
|
|
49
|
+
get modalFooterProps(): ModalFooterProps;
|
|
50
|
+
get container(): HTMLElement | null;
|
|
51
|
+
private init;
|
|
52
|
+
open(options?: ModalManagerOpenOptions): Promise<void>;
|
|
53
|
+
close(): void;
|
|
54
|
+
reset(): void;
|
|
55
|
+
private emit;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
private handleSubmit;
|
|
58
|
+
private checkSuccessFromContent;
|
|
59
|
+
private registerEventListeners;
|
|
60
|
+
private unregisterEventListeners;
|
|
61
|
+
private parseContent;
|
|
62
|
+
private fetchContent;
|
|
63
|
+
}
|
|
64
|
+
export default ModalManager;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL検証の設定
|
|
3
|
+
*/
|
|
4
|
+
export interface SecurityConfig {
|
|
5
|
+
allowedDomains?: string[];
|
|
6
|
+
allowSameOrigin: boolean;
|
|
7
|
+
allowRelativeUrls: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* デフォルトのセキュリティ設定
|
|
11
|
+
*/
|
|
12
|
+
export declare const defaultOptions: SecurityConfig;
|
|
13
|
+
/**
|
|
14
|
+
* URLが安全かどうかを検証する関数
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateUrl(url: string, config?: SecurityConfig): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* セキュリティ設定を管理するクラス
|
|
19
|
+
*/
|
|
20
|
+
export default class SecurityManager {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(options?: Partial<SecurityConfig>);
|
|
23
|
+
/**
|
|
24
|
+
* セキュリティ設定を更新
|
|
25
|
+
*/
|
|
26
|
+
setConfig(newConfig: Partial<SecurityConfig>): void;
|
|
27
|
+
/**
|
|
28
|
+
* 現在の設定を取得
|
|
29
|
+
*/
|
|
30
|
+
get config(): SecurityConfig;
|
|
31
|
+
/**
|
|
32
|
+
* URLを検証
|
|
33
|
+
*/
|
|
34
|
+
validateUrl(url: string): boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ModalManager, { ModalManagerOptions } from './modal-manager.js';
|
|
2
|
+
export interface UseModalManagerOptions extends Partial<ModalManagerOptions> {
|
|
3
|
+
}
|
|
4
|
+
export default function useModalManager(options?: UseModalManagerOptions): {
|
|
5
|
+
state: import("./modal-manager.js").ModalManagerState;
|
|
6
|
+
manager: ModalManager;
|
|
7
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ablogcms/modal",
|
|
3
|
+
"version": "3.2.28-beta.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "data-modal-* 属性トリガー監視とモーダル制御",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://www.a-blogcms.jp",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+ssh://git@bitbucket.org:appleple/ablogcms.git",
|
|
11
|
+
"directory": "packages/frontend/modal"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ablogcms/components": "3.2.28-beta.0",
|
|
16
|
+
"@ablogcms/react-hooks": "3.2.28-beta.0",
|
|
17
|
+
"@ablogcms/utils": "3.2.28-beta.0",
|
|
18
|
+
"@ablogcms/pending": "3.2.28-beta.0",
|
|
19
|
+
"@ablogcms/types": "3.2.28-beta.0",
|
|
20
|
+
"@ablogcms/events": "3.2.28-beta.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/react": "^16.3.2",
|
|
24
|
+
"vitest": "^4.1.9"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^19",
|
|
28
|
+
"@types/react": "^19.2.17"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@types/react": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.mjs",
|
|
39
|
+
"default": "./dist/index.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./acms": {
|
|
42
|
+
"types": "./dist/acms.d.ts",
|
|
43
|
+
"import": "./dist/acms.mjs",
|
|
44
|
+
"default": "./dist/acms.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./content-fetcher": {
|
|
47
|
+
"types": "./dist/store/content-fetcher.d.ts",
|
|
48
|
+
"import": "./dist/store/content-fetcher.mjs",
|
|
49
|
+
"default": "./dist/store/content-fetcher.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./security-manager": {
|
|
52
|
+
"types": "./dist/store/security-manager.d.ts",
|
|
53
|
+
"import": "./dist/store/security-manager.mjs",
|
|
54
|
+
"default": "./dist/store/security-manager.mjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist"
|
|
62
|
+
],
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "node ../../../tools/build-package.mjs"
|
|
65
|
+
}
|
|
66
|
+
}
|