@drivenets/design-system 0.18.1 → 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 +48 -0
- package/dist/index.cjs +17038 -10114
- package/dist/index.d.cts +1355 -444
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1354 -443
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17073 -10172
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +41 -41
- package/src/components/ds-bot-button/ds-bot-button.figma.ts +38 -0
- package/src/components/ds-bot-button/ds-bot-button.module.scss +41 -0
- package/src/components/ds-bot-button/ds-bot-button.tsx +48 -0
- package/src/components/ds-bot-button/ds-bot-button.types.ts +33 -0
- package/src/components/ds-bot-button/index.ts +2 -0
- package/src/components/ds-bot-button/net-gen-logo.tsx +46 -0
- package/src/components/ds-bulk-actions/components/action-button.tsx +42 -0
- package/src/components/ds-bulk-actions/components/menu-entries.tsx +76 -0
- package/src/components/ds-bulk-actions/components/overflow.tsx +75 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.figma.ts +28 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.module.scss +199 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.tsx +220 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.types.ts +170 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.unit.test.ts +195 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.utils.ts +91 -0
- package/src/components/ds-bulk-actions/ds-bulk-actions.utils.unit.test.ts +134 -0
- package/src/components/ds-bulk-actions/hooks/compute-bulk-actions-overflow.ts +85 -0
- package/src/components/ds-bulk-actions/hooks/use-bulk-actions-overflow.ts +153 -0
- package/src/components/ds-bulk-actions/index.ts +18 -0
- package/src/components/ds-button-v3/ds-button-v3.module.scss +2 -2
- package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +1 -1
- 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-dropdown-menu/ds-dropdown-menu.tsx +2 -0
- package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
- package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
- package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
- package/src/components/ds-empty-state/index.ts +3 -0
- package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
- 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 +61 -19
- 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-illustration/ds-illustration.figma.ts +34 -0
- package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
- package/src/components/ds-illustration/ds-illustration.tsx +29 -0
- package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
- package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
- package/src/components/ds-illustration/illustrations/device.tsx +133 -0
- package/src/components/ds-illustration/illustrations/done.tsx +107 -0
- package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
- package/src/components/ds-illustration/illustrations/error.tsx +86 -0
- package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
- package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
- package/src/components/ds-illustration/illustrations/index.ts +39 -0
- package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
- package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
- package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
- package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
- package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
- package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
- package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
- package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
- package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
- package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
- package/src/components/ds-illustration/illustrations/site.tsx +80 -0
- package/src/components/ds-illustration/index.ts +3 -0
- package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
- package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
- package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
- package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
- package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
- package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
- package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
- package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
- package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
- package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
- package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
- package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
- package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
- package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
- package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
- package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
- package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
- package/src/components/ds-main-menu/index.ts +9 -0
- package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
- 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 +164 -41
- package/src/components/ds-popover/ds-popover.types.ts +65 -0
- 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-site-node/ds-site-node.tsx +2 -2
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
- package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
- package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
- package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
- package/src/components/ds-table/context/ds-table-context.tsx +8 -0
- package/src/components/ds-table/ds-table.module.scss +26 -16
- package/src/components/ds-table/ds-table.tsx +46 -14
- package/src/components/ds-table/ds-table.types.ts +33 -3
- package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
- package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
- package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
- package/src/components/ds-table/styles/_variables.scss +0 -2
- package/src/components/ds-table/utils/column-size.ts +274 -1
- package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
- package/src/components/ds-table/utils/constants.ts +13 -5
- package/src/components/ds-table/utils/table-columns.tsx +27 -13
- package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -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.module.scss +18 -0
- package/src/components/ds-toggle/ds-toggle.tsx +15 -9
- 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 +7 -3
- package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
- package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
- package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
- package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
- package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
- package/src/components/ds-top-bar-navigation/index.ts +2 -0
- 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 +8 -0
- package/src/utils/use-is-horizontally-overflowing.ts +35 -0
- package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
- package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
- package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
- package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
- package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: block;
|
|
3
|
+
width: 100%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.panel {
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
|
|
9
|
+
&.invalid {
|
|
10
|
+
outline: 1px solid var(--background-error);
|
|
11
|
+
outline-offset: -1px;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.visuallyHidden {
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: 1px;
|
|
18
|
+
height: 1px;
|
|
19
|
+
margin: -1px;
|
|
20
|
+
padding: 0;
|
|
21
|
+
border: 0;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
clip-path: inset(50%);
|
|
25
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { type ChangeEvent, type FocusEvent, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { DsButtonV3, type ButtonV3Size } from '../ds-button-v3';
|
|
5
|
+
import { DsPopover } from '../ds-popover';
|
|
6
|
+
import { mergeRefs } from '../../utils/merge-refs';
|
|
7
|
+
import { CollapsedCodeField } from './collapsed-code-field';
|
|
8
|
+
import collapsedStyles from './collapsed-code-field/collapsed-code-field.module.scss';
|
|
9
|
+
import { DsCodeInputPanel } from './ds-code-input-panel';
|
|
10
|
+
import panelStyles from './ds-code-input-panel/ds-code-input-panel.module.scss';
|
|
11
|
+
import styles from './ds-code-input.module.scss';
|
|
12
|
+
import type { CodeInputSize, DsCodeInputProps } from './ds-code-input.types';
|
|
13
|
+
import {
|
|
14
|
+
applyTextSelection,
|
|
15
|
+
canonicalizeLf,
|
|
16
|
+
getAdditionalLineCount,
|
|
17
|
+
getLogicalLineCount,
|
|
18
|
+
readTextSelection,
|
|
19
|
+
type TextSelection,
|
|
20
|
+
} from './ds-code-input.utils';
|
|
21
|
+
import { useCollapsedViewport } from './use-collapsed-viewport';
|
|
22
|
+
import { useEditorHost } from './use-editor-host';
|
|
23
|
+
|
|
24
|
+
const toggleSizes: Record<CodeInputSize, ButtonV3Size> = Object.freeze({
|
|
25
|
+
small: 'tiny',
|
|
26
|
+
default: 'small',
|
|
27
|
+
large: 'small',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const OVERLAY_GUTTER_PX = 4;
|
|
31
|
+
|
|
32
|
+
const defaultLocale = Object.freeze({
|
|
33
|
+
expand: 'Expand code editor',
|
|
34
|
+
collapse: 'Collapse code editor',
|
|
35
|
+
searchPlaceholder: 'Search in code',
|
|
36
|
+
codeLabel: 'Code',
|
|
37
|
+
additionalLines: (count: number) => `↵ +${String(count)}`,
|
|
38
|
+
additionalLinesLabel: (count: number) => `${String(count)} additional code lines`,
|
|
39
|
+
linePosition: (current: number, total: number) => `Ln ${String(current)}/${String(total)}`,
|
|
40
|
+
multilineDescription: (total: number) =>
|
|
41
|
+
`Multiline code, ${String(total)} lines. Only one line is visible. Expand to view the complete code.`,
|
|
42
|
+
linePositionDescription: (current: number, total: number) =>
|
|
43
|
+
`Line ${String(current)} of ${String(total)}; other code lines are hidden.`,
|
|
44
|
+
selectionStatus: (selected: number, total: number) =>
|
|
45
|
+
`${String(selected)} lines selected · ${String(total)} total`,
|
|
46
|
+
expandedAnnouncement: (total: number) => `Code editor expanded, ${String(total)} lines.`,
|
|
47
|
+
collapsedAnnouncement: 'Code editor collapsed',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Multiline editor for code, such as a query expression. The collapsed field is a
|
|
52
|
+
* one-row viewport of the same document; expanding opens a larger overlay with search.
|
|
53
|
+
* There is one textarea: collapsed and expanded are two homes it moves between.
|
|
54
|
+
*
|
|
55
|
+
* @summary multiline code field that expands into a searchable overlay
|
|
56
|
+
*/
|
|
57
|
+
const DsCodeInput = ({
|
|
58
|
+
id,
|
|
59
|
+
ref,
|
|
60
|
+
name,
|
|
61
|
+
size = 'default',
|
|
62
|
+
value,
|
|
63
|
+
defaultValue,
|
|
64
|
+
placeholder,
|
|
65
|
+
disabled = false,
|
|
66
|
+
readOnly = false,
|
|
67
|
+
invalid = false,
|
|
68
|
+
'aria-describedby': ariaDescribedBy,
|
|
69
|
+
expanded,
|
|
70
|
+
defaultExpanded = false,
|
|
71
|
+
onExpandChange,
|
|
72
|
+
onChange,
|
|
73
|
+
onValueChange,
|
|
74
|
+
onFocus,
|
|
75
|
+
onBlur,
|
|
76
|
+
maxLength,
|
|
77
|
+
locale,
|
|
78
|
+
slots,
|
|
79
|
+
className,
|
|
80
|
+
style,
|
|
81
|
+
}: DsCodeInputProps) => {
|
|
82
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
|
|
83
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
|
|
84
|
+
// True when the user opened the overlay and should keep typing in it.
|
|
85
|
+
const [focusOnOpen, setFocusOnOpen] = useState(false);
|
|
86
|
+
const [announcement, setAnnouncement] = useState('');
|
|
87
|
+
|
|
88
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
89
|
+
const collapsedSlotRef = useRef<HTMLDivElement>(null);
|
|
90
|
+
const overlaySlotRef = useRef<HTMLDivElement>(null);
|
|
91
|
+
// Caret/selection saved before the textarea moves to the other slot.
|
|
92
|
+
const pendingSelectionRef = useRef<TextSelection | null>(null);
|
|
93
|
+
// True when the user closed the overlay and should land back in the field.
|
|
94
|
+
const focusOnCloseRef = useRef(false);
|
|
95
|
+
// Skip the first expand/collapse announcement so mount is silent.
|
|
96
|
+
const skipAnnouncementRef = useRef(true);
|
|
97
|
+
|
|
98
|
+
const panelId = useId();
|
|
99
|
+
const descriptionId = useId();
|
|
100
|
+
const announcementId = useId();
|
|
101
|
+
|
|
102
|
+
const strings = { ...defaultLocale, ...locale };
|
|
103
|
+
|
|
104
|
+
const isValueControlled = value !== undefined;
|
|
105
|
+
const currentValue = isValueControlled ? value : uncontrolledValue;
|
|
106
|
+
|
|
107
|
+
const isExpandedControlled = expanded !== undefined;
|
|
108
|
+
const isExpanded = isExpandedControlled ? expanded : uncontrolledExpanded;
|
|
109
|
+
|
|
110
|
+
const handleOpenChange = (open: boolean) => {
|
|
111
|
+
if (open) {
|
|
112
|
+
focusOnCloseRef.current = false;
|
|
113
|
+
|
|
114
|
+
// User-open already saved the caret. If the overlay opened some other
|
|
115
|
+
// way, snapshot it now so it still comes back after the move.
|
|
116
|
+
if (!pendingSelectionRef.current) {
|
|
117
|
+
const textarea = textareaRef.current;
|
|
118
|
+
|
|
119
|
+
if (textarea) {
|
|
120
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
setFocusOnOpen(false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isExpandedControlled) {
|
|
128
|
+
setUncontrolledExpanded(open);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onExpandChange?.(open);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const { host, hostReady, isMovingHostRef } = useEditorHost({
|
|
135
|
+
isExpanded,
|
|
136
|
+
collapsedSlotRef,
|
|
137
|
+
overlaySlotRef,
|
|
138
|
+
hostClassName: isExpanded ? panelStyles.editorHost : collapsedStyles.editorHost,
|
|
139
|
+
textareaRef,
|
|
140
|
+
pendingSelectionRef,
|
|
141
|
+
shouldFocus: isExpanded && focusOnOpen,
|
|
142
|
+
});
|
|
143
|
+
const viewport = useCollapsedViewport(textareaRef, currentValue, hostReady, !isExpanded);
|
|
144
|
+
const revealCaretLineRef = useRef(viewport.revealCaretLine);
|
|
145
|
+
|
|
146
|
+
revealCaretLineRef.current = viewport.revealCaretLine;
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (skipAnnouncementRef.current) {
|
|
150
|
+
skipAnnouncementRef.current = false;
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setAnnouncement(
|
|
156
|
+
isExpanded
|
|
157
|
+
? strings.expandedAnnouncement(getLogicalLineCount(currentValue))
|
|
158
|
+
: strings.collapsedAnnouncement,
|
|
159
|
+
);
|
|
160
|
+
// Announce presentation changes only — not every keystroke in the document.
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- currentValue/strings would re-announce while typing
|
|
162
|
+
}, [isExpanded]);
|
|
163
|
+
|
|
164
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
165
|
+
// Some browsers insert \r\n. Rewrite to \n and put the caret back so
|
|
166
|
+
// typing does not jump.
|
|
167
|
+
const newValue = canonicalizeLf(event.target.value);
|
|
168
|
+
|
|
169
|
+
if (newValue !== event.target.value) {
|
|
170
|
+
const selection = readTextSelection(event.target);
|
|
171
|
+
|
|
172
|
+
event.target.value = newValue;
|
|
173
|
+
applyTextSelection(event.target, selection);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isValueControlled) {
|
|
177
|
+
setUncontrolledValue(newValue);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onChange?.(event);
|
|
181
|
+
onValueChange?.(newValue);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
185
|
+
// Moving the host fires fake focus/blur. Ignore those so callers do not
|
|
186
|
+
// think the user left the field.
|
|
187
|
+
if (isMovingHostRef.current) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onFocus?.(event);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
195
|
+
if (isMovingHostRef.current) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onBlur?.(event);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleUserOpen = () => {
|
|
203
|
+
const textarea = textareaRef.current;
|
|
204
|
+
|
|
205
|
+
if (textarea) {
|
|
206
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setFocusOnOpen(true);
|
|
210
|
+
handleOpenChange(true);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const additionalLines = getAdditionalLineCount(currentValue);
|
|
214
|
+
const description =
|
|
215
|
+
!isExpanded && additionalLines > 0
|
|
216
|
+
? viewport.focused
|
|
217
|
+
? strings.linePositionDescription(viewport.visibleLine, viewport.totalLines)
|
|
218
|
+
: strings.multilineDescription(viewport.totalLines)
|
|
219
|
+
: undefined;
|
|
220
|
+
|
|
221
|
+
// One textarea, rendered once and portaled into the host. rows / wrap /
|
|
222
|
+
// className flip when it moves between the collapsed slot and the overlay.
|
|
223
|
+
const editor = host
|
|
224
|
+
? createPortal(
|
|
225
|
+
<textarea
|
|
226
|
+
id={id}
|
|
227
|
+
ref={mergeRefs(ref, textareaRef)}
|
|
228
|
+
name={name}
|
|
229
|
+
className={isExpanded ? panelStyles.codeArea : collapsedStyles.input}
|
|
230
|
+
rows={isExpanded ? undefined : 1}
|
|
231
|
+
wrap={isExpanded ? 'soft' : 'off'}
|
|
232
|
+
value={currentValue}
|
|
233
|
+
placeholder={placeholder}
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
readOnly={readOnly}
|
|
236
|
+
maxLength={maxLength}
|
|
237
|
+
spellCheck={false}
|
|
238
|
+
autoComplete="off"
|
|
239
|
+
aria-multiline="true"
|
|
240
|
+
aria-label={isExpanded && !id ? strings.codeLabel : undefined}
|
|
241
|
+
aria-invalid={invalid || undefined}
|
|
242
|
+
aria-describedby={
|
|
243
|
+
[description && descriptionId, ariaDescribedBy].filter(Boolean).join(' ') || undefined
|
|
244
|
+
}
|
|
245
|
+
onChange={handleChange}
|
|
246
|
+
onSelect={viewport.onSelect}
|
|
247
|
+
onKeyUp={viewport.onKeyUp}
|
|
248
|
+
onClick={viewport.onClick}
|
|
249
|
+
onScroll={viewport.onScroll}
|
|
250
|
+
onBeforeInput={viewport.onBeforeInput}
|
|
251
|
+
onFocus={(event) => {
|
|
252
|
+
if (!isMovingHostRef.current) {
|
|
253
|
+
viewport.onFocus();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
handleFocus(event);
|
|
257
|
+
}}
|
|
258
|
+
onBlur={(event) => {
|
|
259
|
+
if (!isMovingHostRef.current) {
|
|
260
|
+
viewport.onBlur();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handleBlur(event);
|
|
264
|
+
}}
|
|
265
|
+
onKeyDown={(event) => {
|
|
266
|
+
if (event.key === 'Escape' && isExpanded) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
focusOnCloseRef.current = true;
|
|
269
|
+
handleOpenChange(false);
|
|
270
|
+
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!isExpanded && event.altKey && event.key === 'ArrowDown') {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
event.stopPropagation();
|
|
277
|
+
handleUserOpen();
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
/>,
|
|
281
|
+
host,
|
|
282
|
+
)
|
|
283
|
+
: null;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<DsPopover.Root
|
|
287
|
+
open={isExpanded}
|
|
288
|
+
side="bottom"
|
|
289
|
+
align="start"
|
|
290
|
+
gutter={OVERLAY_GUTTER_PX}
|
|
291
|
+
matchAnchorWidth
|
|
292
|
+
// We own focus so the expand button does not steal it.
|
|
293
|
+
restoreFocus={false}
|
|
294
|
+
onOpenAutoFocus={(event) => {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
|
|
297
|
+
if (focusOnOpen) {
|
|
298
|
+
textareaRef.current?.focus({ preventScroll: true });
|
|
299
|
+
}
|
|
300
|
+
}}
|
|
301
|
+
onCloseAutoFocus={(event) => {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
|
|
304
|
+
// Put focus back in the collapsed field and scroll to the caret line.
|
|
305
|
+
if (focusOnCloseRef.current) {
|
|
306
|
+
textareaRef.current?.focus({ preventScroll: true });
|
|
307
|
+
revealCaretLineRef.current();
|
|
308
|
+
focusOnCloseRef.current = false;
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
onOpenChange={handleOpenChange}
|
|
312
|
+
>
|
|
313
|
+
<DsPopover.Anchor>
|
|
314
|
+
<div className={classNames(styles.root, className)} style={style}>
|
|
315
|
+
<CollapsedCodeField
|
|
316
|
+
size={size}
|
|
317
|
+
placeholder={placeholder}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
invalid={invalid}
|
|
320
|
+
showEcho={isExpanded}
|
|
321
|
+
echoText={viewport.echoText}
|
|
322
|
+
fadeStart={viewport.fadeStart}
|
|
323
|
+
fadeEnd={viewport.fadeEnd}
|
|
324
|
+
showEllipsis={viewport.showEllipsis}
|
|
325
|
+
indicator={
|
|
326
|
+
isExpanded && additionalLines > 0
|
|
327
|
+
? { kind: 'line', current: viewport.caretLine, total: viewport.totalLines }
|
|
328
|
+
: isExpanded
|
|
329
|
+
? null
|
|
330
|
+
: viewport.indicator
|
|
331
|
+
}
|
|
332
|
+
additionalLinesLabel={strings.additionalLinesLabel}
|
|
333
|
+
additionalLines={strings.additionalLines}
|
|
334
|
+
linePosition={strings.linePosition}
|
|
335
|
+
selectionStatus={strings.selectionStatus}
|
|
336
|
+
editorSlotRef={collapsedSlotRef}
|
|
337
|
+
onExpand={handleUserOpen}
|
|
338
|
+
adornment={
|
|
339
|
+
<>
|
|
340
|
+
<DsPopover.Trigger>
|
|
341
|
+
<DsButtonV3
|
|
342
|
+
variant="tertiary"
|
|
343
|
+
size={toggleSizes[size]}
|
|
344
|
+
icon={isExpanded ? 'collapse_content' : 'expand_content'}
|
|
345
|
+
aria-label={isExpanded ? strings.collapse : strings.expand}
|
|
346
|
+
aria-expanded={isExpanded}
|
|
347
|
+
aria-controls={panelId}
|
|
348
|
+
onPointerDown={(event) => {
|
|
349
|
+
// Snapshot the caret and request focus before the
|
|
350
|
+
// button would steal it.
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
|
|
353
|
+
if (isExpanded) {
|
|
354
|
+
focusOnCloseRef.current = true;
|
|
355
|
+
} else {
|
|
356
|
+
const textarea = textareaRef.current;
|
|
357
|
+
|
|
358
|
+
if (textarea) {
|
|
359
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setFocusOnOpen(true);
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
</DsPopover.Trigger>
|
|
367
|
+
{slots?.endAdornment}
|
|
368
|
+
</>
|
|
369
|
+
}
|
|
370
|
+
/>
|
|
371
|
+
{description && (
|
|
372
|
+
<span id={descriptionId} className={styles.visuallyHidden}>
|
|
373
|
+
{description}
|
|
374
|
+
</span>
|
|
375
|
+
)}
|
|
376
|
+
<div id={announcementId} className={styles.visuallyHidden} aria-live="polite" aria-atomic="true">
|
|
377
|
+
{announcement}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</DsPopover.Anchor>
|
|
381
|
+
|
|
382
|
+
<DsPopover.Panel
|
|
383
|
+
id={panelId}
|
|
384
|
+
className={classNames(styles.panel, invalid && styles.invalid)}
|
|
385
|
+
aria-label={strings.codeLabel}
|
|
386
|
+
>
|
|
387
|
+
<DsCodeInputPanel
|
|
388
|
+
value={currentValue}
|
|
389
|
+
isOpen={isExpanded}
|
|
390
|
+
searchPlaceholder={strings.searchPlaceholder}
|
|
391
|
+
editorSlotRef={overlaySlotRef}
|
|
392
|
+
/>
|
|
393
|
+
</DsPopover.Panel>
|
|
394
|
+
{editor}
|
|
395
|
+
</DsPopover.Root>
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
DsCodeInput.displayName = 'DsCodeInput';
|
|
400
|
+
|
|
401
|
+
export default DsCodeInput;
|
|
@@ -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
|
+
};
|