@dryui/ui 0.1.2 → 0.1.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/dist/button/button.svelte +51 -1
- package/dist/button-group/button-group.svelte +7 -62
- package/dist/button-group/context.svelte.d.ts +5 -0
- package/dist/button-group/context.svelte.js +11 -0
- package/dist/combobox/combobox-group.svelte +1 -1
- package/dist/date-range-picker/date-range-picker-root.svelte +0 -6
- package/dist/file-upload/file-upload-root.svelte +0 -4
- package/dist/list/list-item-icon.svelte +8 -0
- package/dist/list/list-item-text.svelte +19 -0
- package/dist/list/list-item.svelte +42 -0
- package/dist/list/list-root.svelte +0 -71
- package/dist/list/list-subheader.svelte +11 -0
- package/dist/map/map-marker.svelte +10 -0
- package/dist/map/map-popup.svelte +7 -0
- package/dist/map/map-root.svelte +0 -30
- package/dist/multi-select-combobox/multi-select-combobox-group.svelte +1 -1
- package/dist/radio-group/radio-group-item.svelte +90 -0
- package/dist/radio-group/radio-group.svelte +0 -89
- package/dist/range-calendar/range-calendar-grid.svelte +6 -6
- package/dist/rich-text-editor/rich-text-editor-content.svelte +91 -3
- package/dist/rich-text-editor/rich-text-editor-root.svelte +168 -3
- package/dist/rich-text-editor/rich-text-editor-toolbar.svelte +318 -275
- package/dist/shader-canvas/shader-canvas.svelte +0 -3
- package/dist/sidebar/sidebar-trigger.svelte +3 -2
- package/dist/tabs/tabs-trigger.svelte +7 -4
- package/dist/virtual-list/virtual-list.svelte +187 -3
- package/package.json +2 -2
|
@@ -1,13 +1,101 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
import {
|
|
3
|
+
import { getRichTextEditorCtx } from '@dryui/primitives/rich-text-editor';
|
|
4
4
|
|
|
5
5
|
interface Props extends HTMLAttributes<HTMLDivElement> {}
|
|
6
6
|
|
|
7
7
|
let { ...rest }: Props = $props();
|
|
8
|
+
|
|
9
|
+
const ctx = getRichTextEditorCtx();
|
|
10
|
+
|
|
11
|
+
let contentEl = $state<HTMLDivElement>();
|
|
12
|
+
|
|
13
|
+
// Register the content element with the context
|
|
14
|
+
$effect(() => {
|
|
15
|
+
if (contentEl) {
|
|
16
|
+
ctx.contentEl = contentEl;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Sync value into contenteditable (also clears hint elements on mount)
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (!contentEl) return;
|
|
23
|
+
const html = ctx.html || '';
|
|
24
|
+
if (contentEl.innerHTML !== html && document.activeElement !== contentEl) {
|
|
25
|
+
contentEl.innerHTML = html;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Listen for selectionchange on document to track formatting state
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (!contentEl) return;
|
|
32
|
+
|
|
33
|
+
function onSelectionChange() {
|
|
34
|
+
const sel = window.getSelection();
|
|
35
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
36
|
+
if (contentEl!.contains(sel.anchorNode)) {
|
|
37
|
+
ctx.updateState();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
document.addEventListener('selectionchange', onSelectionChange);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('selectionchange', onSelectionChange);
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function handleInput() {
|
|
48
|
+
ctx.updateState();
|
|
49
|
+
ctx.syncValue();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
53
|
+
if (ctx.readonly) return;
|
|
54
|
+
|
|
55
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
56
|
+
if (!mod) return;
|
|
57
|
+
|
|
58
|
+
switch (event.key.toLowerCase()) {
|
|
59
|
+
case 'b':
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
ctx.toggleBold();
|
|
62
|
+
break;
|
|
63
|
+
case 'i':
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
ctx.toggleItalic();
|
|
66
|
+
break;
|
|
67
|
+
case 'u':
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
ctx.toggleUnderline();
|
|
70
|
+
break;
|
|
71
|
+
case 'k':
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
contentEl?.dispatchEvent(new CustomEvent('rte-link-request', { bubbles: true }));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
8
77
|
</script>
|
|
9
78
|
|
|
10
|
-
|
|
79
|
+
<!--
|
|
80
|
+
The elements below (h1–h3, p, ul, ol, li, a) are created dynamically by the
|
|
81
|
+
browser's execCommand API inside the contenteditable div. They are declared
|
|
82
|
+
here so Svelte can verify the scoped CSS selectors that style them. The
|
|
83
|
+
$effect that syncs ctx.html into innerHTML replaces these on mount.
|
|
84
|
+
-->
|
|
85
|
+
<div
|
|
86
|
+
bind:this={contentEl}
|
|
87
|
+
contenteditable={!ctx.readonly}
|
|
88
|
+
role="textbox"
|
|
89
|
+
aria-multiline="true"
|
|
90
|
+
aria-placeholder={ctx.placeholder || undefined}
|
|
91
|
+
data-placeholder={ctx.placeholder || undefined}
|
|
92
|
+
data-part="content"
|
|
93
|
+
data-rte-content
|
|
94
|
+
data-readonly={ctx.readonly || undefined}
|
|
95
|
+
oninput={handleInput}
|
|
96
|
+
onkeydown={handleKeydown}
|
|
97
|
+
{...rest}
|
|
98
|
+
><h1>.</h1><h2>.</h2><h3>.</h3><p></p><ul><li></li></ul><ol><li></li></ol><a aria-hidden="true" href="https://example.com">.</a></div>
|
|
11
99
|
|
|
12
100
|
<style>
|
|
13
101
|
[data-part='content'] {
|
|
@@ -32,7 +120,7 @@
|
|
|
32
120
|
pointer-events: none;
|
|
33
121
|
}
|
|
34
122
|
|
|
35
|
-
/* Typography
|
|
123
|
+
/* Typography for contenteditable elements created by execCommand */
|
|
36
124
|
[data-part='content'] h1 {
|
|
37
125
|
font-size: var(--dry-type-heading-2-size, var(--dry-text-2xl-size));
|
|
38
126
|
font-weight: 700;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import {
|
|
4
|
+
import { setRichTextEditorCtx } from '@dryui/primitives/rich-text-editor';
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
7
|
value?: string;
|
|
@@ -10,10 +10,175 @@
|
|
|
10
10
|
children: Snippet;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
let {
|
|
13
|
+
let {
|
|
14
|
+
value = $bindable(''),
|
|
15
|
+
placeholder = '',
|
|
16
|
+
readonly: readonlyProp = false,
|
|
17
|
+
children,
|
|
18
|
+
...rest
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
let isBold = $state(false);
|
|
22
|
+
let isItalic = $state(false);
|
|
23
|
+
let isUnderline = $state(false);
|
|
24
|
+
let isStrikethrough = $state(false);
|
|
25
|
+
let isOrderedList = $state(false);
|
|
26
|
+
let isUnorderedList = $state(false);
|
|
27
|
+
let currentHeading = $state<string | null>(null);
|
|
28
|
+
let currentLink = $state<string | null>(null);
|
|
29
|
+
|
|
30
|
+
function execCommand(command: string, cmdValue?: string) {
|
|
31
|
+
if (readonlyProp) return;
|
|
32
|
+
ctx.contentEl?.focus();
|
|
33
|
+
document.execCommand(command, false, cmdValue);
|
|
34
|
+
updateState();
|
|
35
|
+
syncValue();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function updateState() {
|
|
39
|
+
isBold = document.queryCommandState('bold');
|
|
40
|
+
isItalic = document.queryCommandState('italic');
|
|
41
|
+
isUnderline = document.queryCommandState('underline');
|
|
42
|
+
isStrikethrough = document.queryCommandState('strikethrough');
|
|
43
|
+
isOrderedList = document.queryCommandState('insertOrderedList');
|
|
44
|
+
isUnorderedList = document.queryCommandState('insertUnorderedList');
|
|
45
|
+
|
|
46
|
+
const block = document.queryCommandValue('formatBlock');
|
|
47
|
+
if (block && /^h[1-3]$/i.test(block)) {
|
|
48
|
+
currentHeading = block.toLowerCase();
|
|
49
|
+
} else {
|
|
50
|
+
currentHeading = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sel = window.getSelection();
|
|
54
|
+
if (sel && sel.rangeCount > 0) {
|
|
55
|
+
let node: Node | null = sel.anchorNode;
|
|
56
|
+
while (node && node !== ctx.contentEl) {
|
|
57
|
+
if (node instanceof HTMLAnchorElement) {
|
|
58
|
+
currentLink = node.href;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
node = node.parentNode;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
currentLink = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function syncValue() {
|
|
68
|
+
if (ctx.contentEl) {
|
|
69
|
+
value = ctx.contentEl.innerHTML;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ctx = setRichTextEditorCtx({
|
|
74
|
+
get isBold() {
|
|
75
|
+
return isBold;
|
|
76
|
+
},
|
|
77
|
+
get isItalic() {
|
|
78
|
+
return isItalic;
|
|
79
|
+
},
|
|
80
|
+
get isUnderline() {
|
|
81
|
+
return isUnderline;
|
|
82
|
+
},
|
|
83
|
+
get isStrikethrough() {
|
|
84
|
+
return isStrikethrough;
|
|
85
|
+
},
|
|
86
|
+
get isOrderedList() {
|
|
87
|
+
return isOrderedList;
|
|
88
|
+
},
|
|
89
|
+
get isUnorderedList() {
|
|
90
|
+
return isUnorderedList;
|
|
91
|
+
},
|
|
92
|
+
get currentHeading() {
|
|
93
|
+
return currentHeading;
|
|
94
|
+
},
|
|
95
|
+
get currentLink() {
|
|
96
|
+
return currentLink;
|
|
97
|
+
},
|
|
98
|
+
get html() {
|
|
99
|
+
return value;
|
|
100
|
+
},
|
|
101
|
+
get readonly() {
|
|
102
|
+
return readonlyProp;
|
|
103
|
+
},
|
|
104
|
+
get placeholder() {
|
|
105
|
+
return placeholder;
|
|
106
|
+
},
|
|
107
|
+
contentEl: null,
|
|
108
|
+
execCommand,
|
|
109
|
+
toggleBold() {
|
|
110
|
+
execCommand('bold');
|
|
111
|
+
},
|
|
112
|
+
toggleItalic() {
|
|
113
|
+
execCommand('italic');
|
|
114
|
+
},
|
|
115
|
+
toggleUnderline() {
|
|
116
|
+
execCommand('underline');
|
|
117
|
+
},
|
|
118
|
+
toggleStrikethrough() {
|
|
119
|
+
execCommand('strikethrough');
|
|
120
|
+
},
|
|
121
|
+
toggleOrderedList() {
|
|
122
|
+
execCommand('insertOrderedList');
|
|
123
|
+
},
|
|
124
|
+
toggleUnorderedList() {
|
|
125
|
+
execCommand('insertUnorderedList');
|
|
126
|
+
},
|
|
127
|
+
setHeading(level: 'h1' | 'h2' | 'h3' | 'p') {
|
|
128
|
+
if (level === 'p') {
|
|
129
|
+
execCommand('formatBlock', 'p');
|
|
130
|
+
} else {
|
|
131
|
+
if (currentHeading === level) {
|
|
132
|
+
execCommand('formatBlock', 'p');
|
|
133
|
+
} else {
|
|
134
|
+
execCommand('formatBlock', level);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
insertLink(url: string) {
|
|
139
|
+
if (readonlyProp) return;
|
|
140
|
+
const sel = window.getSelection();
|
|
141
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
142
|
+
|
|
143
|
+
if (sel.isCollapsed) {
|
|
144
|
+
const a = document.createElement('a');
|
|
145
|
+
a.href = url;
|
|
146
|
+
a.textContent = url;
|
|
147
|
+
a.target = '_blank';
|
|
148
|
+
a.rel = 'noopener noreferrer';
|
|
149
|
+
const range = sel.getRangeAt(0);
|
|
150
|
+
range.insertNode(a);
|
|
151
|
+
range.setStartAfter(a);
|
|
152
|
+
range.collapse(true);
|
|
153
|
+
sel.removeAllRanges();
|
|
154
|
+
sel.addRange(range);
|
|
155
|
+
} else {
|
|
156
|
+
document.execCommand('createLink', false, url);
|
|
157
|
+
if (ctx.contentEl) {
|
|
158
|
+
const links = ctx.contentEl.querySelectorAll('a[href="' + CSS.escape(url) + '"]');
|
|
159
|
+
links.forEach((link) => {
|
|
160
|
+
link.setAttribute('target', '_blank');
|
|
161
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
updateState();
|
|
166
|
+
syncValue();
|
|
167
|
+
},
|
|
168
|
+
removeLink() {
|
|
169
|
+
execCommand('unlink');
|
|
170
|
+
},
|
|
171
|
+
getContent() {
|
|
172
|
+
return ctx.contentEl?.innerHTML ?? '';
|
|
173
|
+
},
|
|
174
|
+
updateState,
|
|
175
|
+
syncValue
|
|
176
|
+
});
|
|
14
177
|
</script>
|
|
15
178
|
|
|
16
|
-
<
|
|
179
|
+
<div data-part="root" data-rte-root data-readonly={readonlyProp || undefined} {...rest}>
|
|
180
|
+
{@render children()}
|
|
181
|
+
</div>
|
|
17
182
|
|
|
18
183
|
<style>
|
|
19
184
|
[data-part='root'] {
|