@davidsouther/jiffies 1.0.0-beta.1
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/README.md +60 -0
- package/build/assert.d.ts +23 -0
- package/build/assert.js +33 -0
- package/build/case.d.ts +1 -0
- package/build/case.js +5 -0
- package/build/components/button_bar.d.ts +8 -0
- package/build/components/button_bar.js +16 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/build/components/inline_edit.d.ts +12 -0
- package/build/components/inline_edit.js +48 -0
- package/build/components/logger.d.ts +7 -0
- package/build/components/logger.js +22 -0
- package/build/components/select.d.ts +13 -0
- package/build/components/select.js +3 -0
- package/build/components/test.d.ts +1 -0
- package/build/components/test.js +2 -0
- package/build/components/virtual_scroll.d.ts +41 -0
- package/build/components/virtual_scroll.js +94 -0
- package/build/components/virtual_scroll.test.d.ts +1 -0
- package/build/components/virtual_scroll.test.js +21 -0
- package/build/context.d.ts +15 -0
- package/build/context.js +43 -0
- package/build/context.test.d.ts +1 -0
- package/build/context.test.js +46 -0
- package/build/debounce.d.ts +1 -0
- package/build/debounce.js +7 -0
- package/build/display.d.ts +5 -0
- package/build/display.js +3 -0
- package/build/dom/css/border.d.ts +11 -0
- package/build/dom/css/border.js +27 -0
- package/build/dom/css/constants.d.ts +31 -0
- package/build/dom/css/constants.js +28 -0
- package/build/dom/css/core.d.ts +5 -0
- package/build/dom/css/core.js +24 -0
- package/build/dom/css/fstyle.d.ts +5 -0
- package/build/dom/css/fstyle.js +32 -0
- package/build/dom/css/sizing.d.ts +5 -0
- package/build/dom/css/sizing.js +10 -0
- package/build/dom/dom.d.ts +27 -0
- package/build/dom/dom.js +94 -0
- package/build/dom/fc.d.ts +14 -0
- package/build/dom/fc.js +35 -0
- package/build/dom/fc.test.d.ts +1 -0
- package/build/dom/fc.test.js +21 -0
- package/build/dom/form/form.app.d.ts +1 -0
- package/build/dom/form/form.app.js +23 -0
- package/build/dom/form/form.d.ts +25 -0
- package/build/dom/form/form.js +25 -0
- package/build/dom/form/form.test.d.ts +0 -0
- package/build/dom/form/form.test.js +1 -0
- package/build/dom/html.d.ts +117 -0
- package/build/dom/html.js +114 -0
- package/build/dom/html.test.d.ts +1 -0
- package/build/dom/html.test.js +58 -0
- package/build/dom/router/link.d.ts +6 -0
- package/build/dom/router/link.js +3 -0
- package/build/dom/router/router.d.ts +12 -0
- package/build/dom/router/router.js +49 -0
- package/build/dom/svg.d.ts +64 -0
- package/build/dom/svg.js +65 -0
- package/build/dom/test.d.ts +1 -0
- package/build/dom/test.js +2 -0
- package/build/dom/types/css.d.ts +6612 -0
- package/build/dom/types/css.js +23 -0
- package/build/dom/types/dom.d.ts +0 -0
- package/build/dom/types/dom.js +1 -0
- package/build/dom/types/html.d.ts +616 -0
- package/build/dom/types/html.js +1 -0
- package/build/dom/xml.d.ts +1 -0
- package/build/dom/xml.js +5 -0
- package/build/equal.d.ts +4 -0
- package/build/equal.js +22 -0
- package/build/equal.test.d.ts +1 -0
- package/build/equal.test.js +20 -0
- package/build/flags.d.ts +7 -0
- package/build/flags.js +48 -0
- package/build/flags.test.d.ts +1 -0
- package/build/flags.test.js +35 -0
- package/build/generator.d.ts +1 -0
- package/build/generator.js +10 -0
- package/build/generator.test.d.ts +1 -0
- package/build/generator.test.js +24 -0
- package/build/index.d.ts +13 -0
- package/build/index.js +13 -0
- package/build/is_browser.d.ts +1 -0
- package/build/is_browser.js +1 -0
- package/build/loader.d.mts +22 -0
- package/build/loader.mjs +35 -0
- package/build/lock.d.ts +1 -0
- package/build/lock.js +23 -0
- package/build/lock.test.d.ts +1 -0
- package/build/lock.test.js +16 -0
- package/build/log.d.ts +26 -0
- package/build/log.js +34 -0
- package/build/parcel_resolver.d.ts +3 -0
- package/build/parcel_resolver.js +19 -0
- package/build/range.d.ts +1 -0
- package/build/range.js +7 -0
- package/build/result.d.ts +31 -0
- package/build/result.js +65 -0
- package/build/result.test.d.ts +1 -0
- package/build/result.test.js +71 -0
- package/build/safe.d.ts +1 -0
- package/build/safe.js +10 -0
- package/build/scope/describe.d.ts +14 -0
- package/build/scope/describe.js +52 -0
- package/build/scope/display/console.d.ts +2 -0
- package/build/scope/display/console.js +21 -0
- package/build/scope/display/dom.d.ts +3 -0
- package/build/scope/display/dom.js +26 -0
- package/build/scope/display/junit.d.ts +2 -0
- package/build/scope/display/junit.js +17 -0
- package/build/scope/execute.d.ts +12 -0
- package/build/scope/execute.js +85 -0
- package/build/scope/expect.d.ts +23 -0
- package/build/scope/expect.js +107 -0
- package/build/scope/fix.d.ts +4 -0
- package/build/scope/fix.js +22 -0
- package/build/scope/index.d.ts +3 -0
- package/build/scope/index.js +3 -0
- package/build/scope/scope.d.ts +17 -0
- package/build/scope/scope.js +1 -0
- package/build/server/http/apps.d.ts +5 -0
- package/build/server/http/apps.js +23 -0
- package/build/server/http/index.d.ts +21 -0
- package/build/server/http/index.js +71 -0
- package/build/server/http/response.d.ts +4 -0
- package/build/server/http/response.js +37 -0
- package/build/server/http/sitemap.d.ts +2 -0
- package/build/server/http/sitemap.js +42 -0
- package/build/server/http/static.d.ts +2 -0
- package/build/server/http/static.js +21 -0
- package/build/server/http/typescript.d.ts +5 -0
- package/build/server/http/typescript.js +40 -0
- package/build/server/main.d.ts +2 -0
- package/build/server/main.js +9 -0
- package/build/test.d.mts +2 -0
- package/build/test.mjs +23 -0
- package/build/test_all.d.ts +1 -0
- package/build/test_all.js +19 -0
- package/build/transpile.d.mts +3 -0
- package/build/transpile.mjs +18 -0
- package/package.json +36 -0
- package/src/404.html +14 -0
- package/src/assert.ts +50 -0
- package/src/case.ts +5 -0
- package/src/components/_notes +33 -0
- package/src/components/button_bar.ts +38 -0
- package/src/components/inline_edit.ts +77 -0
- package/src/components/logger.ts +36 -0
- package/src/components/select.ts +22 -0
- package/src/components/test.js +2 -0
- package/src/components/virtual_scroll.test.ts +27 -0
- package/src/components/virtual_scroll.ts +194 -0
- package/src/context.test.ts +58 -0
- package/src/context.ts +62 -0
- package/src/debounce.ts +7 -0
- package/src/display.ts +12 -0
- package/src/dom/README.md +102 -0
- package/src/dom/css/border.ts +47 -0
- package/src/dom/css/constants.ts +34 -0
- package/src/dom/css/core.ts +28 -0
- package/src/dom/css/fstyle.ts +42 -0
- package/src/dom/css/sizing.ts +11 -0
- package/src/dom/dom.ts +153 -0
- package/src/dom/fc.test.ts +43 -0
- package/src/dom/fc.ts +79 -0
- package/src/dom/form/form.app.ts +50 -0
- package/src/dom/form/form.test.ts +0 -0
- package/src/dom/form/form.ts +53 -0
- package/src/dom/form/index.html +14 -0
- package/src/dom/html.test.ts +72 -0
- package/src/dom/html.ts +129 -0
- package/src/dom/router/link.ts +14 -0
- package/src/dom/router/router.ts +69 -0
- package/src/dom/svg.ts +77 -0
- package/src/dom/test.ts +2 -0
- package/src/dom/types/css.ts +10106 -0
- package/src/dom/types/dom.ts +0 -0
- package/src/dom/types/html.ts +631 -0
- package/src/dom/xml.ts +12 -0
- package/src/equal.test.ts +23 -0
- package/src/equal.ts +32 -0
- package/src/favicon.ico +0 -0
- package/src/flags.test.ts +43 -0
- package/src/flags.ts +53 -0
- package/src/generator.test.ts +26 -0
- package/src/generator.ts +12 -0
- package/src/hooks/_notes +3 -0
- package/src/index.html +79 -0
- package/src/is_browser.js +1 -0
- package/src/loader.mjs +45 -0
- package/src/lock.test.ts +17 -0
- package/src/lock.ts +22 -0
- package/src/log.ts +61 -0
- package/src/observable/_notes +13 -0
- package/src/observable/observable._js +175 -0
- package/src/range.ts +7 -0
- package/src/result.test.ts +98 -0
- package/src/result.ts +107 -0
- package/src/safe.ts +12 -0
- package/src/scope/describe.ts +70 -0
- package/src/scope/display/console.ts +26 -0
- package/src/scope/display/dom.ts +36 -0
- package/src/scope/display/junit.ts +67 -0
- package/src/scope/execute.ts +108 -0
- package/src/scope/expect.ts +170 -0
- package/src/scope/fix.ts +29 -0
- package/src/scope/index.ts +11 -0
- package/src/scope/scope.ts +21 -0
- package/src/server/http/apps.ts +26 -0
- package/src/server/http/index.ts +119 -0
- package/src/server/http/response.ts +47 -0
- package/src/server/http/sitemap.ts +48 -0
- package/src/server/http/static.ts +27 -0
- package/src/server/http/typescript.ts +46 -0
- package/src/server/main.ts +13 -0
- package/src/test.mjs +29 -0
- package/src/test_all.ts +22 -0
- package/src/transpile.mjs +29 -0
package/build/test.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parse } from "./flags.js";
|
|
3
|
+
import { execute } from "./scope/execute.js";
|
|
4
|
+
import { asXML } from "./scope/display/junit.js";
|
|
5
|
+
import { onConsole } from "./scope/display/console.js";
|
|
6
|
+
await import("./test_all.js");
|
|
7
|
+
(async function () {
|
|
8
|
+
const results = await execute();
|
|
9
|
+
const FLAGS = parse(process.argv);
|
|
10
|
+
switch (FLAGS.asString("mode", "console")) {
|
|
11
|
+
case "junit":
|
|
12
|
+
const xml = asXML(results);
|
|
13
|
+
console.log(xml);
|
|
14
|
+
break;
|
|
15
|
+
case "console":
|
|
16
|
+
default:
|
|
17
|
+
onConsole(results);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
if (results.failed > 0) {
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// This file must be .js for imports to run. Unused imports in .ts files are
|
|
2
|
+
// discarded during transpilation.
|
|
3
|
+
import { describe, expect, it } from "./scope/index.js";
|
|
4
|
+
await Promise.all([
|
|
5
|
+
import("./context.test.js"),
|
|
6
|
+
import("./equal.test.js"),
|
|
7
|
+
import("./flags.test.js"),
|
|
8
|
+
import("./generator.test.js"),
|
|
9
|
+
import("./lock.test.js"),
|
|
10
|
+
import("./result.test.js"),
|
|
11
|
+
]);
|
|
12
|
+
describe("Test executor", () => {
|
|
13
|
+
it("matches equality", () => {
|
|
14
|
+
expect(1).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
it("fails on inequality", () => {
|
|
17
|
+
expect(() => expect(1).toBe(2)).toThrow();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
const compilerOptions = {
|
|
4
|
+
target: ts.ScriptTarget.ESNext,
|
|
5
|
+
module: ts.ModuleKind.ESNext,
|
|
6
|
+
inlineSourceMap: true,
|
|
7
|
+
inlineSources: true,
|
|
8
|
+
};
|
|
9
|
+
const tsmap = new Map();
|
|
10
|
+
export async function transpile(
|
|
11
|
+
/** @type string */ url,
|
|
12
|
+
/** @type {() => Promise<{toString(): string}>} */ get) {
|
|
13
|
+
if (!tsmap.has(url) || true) {
|
|
14
|
+
const source = ts.transpile((await get()).toString(), compilerOptions, url, undefined, url);
|
|
15
|
+
tsmap.set(url, source);
|
|
16
|
+
}
|
|
17
|
+
return tsmap.get(url);
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@davidsouther/jiffies",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"displayName": "JEFRi Jiffies",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./*": "./build/*"
|
|
9
|
+
},
|
|
10
|
+
"typesVersions": {
|
|
11
|
+
"*": {
|
|
12
|
+
"*": [
|
|
13
|
+
"./build/*"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=16.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"node": "node --experimental-loader ./src/loader.mjs",
|
|
22
|
+
"start": "npm run node ./src/server/main.js",
|
|
23
|
+
"test": "npm run node ./src/test.mjs",
|
|
24
|
+
"ci": "node --experimental-loader ./src/loader.mjs ./src/test.mjs --mode=junit",
|
|
25
|
+
"lint": "npx prettier . --check",
|
|
26
|
+
"check": "npx tsc --noEmit",
|
|
27
|
+
"format": "npx prettier . --write",
|
|
28
|
+
"build": "npx tsc",
|
|
29
|
+
"watch": "npx tsc --watch",
|
|
30
|
+
"all": "npm run lint && npm run test && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^17.0.24",
|
|
34
|
+
"typescript": "^4.7.0-dev.20220415"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/404.html
ADDED
package/src/assert.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type AssertMessage = string | (() => string);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Throw an error when a condition is not met.
|
|
5
|
+
*/
|
|
6
|
+
export function assert(
|
|
7
|
+
condition: boolean,
|
|
8
|
+
message: AssertMessage = "Assertion failed"
|
|
9
|
+
): void | never {
|
|
10
|
+
if (!condition) {
|
|
11
|
+
throw new Error(message instanceof Function ? message() : message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Given a value, return it if it is not null nor undefined. Otherwise throw an
|
|
17
|
+
* error.
|
|
18
|
+
*
|
|
19
|
+
* @template T
|
|
20
|
+
* @returns {NonNullable<T>}
|
|
21
|
+
*/
|
|
22
|
+
export function assertExists<T>(
|
|
23
|
+
t: T,
|
|
24
|
+
message: AssertMessage = "Assertion failed: value does not exist"
|
|
25
|
+
): NonNullable<T> {
|
|
26
|
+
assert(t != null, message);
|
|
27
|
+
return t as NonNullable<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {*} n
|
|
32
|
+
* @returns string
|
|
33
|
+
*/
|
|
34
|
+
export function assertString(
|
|
35
|
+
n: unknown,
|
|
36
|
+
message: AssertMessage = () => `Assertion failed: ${n} is not a string`
|
|
37
|
+
): string {
|
|
38
|
+
assert(typeof n == "string", message);
|
|
39
|
+
return n as string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compile time assertion that no value will used at this point in control flow.
|
|
44
|
+
*/
|
|
45
|
+
export function checkExhaustive(
|
|
46
|
+
value: never,
|
|
47
|
+
message: AssertMessage = `Unexpected value ${value}`
|
|
48
|
+
): never {
|
|
49
|
+
throw new Error(message instanceof Function ? message() : message);
|
|
50
|
+
}
|
package/src/case.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Nav
|
|
2
|
+
ButtonBar
|
|
3
|
+
|
|
4
|
+
Form
|
|
5
|
+
FormGroup
|
|
6
|
+
Input
|
|
7
|
+
Select
|
|
8
|
+
CheckGroup
|
|
9
|
+
RadioGroup
|
|
10
|
+
Textarea
|
|
11
|
+
Switch
|
|
12
|
+
Button
|
|
13
|
+
|
|
14
|
+
Datatable
|
|
15
|
+
Column
|
|
16
|
+
Header
|
|
17
|
+
Row
|
|
18
|
+
Cell
|
|
19
|
+
Sort
|
|
20
|
+
|
|
21
|
+
Virtual Scroll
|
|
22
|
+
Logs Viewer
|
|
23
|
+
|
|
24
|
+
Chart
|
|
25
|
+
Axis
|
|
26
|
+
Legend
|
|
27
|
+
Bar
|
|
28
|
+
Scatter
|
|
29
|
+
Line
|
|
30
|
+
|
|
31
|
+
Graph
|
|
32
|
+
Node
|
|
33
|
+
Segment
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Display, display } from "../display.js";
|
|
2
|
+
import { FC } from "../dom/fc.js";
|
|
3
|
+
import { a, li, ul } from "../dom/html.js";
|
|
4
|
+
|
|
5
|
+
const ButtonBar = FC<{
|
|
6
|
+
// T extends Display
|
|
7
|
+
// @ts-ignore TODO(TFC)
|
|
8
|
+
value: T;
|
|
9
|
+
// @ts-ignore TODO(TFC)
|
|
10
|
+
values: T[];
|
|
11
|
+
// @ts-ignore TODO(TFC)
|
|
12
|
+
events: { onSelect: (current: T) => void };
|
|
13
|
+
}>("button-bar", (el, { value, values, events }) =>
|
|
14
|
+
ul(
|
|
15
|
+
{ class: "ButtonBar__wrapper" },
|
|
16
|
+
...values.map((option) =>
|
|
17
|
+
li(
|
|
18
|
+
a(
|
|
19
|
+
{
|
|
20
|
+
href: "#",
|
|
21
|
+
class: `ButtonBar__${`${option}`.replace(/\s+/g, "_").toLowerCase()}
|
|
22
|
+
${option === value ? "" : "secondary"}
|
|
23
|
+
`.replace(/[\n\s]+/, " "),
|
|
24
|
+
events: {
|
|
25
|
+
click: (e) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
events.onSelect(option);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
display(option)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export default ButtonBar;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { width } from "../dom/css/sizing.js";
|
|
2
|
+
import { FC, State } from "../dom/fc.js";
|
|
3
|
+
import { input, span } from "../dom/html.js";
|
|
4
|
+
|
|
5
|
+
const Mode = { VIEW: 0, EDIT: 1 };
|
|
6
|
+
|
|
7
|
+
export interface InlineEditState {
|
|
8
|
+
mode: number;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const InlineEdit = FC<
|
|
13
|
+
{
|
|
14
|
+
mode?: number;
|
|
15
|
+
value: string;
|
|
16
|
+
events: {
|
|
17
|
+
change: (value: string) => void;
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
InlineEditState
|
|
21
|
+
>("inline-edit", (el, { mode = Mode.VIEW, value, events }) => {
|
|
22
|
+
const state = (el[State] ??= { mode, value });
|
|
23
|
+
|
|
24
|
+
const render = () => {
|
|
25
|
+
switch (state.mode) {
|
|
26
|
+
case Mode.EDIT:
|
|
27
|
+
return edit();
|
|
28
|
+
case Mode.VIEW:
|
|
29
|
+
return view();
|
|
30
|
+
default:
|
|
31
|
+
return span();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const view = () =>
|
|
36
|
+
span(
|
|
37
|
+
{
|
|
38
|
+
style: { cursor: "text", ...width("full", "inline") },
|
|
39
|
+
events: {
|
|
40
|
+
click: () => {
|
|
41
|
+
state.mode = Mode.EDIT;
|
|
42
|
+
el.update(render());
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
state.value ?? ""
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const edit = () => {
|
|
50
|
+
const edit = span(
|
|
51
|
+
{ style: { display: "block", position: "relative" } },
|
|
52
|
+
input({
|
|
53
|
+
style: {
|
|
54
|
+
zIndex: "10",
|
|
55
|
+
position: "absolute",
|
|
56
|
+
left: "0",
|
|
57
|
+
marginTop: "-0.375rem",
|
|
58
|
+
},
|
|
59
|
+
events: {
|
|
60
|
+
blur: ({ target }) =>
|
|
61
|
+
events.change((target as HTMLInputElement)?.value ?? ""),
|
|
62
|
+
},
|
|
63
|
+
type: "text",
|
|
64
|
+
value: state.value,
|
|
65
|
+
}),
|
|
66
|
+
"\u00a0" // Hack to get the span to take up space
|
|
67
|
+
);
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
edit.dispatchEvent(new Event("focus"));
|
|
70
|
+
});
|
|
71
|
+
return edit;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return render();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export default InlineEdit;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { display, Display } from "../display.js";
|
|
2
|
+
import { DOMElement, Updatable } from "../dom/dom.js";
|
|
3
|
+
import { div, span, ul, li, pre, code } from "../dom/html.js";
|
|
4
|
+
import { LEVEL, Logger } from "../log.js";
|
|
5
|
+
|
|
6
|
+
export interface HTMLLogger extends Logger {
|
|
7
|
+
root: Updatable<DOMElement>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isHTMLLogger(logger: Logger): logger is HTMLLogger {
|
|
11
|
+
return (logger as HTMLLogger).root != undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makeHTMLLogger(name: string): HTMLLogger {
|
|
15
|
+
let log: Updatable<DOMElement>;
|
|
16
|
+
const root = div(div(span(name)), (log = ul()));
|
|
17
|
+
const logger: Partial<HTMLLogger> = { level: LEVEL.INFO, root };
|
|
18
|
+
|
|
19
|
+
function append(message: string): void {
|
|
20
|
+
log.appendChild(li(pre(code(message))));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const logAt =
|
|
24
|
+
(level: number) =>
|
|
25
|
+
(message: Display): void =>
|
|
26
|
+
level >= (logger.level ?? LEVEL.ERROR)
|
|
27
|
+
? append(display(message))
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
logger.debug = logAt(LEVEL.VERBOSE);
|
|
31
|
+
logger.info = logAt(LEVEL.INFO);
|
|
32
|
+
logger.warn = logAt(LEVEL.WARN);
|
|
33
|
+
logger.error = logAt(LEVEL.ERROR);
|
|
34
|
+
|
|
35
|
+
return logger as HTMLLogger;
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventHandler } from "../dom/dom.js";
|
|
2
|
+
import { FC } from "../dom/fc.js";
|
|
3
|
+
import { option, select } from "../dom/html.js";
|
|
4
|
+
|
|
5
|
+
export const Select = FC<{
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
events: {
|
|
9
|
+
change: EventHandler;
|
|
10
|
+
};
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
options: [string, string][];
|
|
13
|
+
}>(
|
|
14
|
+
"jiffies-select",
|
|
15
|
+
(el, { name, events: { change }, disabled, value, options }) =>
|
|
16
|
+
select(
|
|
17
|
+
{ name, events: { change }, disabled },
|
|
18
|
+
...options.map(([v, name]) =>
|
|
19
|
+
option({ value: v, selected: value === v }, `${name}`)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { State } from "../dom/fc.js";
|
|
2
|
+
import { div } from "../dom/html.js";
|
|
3
|
+
import { describe, it, expect } from "../scope/index.js";
|
|
4
|
+
import VirtualScroll, {
|
|
5
|
+
arrayAdapter,
|
|
6
|
+
VirtualScrollProps,
|
|
7
|
+
} from "./virtual_scroll.js";
|
|
8
|
+
|
|
9
|
+
describe("VirtualScroll", () => {
|
|
10
|
+
it("tracks scroll position", () => {
|
|
11
|
+
const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
12
|
+
|
|
13
|
+
const props: VirtualScrollProps<number, HTMLDivElement> = {
|
|
14
|
+
settings: { count: 3, startIndex: 2 },
|
|
15
|
+
get: arrayAdapter(data),
|
|
16
|
+
row: (i) => div(`${i}`),
|
|
17
|
+
};
|
|
18
|
+
// @ts-ignore TODO(TFC)
|
|
19
|
+
const scroll = VirtualScroll(props);
|
|
20
|
+
|
|
21
|
+
expect(scroll[State].bufferedItems).toBe(9);
|
|
22
|
+
expect(scroll[State].data).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
23
|
+
//expect(scroll.state.topPaddingHeight).toBe(0);
|
|
24
|
+
expect(scroll[State].viewportHeight).toBe(60);
|
|
25
|
+
//expect(scroll.state.totalHeight).toBe(200);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { debounce } from "../debounce.js";
|
|
2
|
+
import { FC, State } from "../dom/fc.js";
|
|
3
|
+
import { div, UHTMLElement } from "../dom/html.js";
|
|
4
|
+
|
|
5
|
+
export interface VirtualScrollSettings {
|
|
6
|
+
minIndex: number;
|
|
7
|
+
maxIndex: number;
|
|
8
|
+
startIndex: number;
|
|
9
|
+
itemHeight: number; // In pixels
|
|
10
|
+
count: number;
|
|
11
|
+
tolerance: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VirtualScrollDataAdapter<T> {
|
|
15
|
+
(offset: number, limit: number): Iterable<T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function arrayAdapter<T>(data: T[]): VirtualScrollDataAdapter<T> {
|
|
19
|
+
return (offset, limit) => data.slice(offset, offset + limit);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VirtualScrollProps<T, U extends HTMLElement> {
|
|
23
|
+
settings: Partial<VirtualScrollSettings>;
|
|
24
|
+
get: VirtualScrollDataAdapter<T>;
|
|
25
|
+
row: (t: T) => UHTMLElement<U>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fillVirtualScrollSettings(
|
|
29
|
+
settings: Partial<VirtualScrollSettings>
|
|
30
|
+
): VirtualScrollSettings {
|
|
31
|
+
const {
|
|
32
|
+
minIndex = 0,
|
|
33
|
+
maxIndex = 1,
|
|
34
|
+
startIndex = 0,
|
|
35
|
+
itemHeight = 20,
|
|
36
|
+
count = maxIndex - minIndex + 1,
|
|
37
|
+
tolerance = count,
|
|
38
|
+
} = settings;
|
|
39
|
+
|
|
40
|
+
return { minIndex, maxIndex, startIndex, itemHeight, count, tolerance };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function initialState<T>(
|
|
44
|
+
settings: VirtualScrollSettings
|
|
45
|
+
): VirtualScrollState<T> {
|
|
46
|
+
// From Denis Hilt, https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/
|
|
47
|
+
const { minIndex, maxIndex, startIndex, itemHeight, count, tolerance } =
|
|
48
|
+
settings;
|
|
49
|
+
const bufferedItems = count + 2 * tolerance;
|
|
50
|
+
const itemsAbove = Math.max(0, startIndex - tolerance - minIndex);
|
|
51
|
+
|
|
52
|
+
const viewportHeight = count * itemHeight;
|
|
53
|
+
const totalHeight = (maxIndex - minIndex + 1) * itemHeight;
|
|
54
|
+
const toleranceHeight = tolerance * itemHeight;
|
|
55
|
+
const bufferHeight = viewportHeight + 2 * toleranceHeight;
|
|
56
|
+
const topPaddingHeight = itemsAbove * itemHeight;
|
|
57
|
+
const bottomPaddingHeight = totalHeight - (topPaddingHeight + bufferHeight);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
scrollTop: 0,
|
|
61
|
+
settings,
|
|
62
|
+
viewportHeight,
|
|
63
|
+
totalHeight,
|
|
64
|
+
toleranceHeight,
|
|
65
|
+
bufferedItems,
|
|
66
|
+
topPaddingHeight,
|
|
67
|
+
bottomPaddingHeight,
|
|
68
|
+
data: [],
|
|
69
|
+
rows: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getData<T>(
|
|
74
|
+
minIndex: number,
|
|
75
|
+
maxIndex: number,
|
|
76
|
+
offset: number,
|
|
77
|
+
limit: number,
|
|
78
|
+
get: VirtualScrollDataAdapter<T>
|
|
79
|
+
): T[] {
|
|
80
|
+
const start = Math.max(0, minIndex, offset);
|
|
81
|
+
const end = Math.min(maxIndex, offset + limit - 1);
|
|
82
|
+
const data = get(start, end - start);
|
|
83
|
+
return [...data];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function doScroll<T>(
|
|
87
|
+
scrollTop: number,
|
|
88
|
+
state: VirtualScrollState<T>,
|
|
89
|
+
get: VirtualScrollDataAdapter<T>
|
|
90
|
+
): {
|
|
91
|
+
scrollTop: number;
|
|
92
|
+
topPaddingHeight: number;
|
|
93
|
+
bottomPaddingHeight: number;
|
|
94
|
+
data: T[];
|
|
95
|
+
} {
|
|
96
|
+
const {
|
|
97
|
+
totalHeight,
|
|
98
|
+
toleranceHeight,
|
|
99
|
+
bufferedItems,
|
|
100
|
+
settings: { itemHeight, minIndex, maxIndex },
|
|
101
|
+
} = state;
|
|
102
|
+
const index =
|
|
103
|
+
minIndex + Math.floor((scrollTop - toleranceHeight) / itemHeight);
|
|
104
|
+
const data = getData(minIndex, maxIndex, index, bufferedItems, get);
|
|
105
|
+
const topPaddingHeight = Math.max((index - minIndex) * itemHeight, 0);
|
|
106
|
+
const bottomPaddingHeight = Math.max(
|
|
107
|
+
totalHeight - (topPaddingHeight + data.length * itemHeight),
|
|
108
|
+
0
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return { scrollTop, topPaddingHeight, bottomPaddingHeight, data };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface VirtualScrollState<T, U extends HTMLElement = HTMLElement> {
|
|
115
|
+
settings: VirtualScrollSettings;
|
|
116
|
+
scrollTop: number; // px
|
|
117
|
+
bufferedItems: number; // count
|
|
118
|
+
totalHeight: number; // px
|
|
119
|
+
viewportHeight: number; // px
|
|
120
|
+
topPaddingHeight: number; // px
|
|
121
|
+
bottomPaddingHeight: number; // px
|
|
122
|
+
toleranceHeight: number; // px
|
|
123
|
+
data: T[];
|
|
124
|
+
rows: UHTMLElement<U>[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// export interface VirtualScroll<T, U extends HTMLElement> {
|
|
128
|
+
// state: VirtualScrollState<T>;
|
|
129
|
+
// rows: UHTMLElement<U>[];
|
|
130
|
+
// }
|
|
131
|
+
|
|
132
|
+
export const VirtualScroll = FC<
|
|
133
|
+
VirtualScrollProps<unknown, HTMLElement>,
|
|
134
|
+
VirtualScrollState<unknown, HTMLElement>
|
|
135
|
+
>("virtual-scroll", (element, props) => {
|
|
136
|
+
const settings = fillVirtualScrollSettings(props.settings);
|
|
137
|
+
const state = (element[State] = {
|
|
138
|
+
...initialState(settings),
|
|
139
|
+
...element[State],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const scrollTo = (
|
|
143
|
+
{ target }: { target?: { scrollTop: number } } = { target: state }
|
|
144
|
+
) => {
|
|
145
|
+
const scrollTop = target?.scrollTop ?? state.topPaddingHeight;
|
|
146
|
+
const updatedSate = {
|
|
147
|
+
...state,
|
|
148
|
+
...doScroll(scrollTop, state, props.get),
|
|
149
|
+
};
|
|
150
|
+
setState(updatedSate);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const viewportElement = div({
|
|
154
|
+
style: { height: `${state.viewportHeight}px`, overflowY: "scroll" },
|
|
155
|
+
events: { scroll: debounce(scrollTo, 0) },
|
|
156
|
+
});
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
viewportElement.scroll({ top: state.scrollTop });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const setState = (newState: VirtualScrollState<unknown>) => {
|
|
162
|
+
state.scrollTop = newState.scrollTop;
|
|
163
|
+
state.topPaddingHeight = newState.topPaddingHeight;
|
|
164
|
+
state.bottomPaddingHeight = newState.bottomPaddingHeight;
|
|
165
|
+
state.data = newState.data;
|
|
166
|
+
element[State].rows = state.data.map(props.row);
|
|
167
|
+
|
|
168
|
+
viewportElement.update(
|
|
169
|
+
div({
|
|
170
|
+
class: "VirtualScroll__topPadding",
|
|
171
|
+
style: { height: `${state.topPaddingHeight}px` },
|
|
172
|
+
}),
|
|
173
|
+
...(element[State].rows ?? []).map((row, i) =>
|
|
174
|
+
div(
|
|
175
|
+
{
|
|
176
|
+
class: `VirtualScroll__item_${i}`,
|
|
177
|
+
style: { height: `${settings.itemHeight}px` },
|
|
178
|
+
},
|
|
179
|
+
row
|
|
180
|
+
)
|
|
181
|
+
),
|
|
182
|
+
div({
|
|
183
|
+
class: "VirtualScroll__bottomPadding",
|
|
184
|
+
style: { height: `${state.bottomPaddingHeight}px` },
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
scrollTo();
|
|
190
|
+
|
|
191
|
+
return viewportElement;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export default VirtualScroll;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Context, Enter, Exit, using } from "./context.js";
|
|
2
|
+
import { Err, isErr, isOk, Ok, unwrap } from "./result.js";
|
|
3
|
+
import { describe, it } from "./scope/describe.js";
|
|
4
|
+
import { expect } from "./scope/expect.js";
|
|
5
|
+
|
|
6
|
+
describe("Context", () => {
|
|
7
|
+
it("performs an operation using a context", () => {
|
|
8
|
+
const context = TestContext();
|
|
9
|
+
const result = using(context, () => Ok(5));
|
|
10
|
+
expect(unwrap(result)).toBe(5);
|
|
11
|
+
expect(context.initialized).toBe(true);
|
|
12
|
+
expect(context.completed).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("reports the result of a thrown error", () => {
|
|
16
|
+
const context = TestContext();
|
|
17
|
+
|
|
18
|
+
const result = using(context, () => {
|
|
19
|
+
throw new Error("Failed");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(isErr(result)).toBe(true);
|
|
23
|
+
expect(Err(result as Err<Error>)).toMatchObject({
|
|
24
|
+
message: "Failed",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("passes the context to the operation", () => {
|
|
29
|
+
const op = using(TestContext, ({ initialized, completed }) => ({
|
|
30
|
+
initialized,
|
|
31
|
+
completed,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
expect(isOk(op)).toBe(true);
|
|
35
|
+
const { completed, initialized } = unwrap(op);
|
|
36
|
+
expect(initialized).toBe(true);
|
|
37
|
+
expect(completed).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
interface TestContext {
|
|
42
|
+
initialized: boolean;
|
|
43
|
+
completed: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function TestContext(): Context & TestContext {
|
|
47
|
+
const context = {
|
|
48
|
+
[Enter]: () => {
|
|
49
|
+
context.initialized = true;
|
|
50
|
+
},
|
|
51
|
+
[Exit]: () => {
|
|
52
|
+
context.completed = true;
|
|
53
|
+
},
|
|
54
|
+
initialized: false,
|
|
55
|
+
completed: false,
|
|
56
|
+
};
|
|
57
|
+
return context;
|
|
58
|
+
}
|