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,2322 @@
|
|
|
1
|
+
import type preact from "preact";
|
|
2
|
+
import { isNode, sort } from "socket-function/src/misc";
|
|
3
|
+
import { isDeploy } from "../3-path-functions/deployCheck";
|
|
4
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
5
|
+
import { PermissionsCheck } from "../4-querysub/permissions";
|
|
6
|
+
import { ClientWatcher, clientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
7
|
+
import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
|
|
8
|
+
import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
|
|
9
|
+
import { SyncWatcher, atomic, atomicObjectWrite, atomicObjectWriteNoFreeze, doProxyOptions, doUnatomicWrites, proxyWatcher, registerSchemaPrefix, unregisterSchemaPrefix } from "../2-proxy/PathValueProxyWatcher";
|
|
10
|
+
import { createPathValueProxy, getProxyPath, isValueProxy, isValueProxy2 } from "../2-proxy/pathValueProxy";
|
|
11
|
+
import { measureBlock, measureCode, measureFnc } from "socket-function/src/profiling/measure";
|
|
12
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
13
|
+
import { errorify, logErrors } from "../errors";
|
|
14
|
+
import { cache, lazy } from "socket-function/src/caching";
|
|
15
|
+
import { getPathStr1, getPathIndexAssert } from "../path";
|
|
16
|
+
import { blue, green, red, yellow } from "socket-function/src/formatting/logColors";
|
|
17
|
+
import { heapTagObj } from "../diagnostics/heapTag";
|
|
18
|
+
import { onHotReload } from "socket-function/hot/HotReloadController";
|
|
19
|
+
import { isClient } from "../config2";
|
|
20
|
+
import { formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
21
|
+
import { lazyImport } from "socket-function/require/RequireController";
|
|
22
|
+
import { Schema2Fncs } from "../2-proxy/schema2";
|
|
23
|
+
|
|
24
|
+
lazyImport(() => import("../5-diagnostics/qreactDebug"));
|
|
25
|
+
|
|
26
|
+
// TIMING:
|
|
27
|
+
// Worst case (trivial child Components), about 20X slower than preact (12.5X with disableMeasurements)
|
|
28
|
+
// Realistically (div, prop, image child Components), about 8X slower than preact (4.3X with disableMeasurements)
|
|
29
|
+
// In practice the page will probably start to lag from the large DOM/layout state
|
|
30
|
+
// before it lags due to the rendering speed.
|
|
31
|
+
|
|
32
|
+
const idSymbol = Symbol("id");
|
|
33
|
+
|
|
34
|
+
interface QComponent {
|
|
35
|
+
id: number;
|
|
36
|
+
parentComponentId?: number;
|
|
37
|
+
|
|
38
|
+
props: { [key: string]: unknown; };
|
|
39
|
+
state: { [key: string]: unknown; };
|
|
40
|
+
vNodeForRender: { value: VirtualDOM } | undefined;
|
|
41
|
+
vNodeForMount: VirtualDOM | undefined;
|
|
42
|
+
newPropSeqNum: number;
|
|
43
|
+
|
|
44
|
+
rootDOMNodes: DOMNode[] | undefined;
|
|
45
|
+
|
|
46
|
+
isParentSVG?: boolean;
|
|
47
|
+
|
|
48
|
+
inlineState: {
|
|
49
|
+
[id: number]: {
|
|
50
|
+
// NOTE: No props, as the child can't re-render independently,
|
|
51
|
+
// so even if the props don't change / aren't accessed,
|
|
52
|
+
// the child will still have to re-render.
|
|
53
|
+
state: { [key: string]: unknown; };
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
interface ErrorHandler {
|
|
58
|
+
(config: { error: any; debugName: string }): VirtualDOM;
|
|
59
|
+
}
|
|
60
|
+
type DOMNode = HTMLElement | SVGElement;
|
|
61
|
+
|
|
62
|
+
const ALLOWED_TO_RENDER = () => { };
|
|
63
|
+
type VirtualDOMElement = Omit<preact.VNode<{ [key: string]: unknown }>, "type"> & {
|
|
64
|
+
type: any;
|
|
65
|
+
sourceModuleId?: string;
|
|
66
|
+
callStack?: string;
|
|
67
|
+
/** XSS protector, which allows us to detect virtual dom which has been accidentally serialized */
|
|
68
|
+
allowedToRender?: typeof ALLOWED_TO_RENDER;
|
|
69
|
+
};
|
|
70
|
+
type VirtualDOMBase = (
|
|
71
|
+
VirtualDOMElement
|
|
72
|
+
| string | number | bigint | boolean | null | undefined
|
|
73
|
+
);
|
|
74
|
+
export type VirtualDOM = VirtualDOMBase | VirtualDOMBase[];
|
|
75
|
+
|
|
76
|
+
type GetTFromAttributes<T> = T extends preact.JSX.HTMLAttributes<infer U> ? U : never;
|
|
77
|
+
|
|
78
|
+
export namespace qreact {
|
|
79
|
+
export namespace JSX {
|
|
80
|
+
export type IntrinsicElements = {
|
|
81
|
+
[key in keyof preact.JSX.IntrinsicElements]: (
|
|
82
|
+
// NOTE: IntrinsicAttributes is not working, but... this works just as well.
|
|
83
|
+
preact.JSX.IntrinsicElements[key] & {
|
|
84
|
+
/** Only called on mount of the element, and not on re-render, or update.
|
|
85
|
+
* - Should always be used in conjunction with a key, otherwise it might
|
|
86
|
+
* be called for an unrelated component, and then skipped on your component.
|
|
87
|
+
*/
|
|
88
|
+
ref2?: (element: GetTFromAttributes<preact.JSX.IntrinsicElements[key]>) => void;
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
export type Element = preact.JSX.Element;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
let schema = rawSchema<{
|
|
98
|
+
[LOCAL_DOMAIN]: {
|
|
99
|
+
qreact: {
|
|
100
|
+
components: {
|
|
101
|
+
[id: number]: QComponent;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
hotreloadSeqNum: number;
|
|
105
|
+
unsyncedSeqNum: number;
|
|
106
|
+
};
|
|
107
|
+
}>();
|
|
108
|
+
|
|
109
|
+
export class Component<T = {}, S = {}> {
|
|
110
|
+
constructor(public props: T & {
|
|
111
|
+
children?: VirtualDOM | object;
|
|
112
|
+
key?: string | number;
|
|
113
|
+
}) { }
|
|
114
|
+
state = {} as S;
|
|
115
|
+
context = {};
|
|
116
|
+
setState(state: S) {
|
|
117
|
+
for (let key in state) {
|
|
118
|
+
(this as any).state[key] = state[key];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
forceUpdate() { }
|
|
122
|
+
render(): preact.ComponentChild { return undefined; }
|
|
123
|
+
|
|
124
|
+
componentWillMount?(): void;
|
|
125
|
+
componentDidMount?(): void;
|
|
126
|
+
componentWillUnmount?(): void;
|
|
127
|
+
}
|
|
128
|
+
export interface QComponentStatic {
|
|
129
|
+
/** Compares props via JSON.stringify when updating them, skipping identical values.
|
|
130
|
+
* - Results in fewer re-renders, but can be slower if the props are large.
|
|
131
|
+
*/
|
|
132
|
+
jsonComparePropUpdates?: boolean;
|
|
133
|
+
/** Decomposes props into individual primitive values when writing (the default state of
|
|
134
|
+
* most synced writes).
|
|
135
|
+
* - Results in minimal re-renders, even skipping intermediate component re-renders
|
|
136
|
+
* where possible, but is MUCH slower if the props are large.
|
|
137
|
+
* - Makes passing exotics and functions more difficult, requiring pre-wrapping them an
|
|
138
|
+
* object with atomicObjectWrite.
|
|
139
|
+
* NOTE: Usually props can be shallow, because the props only change when the parent re-renders.
|
|
140
|
+
* ALSO, it is vastly preferrably to simply pass synced values as props, which will then ===,
|
|
141
|
+
* BUT, which will be able to trigger changed IF nested values are accessed.
|
|
142
|
+
*/
|
|
143
|
+
deepProps?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ComponentJSONCompare<T = {}> extends Component<T> {
|
|
148
|
+
public static jsonComparePropUpdates = true;
|
|
149
|
+
public render(): preact.ComponentChild { return undefined; }
|
|
150
|
+
}
|
|
151
|
+
class ComponentDeepProps<T = {}> extends Component<T> {
|
|
152
|
+
public static deepProps = true;
|
|
153
|
+
public render(): preact.ComponentChild { return undefined; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class qreact {
|
|
157
|
+
public static Component = Component;
|
|
158
|
+
public static createElement = createElement;
|
|
159
|
+
public static Fragment = Fragment;
|
|
160
|
+
|
|
161
|
+
/** See QComponentStatic.jsonComparePropUpdates */
|
|
162
|
+
public static ComponentJSONCompare = ComponentJSONCompare;
|
|
163
|
+
/** See QComponentStatic.deepProps */
|
|
164
|
+
public static ComponentDeepProps = ComponentDeepProps;
|
|
165
|
+
|
|
166
|
+
public static context = getDebugContext;
|
|
167
|
+
|
|
168
|
+
public static mountToDOM = mountToDOM;
|
|
169
|
+
public static renderToDOM = mountToDOM;
|
|
170
|
+
public static render = mountToDOM;
|
|
171
|
+
|
|
172
|
+
// IMPORTANT! The currentTarget is wrapped so we can copy certain properties, as event callbacks
|
|
173
|
+
// might be executed after the initial execution (in order to sync data from the server),
|
|
174
|
+
// and the original event will have it's value changed by then. IF you absolutely need
|
|
175
|
+
// the original target, call getCurrentTarget. OR, more likely, if you need to check
|
|
176
|
+
// if .target === .currentTarget, call isTarget.
|
|
177
|
+
public static getCurrentTarget = getCurrentTarget;
|
|
178
|
+
public static isTarget = isTarget;
|
|
179
|
+
|
|
180
|
+
public static errorHandler: ErrorHandler;
|
|
181
|
+
|
|
182
|
+
public static debug = async () => {
|
|
183
|
+
const { enableDebugComponents } = await import("../5-diagnostics/qreactDebug");
|
|
184
|
+
enableDebugComponents();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
public static getRenderingComponent = getRenderingComponent;
|
|
188
|
+
public static getAncestor = getAncestor;
|
|
189
|
+
public static getAncestorProps = getAncestorProps;
|
|
190
|
+
public static watchDispose = watchDispose;
|
|
191
|
+
public static watchUnmount = watchDispose;
|
|
192
|
+
public static onDispose = onDispose;
|
|
193
|
+
public static onUnmount = onDispose;
|
|
194
|
+
public static watchUnsyncedComponents = watchUnsyncedComponents;
|
|
195
|
+
public static rehydrateVirtualDOM = rehydrateVirtualDOM;
|
|
196
|
+
public static hasEventHandlers = hasEventHandlers;
|
|
197
|
+
public static getCurrentComponentId = getCurrentComponentId;
|
|
198
|
+
|
|
199
|
+
public static allComponents = new Set<ExternalRenderClass>();
|
|
200
|
+
|
|
201
|
+
public static DEBUG_TIME = 0;
|
|
202
|
+
public static INJECT_FILENAMES = false;
|
|
203
|
+
public static INJECT_LINE_NUMBERS = false;
|
|
204
|
+
|
|
205
|
+
public static JSX = null as any as {
|
|
206
|
+
IntrinsicElements: { [key: string]: { [key: string]: unknown } };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
(globalThis as any).qreact = qreact;
|
|
210
|
+
|
|
211
|
+
module.remapExports = (exports, callingModule) => {
|
|
212
|
+
let newExports = { ...exports } as any;
|
|
213
|
+
|
|
214
|
+
function createElement(...args: any[]) {
|
|
215
|
+
return (createElementBase as any)(callingModule, ...args);
|
|
216
|
+
}
|
|
217
|
+
newExports.qreact = new Proxy(newExports.qreact, {
|
|
218
|
+
get(target, prop) {
|
|
219
|
+
if (prop === "createElement") {
|
|
220
|
+
return createElement;
|
|
221
|
+
}
|
|
222
|
+
return target[prop];
|
|
223
|
+
},
|
|
224
|
+
set(target, prop, value) {
|
|
225
|
+
target[prop] = value;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return newExports;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
const elementCtor = heapTagObj("JSXElement");
|
|
234
|
+
function createElement(
|
|
235
|
+
type: preact.ComponentClass | string,
|
|
236
|
+
props?: { [key: string]: unknown },
|
|
237
|
+
...children: preact.ComponentChild[]
|
|
238
|
+
): preact.VNode<{ [key: string]: unknown }> {
|
|
239
|
+
return createElementBase(undefined, type, props, ...children);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createElementBase(
|
|
243
|
+
callingModule: NodeJS.Module | undefined,
|
|
244
|
+
type: preact.ComponentClass | string,
|
|
245
|
+
props?: { [key: string]: unknown },
|
|
246
|
+
...children: preact.ComponentChild[]
|
|
247
|
+
): preact.VNode<{ [key: string]: unknown }> {
|
|
248
|
+
props = props || {};
|
|
249
|
+
if (children.length) {
|
|
250
|
+
props.children = children;
|
|
251
|
+
}
|
|
252
|
+
return elementCtor({
|
|
253
|
+
type,
|
|
254
|
+
props: props as any,
|
|
255
|
+
key: props?.["key"],
|
|
256
|
+
ref: props?.["ref"] as any,
|
|
257
|
+
sourceModuleId: callingModule?.id,
|
|
258
|
+
callStack: qreact.INJECT_LINE_NUMBERS ? new Error().stack : undefined,
|
|
259
|
+
allowedToRender: ALLOWED_TO_RENDER,
|
|
260
|
+
// NOTE: Compatibility with preact is too difficult, because it REQUIRES special minified names. However,
|
|
261
|
+
// if you're using qreact, you really have to use qreact.render anyways, so this is fine...
|
|
262
|
+
// constructor: undefined,
|
|
263
|
+
// __: null,
|
|
264
|
+
// __b: 0,
|
|
265
|
+
// __c: null,
|
|
266
|
+
// __d: undefined,
|
|
267
|
+
// __e: null,
|
|
268
|
+
// __i: -1,
|
|
269
|
+
// __k: null,
|
|
270
|
+
// __u: 0,
|
|
271
|
+
// __v: 1
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
/** IMPORTANT! DO NOT call this on user controlled data, otherwise the user will be able to render
|
|
277
|
+
* arbitrary data clientside, which lets them take over the side with large elements to trick
|
|
278
|
+
* users to perform actions (such as clicking on a div they think is a "confirm" button, but which
|
|
279
|
+
* is above a "delete your account" button).
|
|
280
|
+
*/
|
|
281
|
+
export function rehydrateVirtualDOM<T extends VirtualDOM>(vNode: T): T {
|
|
282
|
+
return temporaryUnsafeVirtualDOM(() => QRenderClass.diffVNodes({
|
|
283
|
+
next: vNode,
|
|
284
|
+
prev: undefined,
|
|
285
|
+
onNewNode(vNode, anchor, parent) {
|
|
286
|
+
if (isVNode(vNode)) {
|
|
287
|
+
vNode.allowedToRender = ALLOWED_TO_RENDER;
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
},
|
|
291
|
+
onRemovedNode(vNode) {
|
|
292
|
+
|
|
293
|
+
},
|
|
294
|
+
onUpdatedNode(prev, next, anchor, parent) {
|
|
295
|
+
return undefined;
|
|
296
|
+
},
|
|
297
|
+
}) as T);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function Fragment(props: { children: preact.ComponentChild[] }) {
|
|
301
|
+
return props.children;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const getSchemaPrefix = lazy(() => {
|
|
305
|
+
return getProxyPath(() => schema()[LOCAL_DOMAIN].qreact.components);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
let seqNum = 0;
|
|
309
|
+
function nextLocalId() {
|
|
310
|
+
return seqNum++;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** All of the fields of QRenderClass which should be externally accessible. */
|
|
314
|
+
export type ExternalRenderClass = {
|
|
315
|
+
data(): QComponent;
|
|
316
|
+
getParent(): ExternalRenderClass | undefined;
|
|
317
|
+
readonly renderWatcher: SyncWatcher;
|
|
318
|
+
readonly VNodeWatcher: SyncWatcher;
|
|
319
|
+
readonly mountDOMWatcher: SyncWatcher;
|
|
320
|
+
readonly debugName: string;
|
|
321
|
+
readonly instance: preact.Component<{ [key: string]: unknown }>;
|
|
322
|
+
getVSCodeLink(): string | undefined;
|
|
323
|
+
// TODO: We can add more types here as needed, exposing parts of QRenderClass which
|
|
324
|
+
// are safe for external callers to access.
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
function getCurrentComponentId() {
|
|
328
|
+
return QRenderClass.renderingComponentId ?? QRenderClass.eventComponentId;
|
|
329
|
+
}
|
|
330
|
+
function getRenderingComponent() {
|
|
331
|
+
return QRenderClass.getInstanceAllowedUndefined(QRenderClass.renderingComponentId ?? -1) as ExternalRenderClass | undefined;
|
|
332
|
+
}
|
|
333
|
+
function getAncestorProps<Props>(
|
|
334
|
+
componentClass: { new(...args: any[]): { props: Props } },
|
|
335
|
+
component?: unknown
|
|
336
|
+
): Props | undefined {
|
|
337
|
+
let id = (component as any)?.[idSymbol];
|
|
338
|
+
let ancestor = getAncestor(componentClass, id);
|
|
339
|
+
if (!ancestor) return undefined;
|
|
340
|
+
return ancestor.instance.props as Props;
|
|
341
|
+
}
|
|
342
|
+
function getAncestor(
|
|
343
|
+
componentClass: unknown,
|
|
344
|
+
componentId = getCurrentComponentId()
|
|
345
|
+
): ExternalRenderClass | undefined {
|
|
346
|
+
let component = QRenderClass.getInstanceAllowedUndefined(componentId ?? -1);
|
|
347
|
+
while (component) {
|
|
348
|
+
if (component.instance.constructor === componentClass) return component;
|
|
349
|
+
component = component.getParent();
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
function onDispose(fnc: () => void) {
|
|
354
|
+
let component = getRenderingComponent();
|
|
355
|
+
if (!component) throw new Error(`Can watch dispose if not inside of a `);
|
|
356
|
+
watchDispose(component, fnc);
|
|
357
|
+
}
|
|
358
|
+
function watchDispose(renderClass: ExternalRenderClass, fnc: () => void) {
|
|
359
|
+
let watchers = disposeWatchers.get(renderClass);
|
|
360
|
+
if (!watchers) {
|
|
361
|
+
watchers = new Set();
|
|
362
|
+
disposeWatchers.set(renderClass, watchers);
|
|
363
|
+
}
|
|
364
|
+
watchers.add(fnc);
|
|
365
|
+
}
|
|
366
|
+
let disposeWatchers = new Map<ExternalRenderClass, Set<() => void>>();
|
|
367
|
+
|
|
368
|
+
let mouseIsDown = false;
|
|
369
|
+
if (!isNode()) {
|
|
370
|
+
document.addEventListener("mousedown", () => mouseIsDown = true);
|
|
371
|
+
document.addEventListener("mouseup", () => mouseIsDown = false);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
let renderClasses = new Map<number, QRenderClass>();
|
|
376
|
+
// NOTE: In theory this COULD BE our root Component, but... it makes compatibility easier
|
|
377
|
+
// if it is separate. It also keeps better isolation between user components and the framework
|
|
378
|
+
// (as it is weird for user component to inherit the framework code...).
|
|
379
|
+
class QRenderClass {
|
|
380
|
+
public static renderingComponentId: number | undefined;
|
|
381
|
+
public static eventComponentId: number | undefined;
|
|
382
|
+
|
|
383
|
+
public id = nextLocalId();
|
|
384
|
+
|
|
385
|
+
public renderWatcher: SyncWatcher;
|
|
386
|
+
public VNodeWatcher: SyncWatcher;
|
|
387
|
+
public mountDOMWatcher: SyncWatcher;
|
|
388
|
+
public instance: preact.Component<{ [key: string]: unknown }>;
|
|
389
|
+
public debugName: string;
|
|
390
|
+
|
|
391
|
+
public domNodes = new Map<VirtualDOM, {
|
|
392
|
+
componentChildren: DOMNode[];
|
|
393
|
+
} | {
|
|
394
|
+
// NOTE: We have to use an array, because VirtualDOM could be a primitive, which can result
|
|
395
|
+
// in multiple indistinguishable TextNodes (apart from by their index within their parent).
|
|
396
|
+
directChildren: DOMNode[];
|
|
397
|
+
}>();
|
|
398
|
+
public childComponents = new Map<VirtualDOM, number>();
|
|
399
|
+
|
|
400
|
+
public vNode: VirtualDOM | undefined;
|
|
401
|
+
public prevMountedVNode: VirtualDOM | undefined;
|
|
402
|
+
public internalRootDOMNodes: DOMNode[] | undefined;
|
|
403
|
+
|
|
404
|
+
public data() {
|
|
405
|
+
return schema()[LOCAL_DOMAIN].qreact.components[this.id];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
constructor(public config: {
|
|
409
|
+
vNode: preact.VNode<{ [key: string]: unknown }> & { type: preact.ComponentClass };
|
|
410
|
+
props: { [key: string]: unknown };
|
|
411
|
+
rawProps: { [key: string]: unknown };
|
|
412
|
+
/* The root of all of the rendering */
|
|
413
|
+
masterRootDOMNode?: DOMNode;
|
|
414
|
+
id?: number;
|
|
415
|
+
// Should JUST be used for debugging, and for things such as context, getParent should be used
|
|
416
|
+
parentId?: number;
|
|
417
|
+
}) {
|
|
418
|
+
qreact.allComponents.add(this);
|
|
419
|
+
this.id = config.id ?? this.id;
|
|
420
|
+
renderClasses.set(this.id, this);
|
|
421
|
+
|
|
422
|
+
let vNode = config.vNode;
|
|
423
|
+
let self = this;
|
|
424
|
+
// Set up watchers for virtual watching AND dom mounting
|
|
425
|
+
let debugName = vNode.type.name + `|${this.id}|`;
|
|
426
|
+
const getDebugName = (type: string) => {
|
|
427
|
+
return vNode.type.name + `|${type}|${this.id}`;
|
|
428
|
+
};
|
|
429
|
+
const registerOwnTime = (type: string, startTime: number) => {
|
|
430
|
+
registerComponentStats(vNode.type.name + "." + type, Date.now() - startTime);
|
|
431
|
+
};
|
|
432
|
+
this.debugName = debugName;
|
|
433
|
+
|
|
434
|
+
let statics = vNode.type as QComponentStatic;
|
|
435
|
+
// NOTE: This used to exist in an `atomicWrites: true` runOnce, but... I don't think that is required anymore
|
|
436
|
+
let instance = new vNode.type(config.props) as preact.Component<{ [key: string]: unknown }>;
|
|
437
|
+
(instance as any)[idSymbol] = this.id;
|
|
438
|
+
if (isValueProxy(instance)) {
|
|
439
|
+
// This will probably result in an error later on. It is usually due to us failing to identify fragments.
|
|
440
|
+
console.warn(`Constructor returned a proxy, instead of a component.`);
|
|
441
|
+
}
|
|
442
|
+
instance.setState = (state) => {
|
|
443
|
+
for (let key in state) {
|
|
444
|
+
(instance as any).state[key] = (state as any)[key];
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
instance.forceUpdate = () => {
|
|
448
|
+
// Delay, as we aren't allowed to have nested trigger?
|
|
449
|
+
if (proxyWatcher.inWatcher()) {
|
|
450
|
+
Querysub.onCommitFinished(() => {
|
|
451
|
+
this.renderWatcher?.explicitlyTrigger();
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
this.renderWatcher?.explicitlyTrigger();
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
this.instance = instance;
|
|
458
|
+
|
|
459
|
+
let initialState = {};
|
|
460
|
+
if (Schema2Fncs.getPaths(instance.state).length > 0) {
|
|
461
|
+
let statePath = getProxyPath(() => this.data().state);
|
|
462
|
+
let state = instance.state;
|
|
463
|
+
registerSchemaPrefix({ schema: state, prefixPathStr: statePath });
|
|
464
|
+
watchDispose(this, () => {
|
|
465
|
+
unregisterSchemaPrefix({ schema: state, prefixPathStr: statePath });
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
initialState = instance.state || {};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// Force state to be a getter/setter
|
|
473
|
+
Object.defineProperty(instance, "state", {
|
|
474
|
+
get() { return self.data().state; },
|
|
475
|
+
set(value) { self.data().state = value; },
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
proxyWatcher.runOnce({
|
|
479
|
+
debugName: getDebugName("setInitialState"),
|
|
480
|
+
atomicWrites: false,
|
|
481
|
+
noLocks: true,
|
|
482
|
+
watchFunction() {
|
|
483
|
+
// TODO: Why are we iterating instead of just setting it directly, with `state = initialState` ?
|
|
484
|
+
for (let [key, value] of Object.entries(initialState)) {
|
|
485
|
+
(instance as any).state[key] = value;
|
|
486
|
+
}
|
|
487
|
+
self.data().parentComponentId = config.parentId;
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
self.instance.componentWillMount && logErrors(proxyWatcher.commitFunction({
|
|
492
|
+
debugName: getDebugName("componentWillMount"),
|
|
493
|
+
watchFunction() {
|
|
494
|
+
self.instance.componentWillMount?.();
|
|
495
|
+
},
|
|
496
|
+
}));
|
|
497
|
+
|
|
498
|
+
const runContextCallback = (code: (() => void) | undefined) => {
|
|
499
|
+
if (!code) return;
|
|
500
|
+
let prevEvent = QRenderClass.eventComponentId;
|
|
501
|
+
QRenderClass.eventComponentId = self.id;
|
|
502
|
+
try {
|
|
503
|
+
code();
|
|
504
|
+
} finally {
|
|
505
|
+
QRenderClass.eventComponentId = prevEvent;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
let nextInlineId = 1;
|
|
511
|
+
|
|
512
|
+
let renderPrevVNode: VirtualDOM | undefined;
|
|
513
|
+
// Watch render. In it's own watcher so it can write with locks (because most code will only read/write from
|
|
514
|
+
// local state, and therefore does not need locks).
|
|
515
|
+
const renderWatcher = this.renderWatcher = proxyWatcher.createWatcher({
|
|
516
|
+
debugName: getDebugName("render"),
|
|
517
|
+
canWrite: true,
|
|
518
|
+
watchFunction() {
|
|
519
|
+
|
|
520
|
+
// NOTE: We might want to consider waiting if we have a synchronous trigger. Synchronous
|
|
521
|
+
// re-renders cause weird things with event bubbling, that is often unexpected (and
|
|
522
|
+
// never intended). Ex, a parent node is re-used, so the event bubbles to what
|
|
523
|
+
// was never the click target's real parent, which could be in a completely different
|
|
524
|
+
// part of the screen.
|
|
525
|
+
// IMPORTANT! isCurrentLoopSynchronous doesn't work, because... our watch loop waits a microtick,
|
|
526
|
+
// while event handling requires waiting to the end of the event loop (a tick, basically).
|
|
527
|
+
// TODO: So... we really need to explicitly handle events, adding a listener at the document so we
|
|
528
|
+
// can wait for them to finish bubbling. AND, we need to handle stopPropagation (and preventDefault?)
|
|
529
|
+
// so we know when to trigger them early.
|
|
530
|
+
// - Fortunately, frame rendering is so slow, that is is easy to verify we don't break anything.
|
|
531
|
+
// If we log the time in the event, and in the render, they should be a few millisecond apart.
|
|
532
|
+
// If they are > 10ms apart (16ms, 32ms, etc), then we waited a frame, which is really bad
|
|
533
|
+
// (it halves our framerate, or more).
|
|
534
|
+
// - The archive viewer has mitigation code via drag.ts, which setTimeouts onDone
|
|
535
|
+
// (as the onDone doesn't happen that often). If we comment that out, the chart will
|
|
536
|
+
// navigate on bar click, which is a good way to verify any kind of fix works.
|
|
537
|
+
// ACTUALLY! Even waiting for bubbling won't work. We need to wait for the onclick to happen,
|
|
538
|
+
// which isn't even queued yet?
|
|
539
|
+
// - We might need to explicitly handle onMouseUp / onClick?
|
|
540
|
+
// if (clientWatcher.isCurrentLoopSynchronous()) {
|
|
541
|
+
// setImmediate(() => {
|
|
542
|
+
// renderWatcher.explicitlyTrigger();
|
|
543
|
+
// });
|
|
544
|
+
// return;
|
|
545
|
+
// }
|
|
546
|
+
|
|
547
|
+
if (self.disposed || self.disposing) return;
|
|
548
|
+
let vNode: VirtualDOM;
|
|
549
|
+
try {
|
|
550
|
+
let time = Date.now();
|
|
551
|
+
QRenderClass.renderingComponentId = self.id;
|
|
552
|
+
self.data().newPropSeqNum;
|
|
553
|
+
schema()[LOCAL_DOMAIN].hotreloadSeqNum;
|
|
554
|
+
if (typeof instance.render !== "function") {
|
|
555
|
+
// The component might be a functional component, in which case we can just call it
|
|
556
|
+
vNode = new config.vNode.type(config.props) as unknown as VirtualDOM;
|
|
557
|
+
} else {
|
|
558
|
+
vNode = instance.render() as VirtualDOM;
|
|
559
|
+
}
|
|
560
|
+
registerOwnTime("render", time);
|
|
561
|
+
|
|
562
|
+
time = Date.now();
|
|
563
|
+
|
|
564
|
+
vNode = QRenderClass.normalizeVNode({ vNode, array: true });
|
|
565
|
+
|
|
566
|
+
registerOwnTime("parseVDom", time);
|
|
567
|
+
|
|
568
|
+
} catch (e: any) {
|
|
569
|
+
// NOTE: Rethrowing the error WON'T let us have an "uncaught exception" (which is useful when
|
|
570
|
+
// debugging, as it makes automatically stepping in easier), because proxyWatcher has to
|
|
571
|
+
// catch it anyways!
|
|
572
|
+
vNode = qreact.errorHandler({ error: e, debugName });
|
|
573
|
+
vNode = QRenderClass.normalizeVNode({ vNode, array: true });
|
|
574
|
+
} finally {
|
|
575
|
+
QRenderClass.renderingComponentId = undefined;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
self.data().vNodeForRender = atomicObjectWriteNoFreeze({ value: vNode });
|
|
579
|
+
componentRendered(self);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Update props, dispose, and create new component children
|
|
584
|
+
this.VNodeWatcher = proxyWatcher.createWatcher({
|
|
585
|
+
debugName: getDebugName("vNodeDiff"),
|
|
586
|
+
canWrite: true,
|
|
587
|
+
atomicWrites: true,
|
|
588
|
+
noLocks: true,
|
|
589
|
+
watchFunction() {
|
|
590
|
+
if (self.disposed || self.disposing) return;
|
|
591
|
+
let vNodeValue = atomic(self.data().vNodeForRender);
|
|
592
|
+
if (!vNodeValue) return;
|
|
593
|
+
let time = Date.now();
|
|
594
|
+
let vNode = vNodeValue?.value;
|
|
595
|
+
|
|
596
|
+
let prev = self.vNode;
|
|
597
|
+
self.data().vNodeForMount = vNode;
|
|
598
|
+
|
|
599
|
+
let prevChildComponents = self.childComponents;
|
|
600
|
+
let nextChildComponents = new Map() as typeof prevChildComponents;
|
|
601
|
+
let onSynced: (() => void)[] = [];
|
|
602
|
+
|
|
603
|
+
QRenderClass.diffVNodes({
|
|
604
|
+
alreadyNormalized: true,
|
|
605
|
+
prev: prev,
|
|
606
|
+
next: vNode,
|
|
607
|
+
onNewNode(vNode) {
|
|
608
|
+
if (!isVNodeClass(vNode)) return undefined;
|
|
609
|
+
|
|
610
|
+
let vNodeId = nextLocalId();
|
|
611
|
+
let syncedData = QRenderClass.getData(vNodeId);
|
|
612
|
+
let props: { [key: string]: unknown } = Object.create(null);
|
|
613
|
+
let rawProps: { [key: string]: unknown } = Object.create(null);
|
|
614
|
+
for (let [key, value] of Object.entries(vNode.props)) {
|
|
615
|
+
rawProps[key] = value;
|
|
616
|
+
}
|
|
617
|
+
if (statics.deepProps) {
|
|
618
|
+
// NOTE: By removing atomic we can leverage the proxy automatically decomposing
|
|
619
|
+
// the props to each individual primitive value.
|
|
620
|
+
doProxyOptions({ atomicWrites: false }, () => {
|
|
621
|
+
syncedData.props = vNode.props;
|
|
622
|
+
props = syncedData.props;
|
|
623
|
+
});
|
|
624
|
+
} else {
|
|
625
|
+
// Have each prop be a synced field, so conditional props work (accessing a prop not
|
|
626
|
+
// provided returns undefined). Atomic writes is on, so write objects
|
|
627
|
+
// aren't destructured (this allows passing functions, exotics, etc).
|
|
628
|
+
for (let [key, value] of Object.entries(vNode.props)) {
|
|
629
|
+
syncedData.props[key] = value;
|
|
630
|
+
// We then need to use a getter in the props, so that it causes a synced read
|
|
631
|
+
// UNLESS that prop is not defined, then it is just undefined
|
|
632
|
+
Object.defineProperty(props, key, {
|
|
633
|
+
configurable: true,
|
|
634
|
+
enumerable: true,
|
|
635
|
+
get() {
|
|
636
|
+
// Always access props, so we incur a watch
|
|
637
|
+
let value = syncedData.props[key];
|
|
638
|
+
// Return undefined, instead of a proxy
|
|
639
|
+
if (rawProps[key] === undefined) return undefined;
|
|
640
|
+
return value;
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
nextChildComponents.set(vNode, vNodeId);
|
|
646
|
+
|
|
647
|
+
onSynced.push(() => new QRenderClass({
|
|
648
|
+
vNode: vNode as any,
|
|
649
|
+
props,
|
|
650
|
+
rawProps,
|
|
651
|
+
id: vNodeId,
|
|
652
|
+
parentId: self.id
|
|
653
|
+
}));
|
|
654
|
+
},
|
|
655
|
+
onRemovedNode(vNode) {
|
|
656
|
+
if (!isVNodeClass(vNode)) return;
|
|
657
|
+
const deadId = prevChildComponents.get(vNode);
|
|
658
|
+
if (deadId !== undefined) {
|
|
659
|
+
onSynced.push(() => {
|
|
660
|
+
recursivelyMarkDisposing(deadId);
|
|
661
|
+
function recursivelyMarkDisposing(id: number) {
|
|
662
|
+
let component = QRenderClass.getInstance(id);
|
|
663
|
+
if (!component) return;
|
|
664
|
+
if (component.disposing) return;
|
|
665
|
+
component.disposing = true;
|
|
666
|
+
for (let childId of component.childComponents.values()) {
|
|
667
|
+
recursivelyMarkDisposing(childId);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
void Promise.resolve().finally(() => {
|
|
671
|
+
QRenderClass.getInstance(deadId).dispose();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
onUpdatedNode(prevVNode, nextVNode) {
|
|
677
|
+
if (!isVNodeClass(prevVNode) || !isVNodeClass(nextVNode)) return undefined;
|
|
678
|
+
const childComponent = prevChildComponents.get(prevVNode);
|
|
679
|
+
if (!childComponent) {
|
|
680
|
+
debugger;
|
|
681
|
+
throw new Error(`Could not find component for vNode ${prevVNode}`);
|
|
682
|
+
}
|
|
683
|
+
nextChildComponents.set(nextVNode, childComponent);
|
|
684
|
+
|
|
685
|
+
// Update props
|
|
686
|
+
let prevProps = prevVNode.props;
|
|
687
|
+
let nextProps = nextVNode.props;
|
|
688
|
+
if ("data-break" in nextProps) {
|
|
689
|
+
debugger;
|
|
690
|
+
}
|
|
691
|
+
if (statics.deepProps) {
|
|
692
|
+
// NOTE: By removing atomic we can leverage the proxy automatically decomposing
|
|
693
|
+
// the props to each individual primitive value, and checking each individual
|
|
694
|
+
// primitive value for changes.
|
|
695
|
+
doProxyOptions({ atomicWrites: false }, () => {
|
|
696
|
+
QRenderClass.getData(childComponent).props = nextProps;
|
|
697
|
+
});
|
|
698
|
+
} else {
|
|
699
|
+
let childInstance = QRenderClass.getInstance(childComponent);
|
|
700
|
+
if (!childInstance) {
|
|
701
|
+
console.warn(`Could not find child instance for component ${childComponent}, and so cannot update props`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const props = childInstance.instance.props;
|
|
705
|
+
const rawProps = childInstance.config.rawProps;
|
|
706
|
+
let syncedData = QRenderClass.getData(childComponent);
|
|
707
|
+
for (let key in nextProps) {
|
|
708
|
+
let prev = prevProps[key];
|
|
709
|
+
let next = nextProps[key];
|
|
710
|
+
// NOTE: Object.is is required to handle NaN case
|
|
711
|
+
if (Object.is(prev, next)) continue;
|
|
712
|
+
let prevPath = isValueProxy2(prev);
|
|
713
|
+
let nextPath = isValueProxy2(next);
|
|
714
|
+
if (prevPath && nextPath && prevPath === nextPath) continue;
|
|
715
|
+
if (statics.jsonComparePropUpdates) {
|
|
716
|
+
if (JSON.stringify(prev) === JSON.stringify(next)) continue;
|
|
717
|
+
}
|
|
718
|
+
// ONLY trigger non-synced side-effects if the component once we know the component is synced.
|
|
719
|
+
// The synced writes will be ignored if we aren't synced, but the non-synced writes will not,
|
|
720
|
+
// so we have to be very careful about them.
|
|
721
|
+
onSynced.push(() => {
|
|
722
|
+
if (!(key in props)) {
|
|
723
|
+
// For a re-render, because if the prop was accessed, there was no
|
|
724
|
+
// getter, so it the path won't have been watched.
|
|
725
|
+
childInstance.data().newPropSeqNum++;
|
|
726
|
+
Object.defineProperty(props, key, {
|
|
727
|
+
configurable: true,
|
|
728
|
+
enumerable: true,
|
|
729
|
+
get() {
|
|
730
|
+
// Always access props, so we incur a watch
|
|
731
|
+
let value = syncedData.props[key];
|
|
732
|
+
// Return undefined, instead of a proxy
|
|
733
|
+
if (rawProps[key] === undefined) return undefined;
|
|
734
|
+
return value;
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
rawProps[key] = next;
|
|
739
|
+
});
|
|
740
|
+
syncedData.props[key] = next;
|
|
741
|
+
}
|
|
742
|
+
for (let key in prevProps) {
|
|
743
|
+
if (nextProps[key] !== undefined) continue;
|
|
744
|
+
syncedData.props[key] = undefined;
|
|
745
|
+
onSynced.push(() => {
|
|
746
|
+
delete props[key];
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// NOTE: I'm not really sure how we can have unsynced accesses anymore, as we don't run with locks,
|
|
755
|
+
// and we should only be accessing local state. But... I guess this doesn't hurt too much?
|
|
756
|
+
// - We might need to eventually remove it for performance reasons though...
|
|
757
|
+
if (Querysub.allSynced()) {
|
|
758
|
+
self.childComponents = nextChildComponents;
|
|
759
|
+
self.vNode = vNode;
|
|
760
|
+
for (let fnc of onSynced) fnc();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
registerOwnTime("diffVDom", time);
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Apply vNode to DOM
|
|
768
|
+
let isFirstMount = true;
|
|
769
|
+
this.mountDOMWatcher = proxyWatcher.createWatcher({
|
|
770
|
+
debugName: getDebugName("updateDOM"),
|
|
771
|
+
canWrite: true,
|
|
772
|
+
atomicWrites: true,
|
|
773
|
+
noLocks: true,
|
|
774
|
+
// Delay DOM execution, so all VNodes can be created before we try to mount anything
|
|
775
|
+
orderGroup: 1,
|
|
776
|
+
// Run in reverse, so children have their dom nodes created first, and then the parents can inject them into their DOM.
|
|
777
|
+
order: -this.id,
|
|
778
|
+
onResultUpdated(result, writes, watcher) {
|
|
779
|
+
if (result && "error" in result) {
|
|
780
|
+
console.error(`Error in ${watcher.debugName}`, result.error);
|
|
781
|
+
setImmediate(() => { throw errorify(result.error); });
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
watchFunction() {
|
|
785
|
+
if (self.disposed || self.disposing) return;
|
|
786
|
+
let time = Date.now();
|
|
787
|
+
let next = atomic(self.data().vNodeForMount);
|
|
788
|
+
if (!next) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
let prev = atomic(self.prevMountedVNode);
|
|
792
|
+
self.prevMountedVNode = next;
|
|
793
|
+
|
|
794
|
+
let prevDOMNodes = self.domNodes;
|
|
795
|
+
let nextDOMNodes = new Map() as typeof prevDOMNodes;
|
|
796
|
+
self.domNodes = nextDOMNodes;
|
|
797
|
+
|
|
798
|
+
let nextRootDOMNodes: DOMNode[] = [];
|
|
799
|
+
function getComponentChildren(vNode: VirtualDOM) {
|
|
800
|
+
let component = self.childComponents.get(vNode);
|
|
801
|
+
if (!component) {
|
|
802
|
+
console.warn(`Could not find childComponent for attempted component mount in ${debugName}`);
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
return atomic(QRenderClass.getData(component).rootDOMNodes) ?? [];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let activeChain: DOMNode[] = [];
|
|
809
|
+
{
|
|
810
|
+
let curActive = document.activeElement;
|
|
811
|
+
while (curActive) {
|
|
812
|
+
activeChain.push(curActive as DOMNode);
|
|
813
|
+
curActive = curActive.parentElement;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
type Anchor = { parent: DOMNode } | { prev: DOMNode } | undefined;
|
|
817
|
+
function replaceNodes(anchor: Anchor, prevNodes: DOMNode[], nextNodes: DOMNode[]) {
|
|
818
|
+
let mustPreserveFocus = false;
|
|
819
|
+
// Moving elements around doesn't only break focus... it also breaks clicks! So... once the user clicks,
|
|
820
|
+
// stop replacing children, and use our non-invasive technique.
|
|
821
|
+
if (mouseIsDown) {
|
|
822
|
+
mustPreserveFocus = true;
|
|
823
|
+
}
|
|
824
|
+
if (!mustPreserveFocus) {
|
|
825
|
+
let prevNodesSet = new Set(prevNodes);
|
|
826
|
+
for (let nextNode of nextNodes) {
|
|
827
|
+
if (prevNodesSet.has(nextNode) && activeChain.includes(nextNode)) {
|
|
828
|
+
mustPreserveFocus = true;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
let changedNodes = new Set<DOMNode>();
|
|
833
|
+
for (let nextNode of nextNodes) {
|
|
834
|
+
if (!prevNodesSet.has(nextNode)) {
|
|
835
|
+
changedNodes.add(nextNode);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
let nextNodesSet = new Set(nextNodes);
|
|
839
|
+
for (let prevNode of prevNodes) {
|
|
840
|
+
if (!nextNodesSet.has(prevNode)) {
|
|
841
|
+
changedNodes.add(prevNode);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// HACK: Due to issues with scroll positions being lost, just use the slow technique
|
|
845
|
+
// unless we have a lot of changes.
|
|
846
|
+
if (changedNodes.size < 3000) {
|
|
847
|
+
mustPreserveFocus = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!mustPreserveFocus && anchor && "parent" in anchor && activeChain.includes(anchor.parent)) {
|
|
851
|
+
mustPreserveFocus = true;
|
|
852
|
+
}
|
|
853
|
+
// Also if we are scrolled, preserved that
|
|
854
|
+
if (!mustPreserveFocus && nextNodes.some(x => x.scrollTop > 0)) {
|
|
855
|
+
mustPreserveFocus = true;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (anchor && "parent" in anchor && !mustPreserveFocus) {
|
|
859
|
+
let prevFocus = document.activeElement;
|
|
860
|
+
anchor.parent.replaceChildren(...nextNodes);
|
|
861
|
+
if (prevFocus !== document.activeElement) {
|
|
862
|
+
console.error(`We broke focus by rendering, fix this`);
|
|
863
|
+
debugger;
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
// NOTE: It is A LOT slower to call remove and insert, but... it preserves focus,
|
|
867
|
+
// and is simple (and works with prev nodes, etc).
|
|
868
|
+
// NOTE: In my testing documentFragments aren't that much faster. There probably is still a way
|
|
869
|
+
// to optimize this though...
|
|
870
|
+
let nextNodesSet = new Set(nextNodes);
|
|
871
|
+
for (let prevNode of prevNodes) {
|
|
872
|
+
if (!nextNodesSet.has(prevNode)) {
|
|
873
|
+
prevNode.remove();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// NOTE: insertInAnchor is smart, and will noop if the insertion does nothing.
|
|
877
|
+
for (let nextElem of nextNodes) {
|
|
878
|
+
insertInAnchor(anchor, nextElem);
|
|
879
|
+
anchor = { prev: nextElem };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function insertInAnchor(anchor: Anchor, node: DOMNode) {
|
|
885
|
+
let prev = document.activeElement;
|
|
886
|
+
if (!anchor) {
|
|
887
|
+
nextRootDOMNodes.push(node);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if ("parent" in anchor) {
|
|
891
|
+
if (anchor.parent.firstChild === node) return;
|
|
892
|
+
anchor.parent.insertBefore(node, anchor.parent.firstChild || null);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (nextRootDOMNodes[nextRootDOMNodes.length - 1] === anchor.prev) {
|
|
896
|
+
nextRootDOMNodes.push(node);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (anchor.prev.nextSibling === node) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// BUT, if the node is in the sibling order, then
|
|
904
|
+
// just remove the nodes which are in the way, and insert the node.
|
|
905
|
+
// - This handles the case of previous nodes being removed, which is far more common
|
|
906
|
+
// than nodes being rearranged (which would be the only reason to node remove the
|
|
907
|
+
// intermediate nodes, such as if the first node became the last).
|
|
908
|
+
if (node.parentElement === anchor.prev.parentElement) {
|
|
909
|
+
// First, check if the node is there
|
|
910
|
+
let cur = anchor.prev.nextSibling;
|
|
911
|
+
while (cur && cur !== node) {
|
|
912
|
+
let next = cur.nextSibling;
|
|
913
|
+
cur = next;
|
|
914
|
+
}
|
|
915
|
+
// If it is, remove the intermediate, and then... we will be the next node!
|
|
916
|
+
if (cur === node) {
|
|
917
|
+
cur = anchor.prev.nextSibling;
|
|
918
|
+
while (cur && cur !== node) {
|
|
919
|
+
let next = cur.nextSibling;
|
|
920
|
+
cur.remove();
|
|
921
|
+
cur = next;
|
|
922
|
+
}
|
|
923
|
+
if (anchor.prev.nextSibling !== node) {
|
|
924
|
+
// Impossible!? We just removed all the other siblings!
|
|
925
|
+
debugger;
|
|
926
|
+
}
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
anchor.prev.after(node);
|
|
932
|
+
if (prev !== document.activeElement) {
|
|
933
|
+
debugger;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
let isParentSVG = !!atomic(self.data().isParentSVG);
|
|
938
|
+
function isSVG(node: DOMNode | undefined) {
|
|
939
|
+
return isParentSVG || node?.namespaceURI === "http://www.w3.org/2000/svg";
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function triggerRef(vNode: VirtualDOM, node: DOMNode) {
|
|
943
|
+
if (!isVNode(vNode)) return;
|
|
944
|
+
const ref = vNode.ref;
|
|
945
|
+
if (typeof ref === "function") {
|
|
946
|
+
// IMPORTANT! We have to detach the ref, otherwise it might access unsynced state, which will
|
|
947
|
+
// break our render function.
|
|
948
|
+
void Promise.resolve().finally(() => {
|
|
949
|
+
logErrors(proxyWatcher.commitFunction({
|
|
950
|
+
canWrite: true,
|
|
951
|
+
watchFunction() {
|
|
952
|
+
ref(node);
|
|
953
|
+
},
|
|
954
|
+
}));
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// NOTE: This is a bit inefficient as it requires running twice, because the first run our child
|
|
960
|
+
// components will have no rootDOMNodes. But... it's probably fine...
|
|
961
|
+
QRenderClass.diffVNodes({
|
|
962
|
+
alreadyNormalized: true,
|
|
963
|
+
next,
|
|
964
|
+
prev,
|
|
965
|
+
onNewNode(vNode, anchor, parent) {
|
|
966
|
+
if (isVNodeClass(vNode)) {
|
|
967
|
+
if (isSVG(parent)) {
|
|
968
|
+
let component = self.childComponents.get(vNode);
|
|
969
|
+
if (component) {
|
|
970
|
+
QRenderClass.getData(component).isParentSVG = true;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let domNodes = getComponentChildren(vNode);
|
|
975
|
+
nextDOMNodes.set(vNode, { componentChildren: domNodes });
|
|
976
|
+
for (let node of domNodes) {
|
|
977
|
+
insertInAnchor(anchor, node);
|
|
978
|
+
anchor = { prev: node };
|
|
979
|
+
}
|
|
980
|
+
return domNodes[domNodes.length - 1];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
let node = createDOMNode(vNode, isSVG(parent));
|
|
984
|
+
if (isVNode(vNode)) {
|
|
985
|
+
let ref2 = (vNode as any).ref2 || vNode.props.ref2;
|
|
986
|
+
if (typeof ref2 === "function") {
|
|
987
|
+
if (!vNode.key) {
|
|
988
|
+
console.warn(`Using ref2 without a key is not recommended. Due to node reuse it is possible for a node to be added with ref that reuses an existing node, which will prevent ref from every being called. Ref used in ${debugName}, with vNode ${vNode.type} and ref ${vNode.ref}. Full stack ${self.getDebugComponentStack().map(x => x.debugName).join(", ")}`);
|
|
989
|
+
}
|
|
990
|
+
// IMPORTANT! We have to detach the ref, otherwise it might access unsynced state, which will
|
|
991
|
+
// break our render function.
|
|
992
|
+
void Promise.resolve().finally(() => {
|
|
993
|
+
logErrors(proxyWatcher.commitFunction({
|
|
994
|
+
canWrite: true,
|
|
995
|
+
watchFunction() {
|
|
996
|
+
ref2(node);
|
|
997
|
+
},
|
|
998
|
+
}));
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
triggerRef(vNode, node);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
let obj = nextDOMNodes.get(vNode);
|
|
1006
|
+
|
|
1007
|
+
if (isVNode(vNode) && typeof vNode.type !== "function" && nextDOMNodes.has(vNode)) {
|
|
1008
|
+
// TODO: We should allow this, it happens a lot when passing components as props
|
|
1009
|
+
throw new Error(`VNode was reused. This is not supported. In ${debugName}, type ${vNode.type}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (!obj) nextDOMNodes.set(vNode, obj = { directChildren: [] });
|
|
1012
|
+
if ("componentChildren" in obj) {
|
|
1013
|
+
debugger;
|
|
1014
|
+
throw new Error(`Internal error, component was matched with a non-component DOM node in ${debugName}`);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (isVNode(vNode) && vNode.props["data-break"]) {
|
|
1018
|
+
debugger;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
obj.directChildren.push(node);
|
|
1022
|
+
updateDOMNodeFields(node, vNode, undefined, self);
|
|
1023
|
+
insertInAnchor(anchor, node);
|
|
1024
|
+
return node;
|
|
1025
|
+
},
|
|
1026
|
+
onRemovedNode(vNode) {
|
|
1027
|
+
let domNodes = prevDOMNodes.get(vNode);
|
|
1028
|
+
if (!domNodes) return;
|
|
1029
|
+
|
|
1030
|
+
if (isVNode(vNode) && vNode.props["data-break"]) {
|
|
1031
|
+
debugger;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// NOTE: We remove all the nodes, even if some are still used. This only impacts identical TextNodes,
|
|
1035
|
+
// which is fine, as reusing them probably doesn't add much performance?
|
|
1036
|
+
prevDOMNodes.delete(vNode);
|
|
1037
|
+
let nodes = "componentChildren" in domNodes ? domNodes.componentChildren : domNodes.directChildren;
|
|
1038
|
+
for (let node of nodes) {
|
|
1039
|
+
// NOTE: Always remove, as we are removing all duplicates with the same vNode,
|
|
1040
|
+
// which may have different parents, some of which may be removed, and some of which are kept.
|
|
1041
|
+
node.remove();
|
|
1042
|
+
/*
|
|
1043
|
+
// We SHOULDN'T have to remove event listeners, but maybe we will have to?
|
|
1044
|
+
let eventNode = node as DOMNode & { _eventHandlers?: { [key: string]: EventListener } };
|
|
1045
|
+
for (let key in eventNode._eventHandlers) {
|
|
1046
|
+
eventNode.removeEventListener(key, eventNode._eventHandlers[key]);
|
|
1047
|
+
}
|
|
1048
|
+
delete eventNode._eventHandlers;
|
|
1049
|
+
*/
|
|
1050
|
+
if (isVNode(vNode)) {
|
|
1051
|
+
if (typeof vNode.ref === "function") {
|
|
1052
|
+
vNode.ref(null);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
onUpdatedNode(prev, vNode, anchor, parent) {
|
|
1058
|
+
// TODO: It MIGHT be faster to remove TextNodes instead of reusing them? I'm not sure
|
|
1059
|
+
// how the browser handles reused TextNodes, but we might be in the case where
|
|
1060
|
+
// we completely rearrange them, which might cause performance issues?
|
|
1061
|
+
let prevNodeBase = prevDOMNodes.get(prev);
|
|
1062
|
+
|
|
1063
|
+
// NOTE: Types don't change here (otherwise it would be a new and remove, not an update),
|
|
1064
|
+
// so it is always the case that `prev.type === next.type`
|
|
1065
|
+
if (isVNode(prev) && isVNode(vNode)) {
|
|
1066
|
+
if (typeof prev.type === "function") {
|
|
1067
|
+
if (!prevNodeBase || !("componentChildren" in prevNodeBase)) {
|
|
1068
|
+
throw new Error(`Internal error, component was matched with a non-component DOM node in ${debugName}`);
|
|
1069
|
+
}
|
|
1070
|
+
let prevNodes = prevNodeBase.componentChildren;
|
|
1071
|
+
let nextNodes = getComponentChildren(vNode);
|
|
1072
|
+
nextDOMNodes.set(vNode, { componentChildren: nextNodes });
|
|
1073
|
+
|
|
1074
|
+
replaceNodes(anchor, prevNodes, nextNodes);
|
|
1075
|
+
|
|
1076
|
+
return nextNodes[nextNodes.length - 1];
|
|
1077
|
+
} else {
|
|
1078
|
+
// Built-in ("div", "span", etc)
|
|
1079
|
+
if (!prevNodeBase || "componentChildren" in prevNodeBase) {
|
|
1080
|
+
throw new Error(`Internal error, non-component DOM node was matched with a component node in ${debugName}`);
|
|
1081
|
+
}
|
|
1082
|
+
let prevNodes = prevNodeBase.directChildren;
|
|
1083
|
+
if (prevNodes.length !== 1) {
|
|
1084
|
+
debugger;
|
|
1085
|
+
throw new Error(`Internal error, non-primitive DOM node had more than one child in ${debugName}`);
|
|
1086
|
+
}
|
|
1087
|
+
if (prev.props["data-break"] || vNode.props["data-break"]) {
|
|
1088
|
+
debugger;
|
|
1089
|
+
}
|
|
1090
|
+
nextDOMNodes.set(vNode, { directChildren: prevNodes });
|
|
1091
|
+
let domNode = prevNodes[0];
|
|
1092
|
+
|
|
1093
|
+
// Call insert, in case the order's changed
|
|
1094
|
+
insertInAnchor(anchor, domNode);
|
|
1095
|
+
updateDOMNodeFields(domNode, vNode, prev, self);
|
|
1096
|
+
|
|
1097
|
+
// Don't trigger ref into the dom node is updated
|
|
1098
|
+
triggerRef(vNode, domNode);
|
|
1099
|
+
|
|
1100
|
+
return domNode;
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
// Primitive
|
|
1104
|
+
if (prevNodeBase && "componentChildren" in prevNodeBase) {
|
|
1105
|
+
throw new Error(`Internal error, non-component DOM node was matched with a component node in ${debugName}`);
|
|
1106
|
+
}
|
|
1107
|
+
// Take the first node
|
|
1108
|
+
let prevNodes = prevNodeBase?.directChildren;
|
|
1109
|
+
let prevNode = prevNodes?.shift();
|
|
1110
|
+
let nextNodes = nextDOMNodes.get(vNode);
|
|
1111
|
+
if (!nextNodes) {
|
|
1112
|
+
nextDOMNodes.set(vNode, nextNodes = { directChildren: [] });
|
|
1113
|
+
}
|
|
1114
|
+
if ("componentChildren" in nextNodes) {
|
|
1115
|
+
throw new Error(`Internal error, next nodes is corrupted ${debugName}`);
|
|
1116
|
+
}
|
|
1117
|
+
if (!prevNode) {
|
|
1118
|
+
prevNode = createDOMNode(vNode, isSVG(parent));
|
|
1119
|
+
} else {
|
|
1120
|
+
updateDOMNodeFields(prevNode, String(vNode), prev, self);
|
|
1121
|
+
}
|
|
1122
|
+
nextNodes.directChildren.push(prevNode);
|
|
1123
|
+
|
|
1124
|
+
insertInAnchor(anchor, prevNode);
|
|
1125
|
+
return prevNode;
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// NOTE: We can't update the root dom nodes inline (such as by finding the previous value, and using that
|
|
1131
|
+
// as an anchor), because if we do it might mess up our parent when they try to update our dom nodes.
|
|
1132
|
+
// (And there shouldn't be too much of a delay between our mount and our parent re-running their mount)
|
|
1133
|
+
|
|
1134
|
+
// Update rootDOMNodes efficiently. If only fields change, we don't need to update them. They only need
|
|
1135
|
+
// to be updated if the instances change.
|
|
1136
|
+
let prevRootDOMNodes = self.internalRootDOMNodes ?? [];
|
|
1137
|
+
self.internalRootDOMNodes = nextRootDOMNodes;
|
|
1138
|
+
function rootDOMNodesChanged() {
|
|
1139
|
+
if (prevRootDOMNodes.length !== nextRootDOMNodes.length) return true;
|
|
1140
|
+
for (let i = 0; i < prevRootDOMNodes.length; i++) {
|
|
1141
|
+
if (prevRootDOMNodes[i] !== nextRootDOMNodes[i]) return true;
|
|
1142
|
+
}
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
if (rootDOMNodesChanged()) {
|
|
1146
|
+
for (let node of nextRootDOMNodes) {
|
|
1147
|
+
if ("setAttribute" in node && !node.hasAttribute("data-qreact-id")) {
|
|
1148
|
+
node.setAttribute("data-qreact-id", String(self.id));
|
|
1149
|
+
node.setAttribute("data-qreact-name", self.debugName);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
self.data().rootDOMNodes = nextRootDOMNodes;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (config.masterRootDOMNode) {
|
|
1156
|
+
replaceNodes(
|
|
1157
|
+
{ parent: config.masterRootDOMNode },
|
|
1158
|
+
Array.from(config.masterRootDOMNode.children) as DOMNode[],
|
|
1159
|
+
nextRootDOMNodes
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (isFirstMount) {
|
|
1164
|
+
isFirstMount = false;
|
|
1165
|
+
if (self.instance.componentDidMount) {
|
|
1166
|
+
void Promise.resolve().finally(() => {
|
|
1167
|
+
logErrors(proxyWatcher.commitFunction({
|
|
1168
|
+
canWrite: true,
|
|
1169
|
+
watchFunction() {
|
|
1170
|
+
QRenderClass.renderingComponentId = self.id;
|
|
1171
|
+
try {
|
|
1172
|
+
runContextCallback(() => self.instance?.componentDidMount?.());
|
|
1173
|
+
} finally {
|
|
1174
|
+
QRenderClass.renderingComponentId = undefined;
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
}));
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (Querysub.anyUnsynced()) {
|
|
1183
|
+
throw new Error(`DOM mounting shouldn't access ANY remote values. If it does, it requires rerunning DOM constructors, which is highly inefficient. Any callbacks should be detached with Promise.resolve().finally(...), etc.`);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
registerOwnTime("mount", time);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private static componentIdDedupeBase = cache((name: string) => {
|
|
1192
|
+
let index = 0;
|
|
1193
|
+
return () => {
|
|
1194
|
+
return name + "_" + index++;
|
|
1195
|
+
};
|
|
1196
|
+
});
|
|
1197
|
+
private static componentIdDedupe = cache((fnc: preact.ComponentType) => {
|
|
1198
|
+
return QRenderClass.componentIdDedupeBase(fnc.name)();
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
/*
|
|
1203
|
+
- Converts all props.children to VirtualDOM[].
|
|
1204
|
+
- Shallow copies everything as well.
|
|
1205
|
+
- Shallow copies props.
|
|
1206
|
+
- Flattens arrays
|
|
1207
|
+
- If "config.array", converts the root to be VirtualDOM[]
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
NOTE: Called inside of diffVNodes, which is already called by default, so this usually shouldn't be
|
|
1211
|
+
called explicitly.
|
|
1212
|
+
*/
|
|
1213
|
+
private static normalizeVNode(config: {
|
|
1214
|
+
vNode: VirtualDOM;
|
|
1215
|
+
array?: boolean;
|
|
1216
|
+
}): VirtualDOM {
|
|
1217
|
+
let result = normalizeNode(config.vNode);
|
|
1218
|
+
if (config.array) {
|
|
1219
|
+
return result as VirtualDOM;
|
|
1220
|
+
}
|
|
1221
|
+
return result[0];
|
|
1222
|
+
function normalizeNode(node: VirtualDOM | VirtualDOM[] | undefined): VirtualDOM[] {
|
|
1223
|
+
let flattened = fullyFlatArrays(node);
|
|
1224
|
+
flattened = flattened.map(sub => {
|
|
1225
|
+
if (!isVNode(sub)) return sub;
|
|
1226
|
+
sub = { ...sub };
|
|
1227
|
+
// Shallow copy props, as even though they might contain proxy or otherwise complex values,
|
|
1228
|
+
// props itself should just be a lookup.
|
|
1229
|
+
sub.props = { ...sub.props };
|
|
1230
|
+
if (typeof sub.type === "string") {
|
|
1231
|
+
let children = sub.props.children;
|
|
1232
|
+
if (!children) {
|
|
1233
|
+
sub.props.children = [];
|
|
1234
|
+
} else {
|
|
1235
|
+
sub.props.children = normalizeNode(children as any);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return sub;
|
|
1239
|
+
});
|
|
1240
|
+
return flattened;
|
|
1241
|
+
|
|
1242
|
+
function fullyFlatArrays(value: VirtualDOM | VirtualDOM[] | VirtualDOM[][], output: VirtualDOM[] = []): VirtualDOM[] {
|
|
1243
|
+
// If we don't detect proxies early we might end up creating watches on values we don't
|
|
1244
|
+
// really care about ("type", etc). So, replace them immediately with strings.
|
|
1245
|
+
if (isValueProxy2(value)) return [String(node)];
|
|
1246
|
+
if (value === undefined || value === null || value === false) {
|
|
1247
|
+
return output;
|
|
1248
|
+
}
|
|
1249
|
+
if (!canHaveChildren(value)) {
|
|
1250
|
+
output.push(value as any);
|
|
1251
|
+
return output;
|
|
1252
|
+
}
|
|
1253
|
+
if (isFragment(value)) {
|
|
1254
|
+
fullyFlatArrays((value as any).props.children as any, output);
|
|
1255
|
+
return output;
|
|
1256
|
+
}
|
|
1257
|
+
if (Array.isArray(value)) {
|
|
1258
|
+
for (let item of value) {
|
|
1259
|
+
fullyFlatArrays(item, output);
|
|
1260
|
+
}
|
|
1261
|
+
return output;
|
|
1262
|
+
}
|
|
1263
|
+
output.push(value);
|
|
1264
|
+
return output;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// NOTE: We call diffVNodes twice every time a component changes. This is fine, diffing is just reading, which is
|
|
1270
|
+
// extremely fast. This also provides the potential for lazy/throttled DOM reconcilation (the slowest part of rendering),
|
|
1271
|
+
// which can make the UI feel considerably more responsive.
|
|
1272
|
+
// NOTE: We don't optimize for cases where only a few nodes have changes, but most are ===. vNode reuse is
|
|
1273
|
+
// rare, and barely helps. Instead they should split their component into smaller components.
|
|
1274
|
+
// NOTE: All nodes are either new, or updates. We allow updates if the types are the same (VNode.type OR if they
|
|
1275
|
+
// are primitives, always the same).
|
|
1276
|
+
public static diffVNodes(config: {
|
|
1277
|
+
alreadyNormalized?: boolean;
|
|
1278
|
+
prev: VirtualDOM | undefined;
|
|
1279
|
+
next: VirtualDOM;
|
|
1280
|
+
onTransformNode?: (
|
|
1281
|
+
prev: VirtualDOM,
|
|
1282
|
+
next: VirtualDOM,
|
|
1283
|
+
) => VirtualDOM;
|
|
1284
|
+
// NOTE: Anchor is the value returned from onNewNode/onUpdatedNode of the parent/prev node. If either returns
|
|
1285
|
+
// undefined we assume the anchor is unchanged, and keep the some one (parent or prev).
|
|
1286
|
+
onNewNode: (
|
|
1287
|
+
vNode: VirtualDOM,
|
|
1288
|
+
anchor: { parent: DOMNode } | { prev: DOMNode } | undefined,
|
|
1289
|
+
parent: DOMNode | undefined,
|
|
1290
|
+
) => DOMNode | undefined;
|
|
1291
|
+
onRemovedNode: (vNode: VirtualDOM) => void;
|
|
1292
|
+
// NOTE: onUpdatedNode might not mean the node is updated, it is just what we call if it isn't new or removed.
|
|
1293
|
+
onUpdatedNode: (
|
|
1294
|
+
prev: VirtualDOM,
|
|
1295
|
+
next: VirtualDOM,
|
|
1296
|
+
anchor: { parent: DOMNode } | { prev: DOMNode } | undefined,
|
|
1297
|
+
parent: DOMNode | undefined
|
|
1298
|
+
) => DOMNode | undefined;
|
|
1299
|
+
}): VirtualDOM {
|
|
1300
|
+
let { prev, next, onNewNode, onRemovedNode, onUpdatedNode, onTransformNode } = config;
|
|
1301
|
+
|
|
1302
|
+
if (!config.alreadyNormalized) {
|
|
1303
|
+
next = QRenderClass.normalizeVNode({ vNode: next, array: true });
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
iterate(next as VirtualDOM[], (prev ?? []) as VirtualDOM[], undefined);
|
|
1307
|
+
return next;
|
|
1308
|
+
function iterate(
|
|
1309
|
+
next: VirtualDOM[],
|
|
1310
|
+
prev: VirtualDOM[],
|
|
1311
|
+
parent: DOMNode | undefined,
|
|
1312
|
+
) {
|
|
1313
|
+
function getNodeKey(node: VirtualDOM) {
|
|
1314
|
+
if (Array.isArray(node)) return 0;
|
|
1315
|
+
if (isVNode(node)) {
|
|
1316
|
+
if (node.key) {
|
|
1317
|
+
return String(node.key);
|
|
1318
|
+
}
|
|
1319
|
+
// So... if you change the value of a select option, it changes the parent select.value. Which is...
|
|
1320
|
+
// odd. We can easily fix this by just using the value as the key, which prevents us from ever
|
|
1321
|
+
// changing the .value
|
|
1322
|
+
if (node.type === "option") {
|
|
1323
|
+
return String(node.props.value);
|
|
1324
|
+
}
|
|
1325
|
+
let type = node.type;
|
|
1326
|
+
if (typeof type === "function") {
|
|
1327
|
+
return QRenderClass.componentIdDedupe(type);
|
|
1328
|
+
}
|
|
1329
|
+
return type;
|
|
1330
|
+
}
|
|
1331
|
+
return typeof node;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
let anchor: { parent: DOMNode } | { prev: DOMNode } | undefined = parent ? { parent } : undefined;
|
|
1336
|
+
|
|
1337
|
+
let sourceNodes = new Map<string | number, VirtualDOM[]>();
|
|
1338
|
+
if (!Array.isArray(prev)) {
|
|
1339
|
+
// IMPORTANT! prev was probably an error, look at `prev.props.children[0]` to see the actual error.
|
|
1340
|
+
console.warn(`prev is not an array, this is unexpected`, prev);
|
|
1341
|
+
debugger;
|
|
1342
|
+
prev = [];
|
|
1343
|
+
}
|
|
1344
|
+
for (let node of prev) {
|
|
1345
|
+
let key = getNodeKey(node);
|
|
1346
|
+
let nodes = sourceNodes.get(key);
|
|
1347
|
+
if (!nodes) sourceNodes.set(key, nodes = []);
|
|
1348
|
+
nodes.push(node);
|
|
1349
|
+
}
|
|
1350
|
+
for (let i = 0; i < next.length; i++) {
|
|
1351
|
+
let node = next[i];
|
|
1352
|
+
let key = getNodeKey(node);
|
|
1353
|
+
let match = sourceNodes.get(key)?.shift();
|
|
1354
|
+
|
|
1355
|
+
if (onTransformNode) {
|
|
1356
|
+
let inputNode = node;
|
|
1357
|
+
node = onTransformNode(match, node);
|
|
1358
|
+
if (node !== inputNode) {
|
|
1359
|
+
node = QRenderClass.normalizeVNode({ vNode: node });
|
|
1360
|
+
next[i] = node;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
let result: DOMNode | undefined;
|
|
1365
|
+
// NOTE: At first components will return undefined, and everything will anchor to the same node.
|
|
1366
|
+
// But once components start having dom values, they will all stack up, and our order will make sense.
|
|
1367
|
+
if (match === undefined) {
|
|
1368
|
+
result = onNewNode(node, anchor, parent);
|
|
1369
|
+
if (result !== undefined) anchor = { prev: result };
|
|
1370
|
+
} else {
|
|
1371
|
+
result = onUpdatedNode(match, node, anchor, parent);
|
|
1372
|
+
if (result !== undefined) anchor = { prev: result };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (isVNode(node)) {
|
|
1376
|
+
if (typeof node.type === "string") {
|
|
1377
|
+
let children = node.props.children;
|
|
1378
|
+
let matchChildren = (match && isVNode(match) && match.props.children) ?? [];
|
|
1379
|
+
iterate(children as VirtualDOM[], matchChildren as VirtualDOM[], result);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
for (let nodes of sourceNodes.values()) {
|
|
1385
|
+
for (let node of nodes) {
|
|
1386
|
+
onRemovedNode(node);
|
|
1387
|
+
removeRecursively(node);
|
|
1388
|
+
function removeRecursively(node: VirtualDOM) {
|
|
1389
|
+
if (isVNode(node) && typeof node.type === "string") {
|
|
1390
|
+
let children = node.props.children as VirtualDOM[];
|
|
1391
|
+
for (let child of children) {
|
|
1392
|
+
onRemovedNode(child);
|
|
1393
|
+
removeRecursively(child);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
private disposing = false;
|
|
1403
|
+
public disposed = false;
|
|
1404
|
+
private dispose() {
|
|
1405
|
+
if (this.disposed) return;
|
|
1406
|
+
this.disposing = true;
|
|
1407
|
+
this.disposed = true;
|
|
1408
|
+
|
|
1409
|
+
componentRendered(this);
|
|
1410
|
+
qreact.allComponents.delete(this);
|
|
1411
|
+
|
|
1412
|
+
let disposers = disposeWatchers.get(this);
|
|
1413
|
+
if (disposers) {
|
|
1414
|
+
disposeWatchers.delete(this);
|
|
1415
|
+
for (let disposer of disposers) {
|
|
1416
|
+
try {
|
|
1417
|
+
disposer();
|
|
1418
|
+
} catch (e: any) {
|
|
1419
|
+
console.error(`Error in dispose callback for ${this.debugName}, ${disposer.name}, ${e.stack}`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
let self = this;
|
|
1425
|
+
let instance = self.instance;
|
|
1426
|
+
|
|
1427
|
+
instance.componentWillUnmount && logErrors(proxyWatcher.commitFunction({
|
|
1428
|
+
debugName: `${this.debugName}.componentWillUnmount`,
|
|
1429
|
+
watchFunction() {
|
|
1430
|
+
instance.componentWillUnmount?.();
|
|
1431
|
+
},
|
|
1432
|
+
}));
|
|
1433
|
+
|
|
1434
|
+
// Recursively dispose all children
|
|
1435
|
+
for (let child of this.childComponents.values()) {
|
|
1436
|
+
QRenderClass.getInstance(child).dispose();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
this.renderWatcher?.dispose();
|
|
1440
|
+
this.VNodeWatcher.dispose();
|
|
1441
|
+
this.mountDOMWatcher.dispose();
|
|
1442
|
+
renderClasses.delete(this.id);
|
|
1443
|
+
|
|
1444
|
+
this.renderWatcher = undefined as any;
|
|
1445
|
+
this.VNodeWatcher = undefined as any;
|
|
1446
|
+
this.mountDOMWatcher = undefined as any;
|
|
1447
|
+
this.instance = undefined as any;
|
|
1448
|
+
this.domNodes = undefined as any;
|
|
1449
|
+
this.childComponents = undefined as any;
|
|
1450
|
+
this.vNode = undefined as any;
|
|
1451
|
+
this.prevMountedVNode = undefined as any;
|
|
1452
|
+
this.internalRootDOMNodes = undefined as any;
|
|
1453
|
+
|
|
1454
|
+
// Clean up all synced fields too
|
|
1455
|
+
logErrors(proxyWatcher.commitFunction({
|
|
1456
|
+
debugName: `${this.debugName}.dispose`,
|
|
1457
|
+
watchFunction() {
|
|
1458
|
+
let data = self.data();
|
|
1459
|
+
for (let key in Object.keys(data)) {
|
|
1460
|
+
delete (data as any)[key];
|
|
1461
|
+
}
|
|
1462
|
+
delete schema()[LOCAL_DOMAIN].qreact.components[self.id];
|
|
1463
|
+
},
|
|
1464
|
+
}));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
public static getInstance(id: number) {
|
|
1468
|
+
let instance = renderClasses.get(id);
|
|
1469
|
+
if (!instance) throw new Error(`Could not find component with id ${id}`);
|
|
1470
|
+
return instance;
|
|
1471
|
+
}
|
|
1472
|
+
public static getData(id: number) {
|
|
1473
|
+
return schema()[LOCAL_DOMAIN].qreact.components[id];
|
|
1474
|
+
}
|
|
1475
|
+
public static getInstanceAllowedUndefined(id: number) {
|
|
1476
|
+
return renderClasses.get(id);
|
|
1477
|
+
}
|
|
1478
|
+
public static getInstanceFromDOM(element: HTMLElement) {
|
|
1479
|
+
let id = "";
|
|
1480
|
+
while (!id && element) {
|
|
1481
|
+
id = element.getAttribute("data-qreact-id") || "";
|
|
1482
|
+
element = element.parentElement as HTMLElement || undefined;
|
|
1483
|
+
}
|
|
1484
|
+
return __INTERNAL__QRenderClass.getInstanceAllowedUndefined(Number(id));
|
|
1485
|
+
}
|
|
1486
|
+
public getParent() {
|
|
1487
|
+
let parentId = atomic(this.data().parentComponentId);
|
|
1488
|
+
if (parentId === undefined) return undefined;
|
|
1489
|
+
return QRenderClass.getInstance(parentId);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
public getVSCodeLink(): string | undefined {
|
|
1493
|
+
return getSourceVSCodeLink(this.internalRootDOMNodes?.[0]);
|
|
1494
|
+
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
public getDebugComponentStack(): QRenderClass[] {
|
|
1498
|
+
let stack: QRenderClass[] = [];
|
|
1499
|
+
let cur: number | undefined = this.id;
|
|
1500
|
+
while (cur !== undefined) {
|
|
1501
|
+
let instance = QRenderClass.getInstanceAllowedUndefined(cur || -1);
|
|
1502
|
+
if (!instance) break;
|
|
1503
|
+
stack.push(instance);
|
|
1504
|
+
cur = instance.config.parentId;
|
|
1505
|
+
}
|
|
1506
|
+
return stack;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export const __INTERNAL__QRenderClass = QRenderClass;
|
|
1511
|
+
|
|
1512
|
+
let defaultErrorHandler: ErrorHandler = ({ error, debugName }) => {
|
|
1513
|
+
if (Querysub.isAllSynced()) {
|
|
1514
|
+
console.error(`Render error in ${debugName}`, error);
|
|
1515
|
+
// Throw, so we get a good callstack
|
|
1516
|
+
setImmediate(() => { throw error; });
|
|
1517
|
+
}
|
|
1518
|
+
return (
|
|
1519
|
+
createElement("div", {
|
|
1520
|
+
style: {
|
|
1521
|
+
background: "#fdd",
|
|
1522
|
+
color: `hsl(0, 0%, 7%)`,
|
|
1523
|
+
whiteSpace: "pre-wrap",
|
|
1524
|
+
padding: "5px 10px",
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
canHaveChildren(error) ? String(error.stack) : String(error)
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
};
|
|
1531
|
+
qreact.errorHandler = defaultErrorHandler;
|
|
1532
|
+
|
|
1533
|
+
export function isFragment(value: VirtualDOM): boolean {
|
|
1534
|
+
if (!isVNode(value)) return false;
|
|
1535
|
+
return (
|
|
1536
|
+
value.type === qreact.Fragment
|
|
1537
|
+
|| (
|
|
1538
|
+
typeof value.type === "function"
|
|
1539
|
+
&& value.type.constructor === Function
|
|
1540
|
+
&& (
|
|
1541
|
+
value.type.toString() === "function d(n) { return n.children; }"
|
|
1542
|
+
// function v(e){return e.children}
|
|
1543
|
+
|| /function .\(.\)\ ?{ ?return .\.children\ ?}/.test(value.type.toString())
|
|
1544
|
+
)
|
|
1545
|
+
)
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
let unsafeVirtualDOM = false;
|
|
1550
|
+
function temporaryUnsafeVirtualDOM<T>(fnc: () => T): T {
|
|
1551
|
+
let prev = unsafeVirtualDOM;
|
|
1552
|
+
unsafeVirtualDOM = true;
|
|
1553
|
+
try {
|
|
1554
|
+
return fnc();
|
|
1555
|
+
} finally {
|
|
1556
|
+
unsafeVirtualDOM = prev;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function isVNode(vNode: VirtualDOM): vNode is VirtualDOMElement {
|
|
1560
|
+
if (
|
|
1561
|
+
!isValueProxy2(vNode) && typeof vNode === "object" && vNode && "type" in vNode
|
|
1562
|
+
|| false
|
|
1563
|
+
) {
|
|
1564
|
+
if (unsafeVirtualDOM) return true;
|
|
1565
|
+
if (vNode.allowedToRender === ALLOWED_TO_RENDER) {
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
if (typeof vNode.type === "function") {
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
if (vNode.constructor === undefined) {
|
|
1572
|
+
// preact.createElement isn't secure enough. It protects against JSON.stringify
|
|
1573
|
+
// XSS attacks, but... we don't use JSON.stringify, and we plan to preserve
|
|
1574
|
+
// the constructor, so it isn't a sufficient check for serialized values.
|
|
1575
|
+
console.warn(`preact/React VNode detected. This is not supported. Use qreact.createElement instead.`, vNode);
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
console.warn(`Invalid vNode detected. If this node has been stored in the database (and you want to render it), use qreact.rehydrateVirtualDOM before rendering it. Otherwise, if a user stored an object in the database where a string/number was required but they stored an object, just ignore this warning. It means we successfully blocked an XSS attempt.`, vNode);
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
return false;
|
|
1582
|
+
|
|
1583
|
+
}
|
|
1584
|
+
function isVNodeClass(vNode: VirtualDOM): vNode is preact.VNode<{ [key: string]: unknown }> {
|
|
1585
|
+
return isVNode(vNode) && typeof vNode.type === "function";
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// NOTE: We accept anything for vNode, otherwise we run into compatibility issues related to the type,
|
|
1589
|
+
// and we want to be able to support at least any preact version.
|
|
1590
|
+
function mountToDOM(vNode: any, parent: DOMNode) {
|
|
1591
|
+
class RootComponent extends qreact.Component {
|
|
1592
|
+
public render() {
|
|
1593
|
+
return vNode;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
new QRenderClass({ vNode: createElement(RootComponent) as any, props: {}, rawProps: {}, masterRootDOMNode: parent });
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
function createDOMNode(vNode: VirtualDOM, isSVG: boolean): DOMNode {
|
|
1601
|
+
if (isVNode(vNode)) {
|
|
1602
|
+
if (typeof vNode.type !== "string") {
|
|
1603
|
+
throw new Error(`Cannot create DOM node for component ${vNode.type?.name}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (vNode.type === "svg" || isSVG) {
|
|
1606
|
+
return document.createElementNS("http://www.w3.org/2000/svg", vNode.type);
|
|
1607
|
+
}
|
|
1608
|
+
return document.createElement(vNode.type);
|
|
1609
|
+
}
|
|
1610
|
+
return document.createTextNode(String(vNode)) as any;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
type EventHandlerType = DOMNode & {
|
|
1614
|
+
_eventHandlers?: {
|
|
1615
|
+
[key: string]: {
|
|
1616
|
+
underlyingCallback: Function;
|
|
1617
|
+
ready: boolean;
|
|
1618
|
+
wrappedCallback: EventListener;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
// https://github.com/preactjs/preact/blob/main/src/constants.js#L15C1-L16C70
|
|
1624
|
+
export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
|
|
1625
|
+
function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: VirtualDOM, owner: QRenderClass): void {
|
|
1626
|
+
if (vNode === prevVNode) return;
|
|
1627
|
+
|
|
1628
|
+
if (isVNode(vNode) && vNode.sourceModuleId) {
|
|
1629
|
+
let sourceModule = require.cache[vNode.sourceModuleId];
|
|
1630
|
+
if (sourceModule) {
|
|
1631
|
+
if (qreact.INJECT_FILENAMES) {
|
|
1632
|
+
domNode.setAttribute("data-qreact-filename", sourceModule.filename);
|
|
1633
|
+
} else {
|
|
1634
|
+
(domNode as any).sourceFileName = sourceModule.filename;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (qreact.INJECT_LINE_NUMBERS && isVNode(vNode)) {
|
|
1639
|
+
(domNode as any).callStack = vNode.callStack;
|
|
1640
|
+
let sourceModule = vNode.sourceModuleId && require.cache[vNode.sourceModuleId];
|
|
1641
|
+
if (sourceModule && sourceModule.source) {
|
|
1642
|
+
(domNode as any).codeSource = sourceModule.source;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (isVNode(vNode)) {
|
|
1647
|
+
function updateField(key: string) {
|
|
1648
|
+
if (key === "children") return;
|
|
1649
|
+
if (key === "ref") return;
|
|
1650
|
+
if (key === "ref2") return;
|
|
1651
|
+
let value = isVNode(vNode) ? vNode.props[key] : undefined;
|
|
1652
|
+
let prevValue = isVNode(prevVNode) ? prevVNode.props[key] : undefined;
|
|
1653
|
+
if (
|
|
1654
|
+
// NOTE: Object.is is required to handle NaN case
|
|
1655
|
+
Object.is(value, prevValue)
|
|
1656
|
+
// NOTE: Some values are SUPPOSED to be mutated outside of our framework, and so we always need
|
|
1657
|
+
// to update them in case they changed (ex, if the user set an invalid value and we want to revert
|
|
1658
|
+
// it back to the previous value, in which case `value === prevValue`, but domNode.value is different).
|
|
1659
|
+
&& !(
|
|
1660
|
+
key === "value"
|
|
1661
|
+
|| key === "checked"
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
) return;
|
|
1665
|
+
|
|
1666
|
+
if (key === "style") {
|
|
1667
|
+
if (typeof value === "string") {
|
|
1668
|
+
// This clobbers previous style object writes
|
|
1669
|
+
domNode.style.cssText = value;
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (canHaveChildren(value)) {
|
|
1673
|
+
if (typeof prevValue === "string") {
|
|
1674
|
+
// We have to clobber cssText
|
|
1675
|
+
domNode.style.cssText = "";
|
|
1676
|
+
}
|
|
1677
|
+
for (let [key, v] of Object.entries(value as Record<string, string | number | undefined>)) {
|
|
1678
|
+
if (typeof v === "number" && !IS_NON_DIMENSIONAL.test(key)) {
|
|
1679
|
+
v = v + "px";
|
|
1680
|
+
}
|
|
1681
|
+
(domNode as any).style[key] = v ?? "";
|
|
1682
|
+
}
|
|
1683
|
+
if (canHaveChildren(prevValue)) {
|
|
1684
|
+
for (let key in prevValue) {
|
|
1685
|
+
if (!(key in value)) {
|
|
1686
|
+
(domNode as any).style[key] = "";
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (!value) {
|
|
1693
|
+
domNode.style.cssText = "";
|
|
1694
|
+
}
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (key.startsWith("on")) {
|
|
1699
|
+
let eventNode = domNode as EventHandlerType;
|
|
1700
|
+
eventNode._eventHandlers = eventNode._eventHandlers || {};
|
|
1701
|
+
let name = key.slice(2);
|
|
1702
|
+
// Guess if we can toLowerCase it by checking if there is a corresponding "on" field (for the full name)
|
|
1703
|
+
// Sometimes we can't, ex "DOMContentLoaded"
|
|
1704
|
+
if (key.toLowerCase() in domNode) {
|
|
1705
|
+
name = name.toLowerCase();
|
|
1706
|
+
}
|
|
1707
|
+
if (typeof value !== "function") {
|
|
1708
|
+
// Remove the callback entirely
|
|
1709
|
+
eventNode.removeEventListener(name, eventNode._eventHandlers[name].wrappedCallback);
|
|
1710
|
+
delete eventNode._eventHandlers[name];
|
|
1711
|
+
} else {
|
|
1712
|
+
let prevHandler = "";
|
|
1713
|
+
if (!eventNode._eventHandlers[name]) {
|
|
1714
|
+
eventNode._eventHandlers[name] = {
|
|
1715
|
+
underlyingCallback: value,
|
|
1716
|
+
ready: false,
|
|
1717
|
+
wrappedCallback: wrapEventCallback(`${owner.debugName}.eventHandlers.${key}`, name,
|
|
1718
|
+
function (this: any, ...args: any[]) {
|
|
1719
|
+
if (!eventNode._eventHandlers?.[name].ready && name === "click") {
|
|
1720
|
+
// UPDATE: ONLY for clicks, otherwise we break blur handlers.
|
|
1721
|
+
// IMPORTANT NOTE: IF you are inside of a click handler and add a handler to
|
|
1722
|
+
// the parent, the parent's handler will be triggered. An event might
|
|
1723
|
+
// trigger a re-render which reuses the parent node for something else,
|
|
1724
|
+
// causing an unrelated handler (which is for a div that is now positioned
|
|
1725
|
+
// in an entirely different UI location) to be triggerd.
|
|
1726
|
+
// - I observed this in chrome, with regular click handlers. BUT, even if the
|
|
1727
|
+
// all browsers paused immediate handlers for a bit... it is possible for us to add
|
|
1728
|
+
// a handler without "adding a handler", by setting the underlyingCallback.
|
|
1729
|
+
// So... we absolutely have to wait until the current handler has finished
|
|
1730
|
+
// before we start
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
let prevEvent = QRenderClass.eventComponentId;
|
|
1734
|
+
QRenderClass.eventComponentId = owner.id;
|
|
1735
|
+
try {
|
|
1736
|
+
eventNode._eventHandlers?.[name]?.underlyingCallback.apply(this, args as any);
|
|
1737
|
+
} finally {
|
|
1738
|
+
QRenderClass.eventComponentId = prevEvent;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
),
|
|
1742
|
+
};
|
|
1743
|
+
eventNode.addEventListener(name, eventNode._eventHandlers[name].wrappedCallback);
|
|
1744
|
+
} else {
|
|
1745
|
+
prevHandler = eventNode._eventHandlers[name].underlyingCallback.toString();
|
|
1746
|
+
}
|
|
1747
|
+
eventNode._eventHandlers[name].underlyingCallback = value;
|
|
1748
|
+
// ONLY if the handler looks like it changes (and we have to use .toString, as it is almost certainly
|
|
1749
|
+
// a lambda, which will never ===), otherwise we run into issues related to call predictions... or something.
|
|
1750
|
+
if (prevHandler !== value.toString()) {
|
|
1751
|
+
eventNode._eventHandlers[name].ready = false;
|
|
1752
|
+
setTimeout(() => {
|
|
1753
|
+
let handler = eventNode._eventHandlers?.[name];
|
|
1754
|
+
if (handler) {
|
|
1755
|
+
handler.ready = true;
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
let isSVG = domNode.namespaceURI === "http://www.w3.org/2000/svg";
|
|
1764
|
+
if (isSVG) {
|
|
1765
|
+
if (key === "className") {
|
|
1766
|
+
key = "class";
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
// It's nice to allow "class" as a key name, as it is shorter than "className", and closer to HTML
|
|
1770
|
+
if (key === "class") {
|
|
1771
|
+
key = "className";
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// TODO: Maybe add the extra logic from preact here
|
|
1776
|
+
// - ALTHOUGH, attributes aren't hugely important anyways. We do pretty much everything with className and events...
|
|
1777
|
+
// - https://github.com/preactjs/preact/blob/main/src/diff/props.js#L77
|
|
1778
|
+
if (key in domNode && !isSVG) {
|
|
1779
|
+
let nodeTyped = domNode as any;
|
|
1780
|
+
if (key === "title") {
|
|
1781
|
+
value = value ?? "";
|
|
1782
|
+
}
|
|
1783
|
+
if (key === "value" && domNode.tagName === "SELECT") {
|
|
1784
|
+
// HACK: We have to set select value later, because only values which are
|
|
1785
|
+
// equal to child options are allowed.
|
|
1786
|
+
void Promise.resolve().finally(() => {
|
|
1787
|
+
nodeTyped[key] = value;
|
|
1788
|
+
});
|
|
1789
|
+
} else {
|
|
1790
|
+
nodeTyped[key] = value;
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
if (value === undefined || value === false) {
|
|
1794
|
+
domNode.removeAttribute(key);
|
|
1795
|
+
} else {
|
|
1796
|
+
domNode.setAttribute(key, String(value ?? ""));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
// IMPORTANT! Remove first, so keys that map to the same value ("class" and "className" => "className"),
|
|
1801
|
+
// don't apply the removal of a "class" property to clobber a valid "className" value.
|
|
1802
|
+
if (isVNode(prevVNode)) {
|
|
1803
|
+
for (let key in prevVNode.props) {
|
|
1804
|
+
if (key in vNode.props) continue;
|
|
1805
|
+
updateField(key);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
for (let key in vNode.props) {
|
|
1809
|
+
updateField(key);
|
|
1810
|
+
}
|
|
1811
|
+
} else {
|
|
1812
|
+
// NOTE: I'm not sure if updating textContent is better or worse than creating a new Text node?
|
|
1813
|
+
// As in, should we use `prevNode.replaceWith(document.createTextNode(result))`?
|
|
1814
|
+
(domNode as any as Text).textContent = String(vNode);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function hasEventHandlers(node: DOMNode, type: string): boolean {
|
|
1819
|
+
let eventNode = node as EventHandlerType;
|
|
1820
|
+
return eventNode._eventHandlers?.[type] !== undefined;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function wrapEventCallback(debugName: string, eventName: string, baseFunction: Function) {
|
|
1824
|
+
return function wrappedCallback(this: any, ...args: any[]) {
|
|
1825
|
+
let event = args[0];
|
|
1826
|
+
if (event && typeof event === "object" && event instanceof Event && event.currentTarget) {
|
|
1827
|
+
let currentTarget = event.currentTarget as HTMLInputElement;
|
|
1828
|
+
let value = currentTarget.value ?? "";
|
|
1829
|
+
let checked = currentTarget.checked ?? false;
|
|
1830
|
+
|
|
1831
|
+
let currentTargetTyped = currentTarget as any as { [key: string]: unknown };
|
|
1832
|
+
|
|
1833
|
+
let proxy = createPathValueProxy({
|
|
1834
|
+
getCallback(pathStr) {
|
|
1835
|
+
if (pathStr === getPathStr1("value")) {
|
|
1836
|
+
return { value };
|
|
1837
|
+
}
|
|
1838
|
+
if (pathStr === getPathStr1("checked")) {
|
|
1839
|
+
return { value: checked };
|
|
1840
|
+
}
|
|
1841
|
+
if (pathStr === getPathStr1("valueAsNumber")) {
|
|
1842
|
+
console.warn(yellow("Avoid using valueAsNumber, which conflicts with InputLabel display rounding (showing 3.0000000000000001 as 3). Instead us `Number(value)` or `+value`."));
|
|
1843
|
+
}
|
|
1844
|
+
let targetValue = currentTargetTyped[getPathIndexAssert(pathStr, 0)];
|
|
1845
|
+
if (typeof targetValue === "function") {
|
|
1846
|
+
targetValue = targetValue.bind(currentTarget);
|
|
1847
|
+
}
|
|
1848
|
+
return { value: targetValue };
|
|
1849
|
+
},
|
|
1850
|
+
setCallback(pathStr, newValue) {
|
|
1851
|
+
let key = getPathIndexAssert(pathStr, 0);
|
|
1852
|
+
if (key === "value") {
|
|
1853
|
+
value = newValue as any;
|
|
1854
|
+
} else if (key === "checked") {
|
|
1855
|
+
checked = newValue as any;
|
|
1856
|
+
}
|
|
1857
|
+
currentTargetTyped[key] = newValue;
|
|
1858
|
+
},
|
|
1859
|
+
getSymbol(pathStr, symbol) {
|
|
1860
|
+
if (symbol === getProxyBase) {
|
|
1861
|
+
return { value: currentTargetTyped };
|
|
1862
|
+
}
|
|
1863
|
+
return { value: currentTargetTyped[symbol as any] };
|
|
1864
|
+
},
|
|
1865
|
+
getKeys(pathStr) {
|
|
1866
|
+
return Object.keys(currentTarget);
|
|
1867
|
+
},
|
|
1868
|
+
});
|
|
1869
|
+
event = cloneEvent(event);
|
|
1870
|
+
event.currentTarget = proxy;
|
|
1871
|
+
args[0] = event;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
let run = () => {
|
|
1875
|
+
let result = baseFunction.apply(this, args);
|
|
1876
|
+
if (eventName === "mousedown") {
|
|
1877
|
+
blurFixOnMouseDownHack(event as MouseEvent);
|
|
1878
|
+
}
|
|
1879
|
+
return result;
|
|
1880
|
+
};
|
|
1881
|
+
if (proxyWatcher.inWatcher()) {
|
|
1882
|
+
if (proxyWatcher.getTriggeredWatcher().options.noLocks) {
|
|
1883
|
+
// A synchronous trigger that happens when we rearrange DOM. This triggers both
|
|
1884
|
+
// onBlur, and onChange. Because we are rearranging the DOM we shouldn't have any locks,
|
|
1885
|
+
// so we have to disconnect the event.
|
|
1886
|
+
void Promise.resolve().finally(() => {
|
|
1887
|
+
wrappedCallback.apply(this, args);
|
|
1888
|
+
});
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
return run();
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// TODO: Expose errors to the component in some way?
|
|
1895
|
+
// - Maybe via a global "componentError" callback, which is subscribed in their main.
|
|
1896
|
+
logErrors(proxyWatcher.commitFunction({
|
|
1897
|
+
debugName,
|
|
1898
|
+
watchFunction: run,
|
|
1899
|
+
synchronousInit: true,
|
|
1900
|
+
}));
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function cloneEvent<T extends Event>(event: T): T {
|
|
1905
|
+
// Need to get every key, to preserve currentTarget correctly
|
|
1906
|
+
let keys = new Set<(keyof Event)>();
|
|
1907
|
+
let proto = event as any;
|
|
1908
|
+
while (proto && proto !== Object.prototype) {
|
|
1909
|
+
proto = Object.getPrototypeOf(proto);
|
|
1910
|
+
for (let key of Object.getOwnPropertyNames(proto)) {
|
|
1911
|
+
keys.add(key as any);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
let cloned = {} as any;
|
|
1915
|
+
for (let key of keys) {
|
|
1916
|
+
let value = event[key];
|
|
1917
|
+
if (typeof value === "function") {
|
|
1918
|
+
cloned[key] = (...args: unknown[]) => (event[key] as any)(...args);
|
|
1919
|
+
} else {
|
|
1920
|
+
cloned[key] = event[key];
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return cloned;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const getProxyBase = Symbol.for("getProxyBase");
|
|
1927
|
+
function getCurrentTarget<T extends EventTarget>(target: T): T {
|
|
1928
|
+
return (target as any)[getProxyBase];
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
function isTarget(event: preact.JSX.TargetedEvent): boolean {
|
|
1932
|
+
return event.target === getCurrentTarget(event.currentTarget);
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
function getDebugContextComponent() {
|
|
1936
|
+
let componentId = getCurrentComponentId();
|
|
1937
|
+
if (!componentId) return undefined;
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
let name = QRenderClass.getInstance(componentId).debugName;
|
|
1941
|
+
|
|
1942
|
+
let stack: string[] = [];
|
|
1943
|
+
let curId: number | undefined = componentId;
|
|
1944
|
+
while (curId) {
|
|
1945
|
+
let instance = QRenderClass.getInstance(curId);
|
|
1946
|
+
stack.push(instance.debugName);
|
|
1947
|
+
curId = instance.config.parentId;
|
|
1948
|
+
}
|
|
1949
|
+
stack.reverse();
|
|
1950
|
+
return { name, stack };
|
|
1951
|
+
}
|
|
1952
|
+
function getDebugContextEvent() {
|
|
1953
|
+
let rawReason = Querysub.getTriggerReason();
|
|
1954
|
+
return {
|
|
1955
|
+
currentFunction: proxyWatcher.getTriggeredWatcher().debugName,
|
|
1956
|
+
sources: Array.from(rawReason?.pathSources || []).map(x => x.source),
|
|
1957
|
+
rawReason,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function getDebugContext() {
|
|
1961
|
+
return { ...getDebugContextComponent(), ...getDebugContextEvent() };
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/** The built in element.contains is bad and throws bad and pointless errors. This code is what it should be. */
|
|
1965
|
+
function contains(parent: HTMLElement, child: HTMLElement): boolean {
|
|
1966
|
+
let cur = child as HTMLElement | null;
|
|
1967
|
+
while (cur) {
|
|
1968
|
+
if (cur === parent) return true;
|
|
1969
|
+
cur = cur.parentElement;
|
|
1970
|
+
}
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/** Blur fix.
|
|
1975
|
+
* - By default, blur happens after a delay from mousedown. However, selection (such as dragging to select
|
|
1976
|
+
* elements) often happen on mousedown, which can trigger a re-render.
|
|
1977
|
+
* This results in a situation where an input might be re-used for a different component, BEFORE we blur. This causes
|
|
1978
|
+
* the wrong component to blur. To fix this, we force blur to happen early, by trying to guess if we are going to blur.
|
|
1979
|
+
* - It should still happen AFTER the mousedown, just directly after, so that we don't have a change
|
|
1980
|
+
* to re-render and change the blur target.
|
|
1981
|
+
*/
|
|
1982
|
+
function blurFixOnMouseDownHack(event: MouseEvent) {
|
|
1983
|
+
let selected = document.activeElement;
|
|
1984
|
+
if (selected === document.body || !selected) return;
|
|
1985
|
+
if (!(selected instanceof HTMLElement)) return;
|
|
1986
|
+
let clicked = event.currentTarget as HTMLElement | null;
|
|
1987
|
+
if (!clicked) return;
|
|
1988
|
+
if (selected === clicked) return;
|
|
1989
|
+
|
|
1990
|
+
let labelRoot = clicked.closest("label");
|
|
1991
|
+
if (labelRoot) {
|
|
1992
|
+
// If we clicked on a label, and the label is for the selected element, then focus won't change
|
|
1993
|
+
if (labelRoot === selected || contains(labelRoot, selected)) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// If we clicked on a child of selected, then focus won't change
|
|
1999
|
+
if (contains(selected, clicked)) return;
|
|
2000
|
+
|
|
2001
|
+
// Looks like we are going to blur, so blur now
|
|
2002
|
+
if (selected instanceof HTMLElement && !selected.hasAttribute("data-no-early-blur")) {
|
|
2003
|
+
console.info(`Simulating early blur to prevent unblurred inputs from existing after mousedown. You can use data-no-early-blur to opt-out of this feature`, selected);
|
|
2004
|
+
selected.blur();
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
onHotReload(() => {
|
|
2009
|
+
triggerRerenderAll();
|
|
2010
|
+
});
|
|
2011
|
+
export function triggerRerenderAll() {
|
|
2012
|
+
Querysub.commit(() => {
|
|
2013
|
+
schema()[LOCAL_DOMAIN].hotreloadSeqNum++;
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// #region Go to source
|
|
2018
|
+
|
|
2019
|
+
export function getSourceInfo(element: DOMNode | undefined) {
|
|
2020
|
+
let sourceFileName = (element as any)?.sourceFileName;
|
|
2021
|
+
let callStack = (element as any)?.callStack;
|
|
2022
|
+
let codeSource = (element as any)?.codeSource;
|
|
2023
|
+
if (!sourceFileName) return undefined;
|
|
2024
|
+
let callerInfo = getCallerInfo({
|
|
2025
|
+
sourceCode: codeSource,
|
|
2026
|
+
callStack: callStack,
|
|
2027
|
+
});
|
|
2028
|
+
return {
|
|
2029
|
+
sourceFileName: sourceFileName,
|
|
2030
|
+
lineNumber: callerInfo?.lineNumber,
|
|
2031
|
+
columnNumber: callerInfo?.columnNumber,
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
export function getSourceVSCodeLink(element: DOMNode | undefined) {
|
|
2035
|
+
let sourceInfo = getSourceInfo(element);
|
|
2036
|
+
if (!sourceInfo?.sourceFileName) return undefined;
|
|
2037
|
+
/*
|
|
2038
|
+
Claude.ai says there are more parameters we can use. As it was right about selection, these might work too.
|
|
2039
|
+
selection: Specifies the line and column to focus on when opening the file. Format: "line,column".
|
|
2040
|
+
readonly: When set to true, opens the file in read-only mode.
|
|
2041
|
+
viewColumn: Determines which editor group to open the file in. Values can be 1, 2, or 3, corresponding to the left, center, and right groups respectively.
|
|
2042
|
+
files: Allows opening multiple files at once. Provide a comma-separated list of file paths relative to the workspace root.
|
|
2043
|
+
folder: Opens a specific folder in the workspace.
|
|
2044
|
+
newWindow: When set to true, opens the file or folder in a new window instead of the current one.
|
|
2045
|
+
|
|
2046
|
+
Additional notes:
|
|
2047
|
+
|
|
2048
|
+
The diff scheme: VS Code also supports a vscode://file/file1/file2 URL format for opening the diff view between two files.
|
|
2049
|
+
Command palette: You can trigger VS Code commands using vscode://file/{file}?command={commandId}.
|
|
2050
|
+
Extensions: Some VS Code extensions may add support for additional URL parameters.
|
|
2051
|
+
*/
|
|
2052
|
+
let path = `vscode://file/${sourceInfo.sourceFileName}`;
|
|
2053
|
+
if (sourceInfo.lineNumber) {
|
|
2054
|
+
path += `:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`;
|
|
2055
|
+
path += `?selection=${sourceInfo.lineNumber},${sourceInfo.columnNumber}`;
|
|
2056
|
+
}
|
|
2057
|
+
return path;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
interface DecodedMapping {
|
|
2061
|
+
generatedLine: number;
|
|
2062
|
+
generatedColumn: number;
|
|
2063
|
+
sourceIndex: number;
|
|
2064
|
+
sourceLine: number;
|
|
2065
|
+
sourceColumn: number;
|
|
2066
|
+
nameIndex?: number;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function decodeVLQ(str: string): number[] {
|
|
2070
|
+
const vlqMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2071
|
+
const decoded: number[] = [];
|
|
2072
|
+
let shift = 0;
|
|
2073
|
+
let value = 0;
|
|
2074
|
+
|
|
2075
|
+
for (let i = 0; i < str.length; i++) {
|
|
2076
|
+
const integer = vlqMap.indexOf(str[i]);
|
|
2077
|
+
value += (integer & 31) << shift;
|
|
2078
|
+
if (integer & 32) {
|
|
2079
|
+
shift += 5;
|
|
2080
|
+
} else {
|
|
2081
|
+
const shouldNegate = value & 1;
|
|
2082
|
+
value >>>= 1;
|
|
2083
|
+
decoded.push(shouldNegate ? -value : value);
|
|
2084
|
+
shift = 0;
|
|
2085
|
+
value = 0;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
return decoded;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function decodeSourceMapString(mappings: string): DecodedMapping[] {
|
|
2093
|
+
const lines = mappings.split(";");
|
|
2094
|
+
const decodedMappings: DecodedMapping[] = [];
|
|
2095
|
+
let generatedLine = 0;
|
|
2096
|
+
let generatedColumn = 0;
|
|
2097
|
+
let sourceIndex = 0;
|
|
2098
|
+
let sourceLine = 0;
|
|
2099
|
+
let sourceColumn = 0;
|
|
2100
|
+
let nameIndex = 0;
|
|
2101
|
+
|
|
2102
|
+
for (const line of lines) {
|
|
2103
|
+
generatedColumn = 0;
|
|
2104
|
+
const segments = line.split(",");
|
|
2105
|
+
|
|
2106
|
+
for (const segment of segments) {
|
|
2107
|
+
if (segment.length === 0) continue;
|
|
2108
|
+
|
|
2109
|
+
const decoded = decodeVLQ(segment);
|
|
2110
|
+
generatedColumn += decoded[0];
|
|
2111
|
+
|
|
2112
|
+
const mapping: DecodedMapping = {
|
|
2113
|
+
generatedLine,
|
|
2114
|
+
generatedColumn,
|
|
2115
|
+
sourceIndex,
|
|
2116
|
+
sourceLine,
|
|
2117
|
+
sourceColumn,
|
|
2118
|
+
};
|
|
2119
|
+
|
|
2120
|
+
if (decoded.length > 1) {
|
|
2121
|
+
sourceIndex += decoded[1];
|
|
2122
|
+
sourceLine += decoded[2];
|
|
2123
|
+
sourceColumn += decoded[3];
|
|
2124
|
+
mapping.sourceIndex = sourceIndex;
|
|
2125
|
+
mapping.sourceLine = sourceLine;
|
|
2126
|
+
mapping.sourceColumn = sourceColumn;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
if (decoded.length > 4) {
|
|
2130
|
+
nameIndex += decoded[4];
|
|
2131
|
+
mapping.nameIndex = nameIndex;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
decodedMappings.push(mapping);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
generatedLine++;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
return decodedMappings;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
function findMappedPosition(sourceMap: SourceMap, position: {
|
|
2145
|
+
line: number;
|
|
2146
|
+
column: number;
|
|
2147
|
+
}): {
|
|
2148
|
+
line: number;
|
|
2149
|
+
column: number;
|
|
2150
|
+
} | null {
|
|
2151
|
+
const decodedMappings = decodeSourceMapString(sourceMap.mappings);
|
|
2152
|
+
let indexAfter = decodedMappings.findIndex(
|
|
2153
|
+
({ generatedLine, generatedColumn }) =>
|
|
2154
|
+
generatedLine > position.line || (generatedLine === position.line && generatedColumn >= position.column)
|
|
2155
|
+
);
|
|
2156
|
+
if (indexAfter === -1) {
|
|
2157
|
+
indexAfter = decodedMappings.length;
|
|
2158
|
+
}
|
|
2159
|
+
const mapping = decodedMappings[indexAfter - 1];
|
|
2160
|
+
if (!mapping) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
return {
|
|
2164
|
+
line: mapping.sourceLine,
|
|
2165
|
+
column: mapping.sourceColumn,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
interface SourceMap {
|
|
2171
|
+
version: number;
|
|
2172
|
+
file?: string;
|
|
2173
|
+
sourceRoot?: string;
|
|
2174
|
+
sources: string[];
|
|
2175
|
+
names: string[];
|
|
2176
|
+
mappings: string;
|
|
2177
|
+
sourcesContent?: string[];
|
|
2178
|
+
}
|
|
2179
|
+
function parseSourceMap(code: string): SourceMap | null {
|
|
2180
|
+
const sourceMappingURL = "//# sourceMappingURL=data:application/json;base64,";
|
|
2181
|
+
const index = code.lastIndexOf(sourceMappingURL);
|
|
2182
|
+
|
|
2183
|
+
if (index === -1) {
|
|
2184
|
+
return null;
|
|
2185
|
+
}
|
|
2186
|
+
let endIndex = code.indexOf("\n", index);
|
|
2187
|
+
|
|
2188
|
+
const base64Start = index + sourceMappingURL.length;
|
|
2189
|
+
const base64Data = code.slice(base64Start, endIndex).trim();
|
|
2190
|
+
|
|
2191
|
+
try {
|
|
2192
|
+
const sourceMapData = Buffer.from(base64Data, "base64").toString("utf-8");
|
|
2193
|
+
return JSON.parse(sourceMapData) as SourceMap;
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
console.error("Failed to decode or parse source map:", error);
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function getCallerInfo(config: {
|
|
2201
|
+
sourceCode?: string;
|
|
2202
|
+
callStack?: string;
|
|
2203
|
+
}): {
|
|
2204
|
+
columnNumber: number;
|
|
2205
|
+
lineNumber: number;
|
|
2206
|
+
} | undefined {
|
|
2207
|
+
let stack = config.callStack;
|
|
2208
|
+
if (!stack) return undefined;
|
|
2209
|
+
let lines = stack.split("\n");
|
|
2210
|
+
let line = lines[3];
|
|
2211
|
+
let match = line.match(/(?::(\d+):(\d+))(?!.*:\d+:\d+)/);
|
|
2212
|
+
if (!match) return { columnNumber: 0, lineNumber: 0 };
|
|
2213
|
+
// Apply the sourcemap, if it exists
|
|
2214
|
+
let lineNumber = Number(match[1]);
|
|
2215
|
+
let columnNumber = Number(match[2]);
|
|
2216
|
+
if (!lineNumber || !columnNumber) return undefined;
|
|
2217
|
+
let sourceCode = config.sourceCode;
|
|
2218
|
+
let sourceMap = parseSourceMap(sourceCode || "");
|
|
2219
|
+
if (sourceMap) {
|
|
2220
|
+
let position = findMappedPosition(sourceMap, { line: lineNumber - 1, column: columnNumber - 1 });
|
|
2221
|
+
if (position) {
|
|
2222
|
+
lineNumber = position.line + 1;
|
|
2223
|
+
columnNumber = position.column + 1;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return { columnNumber, lineNumber };
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// #endregion
|
|
2230
|
+
|
|
2231
|
+
|
|
2232
|
+
// #region Unsynced
|
|
2233
|
+
|
|
2234
|
+
let unsyncedComponents = new Set<QRenderClass>();
|
|
2235
|
+
let triggeringUnsynced = false;
|
|
2236
|
+
function triggerUnsyncedNow() {
|
|
2237
|
+
if (triggeringUnsynced) return;
|
|
2238
|
+
triggeringUnsynced = true;
|
|
2239
|
+
setImmediate(() => {
|
|
2240
|
+
triggeringUnsynced = false;
|
|
2241
|
+
Querysub.commit(() => {
|
|
2242
|
+
schema()[LOCAL_DOMAIN].unsyncedSeqNum++;
|
|
2243
|
+
});
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
function watchUnsyncedComponents(): Set<ExternalRenderClass> {
|
|
2247
|
+
schema()[LOCAL_DOMAIN].unsyncedSeqNum;
|
|
2248
|
+
return unsyncedComponents;
|
|
2249
|
+
}
|
|
2250
|
+
function componentRendered(component: QRenderClass) {
|
|
2251
|
+
if (component.disposed || Querysub.isAllSynced()) {
|
|
2252
|
+
if (unsyncedComponents.has(component)) {
|
|
2253
|
+
unsyncedComponents.delete(component);
|
|
2254
|
+
triggerUnsyncedNow();
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
// NOTE: By only checking for has it means if the synced path count changes
|
|
2258
|
+
// we won't trigger. This is fine, presently the indicator only uses the count, and
|
|
2259
|
+
// we don't want to spend extra time triggering when we are clearly already lagging
|
|
2260
|
+
// (because we have unsynced components).
|
|
2261
|
+
if (!unsyncedComponents.has(component)) {
|
|
2262
|
+
unsyncedComponents.add(component);
|
|
2263
|
+
triggerUnsyncedNow();
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// #endregion
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
// #region Stats
|
|
2272
|
+
let pendingStats = new Map<string, {
|
|
2273
|
+
count: number;
|
|
2274
|
+
sum: number;
|
|
2275
|
+
}>();
|
|
2276
|
+
function registerComponentStats(type: string, time: number) {
|
|
2277
|
+
if (!qreact.DEBUG_TIME) return;
|
|
2278
|
+
let entry = pendingStats.get(type);
|
|
2279
|
+
if (!entry) {
|
|
2280
|
+
pendingStats.set(type, entry = { count: 0, sum: 0 });
|
|
2281
|
+
}
|
|
2282
|
+
entry.count++;
|
|
2283
|
+
entry.sum += time;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (isClient()) {
|
|
2287
|
+
setImmediate(() => {
|
|
2288
|
+
let loopStart = 0;
|
|
2289
|
+
clientWatcher.onLoopStart(() => {
|
|
2290
|
+
if (!qreact.DEBUG_TIME) return;
|
|
2291
|
+
loopStart = Date.now();
|
|
2292
|
+
});
|
|
2293
|
+
clientWatcher.onLoopEnd(info => {
|
|
2294
|
+
if (!qreact.DEBUG_TIME) return;
|
|
2295
|
+
let stats = Array.from(pendingStats.entries());
|
|
2296
|
+
if (stats.length === 0) return;
|
|
2297
|
+
sort(stats, x => -x[1].sum);
|
|
2298
|
+
|
|
2299
|
+
let totalTime = stats.reduce((total, [type, { sum }]) => total + sum, 0);
|
|
2300
|
+
|
|
2301
|
+
if (totalTime > qreact.DEBUG_TIME) {
|
|
2302
|
+
|
|
2303
|
+
let remaining = stats.splice(10, stats.length);
|
|
2304
|
+
let textParts: string[] = [];
|
|
2305
|
+
let loopTime = Date.now() - loopStart;
|
|
2306
|
+
textParts.push(`Render in ${blue(formatTime(totalTime))} (${green(formatPercent(totalTime / loopTime))} of ${formatTime(loopTime)} loop time)`);
|
|
2307
|
+
for (let stat of stats) {
|
|
2308
|
+
let [type, { count, sum }] = stat;
|
|
2309
|
+
textParts.push(`${type}(${count}) ${blue(formatTime(sum))} // ${green(formatPercent(sum / totalTime))}`);
|
|
2310
|
+
}
|
|
2311
|
+
let remainingSum = remaining.reduce((total, [type, { sum }]) => total + sum, 0);
|
|
2312
|
+
textParts.push(`Remaining ${blue(formatTime(remainingSum))} // ${green(formatPercent(remainingSum / totalTime))}`);
|
|
2313
|
+
|
|
2314
|
+
console.log(textParts.join(" | "));
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
pendingStats.clear();
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// #endregion
|