@teriusu/rich-editor 0.11.4 → 0.11.6

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.
@@ -15,8 +15,23 @@
15
15
  */
16
16
  export declare function canEmbedCrossOrigin(): boolean;
17
17
  /**
18
- * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
19
- * 무시되므로 조건 없이 붙여도 해가 없다.
18
+ * 제3자 임베드 iframe 이 **실제로 로드되게** 하는 속성 묶음. 둘은 막는 층이 다르므로
19
+ * 항상 같이 붙인다.
20
+ *
21
+ * 1. `credentialless` — 격리된 문서(COEP `require-corp`)에서 COEP 없는 남의 iframe 을
22
+ * 싣는 유일한 통로. 크로미움이 아니면 무시되므로 조건 없이 붙여도 해가 없다.
23
+ *
24
+ * 2. `referrerpolicy` — ⚠️ **호스트가 Referer 를 끄고 있을 수 있다.** 정올은
25
+ * `app.html` 에 `<meta name="referrer" content="same-origin">` 을 두어 교차 출처로는
26
+ * Referer 를 **아예 안 보낸다.** 그런데 mbus 채널 중에는 Referer 로 출처를 거르는
27
+ * 것이 있어서, 그런 영상은 CDN 청크가 403 으로 막히고 플레이어가
28
+ * `인코딩에 실패했거나 접근 제한되어 재생할 수 없습니다` 를 띄운다 — 영상 자체는
29
+ * 멀쩡한데(최상위 playlist 는 200) 안 나오는, 설명이 안 되는 실패로 보인다.
30
+ * 실측(`ch_17827535`): Referer 없음 → 403 · `https://jungol.co.kr/` → 200 ·
31
+ * 엉뚱한 출처 → 403. 요소에 건 정책이 문서 메타를 이기므로 여기서 되돌린다.
32
+ * ⚠️ 전체 URL 이 아니라 **출처만** 보낸다(`strict-origin-…`) — 어느 글에서 봤는지까지
33
+ * 남의 서버에 알릴 이유는 없다. 앱의 다른 mbus 자리(`MidibusInner`·`legacy-iframe`)가
34
+ * 쓰는 값과 같다.
20
35
  */
21
36
  export declare function applyCrossOriginEmbedAttrs(iframe: HTMLIFrameElement): void;
22
37
  //# sourceMappingURL=embed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/utils/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAI7C;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1E"}
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/utils/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAI7C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAG1E"}
@@ -21,9 +21,25 @@ export function canEmbedCrossOrigin() {
21
21
  return "credentialless" in HTMLIFrameElement.prototype;
22
22
  }
23
23
  /**
24
- * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
25
- * 무시되므로 조건 없이 붙여도 해가 없다.
24
+ * 제3자 임베드 iframe 이 **실제로 로드되게** 하는 속성 묶음. 둘은 막는 층이 다르므로
25
+ * 항상 같이 붙인다.
26
+ *
27
+ * 1. `credentialless` — 격리된 문서(COEP `require-corp`)에서 COEP 없는 남의 iframe 을
28
+ * 싣는 유일한 통로. 크로미움이 아니면 무시되므로 조건 없이 붙여도 해가 없다.
29
+ *
30
+ * 2. `referrerpolicy` — ⚠️ **호스트가 Referer 를 끄고 있을 수 있다.** 정올은
31
+ * `app.html` 에 `<meta name="referrer" content="same-origin">` 을 두어 교차 출처로는
32
+ * Referer 를 **아예 안 보낸다.** 그런데 mbus 채널 중에는 Referer 로 출처를 거르는
33
+ * 것이 있어서, 그런 영상은 CDN 청크가 403 으로 막히고 플레이어가
34
+ * `인코딩에 실패했거나 접근 제한되어 재생할 수 없습니다` 를 띄운다 — 영상 자체는
35
+ * 멀쩡한데(최상위 playlist 는 200) 안 나오는, 설명이 안 되는 실패로 보인다.
36
+ * 실측(`ch_17827535`): Referer 없음 → 403 · `https://jungol.co.kr/` → 200 ·
37
+ * 엉뚱한 출처 → 403. 요소에 건 정책이 문서 메타를 이기므로 여기서 되돌린다.
38
+ * ⚠️ 전체 URL 이 아니라 **출처만** 보낸다(`strict-origin-…`) — 어느 글에서 봤는지까지
39
+ * 남의 서버에 알릴 이유는 없다. 앱의 다른 mbus 자리(`MidibusInner`·`legacy-iframe`)가
40
+ * 쓰는 값과 같다.
26
41
  */
27
42
  export function applyCrossOriginEmbedAttrs(iframe) {
28
43
  iframe.setAttribute("credentialless", "");
44
+ iframe.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
29
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/utils/resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,GAAG,CAAC;AAEnC,MAAM,WAAW,mBAAmB;IACnC,uCAAuC;IACvC,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACjC,mDAAmD;IACnD,OAAO,EAAE,MAAM,eAAe,CAAC;IAC/B,qCAAqC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/E;AA4BD,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,IAAI,CA+HrE"}
1
+ {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/utils/resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,GAAG,CAAC;AAEnC,MAAM,WAAW,mBAAmB;IACnC,uCAAuC;IACvC,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACjC,mDAAmD;IACnD,OAAO,EAAE,MAAM,eAAe,CAAC;IAC/B,qCAAqC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/E;AA4BD,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,IAAI,CA0LrE"}
@@ -45,6 +45,52 @@ export function attachResize(options) {
45
45
  let resizing = false;
46
46
  let start = 0;
47
47
  let startSize = 0;
48
+ /*
49
+ * ── 왜 프레임에 한 번만 쓰는가 ────────────────────────────────────────────────
50
+ *
51
+ * `pointermove` 는 **화면 주사율과 무관하게** 들어온다 — 고폴링 마우스는 한 프레임에
52
+ * 여러 번 던진다. 예전엔 들어오는 족족 `dom.style.width` 를 썼는데, 이 상자는 안에
53
+ * `aspect-ratio` 박스와 **교차 출처 iframe**(영상)을 품고 있어서 한 번 쓸 때마다
54
+ * 레이아웃이 iframe 까지 내려간다. 즉 한 프레임에 같은 일을 여러 번 하고, 마지막 값만
55
+ * 화면에 남는다 — 앞의 것들은 전부 버려지는 계산이다.
56
+ *
57
+ * 값은 즉시 받아 두되 **적용은 프레임당 한 번**으로 접는다. 끄는 감각(지연)은 그대로고
58
+ * 버려지던 레이아웃만 사라진다.
59
+ */
60
+ let pendingSize = 0;
61
+ let frame = 0;
62
+ const applyPending = () => {
63
+ frame = 0;
64
+ if (axis === "x")
65
+ dom.style.width = `${pendingSize}px`;
66
+ else
67
+ dom.style.height = `${pendingSize}px`;
68
+ };
69
+ const flushPending = () => {
70
+ if (!frame)
71
+ return;
72
+ cancelAnimationFrame(frame);
73
+ applyPending();
74
+ };
75
+ /*
76
+ * ⚠️ **드래그하는 동안 iframe 이 포인터를 가로채지 못하게 막는다.**
77
+ *
78
+ * 손잡이는 상자 오른쪽 12px 밖에 있고, 끌면 커서는 곧바로 상자 **안**(영상 iframe 위)을
79
+ * 지난다. 교차 출처 iframe 위에서는 그 이벤트가 iframe 문서로 가고 부모 window 에는
80
+ * 오지 않는다 — 위 `attachWindow` 가 window 에 걸어 둔 리스너도 마찬가지다. 그러면
81
+ * 끌던 도중에 크기가 멈췄다 튀었다 한다.
82
+ *
83
+ * `setPointerCapture` 로도 막히지만 이 코드는 그걸 일부러 피한다(아래 주석) — 대신
84
+ * **드래그 동안만** 자식 임베드의 포인터를 끈다. 드래그가 끝나면 되돌린다.
85
+ * ⚠️ 빈 문자열로 되돌린다(`none` → `''`). `auto` 로 되돌리면 원래 CSS 가 갖고 있던
86
+ * 값을 덮어쓴다.
87
+ */
88
+ const EMBEDS = "iframe, embed, object, video";
89
+ const shieldEmbeds = (on) => {
90
+ for (const el of Array.from(dom.querySelectorAll(EMBEDS))) {
91
+ el.style.pointerEvents = on ? "none" : "";
92
+ }
93
+ };
48
94
  const paint = (active) => {
49
95
  bar.style.background = active ? ACTIVE : IDLE;
50
96
  };
@@ -68,17 +114,21 @@ export function attachResize(options) {
68
114
  if (!resizing)
69
115
  return;
70
116
  const delta = (axis === "x" ? e.clientX : e.clientY) - start;
71
- const next = Math.max(min, Math.min(max, startSize + delta));
72
- if (axis === "x")
73
- dom.style.width = `${next}px`;
74
- else
75
- dom.style.height = `${next}px`;
117
+ pendingSize = Math.max(min, Math.min(max, startSize + delta));
118
+ if (!frame)
119
+ frame = requestAnimationFrame(applyPending);
76
120
  };
77
121
  const endResize = () => {
78
122
  if (!resizing)
79
123
  return;
80
124
  resizing = false;
125
+ /*
126
+ * ⚠️ **밀린 프레임을 먼저 적용한다.** 아래에서 커밋할 값을 `dom.style` 에서 읽는데,
127
+ * 예약만 된 채 아직 안 쓴 값이 있으면 **직전 프레임의 크기로 저장**된다.
128
+ */
129
+ flushPending();
81
130
  paint(false);
131
+ shieldEmbeds(false);
82
132
  detachWindow();
83
133
  document.body.style.removeProperty("user-select");
84
134
  document.body.style.removeProperty("cursor");
@@ -117,6 +167,8 @@ export function attachResize(options) {
117
167
  start = axis === "x" ? e.clientX : e.clientY;
118
168
  startSize = axis === "x" ? rect.width : rect.height;
119
169
  paint(true);
170
+ pendingSize = startSize;
171
+ shieldEmbeds(true);
120
172
  // 드래그 중 텍스트가 선택되며 파랗게 물드는 것을 막는다.
121
173
  document.body.style.userSelect = "none";
122
174
  document.body.style.cursor = preset.cursor;
@@ -130,6 +182,13 @@ export function attachResize(options) {
130
182
  handle.removeEventListener("mouseenter", onEnter);
131
183
  handle.removeEventListener("mouseleave", onLeave);
132
184
  handle.removeEventListener("pointerdown", onPointerDown);
185
+ /* 드래그 도중에 노드가 사라질 수 있다 — 예약 프레임과 가림막을 반드시 걷는다. */
186
+ if (frame) {
187
+ cancelAnimationFrame(frame);
188
+ frame = 0;
189
+ }
190
+ if (resizing)
191
+ shieldEmbeds(false);
133
192
  detachWindow();
134
193
  handle.remove();
135
194
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",