@sveltia/ui 0.34.0 → 0.35.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.
- package/dist/components/button/button.svelte +2 -1
- package/dist/components/calendar/calendar.svelte +17 -25
- package/dist/components/divider/spacer.svelte +2 -1
- package/dist/components/select/combobox.svelte +10 -7
- package/dist/components/text-editor/code-editor.svelte +3 -0
- package/dist/components/text-editor/constants.test.d.ts +1 -0
- package/dist/components/text-editor/constants.test.js +98 -0
- package/dist/components/text-editor/core.js +13 -8
- package/dist/components/text-editor/store.svelte.test.d.ts +1 -0
- package/dist/components/text-editor/store.svelte.test.js +196 -0
- package/dist/components/text-editor/text-editor.svelte +3 -0
- package/dist/components/text-editor/transformers/hr.test.d.ts +1 -0
- package/dist/components/text-editor/transformers/hr.test.js +108 -0
- package/dist/components/text-editor/transformers/table.test.d.ts +1 -0
- package/dist/components/text-editor/transformers/table.test.js +28 -0
- package/dist/components/text-field/number-input.svelte +2 -1
- package/dist/components/text-field/password-input.svelte +2 -1
- package/dist/components/text-field/search-bar.svelte +2 -1
- package/dist/components/text-field/secret-input.svelte +2 -1
- package/dist/components/text-field/text-area.svelte +2 -1
- package/dist/components/text-field/text-input.svelte +41 -2
- package/dist/components/toast/toast.svelte +7 -3
- package/dist/services/events.svelte.js +66 -8
- package/dist/services/events.test.d.ts +1 -0
- package/dist/services/events.test.js +221 -0
- package/dist/services/group.svelte.d.ts +1 -0
- package/dist/services/group.svelte.js +15 -10
- package/dist/services/group.test.d.ts +1 -0
- package/dist/services/group.test.js +763 -0
- package/dist/services/i18n.d.ts +6 -0
- package/dist/services/i18n.js +4 -2
- package/dist/services/i18n.test.d.ts +1 -0
- package/dist/services/i18n.test.js +106 -0
- package/dist/services/popup.svelte.d.ts +1 -0
- package/dist/services/popup.svelte.js +11 -2
- package/dist/services/popup.test.d.ts +1 -0
- package/dist/services/popup.test.js +536 -0
- package/dist/services/select.test.d.ts +1 -0
- package/dist/services/select.test.js +69 -0
- package/dist/typedefs.d.ts +7 -0
- package/dist/typedefs.js +4 -0
- package/package.json +12 -11
|
@@ -260,7 +260,8 @@ button.pill {
|
|
|
260
260
|
border-radius: 80px;
|
|
261
261
|
padding: var(--sui-button-medium-pill-padding, 0 12px);
|
|
262
262
|
}
|
|
263
|
-
button.flex {
|
|
263
|
+
button.flex:not([hidden]) {
|
|
264
|
+
display: inline-flex;
|
|
264
265
|
flex: auto;
|
|
265
266
|
width: -moz-available;
|
|
266
267
|
width: -webkit-fill-available;
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* @property {string} [value] Date.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* List of month names for month selector. We use a fixed date to avoid issues with daylight
|
|
20
|
+
* saving time.
|
|
21
|
+
*/
|
|
22
|
+
const MONTH_NAMES = Array.from({ length: 12 }, (__, i) =>
|
|
23
|
+
new Date(2000, i, 10).toLocaleDateString('en', { month: 'short' }),
|
|
24
|
+
);
|
|
25
|
+
|
|
18
26
|
/**
|
|
19
27
|
* @type {Props & Record<string, any>}
|
|
20
28
|
*/
|
|
@@ -24,10 +32,6 @@
|
|
|
24
32
|
/* eslint-enable prefer-const */
|
|
25
33
|
} = $props();
|
|
26
34
|
|
|
27
|
-
/** @type {{ day: Date }[]} */
|
|
28
|
-
const dayList = [];
|
|
29
|
-
/** @type {{ day: Date }[][]} */
|
|
30
|
-
const weeks = [];
|
|
31
35
|
const now = new Date();
|
|
32
36
|
|
|
33
37
|
const date = $derived(value ? new Date(value) : now);
|
|
@@ -38,29 +42,21 @@
|
|
|
38
42
|
firstDay = new Date(firstDayOfMonth);
|
|
39
43
|
});
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const getWeeks = () => {
|
|
45
|
+
const dayList = $derived.by(() => {
|
|
46
|
+
if (!firstDay) return [];
|
|
47
|
+
|
|
45
48
|
const cursor = new Date(firstDay);
|
|
46
49
|
|
|
47
50
|
// Start from Sunday
|
|
48
51
|
cursor.setDate(1 - cursor.getUTCDay());
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
const
|
|
53
|
+
return Array.from({ length: 42 }, () => {
|
|
54
|
+
const day = new Date(cursor);
|
|
52
55
|
|
|
53
|
-
dayList[i] = { day: new Date(cursor) };
|
|
54
|
-
weeks[week] ||= [];
|
|
55
|
-
weeks[week][cursor.getUTCDay() % 7] = { day: new Date(cursor) };
|
|
56
56
|
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
getWeeks();
|
|
63
|
-
}
|
|
58
|
+
return { day };
|
|
59
|
+
});
|
|
64
60
|
});
|
|
65
61
|
</script>
|
|
66
62
|
|
|
@@ -113,13 +109,9 @@
|
|
|
113
109
|
<Divider orientation="vertical" />
|
|
114
110
|
<div role="group" aria-label={$_('_sui.calendar.month')}>
|
|
115
111
|
<div role="none" class="grid">
|
|
116
|
-
{#each
|
|
112
|
+
{#each MONTH_NAMES as monthName}
|
|
117
113
|
<div role="none">
|
|
118
|
-
<Button>
|
|
119
|
-
{new Date(date.getUTCFullYear(), month, 10).toLocaleDateString('en', {
|
|
120
|
-
month: 'short',
|
|
121
|
-
})}
|
|
122
|
-
</Button>
|
|
114
|
+
<Button>{monthName}</Button>
|
|
123
115
|
</div>
|
|
124
116
|
{/each}
|
|
125
117
|
</div>
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* @import { ComboboxProps, TextInputProps } from '../../typedefs';
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Selector for the currently selected option in the popup. Used to update the selected option
|
|
25
|
+
* when the value is changed externally.
|
|
26
|
+
*/
|
|
27
|
+
const SELECTED_SELECTOR = '[role="option"][aria-selected="true"]';
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* @type {ComboboxProps & TextInputProps & Record<string, any>}
|
|
25
31
|
*/
|
|
@@ -43,7 +49,6 @@
|
|
|
43
49
|
} = $props();
|
|
44
50
|
|
|
45
51
|
const id = $props.id();
|
|
46
|
-
const selectedSelector = '[role="option"][aria-selected="true"]';
|
|
47
52
|
let isPopupOpen = $state(false);
|
|
48
53
|
|
|
49
54
|
/** @type {HTMLElement | undefined} */
|
|
@@ -67,17 +72,15 @@
|
|
|
67
72
|
* Update the {@link label} and selected option when the {@link value} is changed.
|
|
68
73
|
*/
|
|
69
74
|
const _onChange = () => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const target = /** @type {HTMLButtonElement} */ (
|
|
75
|
+
const target = /** @type {HTMLButtonElement | null} */ (
|
|
73
76
|
popupContent?.querySelector(`[role="option"][data-value="${value}"]`)
|
|
74
77
|
);
|
|
75
78
|
|
|
76
79
|
if (target) {
|
|
77
80
|
label = target.dataset.label || target.dataset.value || target.textContent || '';
|
|
78
81
|
|
|
79
|
-
if (selected !==
|
|
80
|
-
|
|
82
|
+
if (target.getAttribute('aria-selected') !== 'true') {
|
|
83
|
+
popupContent?.querySelector(SELECTED_SELECTOR)?.setAttribute('aria-selected', 'false');
|
|
81
84
|
target.setAttribute('aria-selected', 'true');
|
|
82
85
|
}
|
|
83
86
|
}
|
|
@@ -99,7 +102,7 @@
|
|
|
99
102
|
$effect(() => {
|
|
100
103
|
if (popupContent) {
|
|
101
104
|
globalThis.requestAnimationFrame(() => {
|
|
102
|
-
const selected = popupContent?.querySelector(
|
|
105
|
+
const selected = popupContent?.querySelector(SELECTED_SELECTOR);
|
|
103
106
|
|
|
104
107
|
if (selected) {
|
|
105
108
|
_onSelect(/** @type {HTMLButtonElement} */ (selected));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
AVAILABLE_BUTTONS,
|
|
4
|
+
BLOCK_BUTTON_TYPES,
|
|
5
|
+
IMAGE_COMPONENT_IDS,
|
|
6
|
+
INLINE_BUTTON_TYPES,
|
|
7
|
+
TEXT_FORMAT_BUTTON_TYPES,
|
|
8
|
+
} from './constants.js';
|
|
9
|
+
|
|
10
|
+
describe('AVAILABLE_BUTTONS', () => {
|
|
11
|
+
it('should contain all expected button keys', () => {
|
|
12
|
+
const expectedKeys = [
|
|
13
|
+
'bold',
|
|
14
|
+
'italic',
|
|
15
|
+
'strikethrough',
|
|
16
|
+
'code',
|
|
17
|
+
'link',
|
|
18
|
+
'paragraph',
|
|
19
|
+
'heading-1',
|
|
20
|
+
'heading-2',
|
|
21
|
+
'heading-3',
|
|
22
|
+
'heading-4',
|
|
23
|
+
'heading-5',
|
|
24
|
+
'heading-6',
|
|
25
|
+
'bulleted-list',
|
|
26
|
+
'numbered-list',
|
|
27
|
+
'blockquote',
|
|
28
|
+
'code-block',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
expectedKeys.forEach((key) => {
|
|
32
|
+
expect(AVAILABLE_BUTTONS).toHaveProperty(key);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should mark inline buttons correctly', () => {
|
|
37
|
+
expect(AVAILABLE_BUTTONS.bold.inline).toBe(true);
|
|
38
|
+
expect(AVAILABLE_BUTTONS.italic.inline).toBe(true);
|
|
39
|
+
expect(AVAILABLE_BUTTONS.link.inline).toBe(true);
|
|
40
|
+
expect(AVAILABLE_BUTTONS.paragraph.inline).toBe(false);
|
|
41
|
+
expect(AVAILABLE_BUTTONS['heading-1'].inline).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should have a labelKey and icon for each button', () => {
|
|
45
|
+
Object.values(AVAILABLE_BUTTONS).forEach(({ labelKey, icon }) => {
|
|
46
|
+
expect(typeof labelKey).toBe('string');
|
|
47
|
+
expect(labelKey.length).toBeGreaterThan(0);
|
|
48
|
+
expect(typeof icon).toBe('string');
|
|
49
|
+
expect(icon.length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('TEXT_FORMAT_BUTTON_TYPES', () => {
|
|
55
|
+
it('should include bold, italic, strikethrough, code', () => {
|
|
56
|
+
expect(TEXT_FORMAT_BUTTON_TYPES).toEqual(['bold', 'italic', 'strikethrough', 'code']);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('INLINE_BUTTON_TYPES', () => {
|
|
61
|
+
it('should include all text format types plus link', () => {
|
|
62
|
+
expect(INLINE_BUTTON_TYPES).toContain('bold');
|
|
63
|
+
expect(INLINE_BUTTON_TYPES).toContain('italic');
|
|
64
|
+
expect(INLINE_BUTTON_TYPES).toContain('strikethrough');
|
|
65
|
+
expect(INLINE_BUTTON_TYPES).toContain('code');
|
|
66
|
+
expect(INLINE_BUTTON_TYPES).toContain('link');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should be a superset of TEXT_FORMAT_BUTTON_TYPES', () => {
|
|
70
|
+
TEXT_FORMAT_BUTTON_TYPES.forEach((type) => {
|
|
71
|
+
expect(INLINE_BUTTON_TYPES).toContain(type);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('BLOCK_BUTTON_TYPES', () => {
|
|
77
|
+
it('should include paragraph and all heading levels', () => {
|
|
78
|
+
expect(BLOCK_BUTTON_TYPES).toContain('paragraph');
|
|
79
|
+
|
|
80
|
+
for (let i = 1; i <= 6; i += 1) {
|
|
81
|
+
expect(BLOCK_BUTTON_TYPES).toContain(`heading-${i}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should include list, blockquote, and code-block types', () => {
|
|
86
|
+
expect(BLOCK_BUTTON_TYPES).toContain('bulleted-list');
|
|
87
|
+
expect(BLOCK_BUTTON_TYPES).toContain('numbered-list');
|
|
88
|
+
expect(BLOCK_BUTTON_TYPES).toContain('blockquote');
|
|
89
|
+
expect(BLOCK_BUTTON_TYPES).toContain('code-block');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('IMAGE_COMPONENT_IDS', () => {
|
|
94
|
+
it('should include image and linked-image', () => {
|
|
95
|
+
expect(IMAGE_COMPONENT_IDS).toContain('image');
|
|
96
|
+
expect(IMAGE_COMPONENT_IDS).toContain('linked-image');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -212,14 +212,15 @@ const getSelectionTypes = () => {
|
|
|
212
212
|
/**
|
|
213
213
|
* Listen to changes made on the editor and trigger the Update event.
|
|
214
214
|
* @param {LexicalEditor} editor Editor instance.
|
|
215
|
+
* @param {any[]} transformers Active transformers for this editor instance.
|
|
215
216
|
*/
|
|
216
|
-
const onEditorUpdate = (editor) => {
|
|
217
|
+
const onEditorUpdate = (editor, transformers) => {
|
|
217
218
|
editor.getRootElement()?.dispatchEvent(
|
|
218
219
|
new CustomEvent('Update', {
|
|
219
220
|
detail: {
|
|
220
221
|
value: $convertToMarkdownString(
|
|
221
222
|
// Use underscores for italic text in Markdown instead of asterisks
|
|
222
|
-
|
|
223
|
+
transformers.filter((/** @type {any} */ { tag }) => tag !== '*'),
|
|
223
224
|
) // Remove unnecessary backslash for underscore and backslash characters
|
|
224
225
|
// @see https://github.com/sveltia/sveltia-cms/issues/430
|
|
225
226
|
// @see https://github.com/sveltia/sveltia-cms/issues/512
|
|
@@ -245,12 +246,16 @@ export const initEditor = ({
|
|
|
245
246
|
isCodeEditor = false,
|
|
246
247
|
defaultLanguage = 'plain',
|
|
247
248
|
}) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
// Build per-instance copies to avoid permanently mutating the shared module-level arrays
|
|
250
|
+
const localNodes = components.length
|
|
251
|
+
? [...components.map(({ node }) => node), .../** @type {any[]} */ (editorConfig.nodes)]
|
|
252
|
+
: editorConfig.nodes;
|
|
253
|
+
|
|
254
|
+
const localTransformers = components.length
|
|
255
|
+
? [...components.map(({ transformer }) => transformer), ...allTransformers]
|
|
256
|
+
: allTransformers;
|
|
252
257
|
|
|
253
|
-
const editor = createEditor(editorConfig);
|
|
258
|
+
const editor = createEditor({ ...editorConfig, nodes: localNodes });
|
|
254
259
|
|
|
255
260
|
registerRichText(editor);
|
|
256
261
|
registerDragonSupport(editor);
|
|
@@ -326,7 +331,7 @@ export const initEditor = ({
|
|
|
326
331
|
}
|
|
327
332
|
}
|
|
328
333
|
|
|
329
|
-
onEditorUpdate(editor);
|
|
334
|
+
onEditorUpdate(editor, localTransformers);
|
|
330
335
|
});
|
|
331
336
|
});
|
|
332
337
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { createEditorStore } from './store.svelte.js';
|
|
5
|
+
|
|
6
|
+
describe('createEditorStore', () => {
|
|
7
|
+
it('should have correct initial values', () => {
|
|
8
|
+
const store = createEditorStore();
|
|
9
|
+
|
|
10
|
+
expect(store.initialized).toBe(false);
|
|
11
|
+
expect(store.editor).toBeUndefined();
|
|
12
|
+
expect(store.inputValue).toBe('');
|
|
13
|
+
expect(store.useRichText).toBe(true);
|
|
14
|
+
expect(store.hasConverterError).toBe(false);
|
|
15
|
+
expect(store.showConverterError).toBe(false);
|
|
16
|
+
expect(store.selection).toEqual({
|
|
17
|
+
blockNodeKey: null,
|
|
18
|
+
blockType: 'paragraph',
|
|
19
|
+
inlineTypes: [],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return a non-empty editorId string', () => {
|
|
24
|
+
const store = createEditorStore();
|
|
25
|
+
|
|
26
|
+
expect(typeof store.editorId).toBe('string');
|
|
27
|
+
expect(store.editorId.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should generate unique editorIds for each store instance', () => {
|
|
31
|
+
const a = createEditorStore();
|
|
32
|
+
const b = createEditorStore();
|
|
33
|
+
|
|
34
|
+
expect(a.editorId).not.toBe(b.editorId);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should set initialized', () => {
|
|
38
|
+
const store = createEditorStore();
|
|
39
|
+
|
|
40
|
+
store.initialized = true;
|
|
41
|
+
expect(store.initialized).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should set useRichText to true when first mode is rich-text', () => {
|
|
45
|
+
const store = createEditorStore();
|
|
46
|
+
|
|
47
|
+
store.config = {
|
|
48
|
+
modes: ['rich-text'],
|
|
49
|
+
enabledButtons: [],
|
|
50
|
+
components: [],
|
|
51
|
+
isCodeEditor: false,
|
|
52
|
+
};
|
|
53
|
+
expect(store.useRichText).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should set useRichText to false when first mode is plain-text', () => {
|
|
57
|
+
const store = createEditorStore();
|
|
58
|
+
|
|
59
|
+
store.config = {
|
|
60
|
+
modes: ['plain-text'],
|
|
61
|
+
enabledButtons: [],
|
|
62
|
+
components: [],
|
|
63
|
+
isCodeEditor: false,
|
|
64
|
+
};
|
|
65
|
+
expect(store.useRichText).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should set useRichText to true when isCodeEditor is true regardless of modes', () => {
|
|
69
|
+
const store = createEditorStore();
|
|
70
|
+
|
|
71
|
+
store.config = { modes: [], enabledButtons: [], components: [], isCodeEditor: true };
|
|
72
|
+
expect(store.useRichText).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should set useRichText to false when modes is empty and isCodeEditor is false', () => {
|
|
76
|
+
const store = createEditorStore();
|
|
77
|
+
|
|
78
|
+
store.config = { modes: [], enabledButtons: [], components: [], isCodeEditor: false };
|
|
79
|
+
expect(store.useRichText).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should set hasConverterError and cascade useRichText and showConverterError', () => {
|
|
83
|
+
const store = createEditorStore();
|
|
84
|
+
|
|
85
|
+
store.hasConverterError = true;
|
|
86
|
+
expect(store.hasConverterError).toBe(true);
|
|
87
|
+
expect(store.useRichText).toBe(false);
|
|
88
|
+
expect(store.showConverterError).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should allow clearing hasConverterError without re-enabling rich text', () => {
|
|
92
|
+
const store = createEditorStore();
|
|
93
|
+
|
|
94
|
+
store.hasConverterError = true;
|
|
95
|
+
store.hasConverterError = false;
|
|
96
|
+
// Setting to false does NOT automatically re-enable rich text
|
|
97
|
+
expect(store.hasConverterError).toBe(false);
|
|
98
|
+
expect(store.useRichText).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should allow setting showConverterError directly', () => {
|
|
102
|
+
const store = createEditorStore();
|
|
103
|
+
|
|
104
|
+
store.showConverterError = true;
|
|
105
|
+
expect(store.showConverterError).toBe(true);
|
|
106
|
+
store.showConverterError = false;
|
|
107
|
+
|
|
108
|
+
expect(store.showConverterError).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should allow setting and reading selection', () => {
|
|
112
|
+
const store = createEditorStore();
|
|
113
|
+
|
|
114
|
+
const sel = {
|
|
115
|
+
blockNodeKey: 'abc',
|
|
116
|
+
blockType: /** @type {const} */ ('heading-2'),
|
|
117
|
+
inlineTypes: /** @type {import('../../typedefs').TextEditorInlineType[]} */ ([
|
|
118
|
+
'bold',
|
|
119
|
+
'italic',
|
|
120
|
+
]),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
store.selection = sel;
|
|
124
|
+
expect(store.selection).toEqual(sel);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should allow setting inputValue when useRichText is false', () => {
|
|
128
|
+
const store = createEditorStore();
|
|
129
|
+
|
|
130
|
+
store.useRichText = false;
|
|
131
|
+
store.inputValue = 'hello world';
|
|
132
|
+
expect(store.inputValue).toBe('hello world');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not change inputValue on duplicate assignment', () => {
|
|
136
|
+
const store = createEditorStore();
|
|
137
|
+
|
|
138
|
+
store.useRichText = false;
|
|
139
|
+
store.inputValue = 'same';
|
|
140
|
+
store.inputValue = 'same'; // no-op since unchanged
|
|
141
|
+
expect(store.inputValue).toBe('same');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should expose convertMarkdown as a function', () => {
|
|
145
|
+
const store = createEditorStore();
|
|
146
|
+
|
|
147
|
+
expect(typeof store.convertMarkdown).toBe('function');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should allow getting and setting the editor (covers line 63)', () => {
|
|
151
|
+
const store = createEditorStore();
|
|
152
|
+
const fakeEditor = /** @type {any} */ ({ __testId: 'test-editor' });
|
|
153
|
+
|
|
154
|
+
store.editor = fakeEditor; // line 63: editor = newValue
|
|
155
|
+
// Svelte 5 $state wraps objects in a Proxy, so use toEqual for value comparison
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
expect(store.editor?.__testId).toBe('test-editor');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should allow reading the config via getter (covers line 72)', () => {
|
|
161
|
+
const store = createEditorStore();
|
|
162
|
+
const cfg = store.config; // line 72: return config
|
|
163
|
+
|
|
164
|
+
expect(cfg.modes).toHaveLength(0);
|
|
165
|
+
expect(cfg.isCodeEditor).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should call convertMarkdown (returns early) when inputValue changes with useRichText=true', async () => {
|
|
169
|
+
const store = createEditorStore();
|
|
170
|
+
|
|
171
|
+
// useRichText is true by default; no editor set → convertMarkdown returns early (line 40)
|
|
172
|
+
store.inputValue = 'hello'; // triggers line 89: convertMarkdown()
|
|
173
|
+
await new Promise((r) => {
|
|
174
|
+
setTimeout(r, 0);
|
|
175
|
+
});
|
|
176
|
+
expect(store.inputValue).toBe('hello');
|
|
177
|
+
expect(store.hasConverterError).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should set hasConverterError when convertMarkdownToLexical throws', async () => {
|
|
181
|
+
const store = createEditorStore();
|
|
182
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
183
|
+
// Mock editor that has no .update() method → convertMarkdownToLexical will throw inside the
|
|
184
|
+
// new Promise executor, rejecting it and triggering the catch block (lines 48-52)
|
|
185
|
+
const mockEditor = /** @type {any} */ ({ getEditorState: () => ({ isEmpty: () => false }) });
|
|
186
|
+
|
|
187
|
+
store.editor = mockEditor;
|
|
188
|
+
store.initialized = true;
|
|
189
|
+
store.inputValue = 'some text'; // triggers convertMarkdown (line 89), which awaits the rejection
|
|
190
|
+
await new Promise((r) => {
|
|
191
|
+
setTimeout(r, 0);
|
|
192
|
+
});
|
|
193
|
+
expect(store.hasConverterError).toBe(true);
|
|
194
|
+
consoleSpy.mockRestore();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -128,6 +128,9 @@
|
|
|
128
128
|
margin: var(--sui-focus-ring-width);
|
|
129
129
|
width: calc(100% - var(--sui-focus-ring-width) * 2);
|
|
130
130
|
}
|
|
131
|
+
.text-editor.flex:not([hidden]) {
|
|
132
|
+
display: block;
|
|
133
|
+
}
|
|
131
134
|
.text-editor :global(.sui.text-area) {
|
|
132
135
|
margin: 0 !important;
|
|
133
136
|
width: 100% !important;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// cSpell:ignore-words horizontalrule
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { HR } from './hr.js';
|
|
5
|
+
|
|
6
|
+
// Mock @lexical/extension so we can call HR.replace without a real Lexical editor context
|
|
7
|
+
vi.mock('@lexical/extension', () => {
|
|
8
|
+
const mockHRNode = {
|
|
9
|
+
__type: 'horizontalrule',
|
|
10
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
11
|
+
selectNext: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
16
|
+
$createHorizontalRuleNode: vi.fn(() => mockHRNode),
|
|
17
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
18
|
+
$isHorizontalRuleNode: (/** @type {{ __type: string; }} */ node) =>
|
|
19
|
+
node?.__type === 'horizontalrule',
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
HorizontalRuleNode: class {},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('HR transformer', () => {
|
|
28
|
+
it('should have the correct type', () => {
|
|
29
|
+
expect(HR.type).toBe('element');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should include HorizontalRuleNode in dependencies', () => {
|
|
33
|
+
expect(HR.dependencies).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('regExp should match --- divider', () => {
|
|
37
|
+
expect(HR.regExp.test('---')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('regExp should match *** divider', () => {
|
|
41
|
+
expect(HR.regExp.test('***')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('regExp should match ___ divider', () => {
|
|
45
|
+
expect(HR.regExp.test('___')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('regExp should match dividers with trailing space', () => {
|
|
49
|
+
expect(HR.regExp.test('--- ')).toBe(true);
|
|
50
|
+
expect(HR.regExp.test('*** ')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('regExp should not match partial dividers', () => {
|
|
54
|
+
expect(HR.regExp.test('--')).toBe(false);
|
|
55
|
+
expect(HR.regExp.test('**')).toBe(false);
|
|
56
|
+
expect(HR.regExp.test('__')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('export should return null for a non-HR node', () => {
|
|
60
|
+
const fakeNode = { __type: 'paragraph' };
|
|
61
|
+
|
|
62
|
+
expect(HR.export(/** @type {any} */ (fakeNode), () => '')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('export should return "***" for an HR node', () => {
|
|
66
|
+
const hrNode = { __type: 'horizontalrule' };
|
|
67
|
+
|
|
68
|
+
expect(HR.export(/** @type {any} */ (hrNode), () => '')).toBe('***');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('replace should call parentNode.replace when isImport is true', () => {
|
|
72
|
+
const parentNode = {
|
|
73
|
+
replace: vi.fn(),
|
|
74
|
+
insertBefore: vi.fn(),
|
|
75
|
+
getNextSibling: vi.fn().mockReturnValue(null),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), true);
|
|
79
|
+
expect(parentNode.replace).toHaveBeenCalledOnce();
|
|
80
|
+
expect(parentNode.insertBefore).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('replace should call parentNode.replace when there is a next sibling', () => {
|
|
84
|
+
const siblingNode = { __type: 'paragraph' };
|
|
85
|
+
|
|
86
|
+
const parentNode = {
|
|
87
|
+
replace: vi.fn(),
|
|
88
|
+
insertBefore: vi.fn(),
|
|
89
|
+
getNextSibling: vi.fn().mockReturnValue(siblingNode),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), false);
|
|
93
|
+
expect(parentNode.replace).toHaveBeenCalledOnce();
|
|
94
|
+
expect(parentNode.insertBefore).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('replace should call parentNode.insertBefore when not import and no next sibling', () => {
|
|
98
|
+
const parentNode = {
|
|
99
|
+
replace: vi.fn(),
|
|
100
|
+
insertBefore: vi.fn(),
|
|
101
|
+
getNextSibling: vi.fn().mockReturnValue(null),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), false);
|
|
105
|
+
expect(parentNode.insertBefore).toHaveBeenCalledOnce();
|
|
106
|
+
expect(parentNode.replace).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { TABLE } from './table.js';
|
|
3
|
+
|
|
4
|
+
describe('TABLE transformer', () => {
|
|
5
|
+
it('should have the correct type', () => {
|
|
6
|
+
expect(TABLE.type).toBe('element');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should have TableNode, TableRowNode, and TableCellNode in dependencies', () => {
|
|
10
|
+
expect(TABLE.dependencies).toHaveLength(3);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('regExp should match a table row line', () => {
|
|
14
|
+
expect(TABLE.regExp.test('| foo | bar |')).toBe(true);
|
|
15
|
+
expect(TABLE.regExp.test('| single |')).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('regExp should not match lines without pipe borders', () => {
|
|
19
|
+
expect(TABLE.regExp.test('foo bar')).toBe(false);
|
|
20
|
+
expect(TABLE.regExp.test('- item')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('export should return null for a non-table node', () => {
|
|
24
|
+
const fakeNode = { __type: 'paragraph' };
|
|
25
|
+
|
|
26
|
+
expect(TABLE.export(/** @type {any} */ (fakeNode), () => '')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -204,7 +204,8 @@
|
|
|
204
204
|
margin: var(--sui-focus-ring-width);
|
|
205
205
|
min-width: var(--sui-textbox-singleline-min-width);
|
|
206
206
|
}
|
|
207
|
-
.number-input.flex {
|
|
207
|
+
.number-input.flex:not([hidden]) {
|
|
208
|
+
display: inline-flex;
|
|
208
209
|
width: -moz-available;
|
|
209
210
|
width: -webkit-fill-available;
|
|
210
211
|
width: stretch;
|