@hrnec06/react_utils 1.6.0 → 1.7.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.
@@ -1,3 +1,4 @@
1
+ import { forwardRef, RefObject, useDeferredValue, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
1
2
  import useDebugger from "../DebuggerContext";
2
3
  import { matchPath } from "../DebuggerLogic";
3
4
  import { Terminal } from "../DebuggerTerminal";
@@ -5,46 +6,152 @@ import DebugTerminal from "./DebugTerminal";
5
6
  import ValueArray from "./ValueArray";
6
7
  import ValueBoolean from "./ValueBoolean";
7
8
  import ValueFunction from "./ValueFunction";
8
- import ValueKeyword from "./ValueKeyword";
9
9
  import ValueNumber from "./ValueNumber";
10
10
  import ValueObject from "./ValueObject";
11
11
  import ValueString from "./ValueString";
12
12
  import ValueSymbol from "./ValueSymbol";
13
+ import { Nullable } from "@hrnec06/util";
14
+ import ContextMenu from "../../ContextMenu/ContextMenu";
15
+ import ValueConstant from "./ValueConstant";
16
+ import useSyncRef from "../../../hooks/useSyncRef";
17
+ import CTXM from "../../ContextMenu/ContextMenu.types";
18
+ import clsx from "clsx";
19
+ import useDebounce from "../../../hooks/useDebounce";
20
+ import { useCTXMListener } from "../../ContextMenu/ContextMenuCtx";
13
21
 
14
- interface ParseValueProps {
22
+ export interface ParseValueRef {
23
+ copyValue: string,
24
+ name?: string,
25
+ menu?: CTXM.ContextMenuSection,
26
+ }
27
+
28
+ interface ClassifyProps {
29
+ ref: React.Ref<ParseValueRef>,
15
30
  value: unknown,
16
31
  path?: string[]
17
32
  }
18
- export default function ParseValue({
33
+ function Classify({
34
+ ref,
19
35
  value,
20
36
  path = []
21
- }: ParseValueProps) {
37
+ }: ClassifyProps) {
22
38
  const debug = useDebugger();
23
39
 
24
40
  switch (typeof value) {
25
- case 'string': return (<ValueString value={value} />);
41
+ case 'string': return (<ValueString ref={ref} value={value} />);
26
42
  case 'bigint':
27
- case 'number': return (<ValueNumber value={value} />);
28
- case 'boolean': return (<ValueBoolean value={value} />);
29
- case 'undefined': return (<ValueKeyword value={"undefined"} />);
30
- case 'function': return (<ValueFunction value={value} />);
43
+ case 'number': return (<ValueNumber ref={ref} value={value} />);
44
+ case 'boolean': return (<ValueBoolean ref={ref} value={value} />);
45
+ case 'undefined': return (<ValueConstant ref={ref} value="undefined" />)
46
+ case 'function': return (<ValueFunction ref={ref} value={value} />);
31
47
  case 'symbol': return (<ValueSymbol value={value} />)
32
48
  case 'object': {
33
49
  if (value === null)
34
- return (<ValueKeyword value={"null"} />)
50
+ return (<ValueConstant ref={ref} value={"null"} />)
35
51
 
36
52
  const isRootObject = path.length === 0;
37
53
 
38
54
  const shouldBeExpanded = (isRootObject && debug.options.openRoot) || (matchPath(path, debug.paths.open) && !matchPath(path, debug.paths.exclude));
39
55
 
40
56
  if (value instanceof Terminal)
41
- return <DebugTerminal terminal={value} path={path} defaultExpanded={shouldBeExpanded} />
57
+ return <DebugTerminal ref={ref} terminal={value} path={path} defaultExpanded={shouldBeExpanded} />
42
58
 
43
59
  if (Array.isArray(value)) {
44
- return <ValueArray value={value} path={path} defaultExpanded={shouldBeExpanded} />
60
+ return <ValueArray ref={ref} value={value} path={path} defaultExpanded={shouldBeExpanded} />
45
61
  }
46
62
 
47
- return <ValueObject value={value} path={path} defaultExpanded={shouldBeExpanded} />
63
+ return <ValueObject ref={ref} value={value} path={path} defaultExpanded={shouldBeExpanded} />
48
64
  }
49
65
  }
50
- }
66
+ }
67
+
68
+ interface ParseValueProps {
69
+ value: unknown,
70
+ path?: string[],
71
+ /**
72
+ * Children to be put before the parsed value
73
+ */
74
+ children?: React.ReactNode
75
+ }
76
+ function ParseValue({
77
+ value,
78
+ path = [],
79
+ children
80
+ }: ParseValueProps)
81
+ {
82
+ const valueRef = useRef<ParseValueRef>(null);
83
+ const [localValue, setLocalValue] = useState<Nullable<ParseValueRef>>(null);
84
+ const syncedValueRef = useSyncRef(valueRef, setLocalValue);
85
+
86
+ return (
87
+ <DebugCTXMProvider
88
+ path={path}
89
+ ref={localValue}
90
+ >
91
+ {children}
92
+
93
+ <Classify
94
+ ref={syncedValueRef}
95
+ value={value}
96
+ path={path}
97
+ />
98
+ </DebugCTXMProvider>
99
+ );
100
+ }
101
+
102
+ interface DebugCTXMProviderProps extends React.PropsWithChildren {
103
+ path: string[],
104
+ ref: Nullable<ParseValueRef>
105
+ }
106
+ export function DebugCTXMProvider({
107
+ path,
108
+ ref,
109
+ children
110
+ }: DebugCTXMProviderProps)
111
+ {
112
+ const ctxmLabel = useMemo(() => {
113
+ if (!path.length)
114
+ return "root";
115
+
116
+ return path.join('.');
117
+ }, [path]);
118
+
119
+ const handleCopy = ref === null
120
+ ? undefined
121
+ : () => {
122
+ if (!ref) return;
123
+
124
+ navigator.clipboard.writeText(ref.copyValue);
125
+ };
126
+
127
+ return (
128
+ <ContextMenu
129
+ className={clsx(
130
+ "inline align-middle h-full",
131
+ "outline-sky-500 outline-offset-2",
132
+ )}
133
+
134
+ $autoGenerateWrapper
135
+ $id={ctxmLabel}
136
+ $inherit={(menu) => ({
137
+ parent: menu.id,
138
+ children: menu.menu
139
+ })}
140
+ $menu={[
141
+ [
142
+ {
143
+ id: 'copy',
144
+ label: "Copy",
145
+ description: ref?.name,
146
+ onClick: handleCopy
147
+ },
148
+ ],
149
+ ref?.menu ?? null
150
+ ]}
151
+ >
152
+ {children}
153
+ </ContextMenu>
154
+ )
155
+ }
156
+
157
+ export default ParseValue;
@@ -1,8 +1,8 @@
1
- import { createContext, useContext, useLayoutEffect, useRef, useState } from "react";
1
+ import { createContext, forwardRef, useContext, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
2
2
  import { LogItem, LogRole, LogType, Terminal } from "../DebuggerTerminal"
3
3
  import clsx from "clsx";
4
4
  import { KeyboardButton, matchBool, matchKeyEvent, matchMouseEvent, MouseButton, Optional } from "@hrnec06/util";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { ParseValueRef } from "./DebugParser";
6
6
  import { ChevronRight, CircleAlert, SendIcon, TerminalIcon, Trash, TriangleAlert } from "lucide-react";
7
7
  import ResizeableBox from "../../ResizeableBox/ResizeableBox";
8
8
  import { Chevron_Toggle } from "../DebuggerSymbols";
@@ -28,10 +28,25 @@ interface DebugTerminalProps {
28
28
  path: string[],
29
29
  defaultExpanded?: boolean
30
30
  }
31
- export default function DebugTerminal({ terminal, path, defaultExpanded }: DebugTerminalProps)
32
- {
31
+ const DebugTerminal = forwardRef<ParseValueRef, DebugTerminalProps>(({
32
+ terminal,
33
+ path,
34
+ defaultExpanded
35
+ }, ref) => {
33
36
  const [showing, setShowing] = useState(!!defaultExpanded);
34
37
 
38
+ useImperativeHandle(ref, () => ({
39
+ name: `Terminal ${terminal.name}`,
40
+ copyValue: terminal.getLog().reduce((prev, curr) => prev + `${curr.role === LogRole.Input ? '> ' : `${LogType[curr.type]}: `}${JSON.stringify(curr.value)}` + "\n", ""),
41
+ menu: [
42
+ {
43
+ id: "terminal-clear",
44
+ label: "Clear",
45
+ onClick: () => terminal.clear(),
46
+ }
47
+ ]
48
+ }), [terminal.getLog()]);
49
+
35
50
  const handleToggle = (value: boolean) => {
36
51
  setShowing(value);
37
52
  }
@@ -60,8 +75,8 @@ export default function DebugTerminal({ terminal, path, defaultExpanded }: Debug
60
75
 
61
76
  <TerminalMain show={showing} />
62
77
  </LocalTerminalContext.Provider>
63
- )
64
- }
78
+ );
79
+ });
65
80
 
66
81
  interface TerminalMainProps {
67
82
  show: boolean
@@ -338,4 +353,6 @@ function TerminalInput()
338
353
  </button>
339
354
  </div>
340
355
  )
341
- }
356
+ }
357
+
358
+ export default DebugTerminal;
@@ -1,23 +1,37 @@
1
- import React, { useLayoutEffect, useMemo, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useState } from "react";
2
2
  import useDebugger from "../DebuggerContext";
3
3
  import { Char_Bracket, Char_Comma, Chevron_Toggle } from "../DebuggerSymbols";
4
4
  import ParseValueSimple from "./DebugParserSimple";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { ParseValueRef } from "./DebugParser";
6
6
 
7
7
  interface ValueArrayProps {
8
8
  value: unknown[],
9
9
  path: string[],
10
10
  defaultExpanded?: boolean
11
11
  }
12
- export default function ValueArray({
12
+ const ValueArray = forwardRef<ParseValueRef, ValueArrayProps>(({
13
13
  value,
14
14
  path,
15
15
  defaultExpanded = false
16
- }: ValueArrayProps) {
16
+ }, ref) => {
17
17
  const debug = useDebugger();
18
18
 
19
19
  const [expanded, setExpanded] = useState(defaultExpanded);
20
20
 
21
+ useImperativeHandle(ref, () => ({
22
+ name: `Array (${value.length})`,
23
+ copyValue: JSON.stringify(value),
24
+ menu: [
25
+ {
26
+ id: "open-toggle",
27
+ label: expanded ? "Collapse" : "Expand",
28
+ onClick: () => {
29
+ setExpanded(!expanded);
30
+ }
31
+ }
32
+ ]
33
+ }), [value, expanded]);
34
+
21
35
  const children = useMemo(() => {
22
36
  const children: React.ReactNode[] = [];
23
37
 
@@ -106,7 +120,6 @@ export default function ValueArray({
106
120
  onClick={() => setExpanded(true)}
107
121
  >
108
122
  <Char_Bracket text="[" />
109
- {/* <span className="text-zinc-500">...</span> */}
110
123
  {collapsedPreview}
111
124
  <Char_Bracket text="]" />
112
125
  </div>
@@ -114,7 +127,7 @@ export default function ValueArray({
114
127
  }
115
128
  </>
116
129
  );
117
- }
130
+ });
118
131
 
119
132
  interface CompactArrayItemProps {
120
133
  value: unknown,
@@ -140,4 +153,6 @@ function CompactArrayItem({
140
153
  {isLast && (<Char_Comma />)}
141
154
  </>
142
155
  )
143
- }
156
+ }
157
+
158
+ export default ValueArray;
@@ -1,10 +1,21 @@
1
+ import { forwardRef, useImperativeHandle } from "react"
1
2
  import ValueKeyword from "./ValueKeyword"
3
+ import { ParseValueRef } from "./DebugParser";
2
4
 
3
5
  interface ValueBooleanProps {
4
6
  value: boolean
5
7
  }
6
- export default function ValueBoolean({
8
+ const ValueBoolean = forwardRef<ParseValueRef, ValueBooleanProps>(({
7
9
  value
8
- }: ValueBooleanProps) {
9
- return <ValueKeyword value={value ? 'true' : 'false'} />
10
- }
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: "Boolean",
13
+ copyValue: value ? "true" : "false"
14
+ }), [value]);
15
+
16
+ return (
17
+ <ValueKeyword value={value ? 'true' : 'false'} />
18
+ )
19
+ });
20
+
21
+ export default ValueBoolean;
@@ -0,0 +1,21 @@
1
+ import { forwardRef, useImperativeHandle } from "react";
2
+ import ValueKeyword from "./ValueKeyword";
3
+ import { ParseValueRef } from "./DebugParser";
4
+
5
+ interface ValueConstantProps {
6
+ value: string
7
+ }
8
+ const ValueConstant = forwardRef<ParseValueRef, ValueConstantProps>(({
9
+ value
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: value,
13
+ copyValue: value
14
+ }), [value]);
15
+
16
+ return (
17
+ <ValueKeyword value={value} />
18
+ )
19
+ });
20
+
21
+ export default ValueConstant;
@@ -1,10 +1,18 @@
1
+ import { forwardRef, useImperativeHandle } from "react";
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueFunctionProps {
2
5
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3
6
  value: Function
4
7
  }
5
- export default function ValueFunction({
8
+ const ValueFunction = forwardRef<ParseValueRef, ValueFunctionProps>(({
6
9
  value
7
- }: ValueFunctionProps) {
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: "Function",
13
+ copyValue: value.toString()
14
+ }), [value]);
15
+
8
16
  const argsMatch = value.toString().match(/^[^(]*\(\s*([^)]*)\)/);
9
17
  let args: string = '';
10
18
 
@@ -12,14 +20,18 @@ export default function ValueFunction({
12
20
  args = argsMatch[1]!;
13
21
  }
14
22
 
23
+ const displayValue = `${value.name}(${args})`;
24
+
15
25
  return (
16
26
  <>
17
27
  <span className="text-[#f2824a]">
18
28
  {'ƒ '}
19
29
  </span>
20
30
  <span className="text-white">
21
- {`${value.name}(${args})`}
31
+ {displayValue}
22
32
  </span>
23
33
  </>
24
- )
25
- }
34
+ );
35
+ });
36
+
37
+ export default ValueFunction;
@@ -1,12 +1,22 @@
1
+ import { forwardRef, useImperativeHandle } from "react"
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueNumberProps {
2
5
  value: number | bigint | string
3
6
  }
4
- export default function ValueNumber({
7
+ const ValueNumber = forwardRef<ParseValueRef, ValueNumberProps>(({
5
8
  value
6
- }: ValueNumberProps) {
9
+ }, ref) => {
10
+ useImperativeHandle(ref, () => ({
11
+ name: "Number",
12
+ copyValue: value.toString()
13
+ }), [value]);
14
+
7
15
  return (
8
16
  <span className="text-[#a7ce9b]">
9
17
  {value}
10
18
  </span>
11
- )
12
- }
19
+ );
20
+ });
21
+
22
+ export default ValueNumber;
@@ -1,25 +1,41 @@
1
- import React, { useLayoutEffect, useMemo, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import useDebugger from "../DebuggerContext";
3
- import { entries, keys } from "@hrnec06/util";
3
+ import { entries, keys, Nullable } from "@hrnec06/util";
4
4
  import { Char_Bracket, Char_Colon, Char_Comma, Chevron_Toggle } from "../DebuggerSymbols";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { DebugCTXMProvider, ParseValueRef } from "./DebugParser";
6
+ import useSyncRef from "../../../hooks/useSyncRef";
7
+ import { useCTXMListener } from "../../ContextMenu/ContextMenuCtx";
6
8
 
7
9
  interface ValueObjectProps {
8
10
  value: object,
9
11
  path: string[],
10
12
  defaultExpanded?: boolean
11
13
  }
12
- export default function ValueObject({
14
+ const ValueObject = forwardRef<ParseValueRef, ValueObjectProps>(({
13
15
  value,
14
16
  path,
15
17
  defaultExpanded = false
16
- }: ValueObjectProps) {
18
+ }, ref) => {
17
19
  const debug = useDebugger();
18
20
 
19
21
  const [expanded, setExpanded] = useState(defaultExpanded);
20
22
 
21
23
  const objectEntries = entries(value);
22
24
 
25
+ useImperativeHandle(ref, () => ({
26
+ name: `Object {${objectEntries.length}}`,
27
+ copyValue: JSON.stringify(value),
28
+ menu: [
29
+ {
30
+ id: "open-toggle",
31
+ label: expanded ? "Collapse" : "Expand",
32
+ onClick: () => {
33
+ setExpanded(!expanded);
34
+ }
35
+ }
36
+ ]
37
+ }), [value, expanded]);
38
+
23
39
  const collapsedPreview = useMemo(() => {
24
40
  const children: React.ReactNode[] = [];
25
41
  const keyList = keys(value);
@@ -48,6 +64,8 @@ export default function ValueObject({
48
64
  setExpanded(state);
49
65
  }
50
66
 
67
+ const ctxmListener = useCTXMListener();
68
+
51
69
  return (
52
70
  <>
53
71
  <Chevron_Toggle expanded={expanded} onToggle={handleExpand} />
@@ -65,16 +83,13 @@ export default function ValueObject({
65
83
  {
66
84
  objectEntries.map(([key, value], ix) => {
67
85
  return (
68
- <li key={ix}>
69
- <ValueObjectKey text={key} />
70
- <Char_Colon />
71
-
72
- <ParseValue value={value} path={[...path, `${key}`]} />
73
-
74
- {ix < objectEntries.length - 1 && (
75
- <Char_Comma />
76
- )}
77
- </li>
86
+ <ValueObjectEntry
87
+ key={ix}
88
+ entryKey={key}
89
+ value={value}
90
+ path={path}
91
+ isLast={ix === objectEntries.length - 1}
92
+ />
78
93
  );
79
94
  })
80
95
  }
@@ -97,7 +112,7 @@ export default function ValueObject({
97
112
  }
98
113
  </>
99
114
  );
100
- }
115
+ });
101
116
 
102
117
  interface ValueObjectKeyProps {
103
118
  text: string,
@@ -110,4 +125,33 @@ function ValueObjectKey({
110
125
  {text}
111
126
  </span>
112
127
  )
113
- }
128
+ }
129
+
130
+ interface ValueObjectEntryProps {
131
+ entryKey: string,
132
+ value: unknown,
133
+ path: string[],
134
+ isLast: boolean
135
+ }
136
+ function ValueObjectEntry({
137
+ entryKey: key,
138
+ value,
139
+ path,
140
+ isLast
141
+ }: ValueObjectEntryProps)
142
+ {
143
+ return (
144
+ <li>
145
+ <ParseValue value={value} path={[...path, `${key}`]}>
146
+ <ValueObjectKey text={key} />
147
+ <Char_Colon />
148
+ </ParseValue>
149
+
150
+ {!isLast && (
151
+ <Char_Comma />
152
+ )}
153
+ </li>
154
+ )
155
+ }
156
+
157
+ export default ValueObject;
@@ -1,16 +1,25 @@
1
1
  import { padString } from "@hrnec06/util"
2
+ import { forwardRef, useImperativeHandle } from "react"
3
+ import { ParseValueRef } from "./DebugParser";
2
4
 
3
5
  interface ValueStringProps {
4
6
  value: string,
5
7
  noEncase?: boolean
6
8
  }
7
- export default function ValueString({
9
+ const ValueString = forwardRef<ParseValueRef, ValueStringProps>(({
8
10
  value,
9
11
  noEncase = false
10
- }: ValueStringProps) {
12
+ }, ref) => {
13
+ useImperativeHandle(ref, () => ({
14
+ name: "String",
15
+ copyValue: value
16
+ }), [value]);
17
+
11
18
  return (
12
19
  <span className="text-[#ce9178]">
13
20
  {padString(value, noEncase ? 0 : 1, '"')}
14
21
  </span>
15
- )
16
- }
22
+ );
23
+ });
24
+
25
+ export default ValueString;
@@ -3,7 +3,7 @@ import clsx from "clsx";
3
3
  import { useEffect } from "react";
4
4
  import DragAreas from "./DragAreas";
5
5
  import useSignal from "../../hooks/useSignal";
6
- import useUpdatedRef from "../../hooks/useUpdatedRef";
6
+ import useUpdatedRef from "../../hooks/useLatestRef";
7
7
  import useListener from "../../hooks/useListener";
8
8
  import { ResizeableBoxContext } from "./ResizeableBoxContext";
9
9
  import RB from "./ResizeableBox.types";
@@ -0,0 +1,26 @@
1
+ import { useCallback, useLayoutEffect, useState } from "react";
2
+
3
+ export default function useDebounce<T>(value: T, delay: number, updateOnUnmount: boolean = false): T
4
+ {
5
+ const [debounced, setValue] = useState(value);
6
+
7
+ const update = useCallback((value: T) => setValue(value), []);
8
+
9
+ useLayoutEffect(() => {
10
+ let updated = false;
11
+
12
+ const t = setTimeout(() => {
13
+ update(value);
14
+ updated = true;
15
+ }, delay);
16
+
17
+ return () => {
18
+ clearTimeout(t);
19
+
20
+ if (!updated && updateOnUnmount)
21
+ update(value);
22
+ }
23
+ }, [value]);
24
+
25
+ return debounced;
26
+ }
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react";
2
+ import useLatestRef from "./useLatestRef";
3
+
4
+ export default function useEvent<
5
+ F extends (...args: any[]) => any,
6
+ P extends any[] = Parameters<F>,
7
+ R = ReturnType<F>,
8
+ > (
9
+ cb: (...args: P) => R
10
+ )
11
+ {
12
+ const cache = useLatestRef(cb);
13
+
14
+ return useCallback((...args: P) => cache.current(...args), [cache]);
15
+ }
@@ -0,0 +1,12 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import useUpdateEffect from "./useUpdateEffect";
3
+
4
+ export default function useLatestRef<V>(value: V): React.RefObject<V> {
5
+ const ref = useRef(value);
6
+
7
+ useLayoutEffect(() => {
8
+ ref.current = value;
9
+ });
10
+
11
+ return ref;
12
+ }
@@ -0,0 +1,17 @@
1
+ import { Nullable } from '@hrnec06/util'
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
3
+ import useEvent from './useEvent';
4
+
5
+ export default function useSyncRef<T>(
6
+ ref: React.RefObject<T>,
7
+ state: (value: T) => void
8
+ )
9
+ {
10
+ const syncRefs = useEvent((value: T) => {
11
+ ref.current = value;
12
+
13
+ state(value);
14
+ });
15
+
16
+ return syncRefs;
17
+ }
@@ -1,5 +1,5 @@
1
1
  import { disposables, mapRecord, Nullable } from "@hrnec06/util";
2
- import { useLayoutEffect, useRef, useState } from "react";
2
+ import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
3
3
  import useFlags from "./useFlags";
4
4
  import useDisposables from "./useDisposables";
5
5
 
@@ -235,6 +235,7 @@ function prepareTransition(
235
235
  if (inFlight?.current)
236
236
  {
237
237
  prepare();
238
+
238
239
  return;
239
240
  }
240
241