querysub 0.2.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/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { css } from "typesafecss";
|
|
3
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
4
|
+
import { qreact } from "../4-dom/qreact";
|
|
5
|
+
import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
6
|
+
|
|
7
|
+
// TODO: Autogrow mode while typing
|
|
8
|
+
|
|
9
|
+
// NOTE: "value" is optional. If you don't pass "value", we will preserve the value.
|
|
10
|
+
// This is useful for inputs which you want to run an action on, such as "add new item",
|
|
11
|
+
// as it allows you to remove a local state value to cache the value, by just
|
|
12
|
+
// doing the add on "onChangeValue".
|
|
13
|
+
export type InputProps = (
|
|
14
|
+
preact.JSX.HTMLAttributes<HTMLInputElement>
|
|
15
|
+
& {
|
|
16
|
+
flavor?: "large" | "small";
|
|
17
|
+
focusOnMount?: boolean;
|
|
18
|
+
textarea?: boolean;
|
|
19
|
+
/** Update on key stroke, not on blur (just does onInput = onChange, as onInput already does this) */
|
|
20
|
+
hot?: boolean;
|
|
21
|
+
/** Updates arrow keys with modifier behavior to use larger numbers, instead of decimals. */
|
|
22
|
+
integer?: boolean;
|
|
23
|
+
inputRef?: (x: HTMLInputElement | null) => void;
|
|
24
|
+
/** Don't blur on enter key */
|
|
25
|
+
noEnterKeyBlur?: boolean;
|
|
26
|
+
noFocusSelect?: boolean;
|
|
27
|
+
inputKey?: string;
|
|
28
|
+
fillWidth?: boolean;
|
|
29
|
+
|
|
30
|
+
/** By default, we don't accept value updates when focused. This prevents the user's
|
|
31
|
+
* typing from being interrupted by a value update. However, this flag will
|
|
32
|
+
* stop that behavior, allowing the input to update at any time
|
|
33
|
+
* - This can be useful when the value is intentionally being updated by code,
|
|
34
|
+
* while the input is focused.
|
|
35
|
+
*/
|
|
36
|
+
alwaysUseLatestValueWhenFocused?: boolean;
|
|
37
|
+
|
|
38
|
+
// NOTE: onChangeValue is called onChange, which is generally on blur.
|
|
39
|
+
// NOTE: We also trigger onChange (and onChangeValue) whenever
|
|
40
|
+
// e.ctrlKey && (e.code.startsWith("Key") || e.code === "Enter") || e.code === "Enter" && e.shiftKey
|
|
41
|
+
// This is because ctrl usually means a hotkey, and hotkeys usually want committed values.
|
|
42
|
+
onChangeValue?: (value: string) => void;
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export class Input extends qreact.Component<InputProps> {
|
|
47
|
+
state = {};
|
|
48
|
+
onFocusText = "";
|
|
49
|
+
firstFocus = true;
|
|
50
|
+
|
|
51
|
+
elem: HTMLInputElement | null = null;
|
|
52
|
+
lastValue: unknown = null;
|
|
53
|
+
lastChecked: unknown = null;
|
|
54
|
+
|
|
55
|
+
render() {
|
|
56
|
+
let flavorOverrides: preact.JSX.CSSProperties = {};
|
|
57
|
+
const { flavor, textarea, hot, inputKey, fillWidth, ...nativeProps } = this.props;
|
|
58
|
+
let props = { ...nativeProps };
|
|
59
|
+
// Convert undefined to "" (unless it is really a checked component)
|
|
60
|
+
if (!("checked" in props) && props.value === undefined) {
|
|
61
|
+
props.value = "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (props.onChangeValue) {
|
|
65
|
+
let baseOnChange = props.onChange;
|
|
66
|
+
props.onChange = e => {
|
|
67
|
+
baseOnChange?.(e);
|
|
68
|
+
props.onChangeValue?.(e.currentTarget.value);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (flavor === "large") {
|
|
72
|
+
flavorOverrides = {
|
|
73
|
+
fontSize: 18,
|
|
74
|
+
padding: "10px 15px",
|
|
75
|
+
};
|
|
76
|
+
if (props.type === "checkbox") {
|
|
77
|
+
flavorOverrides.width = 16;
|
|
78
|
+
flavorOverrides.height = 16;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (flavor === "small") {
|
|
82
|
+
flavorOverrides = {
|
|
83
|
+
fontSize: 12,
|
|
84
|
+
padding: "5px 10px",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let fontSize = flavorOverrides.fontSize;
|
|
89
|
+
delete flavorOverrides.fontSize;
|
|
90
|
+
|
|
91
|
+
let attributes: preact.JSX.HTMLAttributes<HTMLInputElement> = {
|
|
92
|
+
...nativeProps,
|
|
93
|
+
key: inputKey || "input",
|
|
94
|
+
ref: x => {
|
|
95
|
+
if (x) {
|
|
96
|
+
this.elem = x;
|
|
97
|
+
}
|
|
98
|
+
if (x && props.focusOnMount && this.firstFocus) {
|
|
99
|
+
this.firstFocus = false;
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
x.focus();
|
|
102
|
+
// I'm not sure why, but this initial focus isn't trigger onFocus, so we have to
|
|
103
|
+
// trigger it ourselves
|
|
104
|
+
if (!props.noFocusSelect) {
|
|
105
|
+
x.select();
|
|
106
|
+
}
|
|
107
|
+
}, 0);
|
|
108
|
+
}
|
|
109
|
+
let ref = props.inputRef || props.ref;
|
|
110
|
+
if (typeof ref === "function") {
|
|
111
|
+
ref(x);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
class: undefined,
|
|
115
|
+
className: (
|
|
116
|
+
(props.className || props.class || " ")
|
|
117
|
+
+ css.border("1px solid hsl(0, 0%, 50%)", "soft")
|
|
118
|
+
.background("hsl(0, 0%, 7%)", "soft")
|
|
119
|
+
.color("hsl(0, 0%, 95%)", "soft")
|
|
120
|
+
.display("flex", "soft")
|
|
121
|
+
.outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
|
|
122
|
+
+ (fillWidth && css.fillWidth)
|
|
123
|
+
+ (fontSize && css.fontSize(fontSize, "soft") || "")
|
|
124
|
+
),
|
|
125
|
+
style: {
|
|
126
|
+
...flavorOverrides,
|
|
127
|
+
...props.style as any,
|
|
128
|
+
},
|
|
129
|
+
onFocus: e => {
|
|
130
|
+
this.onFocusText = e.currentTarget.value;
|
|
131
|
+
if (!props.noFocusSelect) {
|
|
132
|
+
e.currentTarget.select();
|
|
133
|
+
}
|
|
134
|
+
props.onFocus?.(e);
|
|
135
|
+
},
|
|
136
|
+
onBlur: e => {
|
|
137
|
+
props.onBlur?.(e);
|
|
138
|
+
// NOTE: We were previous trying to fix a bug where the prop value would update
|
|
139
|
+
// before blur. However... most of the time it updates after blur, and resetting
|
|
140
|
+
// this just results in the blur callback being given a stale prop, which breaks things
|
|
141
|
+
//e.currentTarget.value = String(this.props.value ?? "");
|
|
142
|
+
if (
|
|
143
|
+
e.currentTarget.value === this.lastValue
|
|
144
|
+
&& e.currentTarget.checked === this.lastChecked
|
|
145
|
+
// If we are in a resync loop the values SHOULD be the same, so don't return
|
|
146
|
+
&& !proxyWatcher.getTriggeredWatcherMaybeUndefined()?.countSinceLastFullSync
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Only set our values on the first trigger, not within the resync loop
|
|
151
|
+
if (!proxyWatcher.getTriggeredWatcherMaybeUndefined()?.countSinceLastFullSync) {
|
|
152
|
+
this.lastValue = e.currentTarget.value;
|
|
153
|
+
this.lastChecked = e.currentTarget.checked;
|
|
154
|
+
}
|
|
155
|
+
let callback = props.onInput || props.onChange;
|
|
156
|
+
callback?.(e);
|
|
157
|
+
},
|
|
158
|
+
onChange: e => {
|
|
159
|
+
if (
|
|
160
|
+
e.currentTarget.value === this.lastValue
|
|
161
|
+
&& e.currentTarget.checked === this.lastChecked
|
|
162
|
+
&& !proxyWatcher.getTriggeredWatcherMaybeUndefined()?.countSinceLastFullSync
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!proxyWatcher.getTriggeredWatcherMaybeUndefined()?.countSinceLastFullSync) {
|
|
167
|
+
this.lastValue = e.currentTarget.value;
|
|
168
|
+
this.lastChecked = e.currentTarget.checked;
|
|
169
|
+
}
|
|
170
|
+
props.onChange?.(e);
|
|
171
|
+
},
|
|
172
|
+
onKeyDown: e => {
|
|
173
|
+
if (e.defaultPrevented) return;
|
|
174
|
+
if (textarea && e.code === "Tab") {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
// Inject 4 spaces into the current position
|
|
177
|
+
let elem = e.currentTarget;
|
|
178
|
+
let value = elem.value;
|
|
179
|
+
let start = elem.selectionStart ?? elem.value.length;
|
|
180
|
+
let end = elem.selectionEnd ?? elem.value.length;
|
|
181
|
+
elem.value = value.slice(0, start) + " " + value.slice(end);
|
|
182
|
+
elem.selectionStart = elem.selectionEnd = start + 4;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
props.onKeyDown?.(e);
|
|
186
|
+
if (e.code === "Enter" && e.ctrlKey) {
|
|
187
|
+
e.currentTarget.blur();
|
|
188
|
+
}
|
|
189
|
+
if (this.elem && props.type === "number") {
|
|
190
|
+
let delta = 0;
|
|
191
|
+
let magnitude = 1;
|
|
192
|
+
if (e.shiftKey) {
|
|
193
|
+
if (props.integer) {
|
|
194
|
+
magnitude = 10;
|
|
195
|
+
} else {
|
|
196
|
+
magnitude = 0.1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (e.code === "ArrowUp") {
|
|
200
|
+
delta = magnitude;
|
|
201
|
+
} else if (e.code === "ArrowDown") {
|
|
202
|
+
delta = -magnitude;
|
|
203
|
+
}
|
|
204
|
+
if (delta !== 0) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
let newValue = Math.round(((+this.elem.value || 0) + delta) * 100) / 100;
|
|
207
|
+
e.currentTarget.value = newValue.toString();
|
|
208
|
+
let callback = props.onInput;
|
|
209
|
+
if (!callback && hot) {
|
|
210
|
+
callback = props.onChange;
|
|
211
|
+
}
|
|
212
|
+
callback?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let { noEnterKeyBlur, onInput, onChange } = props;
|
|
216
|
+
// Detach from the synced function, to prevent double calls. This is important, as apparently .blur()
|
|
217
|
+
// synchronously triggers onChange, BUT, only if the input is changing the first time. Which means
|
|
218
|
+
// if this function reruns, it won't trigger the change again. Detaching it causes any triggered
|
|
219
|
+
// functions to become root synced functions, which will allow them to correctly run the sync loop.
|
|
220
|
+
void Promise.resolve().finally(() => {
|
|
221
|
+
if (e.code === "Escape") {
|
|
222
|
+
let changed = e.currentTarget.value !== this.onFocusText;
|
|
223
|
+
e.currentTarget.value = this.onFocusText;
|
|
224
|
+
if (onInput) {
|
|
225
|
+
Querysub.serviceWriteDetached(() => {
|
|
226
|
+
onInput?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
|
|
227
|
+
});
|
|
228
|
+
} else if (changed) {
|
|
229
|
+
if (onChange) {
|
|
230
|
+
Querysub.serviceWriteDetached(() => {
|
|
231
|
+
onChange?.(e);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
e.currentTarget.blur();
|
|
236
|
+
}
|
|
237
|
+
if (!noEnterKeyBlur && e.code === "Enter" && (!textarea || e.shiftKey || e.ctrlKey)) {
|
|
238
|
+
e.currentTarget.blur();
|
|
239
|
+
} else if (
|
|
240
|
+
e.ctrlKey && (
|
|
241
|
+
// No need to commit on on paste or cut. If they want to commit, they can commit like they usually
|
|
242
|
+
// do. This shortcut only marginally affects a few workflows, and has the potential to break many others,
|
|
243
|
+
// so... let's not.
|
|
244
|
+
// // Pasting and cutting might not mean commit, but... they probably mean for it to...
|
|
245
|
+
// e.code === "KeyV" ||
|
|
246
|
+
// e.code === "KeyX" ||
|
|
247
|
+
|
|
248
|
+
// Ctrl+enter means "commit"
|
|
249
|
+
e.code === "Enter"
|
|
250
|
+
)
|
|
251
|
+
// Shift+enter means "commit"
|
|
252
|
+
|| e.shiftKey && e.code === "Enter"
|
|
253
|
+
) {
|
|
254
|
+
Querysub.serviceWriteDetached(() => {
|
|
255
|
+
onChange?.(e);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if ("checked" in props) {
|
|
263
|
+
{
|
|
264
|
+
this.lastChecked = !!props.checked;
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
let elem = this.elem;
|
|
268
|
+
let newValue = props.value;
|
|
269
|
+
if (elem && this.elem === document.activeElement && !this.props.alwaysUseLatestValueWhenFocused) {
|
|
270
|
+
newValue = elem.value;
|
|
271
|
+
}
|
|
272
|
+
attributes.value = newValue;
|
|
273
|
+
{
|
|
274
|
+
this.lastValue = String(props.value);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if (attributes.type === "number") {
|
|
280
|
+
// Fix stuff like 55.00000000000001
|
|
281
|
+
let value = attributes.value;
|
|
282
|
+
if (typeof value === "number") {
|
|
283
|
+
value = niceNumberStringify(value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// We do number handling ourselves
|
|
288
|
+
if (attributes["type"] === "number") {
|
|
289
|
+
delete attributes["type"];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (hot) {
|
|
293
|
+
attributes.onInput = props.onChange;
|
|
294
|
+
}
|
|
295
|
+
if (textarea) {
|
|
296
|
+
return <textarea {...attributes as any} />;
|
|
297
|
+
} else {
|
|
298
|
+
return <input {...attributes} />;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
function niceNumberStringify(valueIn: number) {
|
|
305
|
+
if (Math.abs(valueIn) < 0.0000000001) {
|
|
306
|
+
return "0";
|
|
307
|
+
}
|
|
308
|
+
let value = valueIn.toString();
|
|
309
|
+
// TODO: Do this MUCH better...
|
|
310
|
+
if (value.slice(0, -1).endsWith("00000000000")) {
|
|
311
|
+
value = value.slice(0, -1);
|
|
312
|
+
while (value.endsWith("0")) {
|
|
313
|
+
value = value.slice(0, -1);
|
|
314
|
+
}
|
|
315
|
+
if (value.endsWith(".")) {
|
|
316
|
+
value = value.slice(0, -1);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
if (value.slice(0, -1).endsWith("9999999999")) {
|
|
321
|
+
value = value.slice(0, -1);
|
|
322
|
+
while (value.endsWith("9")) {
|
|
323
|
+
value = value.slice(0, -1);
|
|
324
|
+
}
|
|
325
|
+
if (value.endsWith(".")) {
|
|
326
|
+
value = value.slice(0, -1);
|
|
327
|
+
}
|
|
328
|
+
// NOTE: Interestingly enough... because we remove all trailing 9s, it means if the last number is not 9,
|
|
329
|
+
// so... we can do this hack to round up
|
|
330
|
+
value = value.slice(0, -1) + (parseInt(value.slice(-1)) + 1);
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { Input, InputProps } from "./Input";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
import { URLParam } from "./URLParam";
|
|
5
|
+
import { lazy } from "socket-function/src/caching";
|
|
6
|
+
import { setCSS } from "../4-dom/css";
|
|
7
|
+
import { qreact } from "../4-dom/qreact";
|
|
8
|
+
import { formatDateTime } from "socket-function/src/formatting/format";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export type InputLabelProps = Omit<InputProps, "label" | "title"> & {
|
|
12
|
+
label?: preact.ComponentChild;
|
|
13
|
+
number?: boolean;
|
|
14
|
+
/** A number, AND, an integer. Changes behavior arrow arrow keys as well */
|
|
15
|
+
integer?: boolean;
|
|
16
|
+
checkbox?: boolean;
|
|
17
|
+
isDatetime?: boolean;
|
|
18
|
+
// Show text and a pencil, only showing the input on click
|
|
19
|
+
edit?: boolean;
|
|
20
|
+
editValue?: preact.ComponentChild;
|
|
21
|
+
outerClass?: string;
|
|
22
|
+
maxDecimals?: number;
|
|
23
|
+
percent?: boolean;
|
|
24
|
+
editClass?: string;
|
|
25
|
+
|
|
26
|
+
fontSize?: number;
|
|
27
|
+
|
|
28
|
+
tooltip?: string;
|
|
29
|
+
|
|
30
|
+
fillWidth?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function roundToDecimals(value: number, decimals: number) {
|
|
34
|
+
return Math.round(value * 10 ** decimals) / 10 ** decimals;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setCSS(module, `
|
|
38
|
+
.trigger-hover:not(:hover) .show-on-hover {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
|
|
43
|
+
export class InputLabel extends qreact.Component<InputLabelProps> {
|
|
44
|
+
state = { editting: false };
|
|
45
|
+
render() {
|
|
46
|
+
let props = { ...this.props };
|
|
47
|
+
let label = props.label || this.props.children;
|
|
48
|
+
(this.props as any).title = this.props.tooltip;
|
|
49
|
+
if (props.fontSize !== undefined) {
|
|
50
|
+
props.style = { ...props.style as any, fontSize: props.fontSize };
|
|
51
|
+
}
|
|
52
|
+
if (props.integer) {
|
|
53
|
+
props.number = true;
|
|
54
|
+
}
|
|
55
|
+
if (props.number) {
|
|
56
|
+
props.type = "number";
|
|
57
|
+
}
|
|
58
|
+
if (props.checkbox) {
|
|
59
|
+
props.type = "checkbox";
|
|
60
|
+
}
|
|
61
|
+
if (props.percent) {
|
|
62
|
+
props.value = (Number(props.value) || 0) * 100;
|
|
63
|
+
const baseOnChange = props.onChange;
|
|
64
|
+
if (baseOnChange) {
|
|
65
|
+
props.onChange = e => {
|
|
66
|
+
let value = String(+e.currentTarget.value / 100);
|
|
67
|
+
baseOnChange({ currentTarget: { value } } as any);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
props.maxDecimals = props.maxDecimals ?? 2;
|
|
71
|
+
props.number = props.number ?? true;
|
|
72
|
+
props.type = "number";
|
|
73
|
+
}
|
|
74
|
+
if (props.isDatetime) {
|
|
75
|
+
function formatDateTimeForInput(value: number) {
|
|
76
|
+
value = Number(value) || 0;
|
|
77
|
+
value -= new Date(value).getTimezoneOffset() * 60 * 1000;
|
|
78
|
+
return new Date(value).toISOString().slice(0, -1);
|
|
79
|
+
}
|
|
80
|
+
let time = Number(props.value || 0);
|
|
81
|
+
props.value = formatDateTimeForInput(time);
|
|
82
|
+
props.editValue = formatDateTime(time);
|
|
83
|
+
props.type = "datetime-local";
|
|
84
|
+
const baseOnChangeValue = props.onChangeValue;
|
|
85
|
+
if (baseOnChangeValue) {
|
|
86
|
+
props.onChangeValue = newValue => {
|
|
87
|
+
newValue = new Date(newValue).getTime() as any;
|
|
88
|
+
baseOnChangeValue(newValue);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const baseOnChange = props.onChange;
|
|
92
|
+
if (baseOnChange) {
|
|
93
|
+
props.onChange = e => {
|
|
94
|
+
let value = new Date(e.currentTarget.value).getTime();
|
|
95
|
+
baseOnChange({ currentTarget: { value } } as any);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// NOTE: Hook up onChanges with onBlur, as datetime-local triggers
|
|
100
|
+
// changes more frequently than regular inputs.
|
|
101
|
+
if (!props.hot) {
|
|
102
|
+
let onChangeValue = props.onChangeValue;
|
|
103
|
+
let onChange = props.onChange;
|
|
104
|
+
props.onBlur = (e) => {
|
|
105
|
+
onChangeValue?.(e.currentTarget.value);
|
|
106
|
+
onChange?.(e);
|
|
107
|
+
};
|
|
108
|
+
props.onChangeValue = undefined;
|
|
109
|
+
props.onChange = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let maxDecimals = props.maxDecimals;
|
|
113
|
+
if (typeof maxDecimals === "number") {
|
|
114
|
+
props.value = roundToDecimals(Number(props.value), maxDecimals);
|
|
115
|
+
}
|
|
116
|
+
let style = { ...props.style as any };
|
|
117
|
+
if (props.type === "number") {
|
|
118
|
+
let fontSize = props.fontSize ?? 12;
|
|
119
|
+
style.width = 40 + String(props.value).length * (fontSize * 0.75);
|
|
120
|
+
}
|
|
121
|
+
let onClick: (() => void) | undefined;
|
|
122
|
+
if (props.edit) {
|
|
123
|
+
let baseBlur = props.onBlur;
|
|
124
|
+
props.onBlur = e => {
|
|
125
|
+
this.state.editting = false;
|
|
126
|
+
baseBlur?.(e);
|
|
127
|
+
};
|
|
128
|
+
onClick = () => {
|
|
129
|
+
this.state.editting = true;
|
|
130
|
+
};
|
|
131
|
+
// Default focus on mount, as editting it is the only reason to click on it.
|
|
132
|
+
props.focusOnMount = props.focusOnMount ?? true;
|
|
133
|
+
}
|
|
134
|
+
let input = <Input
|
|
135
|
+
{...props}
|
|
136
|
+
label={String(label)}
|
|
137
|
+
style={style}
|
|
138
|
+
/>;
|
|
139
|
+
if (props.edit && !this.state.editting) {
|
|
140
|
+
input = <span class={css.hbox(2) + " trigger-hover"}>
|
|
141
|
+
<span class={props.editClass}>
|
|
142
|
+
{props.editValue ?? props.value}
|
|
143
|
+
</span>
|
|
144
|
+
<span class={css.opacity(0.1).opacity(1, "hover")}>
|
|
145
|
+
{pencilSVG()}
|
|
146
|
+
</span>
|
|
147
|
+
</span>;
|
|
148
|
+
}
|
|
149
|
+
return (
|
|
150
|
+
<label onClick={onClick} class={
|
|
151
|
+
css.hbox(8).button.relative
|
|
152
|
+
+ " trigger-hover "
|
|
153
|
+
+ props.outerClass
|
|
154
|
+
+ (props.flavor === "large" && css.fontSize(18, "soft"))
|
|
155
|
+
+ (props.fillWidth && css.fillWidth)
|
|
156
|
+
}>
|
|
157
|
+
<div
|
|
158
|
+
class={
|
|
159
|
+
"show-on-hover "
|
|
160
|
+
+ css.hsla(0, 0, 0, 0.2)
|
|
161
|
+
.absolute.pos(-8, -2).size("calc(100% + 16px)" as "100%", "calc(100% + 4px)" as "100%")
|
|
162
|
+
.zIndex(-1)
|
|
163
|
+
.pointerEvents("none")
|
|
164
|
+
}
|
|
165
|
+
/>
|
|
166
|
+
{label && <span class={css.fontWeight("bold").flexShrink0}>{label}</span>}
|
|
167
|
+
{input}
|
|
168
|
+
</label>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pencilSVG = lazy(() => {
|
|
174
|
+
const src = "data:image/svg+xml;base64," + Buffer.from(`
|
|
175
|
+
<svg width="24" height="24" viewBox="-1 1 23 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
176
|
+
<path d="M5.98012 19.3734L8.60809 18.7164C8.62428 18.7124 8.64043 18.7084 8.65654 18.7044C8.87531 18.65 9.08562 18.5978 9.27707 18.4894C9.46852 18.381 9.62153 18.2275 9.7807 18.0679C9.79242 18.0561 9.80418 18.0444 9.81598 18.0325L17.0101 10.8385L17.0101 10.8385L17.0369 10.8117C17.3472 10.5014 17.6215 10.2272 17.8128 9.97638C18.0202 9.70457 18.1858 9.39104 18.1858 9C18.1858 8.60896 18.0202 8.29543 17.8128 8.02361C17.6215 7.77285 17.3472 7.49863 17.0369 7.18835L17.01 7.16152L16.8385 6.98995L16.8117 6.96314C16.5014 6.6528 16.2272 6.37853 15.9764 6.1872C15.7046 5.97981 15.391 5.81421 15 5.81421C14.609 5.81421 14.2954 5.97981 14.0236 6.1872C13.7729 6.37853 13.4986 6.65278 13.1884 6.96311L13.1615 6.98995L5.96745 14.184C5.95565 14.1958 5.94386 14.2076 5.93211 14.2193C5.77249 14.3785 5.61904 14.5315 5.51064 14.7229C5.40225 14.9144 5.34999 15.1247 5.29562 15.3435C5.29162 15.3596 5.28761 15.3757 5.28356 15.3919L4.62003 18.046C4.61762 18.0557 4.61518 18.0654 4.61272 18.0752C4.57411 18.2293 4.53044 18.4035 4.51593 18.5518C4.49978 18.7169 4.50127 19.0162 4.74255 19.2574C4.98383 19.4987 5.28307 19.5002 5.44819 19.4841C5.59646 19.4696 5.77072 19.4259 5.92479 19.3873C5.9346 19.3848 5.94433 19.3824 5.95396 19.38L5.95397 19.38L5.9801 19.3734L5.98012 19.3734Z" stroke="#33363F" stroke-width="1.2" fill="hsl(330, 50%, 60%)" />
|
|
177
|
+
<path d="M12.5 7.5L5.92819 14.0718C5.71566 14.2843 5.60939 14.3906 5.53953 14.5212C5.46966 14.6517 5.44019 14.7991 5.38124 15.0938L4.64709 18.7646C4.58057 19.0972 4.5473 19.2635 4.64191 19.3581C4.73652 19.4527 4.90283 19.4194 5.23544 19.3529L8.90621 18.6188C9.20093 18.5598 9.3483 18.5303 9.47885 18.4605C9.60939 18.3906 9.71566 18.2843 9.92819 18.0718L16.5 11.5L12.5 7.5Z" fill="hsl(45, 100%, 50%)" />
|
|
178
|
+
<path d="M12.5 7.5L16.5 11.5" stroke="#33363F" stroke-width="1.2" />
|
|
179
|
+
</svg>
|
|
180
|
+
`).toString("base64");
|
|
181
|
+
return <img draggable={false} src={src} />;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export class InputLabelURL extends qreact.Component<InputLabelProps & {
|
|
185
|
+
url: URLParam;
|
|
186
|
+
valueDefault?: unknown;
|
|
187
|
+
}> {
|
|
188
|
+
render() {
|
|
189
|
+
let props = { ...this.props };
|
|
190
|
+
if (props.type === "number" || props.number) {
|
|
191
|
+
return <InputLabel {...props} value={Number(props.url.value ?? this.props.valueDefault)} onChange={e => { props.url.value = +e.currentTarget.value; props.onChange?.(e); }} />;
|
|
192
|
+
} else if (props.type === "checkbox" || this.props.checkbox) {
|
|
193
|
+
return <InputLabel {...props} checked={Boolean(props.url.value ?? this.props.valueDefault)} onChange={e => { props.url.value = e.currentTarget.checked; props.onChange?.(e); }} />;
|
|
194
|
+
} else {
|
|
195
|
+
return <InputLabel {...props} value={String(props.url.value ?? this.props.valueDefault)} onChange={e => { props.url.value = e.currentTarget.value; props.onChange?.(e); }} />;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import preact from "preact"; import { qreact } from "../../src/4-dom/qreact";
|
|
2
|
+
import { Button } from "../../src/library-components/Button";
|
|
3
|
+
import { Input } from "../../src/library-components/Input";
|
|
4
|
+
import { isDefined } from "../../src/misc";
|
|
5
|
+
import { sort } from "socket-function/src/misc";
|
|
6
|
+
import { css } from "typesafecss";
|
|
7
|
+
import { Querysub } from "../../src/4-querysub/Querysub";
|
|
8
|
+
import { greenButton, yellowButton } from "./colors";
|
|
9
|
+
|
|
10
|
+
export type InputOption<T> = {
|
|
11
|
+
value: T;
|
|
12
|
+
// Defaults to String(value)
|
|
13
|
+
label?: preact.ComponentChild;
|
|
14
|
+
// Defaults to typeof label === "string" ? label : String(value)
|
|
15
|
+
matchText?: string;
|
|
16
|
+
};
|
|
17
|
+
export type FullInputOption<T> = {
|
|
18
|
+
value: T;
|
|
19
|
+
label: preact.ComponentChild;
|
|
20
|
+
matchText: string;
|
|
21
|
+
};
|
|
22
|
+
/** Allow selecting multiple items from a set of values. */
|
|
23
|
+
export class InputPicker<T> extends qreact.Component<{
|
|
24
|
+
label?: preact.ComponentChild;
|
|
25
|
+
picked: T[];
|
|
26
|
+
options: InputOption<T>[];
|
|
27
|
+
addPicked: (value: T) => void;
|
|
28
|
+
removePicked: (value: T) => void;
|
|
29
|
+
allowNonOptions?: boolean;
|
|
30
|
+
}> {
|
|
31
|
+
state = {
|
|
32
|
+
pendingText: "",
|
|
33
|
+
focused: false,
|
|
34
|
+
};
|
|
35
|
+
render() {
|
|
36
|
+
// Input, and beside it the picked values
|
|
37
|
+
let resolvedOptions = this.props.options.map(option => {
|
|
38
|
+
let value = option.value;
|
|
39
|
+
let label = option.label ?? String(value);
|
|
40
|
+
let matchText = option.matchText ?? (typeof label === "string" ? label : String(value));
|
|
41
|
+
return { value, label, matchText };
|
|
42
|
+
});
|
|
43
|
+
let optionLookup = new Map(resolvedOptions.map((option) => [option.value, option]));
|
|
44
|
+
let pickedOptions = this.props.picked.map(x => optionLookup.get(x) || { value: x, label: String(x), matchText: String(x) });
|
|
45
|
+
let pendingMatches: FullInputOption<T>[] = [];
|
|
46
|
+
let pendingTextFull = this.state.pendingText;
|
|
47
|
+
let pendingText = pendingTextFull.trim().toLowerCase();
|
|
48
|
+
if (pendingText) {
|
|
49
|
+
pendingMatches = resolvedOptions.filter(option => option.matchText.toLowerCase().includes(pendingText));
|
|
50
|
+
sort(pendingMatches, x =>
|
|
51
|
+
x.matchText.startsWith(pendingTextFull) && -10
|
|
52
|
+
|| x.matchText.startsWith(pendingText) && -9
|
|
53
|
+
|| x.matchText.toLowerCase().startsWith(pendingTextFull) && -8
|
|
54
|
+
|| x.matchText.toLowerCase().startsWith(pendingText) && -7
|
|
55
|
+
|| x.matchText.length
|
|
56
|
+
);
|
|
57
|
+
} else if (this.state.focused) {
|
|
58
|
+
pendingMatches = resolvedOptions;
|
|
59
|
+
}
|
|
60
|
+
let extra = pendingMatches.length;
|
|
61
|
+
pendingMatches = pendingMatches.slice(0, 10);
|
|
62
|
+
extra -= pendingMatches.length;
|
|
63
|
+
return (
|
|
64
|
+
<div class={css.hbox(10).alignItems("start")}>
|
|
65
|
+
{this.props.label}
|
|
66
|
+
<Input
|
|
67
|
+
value={this.state.pendingText}
|
|
68
|
+
hot
|
|
69
|
+
alwaysUseLatestValueWhenFocused
|
|
70
|
+
onChangeValue={(x) => this.state.pendingText = x}
|
|
71
|
+
onFocus={() => this.state.focused = true}
|
|
72
|
+
onBlur={() => {
|
|
73
|
+
this.state.focused = false;
|
|
74
|
+
this.state.pendingText = "";
|
|
75
|
+
}}
|
|
76
|
+
onKeyDown={e => {
|
|
77
|
+
// On tab, add first in pendingMatches
|
|
78
|
+
if (e.key === "Tab") {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
if (pendingMatches.length > 0) {
|
|
81
|
+
this.props.addPicked(pendingMatches[0].value);
|
|
82
|
+
this.state.pendingText = "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
{pendingMatches.length > 0 && (
|
|
88
|
+
<div class={css.hbox(4).wrap}>
|
|
89
|
+
{pendingMatches.map((option) => (
|
|
90
|
+
<Button
|
|
91
|
+
key={`add-${option.matchText}`}
|
|
92
|
+
class={css.hbox(5).button + greenButton}
|
|
93
|
+
// On mouse down, so we can add picked BEFORE we blur (otherwise
|
|
94
|
+
// this button disappears before it can be clicked)
|
|
95
|
+
onMouseDown={() => {
|
|
96
|
+
this.props.addPicked(option.value);
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
+ {option.label}
|
|
100
|
+
</Button>
|
|
101
|
+
))}
|
|
102
|
+
{extra > 0 && (
|
|
103
|
+
<Button class={css.hbox(5).button} disabled>
|
|
104
|
+
+ {extra} more...
|
|
105
|
+
</Button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
<div class={css.hbox(4).wrap}>
|
|
110
|
+
{pickedOptions.map((option) => (
|
|
111
|
+
<Button
|
|
112
|
+
key={`remove-${option.matchText}`}
|
|
113
|
+
class={css.hbox(5).button + yellowButton}
|
|
114
|
+
onMouseDown={() => {
|
|
115
|
+
this.props.removePicked(option.value);
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
- {option.label}
|
|
119
|
+
</Button>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|