@aeriz/wysiwyg 1.0.2 → 1.0.3
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 +610 -96
- package/angular/README.md +112 -0
- package/angular/aeriz-editor.directive.ts +243 -0
- package/angular/aeriz-wysiwyg.module.ts +14 -0
- package/angular/index.ts +5 -0
- package/dist/adapters/jquery.cjs +1 -0
- package/dist/adapters/jquery.js +91 -0
- package/dist/adapters/react.cjs +1 -0
- package/dist/adapters/react.js +66 -0
- package/dist/adapters/shared.cjs +1 -0
- package/dist/adapters/shared.js +87 -0
- package/dist/adapters/svelte.cjs +1 -0
- package/dist/adapters/svelte.js +29 -0
- package/dist/adapters/vue.cjs +1 -0
- package/dist/adapters/vue.js +110 -0
- package/dist/adapters/vue2.cjs +1 -0
- package/dist/adapters/vue2.js +117 -0
- package/dist/adapters/wc.cjs +1 -0
- package/dist/adapters/wc.js +207 -0
- package/dist/aeriz-wysiwyg-adapter.js +169 -0
- package/dist/aeriz-wysiwyg-adapter.min.js +1 -0
- package/dist/aeriz-wysiwyg-jquery.js +275 -0
- package/dist/aeriz-wysiwyg-jquery.min.js +1 -0
- package/dist/aeriz-wysiwyg-wc.js +401 -0
- package/dist/aeriz-wysiwyg-wc.min.js +1 -0
- package/dist/aeriz-wysiwyg.cjs +6912 -0
- package/dist/aeriz-wysiwyg.css +37 -129
- package/dist/aeriz-wysiwyg.esm.js +1170 -149
- package/dist/aeriz-wysiwyg.esm.min.js +2057 -1209
- package/dist/aeriz-wysiwyg.js +1169 -148
- package/dist/aeriz-wysiwyg.min.cjs +54 -0
- package/dist/aeriz-wysiwyg.min.css +1 -1
- package/dist/aeriz-wysiwyg.min.js +44 -13
- package/dist/server.cjs +32 -0
- package/dist/server.js +666 -0
- package/dist-types/adapters/jquery.d.ts +34 -0
- package/dist-types/adapters/jquery.d.ts.map +1 -0
- package/dist-types/adapters/react.d.ts +28 -0
- package/dist-types/adapters/react.d.ts.map +1 -0
- package/dist-types/adapters/shared.d.ts +83 -0
- package/dist-types/adapters/shared.d.ts.map +1 -0
- package/dist-types/adapters/svelte.d.ts +38 -0
- package/dist-types/adapters/svelte.d.ts.map +1 -0
- package/dist-types/adapters/vue.d.ts +189 -0
- package/dist-types/adapters/vue.d.ts.map +1 -0
- package/dist-types/adapters/vue2.d.ts +175 -0
- package/dist-types/adapters/vue2.d.ts.map +1 -0
- package/dist-types/adapters/wc.d.ts +96 -0
- package/dist-types/adapters/wc.d.ts.map +1 -0
- package/dist-types/cjs/adapters/jquery.d.cts +34 -0
- package/dist-types/cjs/adapters/react.d.cts +28 -0
- package/dist-types/cjs/adapters/shared.d.cts +83 -0
- package/dist-types/cjs/adapters/svelte.d.cts +38 -0
- package/dist-types/cjs/adapters/vue.d.cts +189 -0
- package/dist-types/cjs/adapters/vue2.d.cts +175 -0
- package/dist-types/cjs/adapters/wc.d.cts +96 -0
- package/dist-types/cjs/core/AerizEditor.d.cts +357 -0
- package/dist-types/cjs/core/AerizViewer.d.cts +61 -0
- package/dist-types/cjs/core/block-meta.d.cts +32 -0
- package/dist-types/cjs/core/blocks.d.cts +368 -0
- package/dist-types/cjs/core/fonts.d.cts +60 -0
- package/dist-types/cjs/core/highlight.d.cts +44 -0
- package/dist-types/cjs/core/html-mini.d.cts +65 -0
- package/dist-types/cjs/core/i18n.d.cts +209 -0
- package/dist-types/cjs/core/import-html.d.cts +39 -0
- package/dist-types/cjs/core/import-markdown.d.cts +37 -0
- package/dist-types/cjs/core/markdown-export.d.cts +30 -0
- package/dist-types/cjs/core/plain-text.d.cts +17 -0
- package/dist-types/cjs/core/render-string.d.cts +68 -0
- package/dist-types/cjs/core/snapshot.d.cts +51 -0
- package/dist-types/cjs/index.d.cts +20 -0
- package/dist-types/cjs/server/index.d.cts +37 -0
- package/dist-types/cjs/types/index.d.cts +61 -0
- package/dist-types/cjs/ui/Toolbar.d.cts +21 -0
- package/dist-types/cjs/ui/dialogs.d.cts +74 -0
- package/dist-types/cjs/ui/icons.d.cts +2 -0
- package/dist-types/cjs/ui/popovers.d.cts +70 -0
- package/dist-types/core/AerizEditor.d.ts +30 -10
- package/dist-types/core/AerizEditor.d.ts.map +1 -1
- package/dist-types/core/AerizViewer.d.ts +16 -4
- package/dist-types/core/AerizViewer.d.ts.map +1 -1
- package/dist-types/core/block-meta.d.ts +32 -0
- package/dist-types/core/block-meta.d.ts.map +1 -0
- package/dist-types/core/blocks.d.ts +62 -7
- package/dist-types/core/blocks.d.ts.map +1 -1
- package/dist-types/core/fonts.d.ts +11 -0
- package/dist-types/core/fonts.d.ts.map +1 -1
- package/dist-types/core/html-mini.d.ts +65 -0
- package/dist-types/core/html-mini.d.ts.map +1 -0
- package/dist-types/core/import-html.d.ts.map +1 -1
- package/dist-types/core/markdown-export.d.ts +30 -0
- package/dist-types/core/markdown-export.d.ts.map +1 -0
- package/dist-types/core/plain-text.d.ts +17 -0
- package/dist-types/core/plain-text.d.ts.map +1 -0
- package/dist-types/core/render-string.d.ts +68 -0
- package/dist-types/core/render-string.d.ts.map +1 -0
- package/dist-types/core/snapshot.d.ts +51 -0
- package/dist-types/core/snapshot.d.ts.map +1 -0
- package/dist-types/index.d.ts +19 -10
- package/dist-types/index.d.ts.map +1 -1
- package/dist-types/server/index.d.ts +37 -0
- package/dist-types/server/index.d.ts.map +1 -0
- package/dist-types/types/index.d.ts +21 -0
- package/dist-types/types/index.d.ts.map +1 -1
- package/dist-types/ui/Toolbar.d.ts +1 -1
- package/dist-types/ui/popovers.d.ts +1 -1
- package/dist-types/ui/popovers.d.ts.map +1 -1
- package/package.json +213 -58
package/README.md
CHANGED
|
@@ -1,96 +1,610 @@
|
|
|
1
|
-
# @aeriz/wysiwyg
|
|
2
|
-
|
|
3
|
-
한국어 친화 contentEditable WYSIWYG 에디터.
|
|
4
|
-
|
|
5
|
-
자세한 동작 사양은 [SPEC.md](./SPEC.md) 를 참고하세요.
|
|
6
|
-
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
# @aeriz/wysiwyg
|
|
2
|
+
|
|
3
|
+
한국어 친화 contentEditable WYSIWYG 에디터.
|
|
4
|
+
|
|
5
|
+
자세한 동작 사양은 [SPEC.md](./SPEC.md) 를 참고하세요.
|
|
6
|
+
|
|
7
|
+
## 지원 환경
|
|
8
|
+
|
|
9
|
+
| 대상 | 진입점 | 방식 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Vanilla JS | `@aeriz/wysiwyg` | 클래스 직접 사용 |
|
|
12
|
+
| Web Component | `@aeriz/wysiwyg/wc` | `<aeriz-editor>` 커스텀 엘리먼트 |
|
|
13
|
+
| jQuery | `@aeriz/wysiwyg/jquery` | `$.fn.aerizEditor` 플러그인 |
|
|
14
|
+
| React 16.8+ / 17 / 18 / 19 | `@aeriz/wysiwyg/react` | `<AerizEditor />` 컴포넌트 |
|
|
15
|
+
| Vue 3 | `@aeriz/wysiwyg/vue` | `v-model` 컴포넌트 |
|
|
16
|
+
| Vue 2.6 / 2.7 | `@aeriz/wysiwyg/vue2` | `v-model` 컴포넌트 |
|
|
17
|
+
| Svelte 3 / 4 / 5 | `@aeriz/wysiwyg/svelte` | `use:aerizEditor` 액션 |
|
|
18
|
+
| Angular 14+ | `@aeriz/wysiwyg/angular` | `[aerizEditor]` 디렉티브 (`ngModel` 지원) |
|
|
19
|
+
| Node / Deno / Bun / Cloudflare Workers | `@aeriz/wysiwyg/server` | DOM 없는 렌더러 · 변환기 |
|
|
20
|
+
|
|
21
|
+
빌드 도구가 없는 페이지도 지원합니다 — 코어뿐 아니라 **어댑터도 스크립트
|
|
22
|
+
태그로 로드**할 수 있습니다 ([CDN 절](#cdn--어댑터도-스크립트-태그로) 참고).
|
|
23
|
+
|
|
24
|
+
프레임워크 패키지는 모두 **optional peerDependency** 입니다. React 프로젝트에
|
|
25
|
+
Vue 가 설치될 일은 없습니다.
|
|
26
|
+
|
|
27
|
+
## 설치
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @aeriz/wysiwyg
|
|
31
|
+
# yarn add @aeriz/wysiwyg
|
|
32
|
+
# pnpm add @aeriz/wysiwyg
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
스타일시트는 어느 프레임워크를 쓰든 한 번 로드해야 합니다.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import '@aeriz/wysiwyg/style.css';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Vanilla JS
|
|
44
|
+
|
|
45
|
+
### 번들러 사용
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg';
|
|
49
|
+
import '@aeriz/wysiwyg/style.css';
|
|
50
|
+
|
|
51
|
+
const editor = new AerizEditor({
|
|
52
|
+
element: document.querySelector('#editor')!,
|
|
53
|
+
initialHTML: '<p>안녕하세요</p>',
|
|
54
|
+
placeholder: '내용을 입력하세요',
|
|
55
|
+
theme: 'dark',
|
|
56
|
+
onChange: (html) => localStorage.setItem('draft', html),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const viewer = new AerizViewer({
|
|
60
|
+
element: document.querySelector('#preview')!,
|
|
61
|
+
});
|
|
62
|
+
editor.onStateChange(() => viewer.setBlocks(editor.serialize()));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### CDN — 스크립트 태그
|
|
66
|
+
|
|
67
|
+
빌드 단계 없이 바로 사용. 라이브러리는 글로벌 `window.AerizWysiwyg` 와 단축
|
|
68
|
+
별칭 `window.AerizEditor` / `window.AerizViewer` 로 노출됩니다.
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<!-- ✓ 프로덕션 — 특정 버전 고정 (immutable, 가장 긴 캐시) -->
|
|
72
|
+
<link
|
|
73
|
+
rel="stylesheet"
|
|
74
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.css"
|
|
75
|
+
/>
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.js"></script>
|
|
77
|
+
|
|
78
|
+
<!-- 1.x 트랙 — 패치만 자동 수신 -->
|
|
79
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.3/dist/aeriz-wysiwyg.min.js"></script>
|
|
80
|
+
|
|
81
|
+
<!-- 항상 최신 — 프로토타이핑용 (프로덕션 권장 X) -->
|
|
82
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@latest/dist/aeriz-wysiwyg.min.js"></script>
|
|
83
|
+
|
|
84
|
+
<!-- unpkg 미러 -->
|
|
85
|
+
<script src="https://unpkg.com/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.js"></script>
|
|
86
|
+
|
|
87
|
+
<div id="editor"></div>
|
|
88
|
+
<script>
|
|
89
|
+
const editor = new AerizEditor({
|
|
90
|
+
element: document.querySelector('#editor'),
|
|
91
|
+
initialHTML: '<p>안녕하세요</p>',
|
|
92
|
+
onChange: (html) => console.log(html),
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### CDN — ES Module
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<link
|
|
101
|
+
rel="stylesheet"
|
|
102
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.css"
|
|
103
|
+
/>
|
|
104
|
+
<script type="module">
|
|
105
|
+
import {
|
|
106
|
+
AerizEditor,
|
|
107
|
+
AerizViewer,
|
|
108
|
+
} from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.esm.min.js';
|
|
109
|
+
|
|
110
|
+
new AerizEditor({ element: document.querySelector('#editor') });
|
|
111
|
+
</script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## CDN — 어댑터도 스크립트 태그로
|
|
117
|
+
|
|
118
|
+
`dist/adapters/*.js` 는 ESM 이라 `<script type="module">` 로 CDN 에서 바로
|
|
119
|
+
부르면 bare import 를 해석하지 못합니다. 그래서 **프레임워크 의존이 없는
|
|
120
|
+
어댑터는 IIFE 로도 배포**합니다. 코어를 번들에 넣지 않고 전역
|
|
121
|
+
`AerizWysiwyg` 를 참조하므로, 코어를 먼저 부르기만 하면 됩니다 (코어가 두 벌
|
|
122
|
+
로드되지 않습니다).
|
|
123
|
+
|
|
124
|
+
| 파일 | 노출 전역 | 역할 |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `dist/aeriz-wysiwyg-wc.min.js` | `AerizWysiwygWC` | `<aeriz-editor>` 자동 등록 |
|
|
127
|
+
| `dist/aeriz-wysiwyg-jquery.min.js` | `AerizWysiwygJQuery` | `$.fn.aerizEditor` 자동 등록 |
|
|
128
|
+
| `dist/aeriz-wysiwyg-adapter.min.js` | `AerizWysiwygAdapter` | `createEditorController` 등 |
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<link
|
|
132
|
+
rel="stylesheet"
|
|
133
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.css"
|
|
134
|
+
/>
|
|
135
|
+
<!-- 1) 코어 먼저 -->
|
|
136
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.js"></script>
|
|
137
|
+
<!-- 2) 필요한 어댑터 -->
|
|
138
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg-wc.min.js"></script>
|
|
139
|
+
|
|
140
|
+
<aeriz-editor theme="dark" placeholder="내용을 입력하세요"></aeriz-editor>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
jQuery 는 jQuery 자체를 먼저 로드해야 자동 등록됩니다.
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
147
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.min.js"></script>
|
|
148
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg-jquery.min.js"></script>
|
|
149
|
+
<script>
|
|
150
|
+
$('#editor').aerizEditor({ value: '<p>안녕하세요</p>' });
|
|
151
|
+
</script>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### React · Vue 를 CDN 으로 쓸 때 — import map
|
|
155
|
+
|
|
156
|
+
React / Vue 어댑터는 IIFE 로 제공하지 않습니다. 소비자 앱의 프레임워크
|
|
157
|
+
인스턴스를 그대로 써야 하는데 전역 하나로 묶으면 인스턴스가 갈라져 훅이
|
|
158
|
+
깨지기 때문입니다. CDN 에서는 import map 이 정답입니다.
|
|
159
|
+
|
|
160
|
+
```html
|
|
161
|
+
<script type="importmap">
|
|
162
|
+
{
|
|
163
|
+
"imports": {
|
|
164
|
+
"vue": "https://esm.sh/vue@3",
|
|
165
|
+
"@aeriz/wysiwyg": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/aeriz-wysiwyg.esm.min.js",
|
|
166
|
+
"@aeriz/wysiwyg/vue": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.3/dist/adapters/vue.js"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
<script type="module">
|
|
171
|
+
import { createApp, h, ref } from 'vue';
|
|
172
|
+
import { AerizEditor } from '@aeriz/wysiwyg/vue';
|
|
173
|
+
|
|
174
|
+
const html = ref('<p>안녕하세요</p>');
|
|
175
|
+
createApp({
|
|
176
|
+
render: () =>
|
|
177
|
+
h(AerizEditor, {
|
|
178
|
+
modelValue: html.value,
|
|
179
|
+
'onUpdate:modelValue': (v) => (html.value = v),
|
|
180
|
+
}),
|
|
181
|
+
}).mount('#app');
|
|
182
|
+
</script>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> `@aeriz/wysiwyg` 항목이 반드시 있어야 합니다 — 어댑터가 그 이름으로 코어를
|
|
186
|
+
> 가져오기 때문입니다. 빠뜨리면 브라우저가 bare specifier 해석에 실패합니다.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Web Component
|
|
191
|
+
|
|
192
|
+
프레임워크 의존이 전혀 없는 최소 공통 분모입니다. Shadow DOM 을 쓰지 않으므로
|
|
193
|
+
전역 스타일시트가 그대로 적용됩니다.
|
|
194
|
+
|
|
195
|
+
```html
|
|
196
|
+
<link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
|
|
197
|
+
<script type="module">
|
|
198
|
+
import '@aeriz/wysiwyg/wc'; // import 만으로 자동 등록
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<form action="/posts" method="post">
|
|
202
|
+
<!-- name 을 주면 폼 전송에 본문 HTML 이 함께 실립니다 (폼 연동 지원 브라우저) -->
|
|
203
|
+
<aeriz-editor name="body" theme="dark" placeholder="내용을 입력하세요">
|
|
204
|
+
</aeriz-editor>
|
|
205
|
+
<button type="submit">저장</button>
|
|
206
|
+
</form>
|
|
207
|
+
|
|
208
|
+
<script type="module">
|
|
209
|
+
const el = document.querySelector('aeriz-editor');
|
|
210
|
+
el.addEventListener('change', (e) => console.log(e.detail.html));
|
|
211
|
+
el.value = '<p>안녕하세요</p>';
|
|
212
|
+
el.onImageUpload = async (file) => uploadAndGetUrl(file);
|
|
213
|
+
</script>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
| 속성 | 값 |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `value` | 본문 HTML (프로퍼티로도 읽고 쓸 수 있음) |
|
|
219
|
+
| `placeholder` | 빈 본문 안내 문구 |
|
|
220
|
+
| `theme` | `light` / `dark` |
|
|
221
|
+
| `locale` | `en` / `ko` / `ja` / `zh` |
|
|
222
|
+
| `toolbar` | `false` · 항목 목록(`"bold italic"`) · JSON 객체 |
|
|
223
|
+
| `image-upload-url`, `file-upload-url` | 업로드 API URL |
|
|
224
|
+
|
|
225
|
+
프로퍼티: `value` · `editor` · `getBlocks()` · `focus()` ·
|
|
226
|
+
`onImageUpload` · `onFileUpload`.
|
|
227
|
+
이벤트: `input` · `change` (`detail.html`) · `aeriz-ready` (`detail.editor`).
|
|
228
|
+
|
|
229
|
+
태그 이름이 충돌하면 접두사를 바꿀 수 있습니다.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { defineAerizElements } from '@aeriz/wysiwyg/wc';
|
|
233
|
+
defineAerizElements('my'); // <my-editor> / <my-viewer>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## jQuery
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
242
|
+
<link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
|
|
243
|
+
<script type="module">
|
|
244
|
+
import '@aeriz/wysiwyg/jquery'; // 전역 jQuery 가 있으면 자동 등록
|
|
245
|
+
|
|
246
|
+
$('#editor').aerizEditor({
|
|
247
|
+
value: '<p>안녕하세요</p>',
|
|
248
|
+
theme: 'dark',
|
|
249
|
+
onChange: (html) => console.log(html),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const html = $('#editor').aerizEditor('getHTML');
|
|
253
|
+
$('#editor').aerizEditor('setHTML', '<p>바꿈</p>');
|
|
254
|
+
$('#editor').aerizEditor('destroy');
|
|
255
|
+
</script>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
번들러 환경에서 전역 jQuery 를 쓰지 않는다면 명시적으로 등록합니다.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import $ from 'jquery';
|
|
262
|
+
import { registerAerizJQuery } from '@aeriz/wysiwyg/jquery';
|
|
263
|
+
|
|
264
|
+
registerAerizJQuery($);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
명령: `getHTML` · `getBlocks` · `setHTML` · `update` · `focus` ·
|
|
268
|
+
`instance` · `destroy`. `aerizViewer` 도 같은 방식으로 동작합니다.
|
|
269
|
+
|
|
270
|
+
같은 노드에 두 번 초기화하면 새 인스턴스를 만들지 않고 갱신으로 처리합니다.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## React
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { useState } from 'react';
|
|
278
|
+
import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/react';
|
|
279
|
+
import '@aeriz/wysiwyg/style.css';
|
|
280
|
+
|
|
281
|
+
export function Compose() {
|
|
282
|
+
const [html, setHtml] = useState('<p>안녕하세요</p>');
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<AerizEditor
|
|
287
|
+
value={html}
|
|
288
|
+
onChange={setHtml}
|
|
289
|
+
theme="dark"
|
|
290
|
+
placeholder="내용을 입력하세요"
|
|
291
|
+
className="my-editor"
|
|
292
|
+
imageUploadUrl="/api/upload"
|
|
293
|
+
/>
|
|
294
|
+
<AerizViewer html={html} />
|
|
295
|
+
</>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`value` 를 그대로 되돌려줘도 캐럿이 튀지 않습니다 — 들어온 값이 에디터의 현재
|
|
301
|
+
출력과 같으면 본문을 교체하지 않기 때문입니다.
|
|
302
|
+
|
|
303
|
+
`ref` 로 명령형 API 에 접근합니다.
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const ref = useRef<AerizEditorHandle>(null);
|
|
307
|
+
// ref.current.editor — 코어 인스턴스 (전체 API)
|
|
308
|
+
// ref.current.getHTML() / setHTML(html) / focus()
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Vue 3
|
|
314
|
+
|
|
315
|
+
```vue
|
|
316
|
+
<script setup>
|
|
317
|
+
import { ref } from 'vue';
|
|
318
|
+
import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/vue';
|
|
319
|
+
import '@aeriz/wysiwyg/style.css';
|
|
320
|
+
|
|
321
|
+
const html = ref('<p>안녕하세요</p>');
|
|
322
|
+
const editor = ref(null);
|
|
323
|
+
</script>
|
|
324
|
+
|
|
325
|
+
<template>
|
|
326
|
+
<AerizEditor
|
|
327
|
+
ref="editor"
|
|
328
|
+
v-model="html"
|
|
329
|
+
theme="dark"
|
|
330
|
+
placeholder="내용을 입력하세요"
|
|
331
|
+
@state-change="onStateChange"
|
|
332
|
+
@ready="onReady"
|
|
333
|
+
/>
|
|
334
|
+
<AerizViewer :html="html" />
|
|
335
|
+
</template>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
전역 등록도 가능합니다.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { AerizWysiwygPlugin } from '@aeriz/wysiwyg/vue';
|
|
342
|
+
app.use(AerizWysiwygPlugin);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
이벤트: `update:modelValue` · `change` · `state-change` · `ready`.
|
|
346
|
+
`ref` 로 노출되는 것: `editor` · `getHTML()` · `setHTML()` · `focus()`.
|
|
347
|
+
|
|
348
|
+
## Vue 2 (2.6 / 2.7)
|
|
349
|
+
|
|
350
|
+
Vue 2 관례에 따라 `v-model` 은 `value` prop + `input` 이벤트를 씁니다.
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
import { AerizEditor } from '@aeriz/wysiwyg/vue2';
|
|
354
|
+
import '@aeriz/wysiwyg/style.css';
|
|
355
|
+
|
|
356
|
+
export default {
|
|
357
|
+
components: { AerizEditor },
|
|
358
|
+
data: () => ({ html: '<p>안녕하세요</p>' }),
|
|
359
|
+
template: `
|
|
360
|
+
<AerizEditor ref="editor" v-model="html" theme="dark" />
|
|
361
|
+
`,
|
|
362
|
+
methods: {
|
|
363
|
+
focusEditor() {
|
|
364
|
+
this.$refs.editor.focus();
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
이 어댑터는 `vue` 를 import 하지 않습니다 (Vue 2 컴포넌트는 순수 옵션 객체).
|
|
371
|
+
따라서 소비자 앱의 Vue 인스턴스와 이중화될 여지가 없습니다.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Svelte (3 / 4 / 5)
|
|
376
|
+
|
|
377
|
+
컴파일된 `.svelte` 컴포넌트가 아니라 **액션**으로 제공합니다. 액션은 순수 함수
|
|
378
|
+
계약이라 Svelte 메이저 버전 세 개를 배포본 하나로 만족시킵니다.
|
|
379
|
+
|
|
380
|
+
```svelte
|
|
381
|
+
<script>
|
|
382
|
+
import { aerizEditor, aerizViewer } from '@aeriz/wysiwyg/svelte';
|
|
383
|
+
import '@aeriz/wysiwyg/style.css';
|
|
384
|
+
|
|
385
|
+
let html = '<p>안녕하세요</p>';
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<div
|
|
389
|
+
use:aerizEditor={{
|
|
390
|
+
value: html,
|
|
391
|
+
onChange: (next) => (html = next),
|
|
392
|
+
theme: 'dark',
|
|
393
|
+
placeholder: '내용을 입력하세요',
|
|
394
|
+
}}
|
|
395
|
+
/>
|
|
396
|
+
|
|
397
|
+
<div use:aerizViewer={{ html }} />
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Angular (14+)
|
|
403
|
+
|
|
404
|
+
Angular 연동은 **TypeScript 소스로 배포**됩니다 — 소비자의 Angular 컴파일러가
|
|
405
|
+
직접 컴파일하므로 Angular 14 이상 어느 버전에서도 같은 파일이 동작합니다.
|
|
406
|
+
설정 방법(택 1)과 상세 문서는 [angular/README.md](./angular/README.md) 를
|
|
407
|
+
참고하세요.
|
|
408
|
+
|
|
409
|
+
`tsconfig.app.json` 의 `include` 에 한 줄 추가하면 끝입니다.
|
|
410
|
+
|
|
411
|
+
```jsonc
|
|
412
|
+
"include": ["src/**/*.ts", "node_modules/@aeriz/wysiwyg/angular/*.ts"]
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { Component } from '@angular/core';
|
|
417
|
+
import { FormsModule } from '@angular/forms';
|
|
418
|
+
import { AerizEditorDirective } from '@aeriz/wysiwyg/angular';
|
|
419
|
+
|
|
420
|
+
@Component({
|
|
421
|
+
standalone: true,
|
|
422
|
+
imports: [FormsModule, AerizEditorDirective],
|
|
423
|
+
template: `
|
|
424
|
+
<div aerizEditor [(ngModel)]="html" theme="dark"></div>
|
|
425
|
+
<div aerizViewer [html]="html"></div>
|
|
426
|
+
`,
|
|
427
|
+
})
|
|
428
|
+
export class ComposeComponent {
|
|
429
|
+
html = '<p>안녕하세요</p>';
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`[(value)]` · `[(ngModel)]` · `formControlName` 세 가지 바인딩을 모두
|
|
434
|
+
지원합니다. 디렉티브는 에디터를 `NgZone` 밖에서 생성하므로 캐럿을 움직일 때마다
|
|
435
|
+
변경 감지가 돌지 않습니다.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 서버 / 엣지 런타임 (DOM 없음)
|
|
440
|
+
|
|
441
|
+
저장된 `BlockSnapshot[]` 을 HTML · 마크다운 · 평문으로 바꿉니다. **CSS 를
|
|
442
|
+
import 하지 않고**, import 그래프 전체가 `document` / `window` 를 참조하지
|
|
443
|
+
않습니다 — Cloudflare Workers(workerd) · Node · Deno · Bun 에서 그대로
|
|
444
|
+
동작합니다.
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import {
|
|
448
|
+
renderBlocksToHTML,
|
|
449
|
+
blocksToMarkdown,
|
|
450
|
+
blocksToPlainText,
|
|
451
|
+
} from '@aeriz/wysiwyg/server';
|
|
452
|
+
|
|
453
|
+
// 발행용 HTML — 같은 입력이면 항상 같은 출력 (결정적 · 멱등)
|
|
454
|
+
const html = renderBlocksToHTML(post.blocks, { wrapper: 'content' });
|
|
455
|
+
|
|
456
|
+
// 포맷 종속을 푸는 탈출구
|
|
457
|
+
const markdown = blocksToMarkdown(post.blocks);
|
|
458
|
+
|
|
459
|
+
// 전문 검색 색인용
|
|
460
|
+
const text = blocksToPlainText(post.blocks, { blockSeparator: ' ' });
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// Cloudflare Worker
|
|
465
|
+
export default {
|
|
466
|
+
async fetch(req, env) {
|
|
467
|
+
const post = await env.DB.get('post:1', 'json');
|
|
468
|
+
return new Response(renderBlocksToHTML(post.blocks, { wrapper: 'content' }), {
|
|
469
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 서버 sanitizer 와 함께 쓰기
|
|
478
|
+
|
|
479
|
+
`getHTML()` 은 기본적으로 테마 색을 인라인 `style` 에 베이크합니다 —
|
|
480
|
+
스타일시트 없이 어디에 붙여도 같은 시각으로 보이지만, `style` 속성을 제거하는
|
|
481
|
+
sanitizer 를 통과하면 서식이 통째로 사라집니다.
|
|
482
|
+
|
|
483
|
+
두 가지 대응이 있습니다.
|
|
484
|
+
|
|
485
|
+
**1. 클래스 기반 출력으로 전환** — `style` 프로퍼티가 40여 개에서 10개로
|
|
486
|
+
줄고, 정렬은 클래스로 표현됩니다.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
new AerizEditor({ element, htmlStyles: 'class' }); // getHTML/onChange 기본값
|
|
490
|
+
editor.getHTML({ styles: 'class' }); // 호출 단위 재정의
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
출력은 `aeriz-wysiwyg.min.css` 를 로드한 페이지의
|
|
494
|
+
`.aeriz-wysiwyg > .aw-content` 안에 넣어야 기본 서식이 적용됩니다.
|
|
495
|
+
`renderBlocksToHTML(blocks, { wrapper: 'content' })` 가 그 컨테이너를 만들어
|
|
496
|
+
줍니다.
|
|
497
|
+
|
|
498
|
+
**2. 허용목록을 출력에 맞추기** — 등장할 수 있는 태그 · 속성 · style
|
|
499
|
+
프로퍼티의 확정 목록이
|
|
500
|
+
[SPEC.md §9 출력 HTML 허용목록](./SPEC.md#9-출력-html-허용목록) 에 있습니다.
|
|
501
|
+
추측이 아니라 실측이며 `npm run allowlist` 로 재생성할 수 있습니다.
|
|
502
|
+
|
|
503
|
+
> ⚠️ **HTML 가져오기 기능은 sanitizer 가 아닙니다.** 원본 스크립트가 실행되고
|
|
504
|
+
> 이벤트 핸들러 속성이 제거되지 않습니다. 저장 전 서버 정화가 필수입니다 —
|
|
505
|
+
> [SPEC.md §11](./SPEC.md#11-html-가져오기의-보안-경계) 참고.
|
|
506
|
+
|
|
507
|
+
## 스냅샷 스키마 버전
|
|
508
|
+
|
|
509
|
+
`editor.serialize()` 는 각 블록에 `version: 1` 을 기록합니다. 저장된 데이터를
|
|
510
|
+
읽는 모든 경로가 `upgradeSnapshots()` 를 통과하므로, 버전이 올라가도 예전
|
|
511
|
+
데이터가 그대로 열립니다. 변경 정책은
|
|
512
|
+
[SPEC.md §10](./SPEC.md#10-blocksnapshot-스키마-버전-정책) 에 있습니다.
|
|
513
|
+
|
|
514
|
+
## 직접 어댑터 만들기
|
|
515
|
+
|
|
516
|
+
목록에 없는 프레임워크(Solid, Qwik, Lit …) 는 프레임워크 중립 컨트롤러 위에
|
|
517
|
+
얇게 올리면 됩니다. 값 바인딩 · 캐럿 보존 · 재생성 판단이 이미 들어 있습니다.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
import { createEditorController } from '@aeriz/wysiwyg/adapter';
|
|
521
|
+
|
|
522
|
+
const controller = createEditorController(hostElement, {
|
|
523
|
+
value: '<p>안녕하세요</p>',
|
|
524
|
+
onChange: (html) => setState(html),
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
controller.update({ value: nextHtml }); // 바뀐 것만 반영
|
|
528
|
+
controller.destroy(); // 언마운트 시
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## 빌드
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
npm install
|
|
535
|
+
npm run build # dist/ 에 IIFE / ESM / CJS / 어댑터 / CSS / .d.ts 생성
|
|
536
|
+
npm test # 서버 렌더러 · getHTML · 어댑터 계약 테스트 (jsdom)
|
|
537
|
+
npm run typecheck
|
|
538
|
+
npm run allowlist # 출력 HTML 허용목록 재생성
|
|
539
|
+
npm run version:sync # 문서·데모의 CDN 버전 표기를 package.json 에 맞춤
|
|
540
|
+
npm run version:check # 뒤처졌으면 실패 (CI 용)
|
|
541
|
+
npm run dev # 데모 페이지 (http://localhost:5173)
|
|
542
|
+
|
|
543
|
+
npm run release:stage # npm 업로드용 release/ 조립 + exports 경로 검증
|
|
544
|
+
npm run release:verify # release/ 에서 npm pack --dry-run
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`dist/` 는 데모 서버의 publicDir 을 겸하고 개발용 비압축본·CDN 번들까지
|
|
548
|
+
담고 있습니다. 실제로 npm 에 올라가는 것은 `release:stage` 가 `"files"` 만
|
|
549
|
+
골라 조립한 `release/` 입니다. 배포 절차는 [RELEASE.md](./RELEASE.md) (붙여넣어
|
|
550
|
+
실행하는 런북), 계정·권한·회수 등 배경은 [PUBLISHING.md](./PUBLISHING.md) 를
|
|
551
|
+
참고하세요.
|
|
552
|
+
|
|
553
|
+
데모는 세 페이지입니다.
|
|
554
|
+
|
|
555
|
+
- `/index.html` — 에디터 기능 데모
|
|
556
|
+
- `/docs.html` — **연동 · 직렬화 레퍼런스**. 이 README 의 내용을 사이드바 · 검색 가능한
|
|
557
|
+
API 표 · 라이브 플레이그라운드로 정리한 단일 페이지 문서입니다. 플레이그라운드에서
|
|
558
|
+
같은 문서가 `inline` / `class` HTML · 마크다운 · 평문 · 스냅샷 · 서버 렌더 여섯 가지로
|
|
559
|
+
어떻게 나오는지 실시간 비교할 수 있습니다.
|
|
560
|
+
- `/frameworks.html` — 프레임워크 어댑터 데모. React · Vue 3 · Web Component ·
|
|
561
|
+
Svelte 액션 · jQuery 를 한 페이지에 올리고, 로드 직후 자동 점검을 실행합니다.
|
|
562
|
+
|
|
563
|
+
### 문서 사이트 배포 — Cloudflare Workers
|
|
564
|
+
|
|
565
|
+
`/docs.html` 을 그대로 정적 사이트로 올릴 수 있습니다. `docs:build` 가
|
|
566
|
+
`examples/docs.html` 과 라이브러리 산출물을 `docs/` 에 모으고, 코드가 없는
|
|
567
|
+
**정적 자산 전용 Worker** (`wrangler.toml`) 가 그 폴더를 서빙합니다. 별도
|
|
568
|
+
Pages 프로젝트가 필요 없습니다.
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
npx wrangler login # 처음 한 번만
|
|
572
|
+
|
|
573
|
+
npm run docs:build # docs/ 조립
|
|
574
|
+
npm run docs:preview # 로컬 Workers 런타임으로 확인 (wrangler dev)
|
|
575
|
+
npm run docs:deploy # 배포
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
페이지 HTML 은 데모와 배포본이 같은 파일입니다 — `examples/docs.html` 이
|
|
579
|
+
라이브러리를 루트 절대 경로로 부르고, 배포 시에도 산출물을 사이트 루트에 같이
|
|
580
|
+
두기 때문에 경로 분기가 없습니다. 캐시·보안 헤더는 `docs/_headers` 로 함께
|
|
581
|
+
생성됩니다. `docs/` 는 파생물이라 git 에 추적하지 않습니다.
|
|
582
|
+
|
|
583
|
+
### 오프라인 단일 파일
|
|
584
|
+
|
|
585
|
+
문서 페이지를 외부에 공유하거나 오프라인으로 열어야 하면 라이브러리를 인라인한
|
|
586
|
+
단일 파일로 만들 수 있습니다 (외부 요청 0건).
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
node scripts/build-docs-artifact.mjs docs-standalone.html
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
설치된 Chrome 으로 그 페이지를 실제 검증할 수 있습니다 (playwright-core 는
|
|
593
|
+
브라우저를 내려받지 않고 시스템 Chrome 을 그대로 씁니다).
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
npm run dev # 다른 터미널에서
|
|
597
|
+
npm run test:browser # 헤드리스 검증 + 스크린샷
|
|
598
|
+
node scripts/browser-check.mjs --headed # 창을 띄워서 보기
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
5173 이 이미 쓰이는 중이면 데모를 다른 포트로 띄우고 주소를 넘기면 됩니다.
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
npx vite --config vite.demo.config.ts --port 5347
|
|
605
|
+
node scripts/browser-check.mjs http://localhost:5347/frameworks.html
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## 라이선스
|
|
609
|
+
|
|
610
|
+
Apache-2.0
|