@team-monolith/cds 1.79.4 → 1.79.6
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/patterns/LexicalEditor/nodes/ProblemInputNode/InputControlledSelectionRange.d.ts +13 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/InputControlledSelectionRange.js +47 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SegmentedInput.js +48 -42
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface SelectionRange {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
/** value, selectionRange에 대한 controlled input 컴포넌트입니다.
|
|
7
|
+
* 입력값과, 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
|
|
8
|
+
*/
|
|
9
|
+
export declare const InputControlledSelectionRange: React.ForwardRefExoticComponent<{
|
|
10
|
+
value: string;
|
|
11
|
+
selectionRange: SelectionRange | null;
|
|
12
|
+
onSelectionChange: (selectionRange: SelectionRange | null) => void;
|
|
13
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value"> & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { useEffect } from "react";
|
|
15
|
+
/** value, selectionRange에 대한 controlled input 컴포넌트입니다.
|
|
16
|
+
* 입력값과, 선택 범위에 대한 state, setState를 props로 받아 사용합니다.
|
|
17
|
+
*/
|
|
18
|
+
export const InputControlledSelectionRange = React.forwardRef(function InputControlledSelectionRange(props, ref) {
|
|
19
|
+
const { value, selectionRange, onSelectionChange } = props, inputProps = __rest(props, ["value", "selectionRange", "onSelectionChange"]);
|
|
20
|
+
// value 또는 selectionRange가 바뀔 때마다,
|
|
21
|
+
// 실제 DOM input의 selection range를 동기화
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof ref === "function" || !(ref === null || ref === void 0 ? void 0 : ref.current) || !selectionRange)
|
|
24
|
+
return;
|
|
25
|
+
const { start, end } = selectionRange;
|
|
26
|
+
try {
|
|
27
|
+
ref.current.setSelectionRange(start, end);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// 범위가 잘못되거나 input이 포커스가 없을 때 에러가 발생할 수 있으므로 주의
|
|
31
|
+
console.warn("setSelectionRange error:", err);
|
|
32
|
+
}
|
|
33
|
+
}, [selectionRange, value]);
|
|
34
|
+
// selectionRange 변경 감지 핸들러 (React에선 onSelect 사용)
|
|
35
|
+
const handleSelect = (e) => {
|
|
36
|
+
const { selectionStart, selectionEnd } = e.currentTarget;
|
|
37
|
+
if (selectionStart != null && selectionEnd != null) {
|
|
38
|
+
onSelectionChange({
|
|
39
|
+
start: selectionStart,
|
|
40
|
+
end: selectionEnd,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return (_jsx("input", Object.assign({ ref: ref,
|
|
45
|
+
// Controlled input
|
|
46
|
+
value: value, onSelect: handleSelect }, inputProps)));
|
|
47
|
+
});
|
|
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
|
3
3
|
import { css, useTheme } from "@emotion/react";
|
|
4
4
|
import styled from "@emotion/styled";
|
|
5
5
|
import { InputBase } from "../../../../components/InputBase";
|
|
6
|
-
import {
|
|
6
|
+
import { useRef, useState } from "react";
|
|
7
7
|
import { useEventListener } from "usehooks-ts";
|
|
8
|
+
import { InputControlledSelectionRange, } from "./InputControlledSelectionRange";
|
|
8
9
|
/**
|
|
9
10
|
* SOT는 value이고, 그 value는 숨겨진 input에 작성됩니다.
|
|
10
11
|
* 그리고 그 value는 split되어 segmented input에 표시됩니다.
|
|
@@ -19,14 +20,23 @@ export function SegmentedInput(props) {
|
|
|
19
20
|
const despacedFormat = splitedFormat.filter((v) => v !== " ");
|
|
20
21
|
// "안녕 하세요" => ["안","녕","하","세","요"]
|
|
21
22
|
const splitedValues = value.split("").filter((v) => v !== " ");
|
|
22
|
-
|
|
23
|
+
// 현재 선택범위를 state로 관리합니다. 현재 커서 위치의 Source of Truth입니다.
|
|
24
|
+
const [selectionRange, setSelectionRange] = useState(null);
|
|
23
25
|
// 글자를 조합중인지 여부 (ex: "안"을 입력중 "아"일 때 해당합니다.)
|
|
24
26
|
const [isComposing, setIsComposing] = useState(false);
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
+
// 현재 포커스된 SquareInput의 index
|
|
28
|
+
const focusedIndex = !selectionRange
|
|
29
|
+
? null
|
|
30
|
+
: isComposing
|
|
31
|
+
? selectionRange.start - 1
|
|
32
|
+
: selectionRange.start;
|
|
27
33
|
const hiddenRef = useRef(null);
|
|
28
|
-
useEventListener("compositionstart",
|
|
29
|
-
|
|
34
|
+
useEventListener("compositionstart", () => {
|
|
35
|
+
setIsComposing(true);
|
|
36
|
+
}, hiddenRef);
|
|
37
|
+
useEventListener("compositionend", () => {
|
|
38
|
+
setIsComposing(false);
|
|
39
|
+
}, hiddenRef);
|
|
30
40
|
/** value의 공백을 제거하고, format에 맞춰 공백을 추가한 값을 리턴합니다. */
|
|
31
41
|
const getFormattedValue = (value) => {
|
|
32
42
|
const despacedValue = value.replace(/ /g, "");
|
|
@@ -39,23 +49,28 @@ export function SegmentedInput(props) {
|
|
|
39
49
|
}
|
|
40
50
|
}, "");
|
|
41
51
|
};
|
|
42
|
-
return (_jsxs(Container, { children: [_jsx(
|
|
52
|
+
return (_jsxs(Container, { children: [_jsx(InputControlledSelectionRange, { type: "input", ref: hiddenRef, value: splitedValues.join(""), readOnly: readOnly, selectionRange: selectionRange, onSelectionChange: setSelectionRange, onKeyDown: (e) => {
|
|
43
53
|
var _a;
|
|
44
54
|
if (e.key === "ArrowLeft") {
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
if (selectionRange !== null && selectionRange.start > 0) {
|
|
57
|
+
setSelectionRange({
|
|
58
|
+
start: selectionRange.start - 1,
|
|
59
|
+
end: selectionRange.start - 1,
|
|
60
|
+
});
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
else if (e.key === "ArrowRight") {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
if (selectionRange !== null &&
|
|
66
|
+
selectionRange.end < despacedFormat.length - 1) {
|
|
67
|
+
setSelectionRange({
|
|
68
|
+
start: selectionRange.end + 1,
|
|
69
|
+
end: selectionRange.end + 1,
|
|
70
|
+
});
|
|
53
71
|
}
|
|
54
72
|
}
|
|
55
73
|
else if (e.key === "Backspace") {
|
|
56
|
-
// 현재 커서의 실제위치(hiddenRef.current?.selectionStart)는 포커스된 SquareInput 글자의 왼쪽 또는 오른쪽에 위치합니다.
|
|
57
|
-
// 사용자는 hiddenInput을 볼 수 없고, 포커싱된 SquareInput만 볼 수 있으므로 실제커서 위치는 예상할 수 없습니다.
|
|
58
|
-
// 그래서 현재 포커싱된 위치를 focusedIndex로 관리합니다.
|
|
59
74
|
// 만약 조합중이라면 원래 동작대로 현재 글자를 지우고,
|
|
60
75
|
// 조합중이 아니라면 focusedIndex의 글자를 지워야 합니다.(delete와 같은 동작)
|
|
61
76
|
if (isComposing)
|
|
@@ -71,32 +86,24 @@ export function SegmentedInput(props) {
|
|
|
71
86
|
onChange(getFormattedValue(newValue));
|
|
72
87
|
// 이전 글자로 포커싱을 이동합니다.
|
|
73
88
|
const newFocusedIndex = Math.max(focusedIndex - 1, 0);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
(_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.setSelectionRange(newFocusedIndex, newFocusedIndex);
|
|
79
|
-
}, 0);
|
|
89
|
+
setSelectionRange({
|
|
90
|
+
start: newFocusedIndex,
|
|
91
|
+
end: newFocusedIndex,
|
|
92
|
+
});
|
|
80
93
|
}
|
|
81
94
|
}, onChange: (e) => {
|
|
82
|
-
var _a;
|
|
95
|
+
var _a, _b;
|
|
83
96
|
onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// tab을 통해 focus가 이동되었을 때, focusedIndex를 설정합니다.
|
|
89
|
-
if (focusedIndex === null) {
|
|
90
|
-
const valueLength = e.target.value.length;
|
|
91
|
-
setFocusedIndex(valueLength);
|
|
92
|
-
(_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.setSelectionRange(valueLength, valueLength);
|
|
93
|
-
}
|
|
97
|
+
setSelectionRange({
|
|
98
|
+
start: (_a = e.target.selectionStart) !== null && _a !== void 0 ? _a : 0,
|
|
99
|
+
end: (_b = e.target.selectionEnd) !== null && _b !== void 0 ? _b : 0,
|
|
100
|
+
});
|
|
94
101
|
}, onBlur: (e) => {
|
|
95
102
|
onChange(getFormattedValue(e.target.value).slice(0, answerFormat.length));
|
|
96
|
-
|
|
103
|
+
setSelectionRange(null);
|
|
97
104
|
}, css: css `
|
|
98
|
-
opacity:
|
|
99
|
-
height:
|
|
105
|
+
opacity: 1;
|
|
106
|
+
height: 10;
|
|
100
107
|
` }), _jsxs(InputWrapper, { children: [_jsx("div", Object.assign({ css: css `
|
|
101
108
|
display: flex;
|
|
102
109
|
align-items: center;
|
|
@@ -116,15 +123,14 @@ export function SegmentedInput(props) {
|
|
|
116
123
|
readOnly,
|
|
117
124
|
tabIndex: -1,
|
|
118
125
|
onFocus: () => {
|
|
126
|
+
var _a;
|
|
119
127
|
if (readOnly)
|
|
120
128
|
return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(_b = hiddenRef.current) === null || _b === void 0 ? void 0 : _b.setSelectionRange(i, i);
|
|
127
|
-
}, 0);
|
|
129
|
+
setSelectionRange({
|
|
130
|
+
start: i,
|
|
131
|
+
end: i,
|
|
132
|
+
});
|
|
133
|
+
(_a = hiddenRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
128
134
|
},
|
|
129
135
|
}, value: splitedValues[i] || "", onChange: () => { } }, i))) })), _jsx(InputMarker, {})] }), _jsx(Text, { children: placeholder })] }));
|
|
130
136
|
}
|