@sveltia/ui 0.35.0 → 0.35.2
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/calendar/calendar.svelte +17 -25
- package/dist/components/select/combobox.svelte +10 -7
- 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/store.svelte.test.d.ts +1 -0
- package/dist/components/text-editor/store.svelte.test.js +196 -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/text-input.svelte +12 -6
- 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/package.json +10 -9
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -59,20 +59,26 @@
|
|
|
59
59
|
* @param {InputEvent} event The `input` event object.
|
|
60
60
|
*/
|
|
61
61
|
const fireInput = (event) => {
|
|
62
|
-
value =
|
|
62
|
+
value = element?.value;
|
|
63
63
|
oninput?.(event);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Handle the `input` event. If `debounce` is `true`, the event will be debounced by 300ms.
|
|
67
|
+
* Handle the `input` event. If `debounce` is `true`, the event will be debounced by 300ms. We use
|
|
68
|
+
* `oninputcapture` to ensure that the event is fired before any `oninput` handlers on parent
|
|
69
|
+
* elements, including the Lexical editor.
|
|
68
70
|
* @param {InputEvent} event The `input` event object.
|
|
69
71
|
*/
|
|
70
72
|
const handleInput = (event) => {
|
|
71
73
|
if (debounce) {
|
|
72
74
|
clearTimeout(debounceTimer);
|
|
73
|
-
debounceTimer =
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
debounceTimer = /** @type {number} */ (
|
|
76
|
+
/** @type {unknown} */ (
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
fireInput(event);
|
|
79
|
+
}, timeout)
|
|
80
|
+
)
|
|
81
|
+
);
|
|
76
82
|
} else {
|
|
77
83
|
fireInput(event);
|
|
78
84
|
}
|
|
@@ -106,7 +112,7 @@
|
|
|
106
112
|
aria-readonly={readonly}
|
|
107
113
|
aria-required={required}
|
|
108
114
|
aria-invalid={invalid}
|
|
109
|
-
|
|
115
|
+
oninputcapture={handleInput}
|
|
110
116
|
use:activateKeyShortcuts={keyShortcuts}
|
|
111
117
|
/>
|
|
112
118
|
{#if ariaLabel && showInlineLabel}
|
|
@@ -112,9 +112,13 @@
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
if (show && duration) {
|
|
115
|
-
timerId =
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
timerId = /** @type {number} */ (
|
|
116
|
+
/** @type {unknown} */ (
|
|
117
|
+
globalThis.setTimeout(() => {
|
|
118
|
+
show = false;
|
|
119
|
+
}, duration)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
118
122
|
}
|
|
119
123
|
});
|
|
120
124
|
</script>
|