@teriusu/rich-editor 0.11.0 → 0.11.1
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.
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
const has = (f: ToolbarFeature) => features.has(f);
|
|
71
71
|
|
|
72
|
+
let mobile = $state(false);
|
|
72
73
|
let showBlockMenu = $state(false);
|
|
73
74
|
let showColors = $state(false);
|
|
74
75
|
let menuEl: HTMLDivElement | undefined = $state();
|
|
@@ -110,11 +111,18 @@
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
async function addLink() {
|
|
114
|
+
const selection = editor.state.selection.getBookmark();
|
|
115
|
+
const documentBeforePrompt = editor.state.doc;
|
|
113
116
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
114
117
|
const url = onPromptLink
|
|
115
118
|
? await onPromptLink(previousUrl)
|
|
116
119
|
: window.prompt(t("promptLinkUrl"), previousUrl);
|
|
117
|
-
if (url === null) return;
|
|
120
|
+
if (url === null || editor.isDestroyed || editor.state.doc !== documentBeforePrompt) return;
|
|
121
|
+
try {
|
|
122
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection.resolve(editor.state.doc)));
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
118
126
|
if (url === "") {
|
|
119
127
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
120
128
|
} else {
|
|
@@ -127,8 +135,13 @@
|
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
function handleDocClick(e:
|
|
138
|
+
function handleDocClick(e: Event) {
|
|
131
139
|
const target = e.target as Node;
|
|
140
|
+
// The upstream plugin treats any focus inside the menu's parent as internal.
|
|
141
|
+
// A body portal needs an explicit boundary so another input/editor dismisses it.
|
|
142
|
+
if (mobile && menuEl && !menuEl.contains(target) && !editor.view.dom.contains(target)) {
|
|
143
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, 'hide'));
|
|
144
|
+
}
|
|
132
145
|
if (showBlockMenu && blockMenuEl && !blockMenuEl.contains(target)) {
|
|
133
146
|
showBlockMenu = false;
|
|
134
147
|
}
|
|
@@ -147,12 +160,74 @@
|
|
|
147
160
|
onMount(() => {
|
|
148
161
|
if (!menuEl) return;
|
|
149
162
|
|
|
163
|
+
const element = menuEl;
|
|
164
|
+
const media = window.matchMedia('(max-width: 767px), (pointer: coarse)');
|
|
165
|
+
const viewport = window.visualViewport;
|
|
166
|
+
const anchor = document.createElement('span');
|
|
167
|
+
anchor.style.cssText = 'position:fixed;width:0;height:0;pointer-events:none;visibility:hidden';
|
|
168
|
+
anchor.setAttribute('aria-hidden', 'true');
|
|
169
|
+
document.body.appendChild(anchor);
|
|
170
|
+
mobile = media.matches;
|
|
171
|
+
|
|
172
|
+
// A portal leaves the host wrapper, where apps map their own theme to editor tokens.
|
|
173
|
+
const themeTokens = [
|
|
174
|
+
'--border', '--background', '--foreground', '--popover', '--muted',
|
|
175
|
+
'--muted-foreground', '--primary', '--primary-foreground', '--accent',
|
|
176
|
+
'--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface'
|
|
177
|
+
];
|
|
178
|
+
const syncTheme = () => {
|
|
179
|
+
const hostStyle = getComputedStyle(editor.view.dom);
|
|
180
|
+
for (const token of themeTokens) {
|
|
181
|
+
const value = mobile ? hostStyle.getPropertyValue(token) : '';
|
|
182
|
+
if (value) element.style.setProperty(token, value);
|
|
183
|
+
else element.style.removeProperty(token);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const syncViewport = () => {
|
|
187
|
+
syncTheme();
|
|
188
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
189
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
190
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
191
|
+
const top = viewport?.offsetTop ?? 0;
|
|
192
|
+
anchor.style.left = `${left + width / 2}px`;
|
|
193
|
+
anchor.style.top = `${top + height}px`;
|
|
194
|
+
element.style.maxWidth = mobile ? `${Math.max(0, width - 16)}px` : '';
|
|
195
|
+
element.style.setProperty('--bubble-popup-height', `${Math.max(44, height - element.getBoundingClientRect().height - 24)}px`);
|
|
196
|
+
};
|
|
197
|
+
syncViewport();
|
|
198
|
+
|
|
199
|
+
const positionOptions = () => mobile
|
|
200
|
+
? { strategy: 'fixed' as const, placement: 'top' as const, flip: false, shift: false, offset: 8 }
|
|
201
|
+
: { strategy: 'absolute' as const, placement: 'top' as const, flip: true, shift: true, offset: 8 };
|
|
202
|
+
const updatePosition = () => {
|
|
203
|
+
if (editor.isDestroyed) return;
|
|
204
|
+
syncViewport();
|
|
205
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, 'updatePosition'));
|
|
206
|
+
};
|
|
207
|
+
const updateMode = () => {
|
|
208
|
+
mobile = media.matches;
|
|
209
|
+
syncViewport();
|
|
210
|
+
editor.view.dispatch(editor.state.tr.setMeta(bubbleToolbarKey, {
|
|
211
|
+
type: 'updateOptions', options: { options: positionOptions() }
|
|
212
|
+
}));
|
|
213
|
+
if (element.isConnected) {
|
|
214
|
+
(mobile ? document.body : editor.view.dom.parentElement)?.appendChild(element);
|
|
215
|
+
}
|
|
216
|
+
updatePosition();
|
|
217
|
+
};
|
|
218
|
+
// Prevent button focus from collapsing the text selection; native OS menus remain enabled.
|
|
219
|
+
const preserveSelection = (event: MouseEvent) => {
|
|
220
|
+
if (mobile && (event.target as HTMLElement).closest('button')) event.preventDefault();
|
|
221
|
+
};
|
|
150
222
|
const plugin = BubbleMenuPlugin({
|
|
151
223
|
pluginKey: bubbleToolbarKey,
|
|
152
224
|
editor,
|
|
153
|
-
element
|
|
225
|
+
element,
|
|
226
|
+
appendTo: () => mobile ? document.body : editor.view.dom.parentElement!,
|
|
227
|
+
getReferencedVirtualElement: () => mobile ? anchor : null,
|
|
154
228
|
shouldShow: ({ editor: e, state }) => {
|
|
155
229
|
const { from, to } = state.selection;
|
|
230
|
+
if (!e.isEditable || (!e.view.hasFocus() && !element.contains(document.activeElement))) return false;
|
|
156
231
|
if (from === to) return false;
|
|
157
232
|
/*
|
|
158
233
|
* ⚠️ **덩어리를 고른 것(NodeSelection)에는 뜨지 않는다.**
|
|
@@ -197,18 +272,36 @@
|
|
|
197
272
|
* 마침 v3 기본값이 `placement: "top"` + `flip`·`shift` 활성이라 화면상 차이는 없었지만,
|
|
198
273
|
* 죽은 설정을 남겨 두면 "위치를 지정해 뒀다"고 착각하게 된다.
|
|
199
274
|
*/
|
|
200
|
-
options: {
|
|
201
|
-
placement: "top",
|
|
202
|
-
},
|
|
275
|
+
options: { ...positionOptions(), onShow: syncTheme },
|
|
203
276
|
});
|
|
204
277
|
|
|
205
278
|
editor.registerPlugin(plugin);
|
|
206
|
-
|
|
279
|
+
const themeObserver = new MutationObserver(syncTheme);
|
|
280
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
|
281
|
+
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
|
282
|
+
const observer = new ResizeObserver(updatePosition);
|
|
283
|
+
observer.observe(element);
|
|
284
|
+
media.addEventListener('change', updateMode);
|
|
285
|
+
viewport?.addEventListener('resize', updatePosition);
|
|
286
|
+
viewport?.addEventListener('scroll', updatePosition);
|
|
287
|
+
window.addEventListener('resize', updatePosition);
|
|
288
|
+
element.addEventListener('mousedown', preserveSelection);
|
|
289
|
+
document.addEventListener("pointerdown", handleDocClick);
|
|
290
|
+
document.addEventListener("focusin", handleDocClick);
|
|
207
291
|
document.addEventListener("keydown", handleKeydown);
|
|
208
292
|
|
|
209
293
|
return () => {
|
|
210
294
|
editor.unregisterPlugin(bubbleToolbarKey);
|
|
211
|
-
|
|
295
|
+
observer.disconnect();
|
|
296
|
+
themeObserver.disconnect();
|
|
297
|
+
anchor.remove();
|
|
298
|
+
media.removeEventListener('change', updateMode);
|
|
299
|
+
viewport?.removeEventListener('resize', updatePosition);
|
|
300
|
+
viewport?.removeEventListener('scroll', updatePosition);
|
|
301
|
+
window.removeEventListener('resize', updatePosition);
|
|
302
|
+
element.removeEventListener('mousedown', preserveSelection);
|
|
303
|
+
document.removeEventListener("pointerdown", handleDocClick);
|
|
304
|
+
document.removeEventListener("focusin", handleDocClick);
|
|
212
305
|
document.removeEventListener("keydown", handleKeydown);
|
|
213
306
|
};
|
|
214
307
|
});
|
|
@@ -234,14 +327,16 @@
|
|
|
234
327
|
);
|
|
235
328
|
</script>
|
|
236
329
|
|
|
237
|
-
<div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
|
|
330
|
+
<div bind:this={menuEl} class="bubble-toolbar-container" class:mobile style="visibility: hidden">
|
|
238
331
|
<div class="flex items-center gap-0.5 px-1.5 py-1 rounded-full hce-floating-panel">
|
|
239
332
|
{#if hasBlockMenu}
|
|
240
333
|
<!-- Block type selector -->
|
|
241
|
-
<div class="relative" bind:this={blockMenuEl}>
|
|
334
|
+
<div class="relative bubble-popover-anchor" bind:this={blockMenuEl}>
|
|
242
335
|
<button
|
|
243
336
|
type="button"
|
|
244
337
|
onclick={() => (showBlockMenu = !showBlockMenu)}
|
|
338
|
+
aria-haspopup="menu"
|
|
339
|
+
aria-expanded={showBlockMenu}
|
|
245
340
|
class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
246
341
|
>
|
|
247
342
|
<Type size={12} />
|
|
@@ -250,7 +345,7 @@
|
|
|
250
345
|
</button>
|
|
251
346
|
{#if showBlockMenu}
|
|
252
347
|
<div
|
|
253
|
-
class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
|
|
348
|
+
class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
|
|
254
349
|
style="min-width: 140px"
|
|
255
350
|
onmousedown={(e) => e.preventDefault()}
|
|
256
351
|
role="menu"
|
|
@@ -271,7 +366,7 @@
|
|
|
271
366
|
>
|
|
272
367
|
<Type size={12} /> {t('paragraph')}
|
|
273
368
|
</button>
|
|
274
|
-
{#each [1, 2, 3] as level}
|
|
369
|
+
{#each [1, 2, 3] as level (level)}
|
|
275
370
|
{#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
|
|
276
371
|
{@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
|
|
277
372
|
<button
|
|
@@ -367,7 +462,7 @@
|
|
|
367
462
|
{/if}
|
|
368
463
|
</div>
|
|
369
464
|
|
|
370
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
465
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
371
466
|
{/if}
|
|
372
467
|
|
|
373
468
|
<!-- Format buttons -->
|
|
@@ -471,7 +566,7 @@
|
|
|
471
566
|
{/if}
|
|
472
567
|
|
|
473
568
|
{#if has('highlight') || has('text-color')}
|
|
474
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
569
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
475
570
|
{/if}
|
|
476
571
|
|
|
477
572
|
{#if has('highlight')}
|
|
@@ -490,10 +585,12 @@
|
|
|
490
585
|
</button>
|
|
491
586
|
{/if}
|
|
492
587
|
{#if has('text-color')}
|
|
493
|
-
<div class="relative" bind:this={colorMenuEl}>
|
|
588
|
+
<div class="relative bubble-popover-anchor" bind:this={colorMenuEl}>
|
|
494
589
|
<button
|
|
495
590
|
type="button"
|
|
496
591
|
onclick={() => (showColors = !showColors)}
|
|
592
|
+
aria-haspopup="menu"
|
|
593
|
+
aria-expanded={showColors}
|
|
497
594
|
aria-label={t('textColor')}
|
|
498
595
|
class={cn(
|
|
499
596
|
"p-1.5 rounded-full transition-colors",
|
|
@@ -506,14 +603,14 @@
|
|
|
506
603
|
</button>
|
|
507
604
|
{#if showColors}
|
|
508
605
|
<div
|
|
509
|
-
class="absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl p-2"
|
|
606
|
+
class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl p-2"
|
|
510
607
|
style="min-width: 160px"
|
|
511
608
|
onmousedown={(e) => e.preventDefault()}
|
|
512
609
|
role="menu"
|
|
513
610
|
tabindex="-1"
|
|
514
611
|
>
|
|
515
612
|
<div class="grid grid-cols-3 gap-1.5">
|
|
516
|
-
{#each TEXT_COLORS as c}
|
|
613
|
+
{#each TEXT_COLORS as c (c.value)}
|
|
517
614
|
<button
|
|
518
615
|
type="button"
|
|
519
616
|
title={t(c.labelKey)}
|
|
@@ -553,7 +650,7 @@
|
|
|
553
650
|
{/if}
|
|
554
651
|
|
|
555
652
|
{#if has('link')}
|
|
556
|
-
<div class="w-px h-5 bg-border mx-0.5"></div>
|
|
653
|
+
<div class="bubble-divider w-px h-5 bg-border mx-0.5"></div>
|
|
557
654
|
<button
|
|
558
655
|
type="button"
|
|
559
656
|
onclick={addLink}
|
|
@@ -570,3 +667,30 @@
|
|
|
570
667
|
{/if}
|
|
571
668
|
</div>
|
|
572
669
|
</div>
|
|
670
|
+
|
|
671
|
+
<style>
|
|
672
|
+
.mobile .hce-floating-panel {
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-wrap: wrap;
|
|
675
|
+
justify-content: center;
|
|
676
|
+
border-radius: 12px;
|
|
677
|
+
padding: 4px;
|
|
678
|
+
padding-bottom: max(4px, env(safe-area-inset-bottom));
|
|
679
|
+
}
|
|
680
|
+
.mobile button {
|
|
681
|
+
min-width: 44px;
|
|
682
|
+
min-height: 44px;
|
|
683
|
+
display: inline-flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
justify-content: center;
|
|
686
|
+
}
|
|
687
|
+
.mobile input[type="color"] { min-width: 44px; min-height: 44px; }
|
|
688
|
+
.mobile .bubble-divider { display: none; }
|
|
689
|
+
.mobile .bubble-popover-anchor { position: static; }
|
|
690
|
+
.mobile .bubble-popover {
|
|
691
|
+
left: 0;
|
|
692
|
+
right: 0;
|
|
693
|
+
max-height: min(var(--bubble-popup-height, 50dvh), 300px);
|
|
694
|
+
overflow-y: auto;
|
|
695
|
+
}
|
|
696
|
+
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;
|
|
1
|
+
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AA+hBJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|