@iyulab/lowline-form 0.0.0-stage → 0.1.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/README.md +39 -2
- package/dist/bundle/lowline-form.bundle.js +11205 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +52 -0
- package/dist/draft.d.ts +50 -0
- package/dist/draft.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lowline-form.d.ts +71 -0
- package/dist/lowline-form.js +212 -0
- package/dist/session.d.ts +111 -0
- package/dist/session.js +173 -0
- package/package.json +40 -4
package/dist/session.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Draft, isEmpty } from './draft.js';
|
|
2
|
+
export const englishStrings = {
|
|
3
|
+
decline: 'Not this',
|
|
4
|
+
fromValue: 'As before with {field} {value}',
|
|
5
|
+
fromSimilar: 'As in a similar record',
|
|
6
|
+
fromJudgement: 'Suggested from what was written',
|
|
7
|
+
fromWriting: 'From what you wrote',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* One record being written: its draft, and what its fields show. A field's answer may be held back once, at random,
|
|
11
|
+
* to check it — `rate` is how often (one in twenty by default), drawn once per field of the record, before its answer
|
|
12
|
+
* is looked at, and only for a value the decider suggests.
|
|
13
|
+
*/
|
|
14
|
+
export class Session {
|
|
15
|
+
constructor(id, opened, rate = 0.05, random = Math.random, strings = englishStrings) {
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.rate = rate;
|
|
18
|
+
this.random = random;
|
|
19
|
+
this.strings = strings;
|
|
20
|
+
this.drawn = new Set();
|
|
21
|
+
/** Fields whose value the form put in unasked, from the suggestion still shown. */
|
|
22
|
+
this.filled = new Set();
|
|
23
|
+
/** What filling from the person's writing offers, by field, until the person picks it or turns it down. */
|
|
24
|
+
this.written = new Map();
|
|
25
|
+
/** The judgement asked for each field, by what it reads; `null` while the host is still judging. */
|
|
26
|
+
this.judgements = new Map();
|
|
27
|
+
this.draft = new Draft(opened);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Takes a round of proposals for the form's `values` now. A field offered a value from the person's writing keeps it
|
|
31
|
+
* in view until the person picks it or turns it down.
|
|
32
|
+
*/
|
|
33
|
+
apply(proposals, values) {
|
|
34
|
+
const round = { fieldStates: Object.fromEntries(this.written), prefill: {} };
|
|
35
|
+
for (const proposal of proposals) {
|
|
36
|
+
const { field, suggestion } = proposal;
|
|
37
|
+
if (!suggestion || this.written.has(field))
|
|
38
|
+
continue;
|
|
39
|
+
if (this.draw(field, suggestion))
|
|
40
|
+
continue;
|
|
41
|
+
if (this.draft.held(field) !== undefined)
|
|
42
|
+
continue;
|
|
43
|
+
// Turned down, it stays down for this record: neither shown again nor put back in.
|
|
44
|
+
if (this.draft.declined(field, suggestion))
|
|
45
|
+
continue;
|
|
46
|
+
this.draft.show(field, suggestion);
|
|
47
|
+
if (proposal.prefill && isEmpty(values[field])) {
|
|
48
|
+
round.prefill[field] = suggestion.value;
|
|
49
|
+
this.filled.add(field);
|
|
50
|
+
}
|
|
51
|
+
round.fieldStates[field] = {
|
|
52
|
+
suggestions: [suggestion.value],
|
|
53
|
+
decline: this.strings.decline,
|
|
54
|
+
...(this.note(proposal, values) ? { note: this.note(proposal, values) } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return round;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The round's proposals with each field that asks for a judgement answered by the one already judged on the same
|
|
61
|
+
* values, if there is one — so a judgement stays in view while the person writes on, and is not asked for again.
|
|
62
|
+
*/
|
|
63
|
+
judged(proposals) {
|
|
64
|
+
return proposals.map((proposal) => {
|
|
65
|
+
if (proposal.abstain !== 'needs-judgement')
|
|
66
|
+
return proposal;
|
|
67
|
+
const known = this.judgements.get(proposal.field);
|
|
68
|
+
return known?.reads === reads(proposal) && known.proposal ? known.proposal : proposal;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The fields of the round that ask for a judgement not asked for yet on what they read; marked as being asked. A field
|
|
73
|
+
* with nothing to read yet is left until there is something.
|
|
74
|
+
*/
|
|
75
|
+
toJudge(proposals) {
|
|
76
|
+
const asking = proposals.filter((p) => p.abstain === 'needs-judgement' && (p.asks?.length ?? 0) > 0 && this.judgements.get(p.field)?.reads !== reads(p));
|
|
77
|
+
for (const p of asking)
|
|
78
|
+
this.judgements.set(p.field, { reads: reads(p), proposal: null });
|
|
79
|
+
return asking.map((p) => p.field);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Takes what the host judged for the fields `asked` — each a proposal as the round would give it; a field it gave
|
|
83
|
+
* nothing for stays as it was, and is not asked about again on the same values. A field asked for again since, on
|
|
84
|
+
* other values, keeps waiting for that one.
|
|
85
|
+
*/
|
|
86
|
+
takeJudged(asked, judged) {
|
|
87
|
+
for (const before of asked) {
|
|
88
|
+
const pending = this.judgements.get(before.field);
|
|
89
|
+
if (pending?.reads !== reads(before))
|
|
90
|
+
continue;
|
|
91
|
+
const answer = judged.find((j) => j.field === before.field) ?? before;
|
|
92
|
+
this.judgements.set(before.field, { reads: pending.reads, proposal: answer });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The host could not judge the fields `asked`: they may be asked about again. */
|
|
96
|
+
notJudged(asked) {
|
|
97
|
+
for (const before of asked) {
|
|
98
|
+
if (this.judgements.get(before.field)?.reads === reads(before))
|
|
99
|
+
this.judgements.delete(before.field);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Takes what filling from the person's writing offers: each value shown as a suggestion from elsewhere, never put in
|
|
104
|
+
* on its own, in place of what the records suggest for the field. Answers what every field shows now.
|
|
105
|
+
*/
|
|
106
|
+
applyFill(proposals) {
|
|
107
|
+
for (const { field, suggestion } of proposals) {
|
|
108
|
+
if (!suggestion)
|
|
109
|
+
continue;
|
|
110
|
+
this.draft.show(field, suggestion);
|
|
111
|
+
this.filled.delete(field);
|
|
112
|
+
this.written.set(field, { suggestions: [suggestion.value], decline: this.strings.decline, note: this.strings.fromWriting });
|
|
113
|
+
}
|
|
114
|
+
return Object.fromEntries(this.written);
|
|
115
|
+
}
|
|
116
|
+
/** The person picked a field's offered value: what was offered from their writing has done its part. */
|
|
117
|
+
picked(field) {
|
|
118
|
+
this.written.delete(field);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The person turned down the field's suggestion. Answers whether the form must empty the field: it holds the value
|
|
122
|
+
* the form put in unasked — saved as it is, the turned-down value would count as taken.
|
|
123
|
+
*/
|
|
124
|
+
decline(field, values) {
|
|
125
|
+
const shown = this.draft.shown(field);
|
|
126
|
+
if (!this.draft.decline(field))
|
|
127
|
+
return false;
|
|
128
|
+
const empty = this.filled.has(field) && values[field] === shown?.value;
|
|
129
|
+
this.filled.delete(field);
|
|
130
|
+
this.written.delete(field);
|
|
131
|
+
return empty;
|
|
132
|
+
}
|
|
133
|
+
/** The record was saved holding `values`: what is written next starts from them. */
|
|
134
|
+
saved(values) {
|
|
135
|
+
this.draft = new Draft(values);
|
|
136
|
+
this.drawn.clear();
|
|
137
|
+
this.filled.clear();
|
|
138
|
+
this.written.clear();
|
|
139
|
+
this.judgements.clear();
|
|
140
|
+
}
|
|
141
|
+
/** Whether the field's answer is held back now — drawn the first time a checkable value is proposed for it. */
|
|
142
|
+
draw(field, suggestion) {
|
|
143
|
+
if (this.drawn.has(field))
|
|
144
|
+
return this.draft.held(field) !== undefined;
|
|
145
|
+
const checkable = (suggestion.origin ?? 'decider') === 'decider' && (suggestion.fit ?? 'exact') === 'exact';
|
|
146
|
+
if (!checkable)
|
|
147
|
+
return false;
|
|
148
|
+
this.drawn.add(field);
|
|
149
|
+
if (this.rate > 0 && this.rate <= 1 && this.random() < this.rate) {
|
|
150
|
+
this.draft.hold(field, this.rate);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
/** Where the suggestion comes from, in words. A field's value is shown as the record holds it now. */
|
|
156
|
+
note(proposal, values) {
|
|
157
|
+
const first = proposal.basis?.[0];
|
|
158
|
+
if (proposal.layer === 'key' && first?.kind === 'value') {
|
|
159
|
+
const now = values[first.field];
|
|
160
|
+
const shown = now === undefined ? first.value : Array.isArray(now) ? now.join(', ') : now;
|
|
161
|
+
return this.strings.fromValue.replace('{field}', first.field).replace('{value}', shown);
|
|
162
|
+
}
|
|
163
|
+
if (proposal.layer === 'similar')
|
|
164
|
+
return this.strings.fromSimilar;
|
|
165
|
+
if (proposal.layer === 'judgement')
|
|
166
|
+
return this.strings.fromJudgement;
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** What a proposal asking for a judgement reads, as one text. */
|
|
171
|
+
function reads(proposal) {
|
|
172
|
+
return JSON.stringify(proposal.asks ?? []);
|
|
173
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/lowline-form",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Lowline form element: a formdown form whose decided fields are suggested from the records people confirmed, talking to a Lowline host's routes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/iyulab/lowline.git",
|
|
9
|
+
"directory": "bindings/web"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"bundle": "vite build --config vite.bundle.config.ts"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@formdown/ui": "^0.24.0",
|
|
35
|
+
"lit": "^3.3.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^6.0.3",
|
|
39
|
+
"vitest": "^5.0.2",
|
|
40
|
+
"vite": "^8.3.1"
|
|
41
|
+
}
|
|
42
|
+
}
|