@teriusu/rich-editor 0.9.8 → 0.9.10
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/dist/extensions/PdfBlock.d.ts.map +1 -1
- package/dist/extensions/PdfBlock.js +288 -226
- package/dist/extensions/PdfBlock.test.js +258 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +0 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/resize.d.ts.map +1 -1
- package/dist/utils/resize.js +18 -1
- package/package.json +1 -1
- package/dist/components/PdfViewer.svelte +0 -160
- package/dist/components/PdfViewer.svelte.d.ts +0 -9
- package/dist/components/PdfViewer.svelte.d.ts.map +0 -1
|
@@ -91,6 +91,7 @@ export const PdfBlock = TiptapNode.create({
|
|
|
91
91
|
// 클로저의 `node` 를 쓰면 그 사이의 다른 속성 변경을 리사이즈가 되돌린다.
|
|
92
92
|
let currentNode = node;
|
|
93
93
|
let detachResize = null;
|
|
94
|
+
const icon = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
94
95
|
const dom = document.createElement("div");
|
|
95
96
|
dom.classList.add("my-2");
|
|
96
97
|
dom.contentEditable = "false";
|
|
@@ -102,11 +103,9 @@ export const PdfBlock = TiptapNode.create({
|
|
|
102
103
|
dom.style.maxWidth = "100%";
|
|
103
104
|
if (node.attrs.width)
|
|
104
105
|
dom.style.width = node.attrs.width;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
wrapper.style.borderColor = "var(--border)";
|
|
109
|
-
dom.appendChild(wrapper);
|
|
106
|
+
const frame = document.createElement("div");
|
|
107
|
+
frame.className = "pdf-frame";
|
|
108
|
+
dom.appendChild(frame);
|
|
110
109
|
// 리사이즈 핸들 (편집 가능 모드에서만)
|
|
111
110
|
if (editor.isEditable) {
|
|
112
111
|
detachResize = attachResize({
|
|
@@ -118,30 +117,30 @@ export const PdfBlock = TiptapNode.create({
|
|
|
118
117
|
label: "PDF 너비 조절",
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
/*
|
|
121
|
+
* 조작부는 판 **안에** 떠 있다. 통째로 `data-pdf-control` 이라 `stopEvent` 가
|
|
122
|
+
* ProseMirror 를 막아 준다 — 버튼·입력칸이 선택/타이핑에 가로채이지 않는다.
|
|
123
|
+
* 빈 영역은 `pointer-events: none` 이라 그 위로 드래그·클릭이 그대로 통과한다.
|
|
124
|
+
*/
|
|
125
|
+
const overlay = document.createElement("div");
|
|
126
|
+
overlay.className = "pdf-overlay";
|
|
127
|
+
overlay.setAttribute("data-pdf-control", "");
|
|
128
|
+
// 판에는 캔버스·상태문구 **뒤에** 붙인다(아래) — 여기서는 만들어 채우기만 한다.
|
|
129
|
+
const endCluster = document.createElement("div");
|
|
130
|
+
endCluster.className = "pdf-cluster pdf-cluster-end";
|
|
131
|
+
overlay.appendChild(endCluster);
|
|
129
132
|
const cleanName = (raw) => (raw || "").replace(/[?#].*$/, "").trim() || "PDF";
|
|
130
|
-
|
|
131
|
-
*
|
|
133
|
+
/*
|
|
134
|
+
* ⚠️ **표시 이름은 화면에서 뺐다**(사용자 결정) — 편집용 입력칸도, 읽기용 글자도
|
|
135
|
+
* 없다. 판 위에 이름표가 늘 붙어 있는 게 소음이었다.
|
|
132
136
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
137
|
+
* 그래도 `label` 속성 자체는 **스키마에 남긴다.** 이미 이름을 지정해 둔 문서가
|
|
138
|
+
* 있고, 속성을 지우면 그 문서를 한 번 열었다 저장하는 것만으로 값이 조용히
|
|
139
|
+
* 사라진다. 지금은 아무도 읽지 않지만 왕복은 그대로 된다.
|
|
140
|
+
*
|
|
141
|
+
* 다운로드 파일명은 원래부터 `label` 이 아니라 `name` 이다 — 표시 이름을
|
|
142
|
+
* `1강 자료` 로 바꿨다고 확장자 없는 파일이 내려가면 안 되기 때문.
|
|
135
143
|
*/
|
|
136
|
-
let resolvedName = null;
|
|
137
|
-
const fallbackName = () => cleanName(resolvedName ?? currentNode.attrs.name);
|
|
138
|
-
const displayName = () => {
|
|
139
|
-
const label = currentNode.attrs.label;
|
|
140
|
-
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
141
|
-
return trimmed || fallbackName();
|
|
142
|
-
};
|
|
143
|
-
let nameSpan = null;
|
|
144
|
-
let nameInput = null;
|
|
145
144
|
/** 노드 속성을 되쓴다. 리사이즈(`attachResize`)와 **같은 방식**. */
|
|
146
145
|
const setAttrs = (patch) => {
|
|
147
146
|
const pos = getPos();
|
|
@@ -152,110 +151,58 @@ export const PdfBlock = TiptapNode.create({
|
|
|
152
151
|
...patch,
|
|
153
152
|
}));
|
|
154
153
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*/
|
|
185
|
-
"pdf-name-input select-text text-xs text-muted-foreground min-w-0 bg-background rounded px-1.5 py-0.5 border border-border hover:border-ring focus:border-ring focus:text-foreground outline-none transition-colors";
|
|
186
|
-
nameInput.style.maxWidth = "220px";
|
|
187
|
-
nameInput.contentEditable = "false";
|
|
188
|
-
nameInput.draggable = false;
|
|
189
|
-
// ProseMirror 가 이 칸의 키/포인터를 가져가지 않도록 하는 표식(`stopEvent`).
|
|
190
|
-
nameInput.setAttribute("data-pdf-control", "");
|
|
191
|
-
nameInput.title = "표시 이름 (비우면 파일명)";
|
|
192
|
-
nameInput.setAttribute("aria-label", "PDF 표시 이름");
|
|
193
|
-
nameInput.value = displayName();
|
|
194
|
-
fitInput();
|
|
195
|
-
/** 빈칸이거나 파일명과 같으면 `label: null` — 다시 파일명을 따라간다. */
|
|
196
|
-
const commitLabel = () => {
|
|
197
|
-
if (!nameInput)
|
|
198
|
-
return;
|
|
199
|
-
const typed = nameInput.value.trim();
|
|
200
|
-
const next = typed && typed !== fallbackName() ? typed : null;
|
|
201
|
-
const current = currentNode.attrs.label ?? null;
|
|
202
|
-
if (next !== current)
|
|
203
|
-
setAttrs({ label: next });
|
|
204
|
-
nameInput.value = next || fallbackName();
|
|
205
|
-
fitInput();
|
|
206
|
-
};
|
|
207
|
-
nameInput.addEventListener("input", fitInput);
|
|
208
|
-
nameInput.addEventListener("keydown", (e) => {
|
|
209
|
-
// tiptap 이 단축키·타이핑을 가로채면 이 칸에 글자가 안 들어간다.
|
|
210
|
-
e.stopPropagation();
|
|
211
|
-
if (e.key === "Enter") {
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
commitLabel();
|
|
214
|
-
nameInput?.blur();
|
|
215
|
-
}
|
|
216
|
-
else if (e.key === "Escape") {
|
|
217
|
-
e.preventDefault();
|
|
218
|
-
// 되돌린 뒤 blur — 값이 현재와 같아져 blur 의 커밋은 무시된다.
|
|
219
|
-
if (nameInput)
|
|
220
|
-
nameInput.value = displayName();
|
|
221
|
-
nameInput?.blur();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
nameInput.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
225
|
-
nameInput.addEventListener("blur", commitLabel);
|
|
226
|
-
header.appendChild(nameInput);
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
nameSpan = document.createElement("span");
|
|
230
|
-
nameSpan.className =
|
|
231
|
-
"text-xs text-muted-foreground truncate select-none min-w-0";
|
|
232
|
-
nameSpan.style.maxWidth = "220px";
|
|
233
|
-
nameSpan.style.userSelect = "none";
|
|
234
|
-
nameSpan.textContent = displayName();
|
|
235
|
-
header.appendChild(nameSpan);
|
|
236
|
-
}
|
|
237
|
-
const spacer = document.createElement("div");
|
|
238
|
-
spacer.className = "flex-1";
|
|
239
|
-
header.appendChild(spacer);
|
|
240
|
-
// 너비 프리셋 (편집 가능 모드에서만)
|
|
241
|
-
const PRESET_BASE = "px-1.5 py-0.5 rounded text-xs leading-none tabular-nums transition-colors select-none";
|
|
242
|
-
const PRESET_IDLE = `${PRESET_BASE} text-muted-foreground hover:bg-background pdf-width-preset`;
|
|
243
|
-
const PRESET_ACTIVE = `${PRESET_BASE} bg-primary text-primary-foreground pdf-width-preset is-active`;
|
|
154
|
+
/*
|
|
155
|
+
* ── 배율 프리셋 ────────────────────────────────────────────────────
|
|
156
|
+
*
|
|
157
|
+
* ⚠️ 이 칩들은 한때 **칼럼 대비 비율**(`width: 50%`)을 썼다. 그게 틀린 축이었다.
|
|
158
|
+
* 저자가 고르는 건 "칼럼의 절반"이 아니라 "이 정도 크기로 보였으면"인데, 칼럼 폭은
|
|
159
|
+
* 화면마다 다르다 — 문제 편집 화면은 1378px 이지만 코딩 패널 안에서는 훨씬 좁고,
|
|
160
|
+
* 거기서 같은 50% 는 절반 이하로 줄어든다. 저장된 값이 **보는 곳마다 다른 크기**가
|
|
161
|
+
* 된다는 뜻이다. (손잡이 드래그는 원래부터 px 로 저장하고 있었으니 한 문서 안에
|
|
162
|
+
* 두 체계가 섞여 있기도 했다.)
|
|
163
|
+
*
|
|
164
|
+
* 기준은 **PDF 본문 글자가 화면에서 몇 px 로 그려지느냐**다 — 그게 곧 읽히느냐다.
|
|
165
|
+
* pdf.js 는 배율 1 에서 1 PDF 단위 = 1 CSS px 이라 그대로 환산된다:
|
|
166
|
+
*
|
|
167
|
+
* 배율 A4 세로 폭 10pt 본문 쓸모
|
|
168
|
+
* 50% 298px 5px 썸네일(읽기 불가)
|
|
169
|
+
* 75% 446px 7.5px 도표 파악 · 가로형 슬라이드는 읽힘
|
|
170
|
+
* 100% 595px 10px 읽을 수 있음(작음) — 기준점
|
|
171
|
+
* 150% 893px 15px 편하게 읽음
|
|
172
|
+
*
|
|
173
|
+
* 배율은 **페이지 원본 크기 기준**이라 방향·용지 크기를 알아서 반영한다. 같은 칩이
|
|
174
|
+
* 세로형에는 595px, 가로형에는 842px 을 준다 — 글자 크기가 같으니 맞는 결과다.
|
|
175
|
+
* (폭 기준이던 시절 세로형이 부당하게 커 보이던 것이 이래서였다.)
|
|
176
|
+
*
|
|
177
|
+
* ⚠️ `폭맞춤`(칼럼 채우기)은 **일부러 뺐다.** 1378px 칼럼에서 세로형에 걸면 배율
|
|
178
|
+
* 231%, 높이 1950px 로 화면 밖으로 나간다 — 저자 의도가 아니라 우연히 나오는 값이다.
|
|
179
|
+
*/
|
|
180
|
+
const PRESET_IDLE = "pdf-preset";
|
|
181
|
+
const PRESET_ACTIVE = "pdf-preset is-active";
|
|
182
|
+
const ZOOMS = [0.5, 0.75, 1, 1.5];
|
|
244
183
|
const presetButtons = [];
|
|
184
|
+
/** 페이지 원본 폭(CSS px). PDF 를 읽기 전에는 모른다. */
|
|
185
|
+
let naturalWidth = 0;
|
|
186
|
+
/** 배율 → 저장할 px. 원본 폭을 아직 모르면 `null`. */
|
|
187
|
+
const zoomToWidth = (zoom) => naturalWidth > 0 ? `${Math.round(naturalWidth * zoom)}px` : null;
|
|
245
188
|
if (editor.isEditable) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
189
|
+
/*
|
|
190
|
+
* ⚠️ 프리셋은 **문서에 저장되는 크기**이지 보기 상태가 아니다. 옆의 다운로드·새 탭과
|
|
191
|
+
* 성격이 달라(저장됨 vs 그때뿐) 구분선으로 갈라 둔다.
|
|
192
|
+
*/
|
|
193
|
+
for (const zoom of ZOOMS) {
|
|
194
|
+
const label = `${zoom * 100}%`;
|
|
250
195
|
const btn = document.createElement("button");
|
|
251
196
|
btn.type = "button";
|
|
252
197
|
btn.className = PRESET_IDLE;
|
|
253
|
-
btn.title =
|
|
254
|
-
btn.textContent =
|
|
198
|
+
btn.title = `페이지 원본 크기의 ${label}`;
|
|
199
|
+
btn.textContent = label;
|
|
200
|
+
// 원본 폭을 알기 전(로딩 중)에는 누를 수 없다 — 계산할 근거가 없다.
|
|
201
|
+
btn.disabled = true;
|
|
255
202
|
/*
|
|
256
203
|
* ⚠️ `mousedown` 에서 기본 동작을 막는다. 안 막으면 브라우저가 여기서 텍스트 선택을
|
|
257
|
-
* 시작하고, 곧바로 `setAttrs` 가 NodeView 를 다시 그리면서 그 선택이
|
|
258
|
-
* 번져
|
|
204
|
+
* 시작하고, 곧바로 `setAttrs` 가 NodeView 를 다시 그리면서 그 선택이 **조작부 전체**로
|
|
205
|
+
* 번져 칩·버튼이 통째로 파랗게 칠해졌다(사용자 지적).
|
|
259
206
|
* 클릭 자체는 `click` 에서 처리하므로 동작에는 영향이 없다.
|
|
260
207
|
*/
|
|
261
208
|
btn.addEventListener("mousedown", (e) => {
|
|
@@ -265,32 +212,41 @@ export const PdfBlock = TiptapNode.create({
|
|
|
265
212
|
btn.addEventListener("click", (e) => {
|
|
266
213
|
e.preventDefault();
|
|
267
214
|
e.stopPropagation();
|
|
268
|
-
|
|
215
|
+
const next = zoomToWidth(zoom);
|
|
216
|
+
if (next)
|
|
217
|
+
setAttrs({ width: next });
|
|
269
218
|
});
|
|
270
|
-
|
|
271
|
-
presetButtons.push({
|
|
219
|
+
endCluster.appendChild(btn);
|
|
220
|
+
presetButtons.push({ zoom, el: btn });
|
|
272
221
|
}
|
|
273
|
-
|
|
222
|
+
const divider = document.createElement("div");
|
|
223
|
+
divider.className = "pdf-divider";
|
|
224
|
+
endCluster.appendChild(divider);
|
|
274
225
|
}
|
|
275
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* 현재 `width` 와 같은 프리셋을 눌린 상태로 만든다.
|
|
228
|
+
*
|
|
229
|
+
* ⚠️ 문자열 비교로는 안 된다. 저장값이 `"893px"` 처럼 계산된 px 라 반올림 오차가
|
|
230
|
+
* 있고, 손잡이로 끌면 아무 px 이나 들어온다. 1px 여유로 견준다.
|
|
231
|
+
* 옛 문서의 `"50%"` 같은 값은 어느 칩과도 안 맞는다 — **그대로 두는 게 맞다.**
|
|
232
|
+
* 저자가 다시 고를 때까지 예전 크기로 렌더되고, 스키마는 건드리지 않는다.
|
|
233
|
+
*/
|
|
276
234
|
const syncPresets = (width) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
235
|
+
const px = width?.endsWith("px") ? Number.parseFloat(width) : NaN;
|
|
236
|
+
for (const { zoom, el } of presetButtons) {
|
|
237
|
+
const target = naturalWidth > 0 ? naturalWidth * zoom : NaN;
|
|
238
|
+
const on = Number.isFinite(px) && Math.abs(px - target) <= 1;
|
|
239
|
+
el.className = on ? PRESET_ACTIVE : PRESET_IDLE;
|
|
240
|
+
el.setAttribute("aria-pressed", on ? "true" : "false");
|
|
280
241
|
}
|
|
281
242
|
};
|
|
282
243
|
syncPresets(node.attrs.width ?? null);
|
|
283
|
-
const btnGroup = document.createElement("div");
|
|
284
|
-
btnGroup.className = "flex items-center gap-1";
|
|
285
|
-
header.appendChild(btnGroup);
|
|
286
244
|
const downloadLink = document.createElement("a");
|
|
287
245
|
downloadLink.rel = "noopener noreferrer";
|
|
288
246
|
downloadLink.setAttribute("download", cleanName(node.attrs.name));
|
|
289
|
-
downloadLink.className =
|
|
290
|
-
"p-1 rounded hover:bg-muted transition-colors text-muted-foreground cursor-pointer";
|
|
247
|
+
downloadLink.className = "pdf-ctl";
|
|
291
248
|
downloadLink.title = "\uB2E4\uC6B4\uB85C\uB4DC";
|
|
292
|
-
downloadLink.innerHTML =
|
|
293
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
|
249
|
+
downloadLink.innerHTML = icon('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>');
|
|
294
250
|
downloadLink.addEventListener("click", async (e) => {
|
|
295
251
|
if (!downloadLink.href)
|
|
296
252
|
return;
|
|
@@ -314,68 +270,178 @@ export const PdfBlock = TiptapNode.create({
|
|
|
314
270
|
window.open(downloadLink.href, "_blank", "noopener,noreferrer");
|
|
315
271
|
}
|
|
316
272
|
});
|
|
317
|
-
|
|
273
|
+
endCluster.appendChild(downloadLink);
|
|
274
|
+
/*
|
|
275
|
+
* \uC804\uCCB4\uD654\uBA74 \uB300\uC2E0 **\uC0C8 \uD0ED**\uC774\uB2E4(\uC0AC\uC6A9\uC790 \uACB0\uC815). \uBE0C\uB77C\uC6B0\uC800 \uAE30\uBCF8 PDF \uBDF0\uC5B4\uAC00 \uCC3D \uC804\uCCB4\uB97C \uC4F0\uACE0
|
|
276
|
+
* \uAC80\uC0C9\u00B7\uC778\uC1C4\u00B7\uD655\uB300\uAE4C\uC9C0 \uC5B9\uD600 \uC624\uBBC0\uB85C, \uC6B0\uB9AC\uAC00 \uB9CC\uB4E0 \uC804\uCCB4\uD654\uBA74\uBCF4\uB2E4 \uC77D\uAE30\uC5D0 \uB0AB\uB2E4.
|
|
277
|
+
*/
|
|
318
278
|
const openLink = document.createElement("a");
|
|
319
279
|
openLink.target = "_blank";
|
|
320
280
|
openLink.rel = "noopener noreferrer";
|
|
321
|
-
openLink.className =
|
|
322
|
-
"p-1 rounded hover:bg-muted transition-colors text-muted-foreground";
|
|
281
|
+
openLink.className = "pdf-ctl";
|
|
323
282
|
openLink.title = "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30";
|
|
324
|
-
openLink.innerHTML =
|
|
325
|
-
|
|
326
|
-
btnGroup.appendChild(openLink);
|
|
283
|
+
openLink.innerHTML = icon('<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>');
|
|
284
|
+
endCluster.appendChild(openLink);
|
|
327
285
|
if (editor.isEditable) {
|
|
328
286
|
const deleteBtn = document.createElement("button");
|
|
329
287
|
deleteBtn.type = "button";
|
|
330
|
-
deleteBtn.className =
|
|
331
|
-
"p-1 rounded hover:bg-destructive/10 transition-colors text-muted-foreground hover:text-destructive";
|
|
288
|
+
deleteBtn.className = "pdf-ctl pdf-ctl-danger";
|
|
332
289
|
deleteBtn.title = "\uC0AD\uC81C";
|
|
333
|
-
deleteBtn.innerHTML =
|
|
334
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>';
|
|
290
|
+
deleteBtn.innerHTML = icon('<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>');
|
|
335
291
|
deleteBtn.addEventListener("click", () => {
|
|
336
292
|
const pos = getPos();
|
|
337
293
|
if (pos != null) {
|
|
338
294
|
editor.commands.deleteRange({
|
|
339
295
|
from: pos,
|
|
340
|
-
to: pos +
|
|
296
|
+
to: pos + currentNode.nodeSize,
|
|
341
297
|
});
|
|
342
298
|
}
|
|
343
299
|
});
|
|
344
|
-
|
|
300
|
+
endCluster.appendChild(deleteBtn);
|
|
345
301
|
}
|
|
346
|
-
// Content area
|
|
347
|
-
const contentArea = document.createElement("div");
|
|
348
|
-
contentArea.className = "flex items-center justify-center";
|
|
349
|
-
wrapper.appendChild(contentArea);
|
|
350
302
|
const canvas = document.createElement("canvas");
|
|
351
|
-
canvas
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
'<p class="text-sm text-muted-foreground animate-pulse">PDF \uB85C\uB529 \uC911...</p>';
|
|
357
|
-
contentArea.appendChild(loadingDiv);
|
|
303
|
+
frame.appendChild(canvas);
|
|
304
|
+
const statusDiv = document.createElement("div");
|
|
305
|
+
statusDiv.className = "pdf-status";
|
|
306
|
+
statusDiv.textContent = "PDF \uB85C\uB529 \uC911...";
|
|
307
|
+
frame.appendChild(statusDiv);
|
|
358
308
|
canvas.style.display = "none";
|
|
359
|
-
|
|
309
|
+
// \uC870\uC791\uBD80\uB294 \uB9C8\uC9C0\uB9C9 \u2014 `position:absolute` \uB77C \uC21C\uC11C\uC640 \uBB34\uAD00\uD558\uAC8C \uC704\uC5D0 \uADF8\uB824\uC9C0\uC9C0\uB9CC,
|
|
310
|
+
// \uB0B4\uC6A9 \uB2E4\uC74C\uC5D0 \uC624\uB294 \uAC8C \uBCF4\uC870\uAE30\uC220\uC5D0\uAC8C\uB3C4 \uB9DE\uB294 \uC21C\uC11C\uB2E4.
|
|
311
|
+
frame.appendChild(overlay);
|
|
312
|
+
/* \u2500\u2500 \uD398\uC774\uC9C0 \uC774\uB3D9 \u2014 \uC88C\uC6B0 \uD654\uC0B4\uD45C + \uC544\uB798 \uAC00\uC6B4\uB370 \uCABD\uC218 \u2500\u2500 */
|
|
313
|
+
const prevBtn = document.createElement("button");
|
|
314
|
+
prevBtn.type = "button";
|
|
315
|
+
prevBtn.className = "pdf-ctl pdf-nav pdf-nav-prev pdf-cluster";
|
|
316
|
+
prevBtn.title = "\uC774\uC804 \uD398\uC774\uC9C0";
|
|
317
|
+
prevBtn.setAttribute("aria-label", "\uC774\uC804 \uD398\uC774\uC9C0");
|
|
318
|
+
prevBtn.innerHTML = icon('<path d="m15 18-6-6 6-6"/>');
|
|
319
|
+
const nextBtn = document.createElement("button");
|
|
320
|
+
nextBtn.type = "button";
|
|
321
|
+
nextBtn.className = "pdf-ctl pdf-nav pdf-nav-next pdf-cluster";
|
|
322
|
+
nextBtn.title = "\uB2E4\uC74C \uD398\uC774\uC9C0";
|
|
323
|
+
nextBtn.setAttribute("aria-label", "\uB2E4\uC74C \uD398\uC774\uC9C0");
|
|
324
|
+
nextBtn.innerHTML = icon('<path d="m9 18 6-6-6-6"/>');
|
|
325
|
+
/*
|
|
326
|
+
* \uD310\uC774 \uD654\uBA74\uBCF4\uB2E4 \uAE38 \uB54C \uC77D\uAE30 \uC870\uC791\uBD80\uAC00 \uB530\uB77C\uC624\uAC8C \uD558\uB294 \uB808\uC77C. `position: sticky` \uB77C
|
|
327
|
+
* \uD310\uC744 \uC9C0\uB098\uB294 \uB3D9\uC548 \uD56D\uC0C1 \uBCF4\uC774\uB294 \uC601\uC5ED\uC5D0 \uBA38\uBB34\uB978\uB2E4. \uD310\uC774 \uD654\uBA74\uBCF4\uB2E4 \uC9E7\uC73C\uBA74
|
|
328
|
+
* `max-height: 100%` \uAC00 \uB808\uC77C\uC744 \uD310 \uD06C\uAE30\uB85C \uC904\uC5EC \uC608\uC804\uACFC \uAC19\uC774 \uD310 \uAC00\uC6B4\uB370\u00b7\uBC11\uC5D0 \uB193\uC778\uB2E4.
|
|
329
|
+
*/
|
|
330
|
+
const navRail = document.createElement("div");
|
|
331
|
+
navRail.className = "pdf-nav-rail";
|
|
332
|
+
const pageCluster = document.createElement("div");
|
|
333
|
+
pageCluster.className = "pdf-cluster pdf-cluster-page";
|
|
334
|
+
const pageBtn = document.createElement("button");
|
|
335
|
+
pageBtn.type = "button";
|
|
336
|
+
pageBtn.className = "pdf-page";
|
|
337
|
+
pageBtn.title = "\uCABD \uBC88\uD638\uB85C \uC774\uB3D9";
|
|
338
|
+
pageCluster.appendChild(pageBtn);
|
|
339
|
+
/*
|
|
340
|
+
* \uCABD\uC218\uB97C \uB20C\uB7EC **\uBC88\uD638\uB85C \uAC74\uB108\uB6F4\uB2E4.** \uC774\uAC8C \uC5C6\uC73C\uBA74 30\uCABD\uC9DC\uB9AC \uC790\uB8CC\uC5D0\uC11C \uB4A4\uB85C \uAC00\uB824\uBA74
|
|
341
|
+
* \uD654\uC0B4\uD45C\uB97C 30\uBC88 \uB20C\uB7EC\uC57C \uD55C\uB2E4 \u2014 \uD398\uC774\uC9C0 \uB118\uAE40 \uBAA8\uB378\uC758 \uAC00\uC7A5 \uC544\uD508 \uC9C0\uC810\uC774\uB2E4.
|
|
342
|
+
*/
|
|
343
|
+
const pageInput = document.createElement("input");
|
|
344
|
+
pageInput.type = "text";
|
|
345
|
+
pageInput.inputMode = "numeric";
|
|
346
|
+
pageInput.className = "pdf-page-input";
|
|
347
|
+
pageInput.setAttribute("aria-label", "\uCABD \uBC88\uD638");
|
|
348
|
+
pageInput.style.display = "none";
|
|
349
|
+
pageCluster.appendChild(pageInput);
|
|
360
350
|
let currentPage = 1;
|
|
361
351
|
let totalPages = 0;
|
|
362
352
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
363
353
|
let pdfDoc = null;
|
|
364
354
|
let rendering = false;
|
|
355
|
+
let renderQueued = false;
|
|
356
|
+
/** 쪽수 표시와 화살표 비활성 상태를 현재 페이지에 맞춘다. */
|
|
357
|
+
const syncPageUi = () => {
|
|
358
|
+
pageBtn.textContent = `${currentPage} / ${totalPages}`;
|
|
359
|
+
prevBtn.disabled = currentPage <= 1;
|
|
360
|
+
nextBtn.disabled = currentPage >= totalPages;
|
|
361
|
+
};
|
|
362
|
+
const goToPage = (next) => {
|
|
363
|
+
const clamped = Math.min(totalPages, Math.max(1, next));
|
|
364
|
+
if (clamped === currentPage)
|
|
365
|
+
return;
|
|
366
|
+
currentPage = clamped;
|
|
367
|
+
syncPageUi();
|
|
368
|
+
renderPage();
|
|
369
|
+
};
|
|
370
|
+
prevBtn.addEventListener("click", () => goToPage(currentPage - 1));
|
|
371
|
+
nextBtn.addEventListener("click", () => goToPage(currentPage + 1));
|
|
372
|
+
const closePageInput = () => {
|
|
373
|
+
pageInput.style.display = "none";
|
|
374
|
+
pageBtn.style.display = "";
|
|
375
|
+
};
|
|
376
|
+
pageBtn.addEventListener("click", () => {
|
|
377
|
+
pageInput.value = String(currentPage);
|
|
378
|
+
pageBtn.style.display = "none";
|
|
379
|
+
pageInput.style.display = "";
|
|
380
|
+
pageInput.focus();
|
|
381
|
+
pageInput.select();
|
|
382
|
+
});
|
|
383
|
+
pageInput.addEventListener("keydown", (e) => {
|
|
384
|
+
// tiptap 이 단축키를 가로채면 숫자가 안 들어간다(이름칸과 같은 이유).
|
|
385
|
+
e.stopPropagation();
|
|
386
|
+
if (e.key === "Enter") {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
const parsed = Number.parseInt(pageInput.value, 10);
|
|
389
|
+
if (Number.isFinite(parsed))
|
|
390
|
+
goToPage(parsed);
|
|
391
|
+
closePageInput();
|
|
392
|
+
}
|
|
393
|
+
else if (e.key === "Escape") {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
closePageInput();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
pageInput.addEventListener("blur", closePageInput);
|
|
399
|
+
/*
|
|
400
|
+
* 화살표 키는 **조작부 안에 포커스가 있을 때만** 받는다. 블록 전체에서 가로채면
|
|
401
|
+
* 편집 중 이 블록을 지나 위아래로 빠져나가는 길이 막힌다(atom 노드라 화살표가
|
|
402
|
+
* ProseMirror 의 탈출 경로다). 넘김 버튼을 한 번 누르면 그때부터 키가 듣는다.
|
|
403
|
+
*/
|
|
404
|
+
overlay.addEventListener("keydown", (e) => {
|
|
405
|
+
if (e.target instanceof HTMLInputElement)
|
|
406
|
+
return;
|
|
407
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
408
|
+
return;
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
e.stopPropagation();
|
|
411
|
+
goToPage(currentPage + (e.key === "ArrowLeft" ? -1 : 1));
|
|
412
|
+
});
|
|
413
|
+
/*
|
|
414
|
+
* ⚠️ **그리는 중에 들어온 요청을 버리면 안 된다.** 예전엔 `if (rendering) return` 으로
|
|
415
|
+
* 그냥 흘려보냈는데, 폭 프리셋이나 창 크기 변경이 **직전 렌더가 끝나기 전에** 오면
|
|
416
|
+
* 그 요청이 통째로 사라져 캔버스가 옛 크기에 머문다(다음 렌더까지 아무도 다시
|
|
417
|
+
* 부르지 않는다). 겹치면 한 번 예약해 뒀다가 끝나고 이어서 그린다.
|
|
418
|
+
* pdf.js 는 같은 캔버스에 렌더가 겹치면 던지므로 **직렬화 자체는 유지**한다.
|
|
419
|
+
*/
|
|
365
420
|
async function renderPage() {
|
|
366
|
-
if (!pdfDoc ||
|
|
421
|
+
if (!pdfDoc || destroyed)
|
|
367
422
|
return;
|
|
423
|
+
if (rendering) {
|
|
424
|
+
renderQueued = true;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
368
427
|
rendering = true;
|
|
369
428
|
try {
|
|
370
429
|
const pageObj = await pdfDoc.getPage(currentPage);
|
|
371
430
|
if (destroyed)
|
|
372
431
|
return;
|
|
373
432
|
const unscaledViewport = pageObj.getViewport({ scale: 1 });
|
|
374
|
-
const availableWidth =
|
|
375
|
-
if (availableWidth <= 0)
|
|
376
|
-
rendering = false;
|
|
433
|
+
const availableWidth = frame.clientWidth;
|
|
434
|
+
if (availableWidth <= 0)
|
|
377
435
|
return;
|
|
378
|
-
|
|
436
|
+
/*
|
|
437
|
+
* 폭에만 맞춘다 — **높이는 제한하지 않는다.**
|
|
438
|
+
*
|
|
439
|
+
* ⚠️ 한때 `min(폭맞춤, 0.78vh)` 로 한 장이 화면에 들어가게 했다가 되돌렸다.
|
|
440
|
+
* A4 는 세로가 길어서 웬만한 창에서는 **높이 쪽이 항상 먼저 걸리고**, 그러면
|
|
441
|
+
* 폭 프리셋 50%/75%/100% 가 **전부 같은 크기로 그려진다**(실측: 50% 도 100% 도
|
|
442
|
+
* 캔버스 673px). 저자가 고른 폭이 무의미해지는 건 얻는 것보다 잃는 게 크다.
|
|
443
|
+
* 판이 화면보다 길 때 조작부가 따라오게 하는 건 CSS(`.pdf-nav-rail`)가 맡는다.
|
|
444
|
+
*/
|
|
379
445
|
const scale = availableWidth / unscaledViewport.width;
|
|
380
446
|
const viewport = pageObj.getViewport({ scale });
|
|
381
447
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -384,10 +450,8 @@ export const PdfBlock = TiptapNode.create({
|
|
|
384
450
|
canvas.style.width = `${viewport.width}px`;
|
|
385
451
|
canvas.style.height = `${viewport.height}px`;
|
|
386
452
|
const ctx = canvas.getContext("2d");
|
|
387
|
-
if (!ctx)
|
|
388
|
-
rendering = false;
|
|
453
|
+
if (!ctx)
|
|
389
454
|
return;
|
|
390
|
-
}
|
|
391
455
|
ctx.resetTransform();
|
|
392
456
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
393
457
|
ctx.scale(dpr, dpr);
|
|
@@ -396,10 +460,9 @@ export const PdfBlock = TiptapNode.create({
|
|
|
396
460
|
finally {
|
|
397
461
|
rendering = false;
|
|
398
462
|
}
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
pageSpan.textContent = `${currentPage} / ${totalPages}`;
|
|
463
|
+
if (renderQueued && !destroyed) {
|
|
464
|
+
renderQueued = false;
|
|
465
|
+
await renderPage();
|
|
403
466
|
}
|
|
404
467
|
}
|
|
405
468
|
async function loadPdf(src) {
|
|
@@ -412,60 +475,42 @@ export const PdfBlock = TiptapNode.create({
|
|
|
412
475
|
if (destroyed)
|
|
413
476
|
return;
|
|
414
477
|
totalPages = pdfDoc.numPages;
|
|
415
|
-
|
|
478
|
+
/*
|
|
479
|
+
* 배율 프리셋의 기준점 — **1쪽의 원본 폭**. pdf.js 는 배율 1 에서
|
|
480
|
+
* 1 PDF 단위 = 1 CSS px 이므로 이 값이 곧 "원본 크기"다.
|
|
481
|
+
* 쪽마다 크기가 다른 문서가 있지만, 저자가 블록 크기를 고르는 기준은 하나여야
|
|
482
|
+
* 하므로 1쪽으로 고정한다(넘길 때마다 블록이 들썩이면 안 된다).
|
|
483
|
+
*/
|
|
484
|
+
const firstPage = await pdfDoc.getPage(1);
|
|
485
|
+
if (destroyed)
|
|
486
|
+
return;
|
|
487
|
+
naturalWidth = firstPage.getViewport({ scale: 1 }).width;
|
|
488
|
+
for (const { el } of presetButtons)
|
|
489
|
+
el.disabled = false;
|
|
490
|
+
syncPresets(currentNode.attrs.width ?? null);
|
|
491
|
+
statusDiv.style.display = "none";
|
|
416
492
|
canvas.style.display = "";
|
|
493
|
+
// \uD55C \uC7A5\uC9DC\uB9AC\uC5D4 \uB118\uAE40 \uC870\uC791\uC744 \uC544\uC608 \uBD99\uC774\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uB204\uB97C \uC218 \uC5C6\uB294 \uBC84\uD2BC\uC740 \uC18C\uC74C\uC774\uB2E4.
|
|
417
494
|
if (totalPages > 1) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
prevBtn.type = "button";
|
|
424
|
-
prevBtn.className =
|
|
425
|
-
"p-1.5 rounded hover:bg-muted transition-colors disabled:opacity-30 text-foreground";
|
|
426
|
-
prevBtn.innerHTML =
|
|
427
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
|
|
428
|
-
prevBtn.addEventListener("click", () => {
|
|
429
|
-
if (currentPage > 1) {
|
|
430
|
-
currentPage--;
|
|
431
|
-
prevBtn.disabled = currentPage <= 1;
|
|
432
|
-
nextBtn.disabled = currentPage >= totalPages;
|
|
433
|
-
renderPage();
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
navDiv.appendChild(prevBtn);
|
|
437
|
-
const pageSpan = document.createElement("span");
|
|
438
|
-
pageSpan.className = "text-sm tabular-nums page-counter select-none";
|
|
439
|
-
pageSpan.style.userSelect = "none";
|
|
440
|
-
pageSpan.textContent = `1 / ${totalPages}`;
|
|
441
|
-
navDiv.appendChild(pageSpan);
|
|
442
|
-
const nextBtn = document.createElement("button");
|
|
443
|
-
nextBtn.type = "button";
|
|
444
|
-
nextBtn.className =
|
|
445
|
-
"p-1.5 rounded hover:bg-muted transition-colors disabled:opacity-30 text-foreground";
|
|
446
|
-
nextBtn.innerHTML =
|
|
447
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
|
|
448
|
-
nextBtn.addEventListener("click", () => {
|
|
449
|
-
if (currentPage < totalPages) {
|
|
450
|
-
currentPage++;
|
|
451
|
-
prevBtn.disabled = currentPage <= 1;
|
|
452
|
-
nextBtn.disabled = currentPage >= totalPages;
|
|
453
|
-
renderPage();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
navDiv.appendChild(nextBtn);
|
|
457
|
-
wrapper.appendChild(navDiv);
|
|
495
|
+
navRail.appendChild(prevBtn);
|
|
496
|
+
navRail.appendChild(nextBtn);
|
|
497
|
+
navRail.appendChild(pageCluster);
|
|
498
|
+
overlay.appendChild(navRail);
|
|
499
|
+
syncPageUi();
|
|
458
500
|
}
|
|
459
501
|
renderPage();
|
|
502
|
+
/*
|
|
503
|
+
* \u26A0\uFE0F \uAD00\uCC30 \uB300\uC0C1\uC740 `dom` \uC774\uC9C0 \uD310\uC774 \uC544\uB2C8\uB2E4. \uD310\uC744 \uBCF4\uBA74 \uCE94\uBC84\uC2A4 \uB192\uC774\uAC00 \uBC14\uB014 \uB54C\uB9C8\uB2E4
|
|
504
|
+
* \uB2E4\uC2DC \uBD88\uB824 \uB418\uBA39\uC784\uC774 \uC0DD\uAE34\uB2E4 \u2014 \uD3ED\uC740 `dom` \uC5D0\uC11C\uB9CC \uBC14\uB010\uB2E4.
|
|
505
|
+
*/
|
|
460
506
|
resizeObserver = new ResizeObserver(() => {
|
|
461
507
|
clearTimeout(resizeTimeout);
|
|
462
508
|
resizeTimeout = setTimeout(renderPage, 100);
|
|
463
509
|
});
|
|
464
|
-
resizeObserver.observe(
|
|
510
|
+
resizeObserver.observe(dom);
|
|
465
511
|
}
|
|
466
512
|
catch {
|
|
467
|
-
|
|
468
|
-
'<p class="text-sm text-muted-foreground">PDF\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p>';
|
|
513
|
+
statusDiv.textContent = "PDF\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
469
514
|
}
|
|
470
515
|
}
|
|
471
516
|
// URL 해결 후 PDF 로드
|
|
@@ -497,10 +542,8 @@ export const PdfBlock = TiptapNode.create({
|
|
|
497
542
|
resolver(node.attrs.fileId)
|
|
498
543
|
.then((result) => {
|
|
499
544
|
if (result.name) {
|
|
500
|
-
//
|
|
501
|
-
resolvedName = result.name;
|
|
545
|
+
// \uB2E4\uC6B4\uB85C\uB4DC \uD30C\uC77C\uBA85\uC740 \uD56D\uC0C1 \uC2E4\uC81C \uD30C\uC77C\uBA85\uC774\uB2E4.
|
|
502
546
|
downloadLink.setAttribute("download", result.name);
|
|
503
|
-
syncName();
|
|
504
547
|
}
|
|
505
548
|
})
|
|
506
549
|
.catch(() => { });
|
|
@@ -513,12 +556,24 @@ export const PdfBlock = TiptapNode.create({
|
|
|
513
556
|
if (updatedNode.type.name !== "pdfBlock")
|
|
514
557
|
return false;
|
|
515
558
|
const newWidth = updatedNode.attrs.width;
|
|
516
|
-
|
|
559
|
+
const changed = newWidth !== currentNode.attrs.width;
|
|
560
|
+
if (changed)
|
|
517
561
|
dom.style.width = newWidth || "";
|
|
518
|
-
}
|
|
519
562
|
currentNode = updatedNode;
|
|
520
563
|
syncPresets(newWidth ?? null);
|
|
521
|
-
|
|
564
|
+
/*
|
|
565
|
+
* ⚠️ **크기가 바뀌면 여기서 직접 다시 그린다.** 예전엔 `ResizeObserver` 가
|
|
566
|
+
* 알아서 잡아 주길 기대했는데, 그건 크기를 **누가 바꿨는지 모를 때**를 위한
|
|
567
|
+
* 장치다. 프리셋 클릭·드래그 커밋은 새 폭을 이미 알고 들어오는 경로이므로
|
|
568
|
+
* 관찰자를 한 바퀴 돌 이유가 없다.
|
|
569
|
+
*
|
|
570
|
+
* 실제로 이것 때문에 프리셋이 조용히 반쪽만 동작했다 — 블록 폭은 595px 로
|
|
571
|
+
* 줄었는데 캔버스는 1376px 그대로였다. (자동화 탭은 `visibilityState: hidden`
|
|
572
|
+
* 이라 `ResizeObserver` 콜백이 아예 안 와서 오래 못 잡은 증상이기도 하다.)
|
|
573
|
+
* `frame.clientWidth` 는 읽는 순간 레이아웃을 강제하므로 새 폭이 바로 잡힌다.
|
|
574
|
+
*/
|
|
575
|
+
if (changed)
|
|
576
|
+
renderPage();
|
|
522
577
|
return true;
|
|
523
578
|
},
|
|
524
579
|
// 이름 입력칸 위의 이벤트는 ProseMirror 가 가로채면 안 된다(CardBlock 과 같은 방식).
|
|
@@ -526,8 +581,15 @@ export const PdfBlock = TiptapNode.create({
|
|
|
526
581
|
const target = event.target;
|
|
527
582
|
return target instanceof Element && !!target.closest("[data-pdf-control]");
|
|
528
583
|
},
|
|
529
|
-
|
|
530
|
-
|
|
584
|
+
/*
|
|
585
|
+
* ⚠️ 여기 `selectNode`/`deselectNode` 를 빈 함수로 두면 안 된다.
|
|
586
|
+
*
|
|
587
|
+
* ProseMirror 는 이 훅이 **있으면** "노드뷰가 알아서 표시하겠다"로 보고
|
|
588
|
+
* `ProseMirror-selectednode` 클래스를 **아예 붙이지 않는다.** 빈 함수라도 마찬가지라,
|
|
589
|
+
* 선택 표시를 CSS 로 아무리 써도 매칭될 선택자가 생기지 않는다 — 편집 중 PDF 를
|
|
590
|
+
* 클릭해도 아무 표시가 없던 이유가 이것이었다(사용자 지적).
|
|
591
|
+
* 훅을 아예 두지 않으면 클래스 관리는 ProseMirror 가 한다.
|
|
592
|
+
*/
|
|
531
593
|
destroy: () => {
|
|
532
594
|
destroyed = true;
|
|
533
595
|
clearTimeout(resizeTimeout);
|