sliftutils 0.12.0 → 0.14.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/.gitignore CHANGED
@@ -35,4 +35,5 @@ test*.json
35
35
  center_channel.txt
36
36
  *.part
37
37
  *.ytdl
38
- cloudflare.json
38
+ cloudflare.json
39
+ data
package/index.d.ts CHANGED
@@ -135,7 +135,6 @@ declare module "sliftutils/render-utils/Input" {
135
135
  declare module "sliftutils/render-utils/InputLabel" {
136
136
  import preact from "preact";
137
137
  import { InputProps } from "./Input";
138
- import { URLParamStr } from "./URLParam";
139
138
  export type InputLabelProps = Omit<InputProps, "label" | "title"> & {
140
139
  label?: preact.ComponentChild;
141
140
  number?: boolean;
@@ -164,7 +163,9 @@ declare module "sliftutils/render-utils/InputLabel" {
164
163
  render(): preact.JSX.Element;
165
164
  }
166
165
  export declare class InputLabelURL extends preact.Component<InputLabelProps & {
167
- persisted: URLParamStr;
166
+ persisted: {
167
+ value: unknown;
168
+ };
168
169
  }> {
169
170
  render(): preact.JSX.Element;
170
171
  }
@@ -318,19 +319,23 @@ declare module "sliftutils/render-utils/Table" {
318
319
  }
319
320
 
320
321
  declare module "sliftutils/render-utils/URLParam" {
321
- export declare class URLParamStr {
322
- readonly urlKey: string;
323
- private state;
324
- lastSetValue: string;
325
- constructor(urlKey: string);
326
- forceUpdate(): void;
327
- get(): string;
328
- set(value: string): void;
329
- get value(): string;
330
- set value(value: string);
322
+ export declare class URLParam<T = unknown> {
323
+ readonly key: string;
324
+ private defaultValue;
325
+ constructor(key: string, defaultValue?: T);
326
+ valueSeqNum: {
327
+ value: number;
328
+ };
329
+ get(): T;
330
+ set(value: T): void;
331
+ reset(): void;
332
+ getOverride(value: T): [string, string];
333
+ get value(): T;
334
+ set value(value: T);
331
335
  }
332
- export declare function batchUrlUpdate<T>(code: () => T): T;
333
- export declare function createLink(params: [URLParamStr, string][]): string;
336
+ export declare function getResolvedParam(param: [URLParam, unknown] | [string, string]): [string, string];
337
+ export declare function batchURLParamUpdate(params: ([URLParam, unknown] | [string, string])[]): void;
338
+ export declare function getCurrentUrl(): string;
334
339
 
335
340
  }
336
341
 
@@ -356,6 +361,15 @@ declare module "sliftutils/render-utils/modal" {
356
361
 
357
362
  }
358
363
 
364
+ declare module "sliftutils/render-utils/niceStringify" {
365
+ export declare const niceStringifyTrue = "";
366
+ export declare const niceStringifyNan = "{NaN}";
367
+ export declare const niceStringifyUndefined = "{Undefined}";
368
+ export declare function niceStringify(value: unknown): string;
369
+ export declare function niceParse(str: string | undefined, noSpecialTrue?: boolean): unknown;
370
+
371
+ }
372
+
359
373
  declare module "sliftutils/render-utils/observer" {
360
374
  import * as preact from "preact";
361
375
  import { Reaction } from "mobx";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -12,6 +12,7 @@
12
12
  "update-types": "yarn emit-dts && yarn generate-index-dts",
13
13
  "emit-dts": "yarn tsc --project tsconfig.declarations.json || true",
14
14
  "generate-index-dts": "typenode ./builders/generateIndexDts.ts",
15
+ "prepublishOnly": "yarn update-types",
15
16
  "run-nodejs": "node ./build-nodejs/server.js",
16
17
  "run-nodejs-dev": "typenode ./nodejs/server.ts",
17
18
  "run-web": "node ./builders/webRun.js",
@@ -0,0 +1,62 @@
1
+ import preact from "preact";
2
+ import { URLParam, batchURLParamUpdate, getResolvedParam } from "./URLParam";
3
+ import { css, isNode } from "typesafecss";
4
+
5
+ export const AnchorClass = (
6
+ css.textDecoration("none").color("hsl(210, 75%, 65%)").opacity(0.8, "hover")
7
+ );
8
+
9
+ export class Anchor extends preact.Component<{
10
+ className?: string;
11
+ params: ([URLParam, unknown] | [string, string])[];
12
+ button?: boolean;
13
+ } & Omit<preact.JSX.HTMLAttributes<HTMLAnchorElement>, "href">> {
14
+ render() {
15
+ const { params, button, className, ...remaining } = this.props;
16
+ let resolvedParams = params.map(getResolvedParam);
17
+ let searchObj = new URLSearchParams(window.location.search);
18
+ let selected = resolvedParams.every(([param, value]) => searchObj.get(param) === value);
19
+ let link = (
20
+ <a
21
+ {...remaining}
22
+ className={
23
+ css.textDecoration("none")
24
+ .opacity(0.8, "hover")
25
+ + (selected && css.color("hsl(110, 75%, 65%)", "soft"))
26
+ + (!selected && css.color("hsl(210, 75%, 65%)", "soft"))
27
+ + className
28
+ }
29
+ href={createLink(resolvedParams)}
30
+ onClick={e => {
31
+ if (this.props.target) return;
32
+ e.preventDefault();
33
+ e.stopPropagation();
34
+ batchURLParamUpdate(params);
35
+ }}
36
+ >
37
+ {this.props.children}
38
+ </a>
39
+ );
40
+ if (button) {
41
+ return <button className={css.button} onClick={() => {
42
+ batchURLParamUpdate(params);
43
+ }}>
44
+ {link}
45
+ </button>;
46
+ }
47
+ return link;
48
+ }
49
+ }
50
+
51
+ function createLink(params: ([string, string])[]) {
52
+ let searchParams = new URLSearchParams(isNode() ? "https://planquickly.com" : window.location.search);
53
+ for (let [param, value] of params) {
54
+ searchParams.set(param, value);
55
+ }
56
+ return "?" + searchParams.toString();
57
+ }
58
+
59
+ export function createLinkRaw(params: ([URLParam, unknown])[]) {
60
+ let paramsText = params.map(getResolvedParam);
61
+ return createLink(paramsText);
62
+ }
@@ -1,6 +1,5 @@
1
1
  import preact from "preact";
2
2
  import { InputProps } from "./Input";
3
- import { URLParamStr } from "./URLParam";
4
3
  export type InputLabelProps = Omit<InputProps, "label" | "title"> & {
5
4
  label?: preact.ComponentChild;
6
5
  number?: boolean;
@@ -29,7 +28,9 @@ export declare class InputLabel extends preact.Component<InputLabelProps> {
29
28
  render(): preact.JSX.Element;
30
29
  }
31
30
  export declare class InputLabelURL extends preact.Component<InputLabelProps & {
32
- persisted: URLParamStr;
31
+ persisted: {
32
+ value: unknown;
33
+ };
33
34
  }> {
34
35
  render(): preact.JSX.Element;
35
36
  }
@@ -1,7 +1,6 @@
1
1
  import preact from "preact";
2
2
  import { Input, InputProps } from "./Input";
3
3
  import { css } from "typesafecss";
4
- import { URLParamStr } from "./URLParam";
5
4
  import { lazy } from "socket-function/src/caching";
6
5
  import { observer } from "./observer";
7
6
  import { observable } from "mobx";
@@ -83,29 +82,21 @@ export class InputLabel extends preact.Component<InputLabelProps> {
83
82
 
84
83
  if ((!props.type || props.type === "number") && props.useDateUI) {
85
84
  let value = String(props.value);
86
- if (
87
- value === "date-1be2200d-9742-4e90-a6c2-e8a790277414"
88
- || isJSNumber(value) && startGuessDateRange < +value && +value < endGuessDateRange
89
- // NOTE: Showing the date selector is a problem, as there is not an easy way to undo this. So if the
90
- // user typed in "2020", it would return the input into a selector and not let them keeping type.
91
- // || startGuessDateRange < +new Date(String(value)) && +new Date(String(value)) < endGuessDateRange
92
- ) {
93
- props.type = "datetime-local";
94
- props.edit = false;
95
- props.textarea = false;
96
- props.number = false;
97
- props.forceInputValueUpdatesWhenFocused = true;
98
- // NOTE: When using forceInputValueUpdatesWhenFocused we need hot, otherwise the user's updates
99
- // won't be visible.
100
- props.hot = true;
101
- if (isJSNumber(value)) {
102
- value = formatDateTimeForInput(+value);
103
- } else {
104
- value = "";
105
- }
106
- props.value = value;
107
- addValueMapping(value => (+new Date(value).getTime() || "") + "");
85
+ props.type = "datetime-local";
86
+ props.edit = false;
87
+ props.textarea = false;
88
+ props.number = false;
89
+ props.forceInputValueUpdatesWhenFocused = true;
90
+ // NOTE: When using forceInputValueUpdatesWhenFocused we need hot, otherwise the user's updates
91
+ // won't be visible.
92
+ props.hot = true;
93
+ if (isJSNumber(value)) {
94
+ value = formatDateTimeForInput(+value);
95
+ } else {
96
+ value = "";
108
97
  }
98
+ props.value = value;
99
+ addValueMapping(value => (+new Date(value).getTime() || "") + "");
109
100
  }
110
101
  if (props.fontSize !== undefined) {
111
102
  props.style = { ...props.style as any, fontSize: props.fontSize };
@@ -260,7 +251,7 @@ const pencilSVG = lazy(() => {
260
251
 
261
252
  @observer
262
253
  export class InputLabelURL extends preact.Component<InputLabelProps & {
263
- persisted: URLParamStr;
254
+ persisted: { value: unknown };
264
255
  }> {
265
256
  render() {
266
257
  this.props.persisted.value;
@@ -1,13 +1,17 @@
1
- export declare class URLParamStr {
2
- readonly urlKey: string;
3
- private state;
4
- lastSetValue: string;
5
- constructor(urlKey: string);
6
- forceUpdate(): void;
7
- get(): string;
8
- set(value: string): void;
9
- get value(): string;
10
- set value(value: string);
1
+ export declare class URLParam<T = unknown> {
2
+ readonly key: string;
3
+ private defaultValue;
4
+ constructor(key: string, defaultValue?: T);
5
+ valueSeqNum: {
6
+ value: number;
7
+ };
8
+ get(): T;
9
+ set(value: T): void;
10
+ reset(): void;
11
+ getOverride(value: T): [string, string];
12
+ get value(): T;
13
+ set value(value: T);
11
14
  }
12
- export declare function batchUrlUpdate<T>(code: () => T): T;
13
- export declare function createLink(params: [URLParamStr, string][]): string;
15
+ export declare function getResolvedParam(param: [URLParam, unknown] | [string, string]): [string, string];
16
+ export declare function batchURLParamUpdate(params: ([URLParam, unknown] | [string, string])[]): void;
17
+ export declare function getCurrentUrl(): string;
@@ -1,84 +1,101 @@
1
1
  import { isNode } from "typesafecss";
2
2
  import { observable } from "mobx";
3
+ import { throttleFunction } from "socket-function/src/misc";
4
+ import { niceParse, niceStringify } from "./niceStringify";
3
5
 
4
- let allParams: URLParamStr[] = [];
6
+ let urlParamLookup = new Map<string, URLParam<unknown>>();
7
+ let pauseUpdate = false;
5
8
 
6
- let updated: URLParamStr[] = [];
7
-
8
- export class URLParamStr {
9
- private state = observable({
10
- seqNum: 0
11
- });
12
- public lastSetValue = "";
13
- constructor(public readonly urlKey: string) {
14
- allParams.push(this);
9
+ export class URLParam<T = unknown> {
10
+ constructor(public readonly key: string, private defaultValue: T = "" as any) {
11
+ urlParamLookup.set(key, this);
15
12
  }
16
- public forceUpdate() {
17
- this.state.seqNum++;
13
+ valueSeqNum = observable({ value: 1 });
14
+ public get(): T {
15
+ urlBackSeqNum.value;
16
+ this.valueSeqNum.value;
17
+ let value = new URL(getCurrentUrl()).searchParams.get(this.key);
18
+ if (value === null) {
19
+ return this.defaultValue;
20
+ }
21
+ return niceParse(value) as T;
18
22
  }
19
-
20
- public get() {
21
- this.state.seqNum;
22
- return new URLSearchParams(window.location.search).get(this.urlKey) || "";
23
+ public set(value: T) {
24
+ let url = new URL(getCurrentUrl());
25
+ if (value === this.defaultValue) {
26
+ url.searchParams.delete(this.key);
27
+ } else {
28
+ url.searchParams.set(this.key, niceStringify(value));
29
+ }
30
+ if (!pauseUpdate) {
31
+ void throttledUrlPush(url.toString());
32
+ this.valueSeqNum.value++;
33
+ }
23
34
  }
24
- public set(value: string) {
25
- if (value === this.get()) return;
26
- this.lastSetValue = value;
27
- batchUrlUpdate(() => {
28
- updated.push(this);
29
- });
30
- this.state.seqNum++;
35
+ public reset() {
36
+ let url = new URL(getCurrentUrl());
37
+ url.searchParams.delete(this.key);
38
+ if (!pauseUpdate) {
39
+ void throttledUrlPush(url.toString());
40
+ this.valueSeqNum.value++;
41
+ }
42
+ }
43
+
44
+ public getOverride(value: T): [string, string] {
45
+ return [this.key, value as any];
31
46
  }
32
47
 
33
48
  public get value() {
34
49
  return this.get();
35
50
  }
36
- public set value(value: string) {
51
+ public set value(value: T) {
37
52
  this.set(value);
38
53
  }
39
54
  }
40
55
 
41
- let inBatchUpdate = false;
42
- export function batchUrlUpdate<T>(code: () => T): T {
43
- if (inBatchUpdate) return code();
44
- inBatchUpdate = true;
56
+ export function getResolvedParam(param: [URLParam, unknown] | [string, string]): [string, string] {
57
+ if (typeof param[0] === "string") {
58
+ return [param[0], niceStringify(param[1])];
59
+ }
60
+ return [param[0].key, niceStringify(param[1])];
61
+ }
62
+ export function batchURLParamUpdate(params: ([URLParam, unknown] | [string, string])[]) {
63
+ let resolvedParams = params.map(getResolvedParam);
64
+ pauseUpdate = true;
65
+ let url = new URL(location.href);
45
66
  try {
46
- return code();
47
- } finally {
48
- inBatchUpdate = false;
49
-
50
- let prevUpdated = updated;
51
- updated = [];
52
- let searchParams = new URLSearchParams(window.location.search);
53
- for (let obj of prevUpdated) {
54
- searchParams.set(obj.urlKey, obj.lastSetValue);
55
- }
56
- let newURL = "?" + searchParams.toString();
57
- if (window.location.hash) {
58
- newURL += window.location.hash;
67
+ for (let [key, value] of resolvedParams) {
68
+ url.searchParams.set(key, value);
69
+ let urlParam = urlParamLookup.get(key);
70
+ urlParam?.set(niceParse(value));
59
71
  }
60
- window.history.pushState({}, "", newURL);
72
+ } finally {
73
+ pauseUpdate = false;
61
74
  }
75
+ urlBackSeqNum.value++;
76
+ void throttledUrlPush(url.toString());
62
77
  }
63
78
 
64
- export function createLink(params: [URLParamStr, string][]) {
65
- let searchParams = new URLSearchParams(window.location.search);
66
- for (let [param, value] of params) {
67
- searchParams.set(param.urlKey, value);
68
- }
69
- let newURL = "?" + searchParams.toString();
70
- if (window.location.hash) {
71
- newURL += window.location.hash;
72
- }
73
- return newURL;
79
+ export function getCurrentUrl() {
80
+ return currentBatchedUrl ?? location.href;
74
81
  }
75
82
 
83
+
84
+ let currentBatchedUrl: string | undefined;
85
+ function throttledUrlPush(url: string) {
86
+ history.pushState({}, "", url);
87
+ //currentBatchedUrl = url;
88
+ // NOTE: Stopped throttling, so when you click on links, it immediately updates the selected state.
89
+ //void throttledUrlPushBase(url);
90
+ }
91
+ const throttledUrlPushBase = throttleFunction(1000, (url: string) => {
92
+ currentBatchedUrl = undefined;
93
+ history.pushState({}, "", url);
94
+ });
95
+
96
+ let urlBackSeqNum = observable({ value: 1 });
76
97
  if (!isNode()) {
77
- // Watch for url push states
78
98
  window.addEventListener("popstate", () => {
79
- // Force all to update, in case their param changed
80
- for (let param of allParams) {
81
- param.forceUpdate();
82
- }
99
+ urlBackSeqNum.value++;
83
100
  });
84
101
  }
@@ -0,0 +1,5 @@
1
+ export declare const niceStringifyTrue = "";
2
+ export declare const niceStringifyNan = "{NaN}";
3
+ export declare const niceStringifyUndefined = "{Undefined}";
4
+ export declare function niceStringify(value: unknown): string;
5
+ export declare function niceParse(str: string | undefined, noSpecialTrue?: boolean): unknown;
@@ -0,0 +1,83 @@
1
+ // true => ""
2
+ // "" => JSON.stringify("")
3
+
4
+ export const niceStringifyTrue = "";
5
+ // Starting/ending with a JSON character means anything string that looks like this
6
+ // will be encoded like: `"{Nan`, and it is impossible for an object to serialize to look like this.
7
+ export const niceStringifyNan = `{NaN}`;
8
+ export const niceStringifyUndefined = `{Undefined}`;
9
+
10
+
11
+ // BUG: This is actually broken for hex strings. Hex strings may sometimes be entirely numbers,
12
+ // which means they will randomly change type.
13
+ function looksLikeJSON(str: string) {
14
+ return (
15
+ str === "null"
16
+ || str === "true"
17
+ || str === "false"
18
+ || str[0] === `"` && str[str.length - 1] === `"`
19
+ || str[0] === `[` && str[str.length - 1] === `]`
20
+ || str[0] === `{` && str[str.length - 1] === `}`
21
+ || (48 <= str.charCodeAt(0) && str.charCodeAt(0) <= 57)
22
+ || str.length > 1 && str[0] === "-" && (48 <= str.charCodeAt(1) && str.charCodeAt(1) <= 57)
23
+ || str === niceStringifyTrue
24
+ || str === niceStringifyUndefined
25
+ );
26
+ }
27
+
28
+ export function niceStringify(value: unknown): string {
29
+ if (value === undefined) {
30
+ return niceStringifyUndefined;
31
+ }
32
+ if (value === true) return niceStringifyTrue;
33
+ if (Number.isNaN(value)) return niceStringifyNan;
34
+
35
+ // Any strings that don't look like JSON, don't need to encoded as JSON, and can instead
36
+ // just be stored as strings.
37
+ if (typeof value === "string" && !looksLikeJSON(value)) {
38
+ return value;
39
+ }
40
+
41
+
42
+ let str = JSON.stringify(value);
43
+ if (typeof value !== "object") {
44
+ let testParse = niceParse(str);
45
+ if (testParse !== value) {
46
+ console.log(`niceStringify did not reverse correctly. Should have received ${JSON.stringify(value)}, did received ${JSON.stringify(testParse)}`);
47
+ debugger;
48
+ }
49
+ }
50
+
51
+ return str;
52
+ }
53
+
54
+ export function niceParse(str: string | undefined, noSpecialTrue = false): unknown {
55
+ if (str === undefined) {
56
+ return undefined;
57
+ }
58
+ if (str === niceStringifyTrue && !noSpecialTrue) return true;
59
+ if (str === niceStringifyNan) return Number.NaN;
60
+ if (str === niceStringifyUndefined) return undefined;
61
+ if (str === "") return str;
62
+
63
+ if (looksLikeJSON(str)) {
64
+ try {
65
+ return JSON.parse(str);
66
+ } catch { }
67
+ }
68
+ return str;
69
+ }
70
+
71
+ /*
72
+
73
+ function setFromUrlValue(key: string, valueJSON: string) {
74
+ if(isMaybeJSON(valueJSON)) {
75
+ try {
76
+ values[key] = JSON.parse(valueJSON);
77
+ return;
78
+ } catch { }
79
+ }
80
+ // Always set it, if it isn't JSON, just assume it is raw text.
81
+ values[key] = valueJSON;
82
+ }
83
+ */
package/web/browser.tsx CHANGED
@@ -4,6 +4,9 @@ import { observer } from "../render-utils/observer";
4
4
  import { css, isNode } from "typesafecss";
5
5
  import { list } from "socket-function/src/misc";
6
6
  import { enableHotReloading } from "../builders/hotReload";
7
+ import { URLParam } from "../render-utils/URLParam";
8
+
9
+ const exampleUrlParam = new URLParam("example", "");
7
10
 
8
11
  @observer
9
12
  class App extends preact.Component {