@drivenets/design-system 0.19.0 → 0.20.0
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/CHANGELOG.md +28 -0
- package/dist/index.cjs +12409 -10548
- package/dist/index.d.cts +678 -312
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +679 -313
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12404 -10550
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +29 -29
- package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
- package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
- package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
- package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
- package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
- package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
- package/src/components/ds-code-input/ds-code-input.tsx +401 -0
- package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
- package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
- package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
- package/src/components/ds-code-input/index.ts +2 -0
- package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
- package/src/components/ds-code-input/use-editor-host.ts +87 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
- package/src/components/ds-filters-bar/components/index.ts +11 -0
- package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
- package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
- package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
- package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
- package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
- package/src/components/ds-filters-bar/index.ts +1 -0
- package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
- package/src/components/ds-form-control/ds-form-control.tsx +43 -9
- package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
- package/src/components/ds-icon/custom-icons/index.ts +2 -0
- package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
- package/src/components/ds-pin-toggle/index.ts +2 -0
- package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
- package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
- package/src/components/ds-popover/ds-popover.context.ts +18 -0
- package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
- package/src/components/ds-popover/ds-popover.tsx +163 -41
- package/src/components/ds-popover/ds-popover.types.ts +59 -1
- package/src/components/ds-popover/ds-popover.utils.ts +60 -0
- package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
- package/src/components/ds-saved-filters/components/index.ts +2 -0
- package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
- package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
- package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
- package/src/components/ds-saved-filters/hooks/index.ts +1 -0
- package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
- package/src/components/ds-saved-filters/index.ts +2 -0
- package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
- package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
- package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
- package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
- package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
- package/src/components/ds-tag/ds-tag.figma.ts +23 -3
- package/src/components/ds-tag/ds-tag.module.scss +71 -7
- package/src/components/ds-tag/ds-tag.tsx +115 -14
- package/src/components/ds-tag/ds-tag.types.ts +70 -3
- package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
- package/src/components/ds-tag/index.ts +2 -2
- package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
- package/src/components/ds-toggle/ds-toggle.tsx +19 -15
- package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
- package/src/components/ds-toggle-filter-data/index.ts +1 -0
- package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
- package/src/components/ds-tooltip/ds-tooltip.tsx +1 -1
- package/src/components/ds-tree/ds-tree.tsx +10 -10
- package/src/components/ds-tree/ds-tree.types.ts +40 -7
- package/src/index.ts +2 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
export const codeInputSizes = ['small', 'default', 'large'] as const;
|
|
4
|
+
export type CodeInputSize = (typeof codeInputSizes)[number];
|
|
5
|
+
|
|
6
|
+
export interface DsCodeInputProps {
|
|
7
|
+
/**
|
|
8
|
+
* Associates the editor with a `<label htmlFor>`. When set, the expanded
|
|
9
|
+
* textarea keeps that name instead of `locale.codeLabel`.
|
|
10
|
+
*/
|
|
11
|
+
id?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The ref to the editor textarea
|
|
14
|
+
*/
|
|
15
|
+
ref?: React.Ref<HTMLTextAreaElement>;
|
|
16
|
+
/**
|
|
17
|
+
* The name of the input
|
|
18
|
+
*/
|
|
19
|
+
name?: string;
|
|
20
|
+
/**
|
|
21
|
+
* @default default
|
|
22
|
+
*/
|
|
23
|
+
size?: CodeInputSize;
|
|
24
|
+
/**
|
|
25
|
+
* The current value. Shared by the collapsed viewport and the overlay.
|
|
26
|
+
*/
|
|
27
|
+
value?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The initial value when rendered.
|
|
30
|
+
* Use when you don't need to control the value of the input.
|
|
31
|
+
*/
|
|
32
|
+
defaultValue?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The placeholder text
|
|
35
|
+
*/
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the input is disabled. The overlay still opens so long values stay searchable.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the value is read only. The overlay still opens so long values stay searchable.
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
readOnly?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Shows the error border and sets `aria-invalid`. Point `aria-describedby` at the message.
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
invalid?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Id of an element that describes the value, such as an error message
|
|
54
|
+
*/
|
|
55
|
+
'aria-describedby'?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the overlay is open. Pair with `onExpandChange`.
|
|
58
|
+
*/
|
|
59
|
+
expanded?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Initial overlay state when uncontrolled.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
defaultExpanded?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Fires when the overlay opens or closes
|
|
67
|
+
*/
|
|
68
|
+
onExpandChange?: (expanded: boolean) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Callback when the value changes
|
|
71
|
+
*/
|
|
72
|
+
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Value change event handler (provides just the value)
|
|
75
|
+
*/
|
|
76
|
+
onValueChange?: (value: string) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Event handler called when the editor receives focus
|
|
79
|
+
*/
|
|
80
|
+
onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Event handler called when the editor loses focus
|
|
83
|
+
*/
|
|
84
|
+
onBlur?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
85
|
+
/**
|
|
86
|
+
* The maximum number of characters
|
|
87
|
+
*/
|
|
88
|
+
maxLength?: number;
|
|
89
|
+
/**
|
|
90
|
+
* User-facing strings
|
|
91
|
+
*/
|
|
92
|
+
locale?: DsCodeInputLocale;
|
|
93
|
+
/**
|
|
94
|
+
* Optional render slots for customizing parts of the collapsed field.
|
|
95
|
+
*/
|
|
96
|
+
slots?: {
|
|
97
|
+
/**
|
|
98
|
+
* Adornment to display after the expand button
|
|
99
|
+
*/
|
|
100
|
+
endAdornment?: React.ReactNode;
|
|
101
|
+
};
|
|
102
|
+
className?: string;
|
|
103
|
+
style?: React.CSSProperties;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DsCodeInputLocale {
|
|
107
|
+
/**
|
|
108
|
+
* Accessible name of the button that opens the overlay
|
|
109
|
+
* @default 'Expand code editor'
|
|
110
|
+
*/
|
|
111
|
+
expand?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Accessible name of the button that closes the overlay
|
|
114
|
+
* @default 'Collapse code editor'
|
|
115
|
+
*/
|
|
116
|
+
collapse?: string;
|
|
117
|
+
/**
|
|
118
|
+
* @default 'Search in code'
|
|
119
|
+
*/
|
|
120
|
+
searchPlaceholder?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Accessible name of the overlay when the compact field is inert
|
|
123
|
+
* @default 'Code'
|
|
124
|
+
*/
|
|
125
|
+
codeLabel?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Visual label for hidden logical lines. Receives the additional-line count.
|
|
128
|
+
* @default (count) => `↵ +${count}`
|
|
129
|
+
*/
|
|
130
|
+
additionalLines?: (count: number) => string;
|
|
131
|
+
/**
|
|
132
|
+
* Accessible name for hidden logical lines. Receives the additional-line count.
|
|
133
|
+
* @default (count) => `${count} additional code lines`
|
|
134
|
+
*/
|
|
135
|
+
additionalLinesLabel?: (count: number) => string;
|
|
136
|
+
/**
|
|
137
|
+
* Visual focused line indicator. Receives the visible line and the total.
|
|
138
|
+
* @default (current, total) => `Ln ${current}/${total}`
|
|
139
|
+
*/
|
|
140
|
+
linePosition?: (current: number, total: number) => string;
|
|
141
|
+
/**
|
|
142
|
+
* Accessible description while collapsed and multiline.
|
|
143
|
+
* @default (total) => `Multiline code, ${total} lines. Only one line is visible. Expand to view the complete code.`
|
|
144
|
+
*/
|
|
145
|
+
multilineDescription?: (total: number) => string;
|
|
146
|
+
/**
|
|
147
|
+
* Accessible description while focused on a hidden line.
|
|
148
|
+
* @default (current, total) => `Line ${current} of ${total}; other code lines are hidden.`
|
|
149
|
+
*/
|
|
150
|
+
linePositionDescription?: (current: number, total: number) => string;
|
|
151
|
+
/**
|
|
152
|
+
* Visual status when a selection spans hidden lines.
|
|
153
|
+
* @default (selected, total) => `${selected} lines selected · ${total} total`
|
|
154
|
+
*/
|
|
155
|
+
selectionStatus?: (selected: number, total: number) => string;
|
|
156
|
+
/**
|
|
157
|
+
* Announced once when the overlay opens. Receives the logical line count.
|
|
158
|
+
* @default (total) => `Code editor expanded, ${total} lines.`
|
|
159
|
+
*/
|
|
160
|
+
expandedAnnouncement?: (total: number) => string;
|
|
161
|
+
/**
|
|
162
|
+
* Announced once when the overlay closes
|
|
163
|
+
* @default 'Code editor collapsed'
|
|
164
|
+
*/
|
|
165
|
+
collapsedAnnouncement?: string;
|
|
166
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type TextSelectionDirection = 'forward' | 'backward' | 'none';
|
|
2
|
+
|
|
3
|
+
export interface TextSelection {
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
direction: TextSelectionDirection;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CANVAS_TAB_SIZE_FALLBACK = 2;
|
|
10
|
+
|
|
11
|
+
let measureContext: CanvasRenderingContext2D | null | undefined;
|
|
12
|
+
|
|
13
|
+
export const canonicalizeLf = (text: string) => text.replace(/\r\n?/g, '\n');
|
|
14
|
+
|
|
15
|
+
const getLogicalLines = (text: string) => canonicalizeLf(text).split('\n');
|
|
16
|
+
|
|
17
|
+
export const getLogicalLineCount = (text: string) => getLogicalLines(text).length;
|
|
18
|
+
|
|
19
|
+
export const getAdditionalLineCount = (text: string) => Math.max(0, getLogicalLineCount(text) - 1);
|
|
20
|
+
|
|
21
|
+
export const getLineAtIndex = (text: string, lineIndex: number) => getLogicalLines(text)[lineIndex] ?? '';
|
|
22
|
+
|
|
23
|
+
export const getLineIndexAtOffset = (text: string, offset: number) => {
|
|
24
|
+
const lf = canonicalizeLf(text);
|
|
25
|
+
const clamped = Math.min(Math.max(offset, 0), lf.length);
|
|
26
|
+
|
|
27
|
+
return lf.slice(0, clamped).split('\n').length - 1;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getSelectedLineCount = (text: string, start: number, end: number) => {
|
|
31
|
+
if (start === end) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const from = Math.min(start, end);
|
|
36
|
+
const to = Math.max(start, end);
|
|
37
|
+
|
|
38
|
+
return getLineIndexAtOffset(text, to) - getLineIndexAtOffset(text, from) + 1;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const readTextSelection = (element: HTMLTextAreaElement): TextSelection => ({
|
|
42
|
+
start: element.selectionStart,
|
|
43
|
+
end: element.selectionEnd,
|
|
44
|
+
direction: element.selectionDirection,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const applyTextSelection = (element: HTMLTextAreaElement, selection: TextSelection) => {
|
|
48
|
+
const length = element.value.length;
|
|
49
|
+
const start = Math.min(selection.start, length);
|
|
50
|
+
const end = Math.min(selection.end, length);
|
|
51
|
+
|
|
52
|
+
element.setSelectionRange(start, end, selection.direction);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getLineHeight = (element: HTMLTextAreaElement) => {
|
|
56
|
+
const parsed = Number.parseFloat(getComputedStyle(element).lineHeight);
|
|
57
|
+
|
|
58
|
+
if (Number.isFinite(parsed)) {
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Number.parseFloat(getComputedStyle(element).fontSize);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const scrollToLogicalLine = (element: HTMLTextAreaElement, lineIndex: number) => {
|
|
66
|
+
element.scrollTop = Math.max(0, lineIndex) * getLineHeight(element);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getVisibleLineIndex = (element: HTMLTextAreaElement) => {
|
|
70
|
+
const lineHeight = getLineHeight(element);
|
|
71
|
+
|
|
72
|
+
if (lineHeight <= 0) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Math.round(element.scrollTop / lineHeight);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const measureTextWidth = (element: HTMLElement, text: string) => {
|
|
80
|
+
if (measureContext === undefined) {
|
|
81
|
+
measureContext = document.createElement('canvas').getContext('2d');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!measureContext) {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const computed = getComputedStyle(element);
|
|
89
|
+
const tabSize = Number.parseInt(computed.tabSize, 10) || CANVAS_TAB_SIZE_FALLBACK;
|
|
90
|
+
|
|
91
|
+
measureContext.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
|
|
92
|
+
|
|
93
|
+
return measureContext.measureText(text.replaceAll('\t', ' '.repeat(tabSize))).width;
|
|
94
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
applyTextSelection,
|
|
4
|
+
canonicalizeLf,
|
|
5
|
+
getAdditionalLineCount,
|
|
6
|
+
getLineAtIndex,
|
|
7
|
+
getLineIndexAtOffset,
|
|
8
|
+
getLogicalLineCount,
|
|
9
|
+
getSelectedLineCount,
|
|
10
|
+
type TextSelection,
|
|
11
|
+
} from './ds-code-input.utils';
|
|
12
|
+
|
|
13
|
+
const multiline = 'SELECT *\nFROM devices\nWHERE id = 1';
|
|
14
|
+
|
|
15
|
+
const stubTextarea = (value: string) => {
|
|
16
|
+
const setSelectionRange = vi.fn();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
element: { value, setSelectionRange } as unknown as HTMLTextAreaElement,
|
|
20
|
+
setSelectionRange,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('canonicalizeLf', () => {
|
|
25
|
+
it('rewrites CRLF to LF', () => {
|
|
26
|
+
expect(canonicalizeLf('a\r\nb')).toBe('a\nb');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('rewrites a lone CR to LF', () => {
|
|
30
|
+
expect(canonicalizeLf('a\rb')).toBe('a\nb');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('leaves LF unchanged', () => {
|
|
34
|
+
expect(canonicalizeLf('a\nb')).toBe('a\nb');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('normalizes mixed endings in one string', () => {
|
|
38
|
+
expect(canonicalizeLf('a\r\nb\rc\nd')).toBe('a\nb\nc\nd');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getLogicalLineCount', () => {
|
|
43
|
+
it('counts an empty string as one logical line', () => {
|
|
44
|
+
expect(getLogicalLineCount('')).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('counts a trailing newline as an extra empty line', () => {
|
|
48
|
+
expect(getLogicalLineCount('a\n')).toBe(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('counts each logical line in multiline text', () => {
|
|
52
|
+
expect(getLogicalLineCount(multiline)).toBe(3);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getAdditionalLineCount', () => {
|
|
57
|
+
it('is zero for a single line, including empty text', () => {
|
|
58
|
+
expect(getAdditionalLineCount('')).toBe(0);
|
|
59
|
+
expect(getAdditionalLineCount('SELECT *')).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is the number of lines beyond the first', () => {
|
|
63
|
+
expect(getAdditionalLineCount('a\n')).toBe(1);
|
|
64
|
+
expect(getAdditionalLineCount(multiline)).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('getLineAtIndex', () => {
|
|
69
|
+
it('returns the line at a valid index', () => {
|
|
70
|
+
expect(getLineAtIndex(multiline, 0)).toBe('SELECT *');
|
|
71
|
+
expect(getLineAtIndex(multiline, 1)).toBe('FROM devices');
|
|
72
|
+
expect(getLineAtIndex(multiline, 2)).toBe('WHERE id = 1');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns an empty string when the index is out of range', () => {
|
|
76
|
+
expect(getLineAtIndex(multiline, -1)).toBe('');
|
|
77
|
+
expect(getLineAtIndex(multiline, 3)).toBe('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('splits CRLF text on logical lines', () => {
|
|
81
|
+
expect(getLineAtIndex('hello\r\nworld', 0)).toBe('hello');
|
|
82
|
+
expect(getLineAtIndex('hello\r\nworld', 1)).toBe('world');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getLineIndexAtOffset', () => {
|
|
87
|
+
const text = 'hello\nworld';
|
|
88
|
+
|
|
89
|
+
it('returns the first line at the start of the text', () => {
|
|
90
|
+
expect(getLineIndexAtOffset(text, 0)).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('stays on the previous line when the offset sits on a newline', () => {
|
|
94
|
+
expect(getLineIndexAtOffset(text, 5)).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('moves to the next line once the offset is past the newline', () => {
|
|
98
|
+
expect(getLineIndexAtOffset(text, 6)).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('clamps offsets below 0 and past the text length', () => {
|
|
102
|
+
expect(getLineIndexAtOffset(text, -4)).toBe(0);
|
|
103
|
+
expect(getLineIndexAtOffset(text, 100)).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('maps offsets in CRLF text onto the same logical lines as LF', () => {
|
|
107
|
+
const crlf = 'hello\r\nworld';
|
|
108
|
+
|
|
109
|
+
expect(getLineIndexAtOffset(crlf, 5)).toBe(0);
|
|
110
|
+
expect(getLineIndexAtOffset(crlf, 6)).toBe(1);
|
|
111
|
+
expect(getLineIndexAtOffset(crlf, crlf.length)).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getSelectedLineCount', () => {
|
|
116
|
+
const text = 'a\nb\nc';
|
|
117
|
+
|
|
118
|
+
it('is zero when the range is collapsed, even in multiline text', () => {
|
|
119
|
+
expect(getSelectedLineCount(text, 0, 0)).toBe(0);
|
|
120
|
+
expect(getSelectedLineCount(text, 2, 2)).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('is one when the range stays on a single line', () => {
|
|
124
|
+
expect(getSelectedLineCount(text, 0, 1)).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('counts each line the range crosses', () => {
|
|
128
|
+
expect(getSelectedLineCount(text, 0, 2)).toBe(2);
|
|
129
|
+
expect(getSelectedLineCount(text, 0, text.length)).toBe(3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('treats a reversed range the same as start-to-end', () => {
|
|
133
|
+
expect(getSelectedLineCount(text, 2, 0)).toBe(getSelectedLineCount(text, 0, 2));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('stays on one line when the range ends on a newline', () => {
|
|
137
|
+
expect(getSelectedLineCount('hello\nworld', 0, 5)).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('applyTextSelection', () => {
|
|
142
|
+
it('restores the range and direction on the element', () => {
|
|
143
|
+
const { element, setSelectionRange } = stubTextarea('abc');
|
|
144
|
+
const selection: TextSelection = { start: 1, end: 2, direction: 'forward' };
|
|
145
|
+
|
|
146
|
+
applyTextSelection(element, selection);
|
|
147
|
+
|
|
148
|
+
expect(setSelectionRange).toHaveBeenCalledWith(1, 2, 'forward');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('clamps the range to the current value length', () => {
|
|
152
|
+
const { element, setSelectionRange } = stubTextarea('abc');
|
|
153
|
+
|
|
154
|
+
applyTextSelection(element, { start: 10, end: 20, direction: 'none' });
|
|
155
|
+
|
|
156
|
+
expect(setSelectionRange).toHaveBeenCalledWith(3, 3, 'none');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('clamps only the end when start is still in range', () => {
|
|
160
|
+
const { element, setSelectionRange } = stubTextarea('abc');
|
|
161
|
+
|
|
162
|
+
applyTextSelection(element, { start: 1, end: 99, direction: 'backward' });
|
|
163
|
+
|
|
164
|
+
expect(setSelectionRange).toHaveBeenCalledWith(1, 3, 'backward');
|
|
165
|
+
});
|
|
166
|
+
});
|