formlift 0.1.0 → 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/CHANGELOG.md +19 -3
- package/README.md +206 -2
- package/dist/{chunk-5VHPTLLV.cjs → chunk-33XWO4KB.cjs} +156 -34
- package/dist/chunk-33XWO4KB.cjs.map +1 -0
- package/dist/chunk-7NZPYWL3.cjs.map +1 -1
- package/dist/{chunk-BQAVPA7Q.js → chunk-NUKLGHKS.js} +157 -35
- package/dist/chunk-NUKLGHKS.js.map +1 -0
- package/dist/filters-Yzd4tG8r.d.ts +201 -0
- package/dist/filters-q6gqilzw.d.cts +201 -0
- package/dist/index.cjs +24 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -56
- package/dist/index.d.ts +5 -56
- package/dist/index.js +27 -5
- package/dist/react/index.cjs +1052 -836
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +528 -45
- package/dist/react/index.d.ts +528 -45
- package/dist/react/index.js +1278 -1062
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5VHPTLLV.cjs.map +0 -1
- package/dist/chunk-BQAVPA7Q.js.map +0 -1
- package/dist/layout-ExhBeO6b.d.cts +0 -27
- package/dist/layout-ExhBeO6b.d.ts +0 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Written automatically by [release-please](https://github.com/googleapis/release-please) from the
|
|
4
|
+
commit messages. The project uses [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.2.0](https://github.com/harendra-singh-negi/formlift/compare/v0.1.1...v0.2.0) (2026-11-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### ⚠ BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* matches, NO_FILTER and QuestionFilter are removed. Filter with filterQuestions and filter objects instead.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* customisable UI, headless hooks and a filter registry ([50a2571](https://github.com/harendra-singh-negi/formlift/commit/50a2571964a09d17acbc82fd65d83bac1ab1b70a))
|
|
16
|
+
|
|
17
|
+
## [0.1.1] - 2026-11-10
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Releases are now published from GitHub Actions through npm Trusted Publishing (no tokens) and staged for
|
|
22
|
+
maintainer approval before they go live. No code changes.
|
|
7
23
|
|
|
8
24
|
## [0.1.0] - 2026-11-10
|
|
9
25
|
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Auto-detect fields · Drag-and-drop builder · Conditional logic · Validation
|
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
[](https://github.com/harendra-singh-negi/formlift/actions/workflows/ci.yml)
|
|
14
14
|
|
|
15
|
-
**[▶ Live demo](https://harendra-singh-negi.github.io/formlift/)** · [Quick start](#quick-start) · [API](#api-reference) · [Server](#server-side) · [Theming](#theming) · [FAQ](#faq)
|
|
15
|
+
**[▶ Live demo](https://harendra-singh-negi.github.io/formlift/)** · [Quick start](#quick-start) · [API](#api-reference) · [Server](#server-side) · [Theming](#theming) · [Your own UI](#your-own-ui) · [FAQ](#faq)
|
|
16
16
|
|
|
17
17
|
<img src="./docs/builder.png" alt="formlift builder: suggested fields over a PDF page" width="860" />
|
|
18
18
|
|
|
@@ -242,6 +242,13 @@ uploads are embedded as PDF attachments.
|
|
|
242
242
|
| `onError` | `(err: unknown) => void` | `console.error` | Error messages |
|
|
243
243
|
| `className` | `string` | `'h-full'` | Classes on the root element |
|
|
244
244
|
| `ref` | `Ref<FormEditorHandle>` | – | `{ save(), dirty, value }`, for example to save before a workflow step |
|
|
245
|
+
| `hide` | `EditorPart[]` | `[]` | Parts to leave out: `back`, `title`, `status`, `undo`, `snap`, `zoom`, `editText`, `autoDetect`, `preview`, `save`, `tools`, `fieldTools`, `layoutTools`, `panel`, `propertiesTab`, `fieldsTab`, `pagesTab`, `problems` |
|
|
246
|
+
| `fieldTypes` | `FieldType[]` | all | Field types that can be added |
|
|
247
|
+
| `elementTools` | `ElementTool[]` | all | Layout tools that can be added (`[]` = none) |
|
|
248
|
+
| `components` | `FormliftComponents` | – | Replacement UI components and answer controls ([Your own UI](#your-own-ui)) |
|
|
249
|
+
| `keyboard` | `boolean` | `true` | Keyboard shortcuts |
|
|
250
|
+
| `warnUnsaved` | `boolean` | `true` | Browser warning when leaving with unsaved changes |
|
|
251
|
+
| `onDetected` | `(found: FieldDef[]) => void` | – | Called with the fields auto-detect suggested |
|
|
245
252
|
|
|
246
253
|
**Keyboard:** <kbd>Del</kbd> delete · <kbd>Ctrl</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd> undo/redo ·
|
|
247
254
|
<kbd>Ctrl</kbd>+<kbd>D</kbd> duplicate · arrow keys nudge (hold <kbd>Shift</kbd> for larger steps) ·
|
|
@@ -262,8 +269,25 @@ uploads are embedded as PDF attachments.
|
|
|
262
269
|
| `loadFile` | `(id) => Promise<Blob>` | – | Loads stored signatures and opens attachments |
|
|
263
270
|
| `files` | `AnswerFileInfo[]` | `[]` | Files already attached to the draft |
|
|
264
271
|
| `readOnly` | `boolean` | `false` | View a submitted form |
|
|
272
|
+
| `hide` | `FillerPart[]` | `[]` | Parts to leave out: `back`, `title`, `views`, `zoom`, `saveStatus`, `saveDraft`, `submit`, `attachments`, `errors`, `requiredMark` |
|
|
273
|
+
| `views` | `('document' \| 'list')[]` | both | Views offered; the first is shown first |
|
|
274
|
+
| `confirmSubmit` | `boolean` | `true` | Ask "Submit this form?" first |
|
|
275
|
+
| `listFilters` | `(FilterSource \| string)[]` | `defaultFilters` | The list view's filters ([Filters](#4-filters-you-choose-formlift-computes)) |
|
|
276
|
+
| `listSearch` | `boolean` | `true` | Search box over the list |
|
|
277
|
+
| `hideUnavailableFilters` | `boolean` | `true` | Leave out filters this form has nothing for |
|
|
278
|
+
| `renderFilters` | `(fs: QuestionFilterState) => ReactNode` | – | Your own search / filter bar instead of the built-in one |
|
|
279
|
+
| `toQuestions` | `(fields, textOf) => Question[]` | `toQuestions` | How fields become list questions |
|
|
280
|
+
| `components` | `FormliftComponents` | – | Replacement UI components and answer controls |
|
|
265
281
|
| `title`, `onBack`, `toolbar`, `notify`, `onError`, `className` | | | Same as `FormEditor` |
|
|
266
282
|
|
|
283
|
+
### Hooks (`formlift/react`)
|
|
284
|
+
|
|
285
|
+
| Hook | Gives |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| `useFormEditor(options)` | Everything `FormEditor` does, without UI: document, undo/redo, selection, drag/resize, pages, detect, save |
|
|
288
|
+
| `useFormFiller(options)` | Everything `FormFiller` does, without UI: answers, conditions, validation, autosave, uploads, questions |
|
|
289
|
+
| `useQuestionFilter(questions, answers, options)` | Search and filters as data: `groups`, `inUse`, `list` |
|
|
290
|
+
|
|
267
291
|
### Core functions (`formlift`)
|
|
268
292
|
|
|
269
293
|
| Function | Returns | Description |
|
|
@@ -278,7 +302,8 @@ uploads are embedded as PDF attachments.
|
|
|
278
302
|
| `defaultAnswers(fields)` | `Answers` | The answers a new submission starts with |
|
|
279
303
|
| `defaultLayout(pageSizes)` | `Layout` | The layout of an untouched document |
|
|
280
304
|
| `detectDocument(pdf, OPS, pages, existing, onProgress?, signal?)` | `Promise<FieldDef[]>` | Suggests fields for a whole document |
|
|
281
|
-
| `toQuestions(fields, textOf?)` | `Question[]` | Fields grouped as questions, used by the list view |
|
|
305
|
+
| `toQuestions(fields, textOf?, { categoryCode? })` | `Question[]` | Fields grouped as questions, used by the list view |
|
|
306
|
+
| `filterQuestions(questions, answers, filters, values, search?)` | `Question[]` | The questions passing a search and picked filter values (see [Filters](#4-filters-you-choose-formlift-computes)) |
|
|
282
307
|
|
|
283
308
|
### Server (`formlift/server`)
|
|
284
309
|
|
|
@@ -433,6 +458,185 @@ The styles live inside `.fl-root` and have no global reset. Override any variabl
|
|
|
433
458
|
|
|
434
459
|
The full list of variables is in [`src/style.css`](./src/style.css).
|
|
435
460
|
|
|
461
|
+
## Your own UI
|
|
462
|
+
|
|
463
|
+
formlift's logic is separate from its look. Pick how much you replace:
|
|
464
|
+
|
|
465
|
+
| Level | What you change | How |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| 1. Theme | colours, font | `--fl-*` CSS variables (above) |
|
|
468
|
+
| 2. Parts and features | what shows, which field types / views exist | `hide`, `views`, `fieldTypes`, `listFilters`… |
|
|
469
|
+
| 3. Components | any button, input, modal, chip…, the answer control, or one field type | `components` prop / `<FormliftProvider>` |
|
|
470
|
+
| 4. Filters | which filters exist, your own filters, your own filter UI | `listFilters`, filter objects, `renderFilters` / `useQuestionFilter` |
|
|
471
|
+
| 5. Headless | the whole screen | `useFormEditor` / `useFormFiller` + your own markup |
|
|
472
|
+
|
|
473
|
+
### 2. Choose what shows
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
<FormEditor
|
|
477
|
+
pdf={url}
|
|
478
|
+
onSave={save}
|
|
479
|
+
fieldTypes={['text', 'date', 'checkbox', 'signature']} // only these in the palette
|
|
480
|
+
elementTools={['text']} // layout tools offered ([] = none)
|
|
481
|
+
hide={['autoDetect', 'editText', 'pagesTab', 'snap']} // EditorPart: leave these out
|
|
482
|
+
autoDetect={false}
|
|
483
|
+
/>
|
|
484
|
+
|
|
485
|
+
<FormFiller
|
|
486
|
+
pdf={url}
|
|
487
|
+
fields={fields}
|
|
488
|
+
views={['list']} // only the question list ('document', 'list'; first = default)
|
|
489
|
+
listFilters={['answered', 'category']} // only these filters (ids, or filter objects: see 4.)
|
|
490
|
+
hide={['saveDraft', 'attachments']} // FillerPart: leave these out
|
|
491
|
+
confirmSubmit={false}
|
|
492
|
+
/>
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Everything hidden still works through the hooks and `ref` (hide `save` and call `ref.current.save()` from your
|
|
496
|
+
own button). `EditorPart` and `FillerPart` list every part that can be hidden.
|
|
497
|
+
|
|
498
|
+
### 3. Swap components
|
|
499
|
+
|
|
500
|
+
Every building block formlift draws (`Button`, `IconButton`, `TextInput`, `Select`, `TextArea`, `Checkbox`,
|
|
501
|
+
`FormField`, `Modal`, `ModalHeader`, `ModalBody`, `ModalFooter`, `ConfirmModal`, `SegmentedTabs`, `PanelHeading`,
|
|
502
|
+
`EmptyText`, `Chip`, `ChipGroup`, `RemovableChip`, `Popover`, `Tag`, `SignaturePad`) can be replaced, inside the
|
|
503
|
+
editor's panels too. A replacement gets the same props as the built-in one:
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
import { FormliftProvider, FormFiller, type UIComponents } from 'formlift/react'
|
|
507
|
+
import type { ComponentProps } from 'react'
|
|
508
|
+
|
|
509
|
+
function MyButton({ variant, size, icon: Icon, children, ...rest }: ComponentProps<UIComponents['Button']>) {
|
|
510
|
+
return <button className={`my-btn my-btn--${variant ?? 'primary'}`} {...rest}>{Icon && <Icon size={14} />}{children}</button>
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// One screen…
|
|
514
|
+
<FormFiller pdf={url} fields={fields} components={{ Button: MyButton, FieldControl: MyAnswerControl }} />
|
|
515
|
+
|
|
516
|
+
// …or the whole app.
|
|
517
|
+
<FormliftProvider components={{ Button: MyButton, Select: MySelect }}>
|
|
518
|
+
<App />
|
|
519
|
+
</FormliftProvider>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The answer controls are swappable too, all at once (`FieldControl`) or per field type (`fields`, which wins). Each
|
|
523
|
+
gets `FieldControlProps` (`field`, `value`, `onChange`, `readOnly`, `error`…):
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
<FormFiller
|
|
527
|
+
pdf={url}
|
|
528
|
+
fields={fields}
|
|
529
|
+
components={{ fields: { date: MyDatePicker, signature: MySignaturePad } }} // other types stay built-in
|
|
530
|
+
/>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 4. Filters: you choose, formlift computes
|
|
534
|
+
|
|
535
|
+
formlift decides nothing about which filters a form gets. A filter is a plain object (`id`, `title`, `options()`,
|
|
536
|
+
`test()`); the built-ins are presets you pick from, and you can write your own. For each filter you pass,
|
|
537
|
+
formlift returns its options **for this form** with counts and an `available` flag (does this form have anything
|
|
538
|
+
to filter on?). Whether to show it is your call.
|
|
539
|
+
|
|
540
|
+
| Built-in (`formlift`) | id | Offers |
|
|
541
|
+
|---|---|---|
|
|
542
|
+
| `answeredFilter()` | `answered` | Answered / Not answered |
|
|
543
|
+
| `partFilter({ id, title, match })` | yours | Written / Empty for parts whose name matches `match` (presets: `remarks` = `/remark/i`, `comments` = `/comment/i`) |
|
|
544
|
+
| `choiceFilter()` / `pickedFilter()` | `choices` / `picked` | answer choices offered / ticked ("Yes", "NA"…) |
|
|
545
|
+
| `categoryFilter()` | `category` | category codes read from the form (`Question.category`) |
|
|
546
|
+
| `tagFilters()` / `tagFilter(key)` | `tag:<key>` | "Risk: H" style tags read from the form |
|
|
547
|
+
| `typeFilter()` | `type` | answer types |
|
|
548
|
+
| `customFilter(id, title, valuesOf)` | yours | anything you compute from a question |
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
import { categoryFilter, customFilter, partFilter } from 'formlift'
|
|
552
|
+
|
|
553
|
+
const hasCategories = form.kind === 'inspection' // your rule, not formlift's
|
|
554
|
+
|
|
555
|
+
<FormFiller
|
|
556
|
+
pdf={url}
|
|
557
|
+
fields={fields}
|
|
558
|
+
listFilters={[
|
|
559
|
+
'answered', // a built-in, by id
|
|
560
|
+
partFilter({ id: 'finding', title: 'Finding', match: /finding|observation/i }),
|
|
561
|
+
...(hasCategories ? [categoryFilter({ title: 'Chapter' })] : []),
|
|
562
|
+
customFilter('deck', 'Deck', (q) => [q.page <= 2 ? 'Upper' : 'Lower']),
|
|
563
|
+
]}
|
|
564
|
+
listSearch={true}
|
|
565
|
+
hideUnavailableFilters={false} // show listed filters even when this form has nothing for them
|
|
566
|
+
/>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
What counts as a category is yours too: `toQuestions(fields, text, { categoryCode: /^(HW|PR|HF)$/ })`, or
|
|
570
|
+
`categoryCode: false` and set `Question.category` yourself through `useFormFiller`'s `toQuestions` option.
|
|
571
|
+
|
|
572
|
+
**Your own filter UI.** `renderFilters` replaces the built-in bar; `fs.groups` has every filter you passed, each
|
|
573
|
+
with `options` (label + count), `available`, `value`, `set`, and a `section` hint:
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
<FormFiller
|
|
577
|
+
pdf={url}
|
|
578
|
+
fields={fields}
|
|
579
|
+
renderFilters={(fs) => (
|
|
580
|
+
<MyToolbar>
|
|
581
|
+
<MySearch value={fs.search ?? ''} onChange={fs.setSearch} />
|
|
582
|
+
{fs.groups
|
|
583
|
+
.filter((g) => g.available)
|
|
584
|
+
.map((g) => <MySelect key={g.id} label={g.title} options={g.options} value={g.value} onChange={g.set} />)}
|
|
585
|
+
{fs.inUse.map((c) => <MyChip key={c.id} label={c.label} onRemove={c.clear} />)}
|
|
586
|
+
<span>{fs.list.length} of {fs.total}</span>
|
|
587
|
+
</MyToolbar>
|
|
588
|
+
)}
|
|
589
|
+
/>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Headless: `useQuestionFilter(f.questions, f.answers, { filters, search, initial, predicate, onChange })` gives the
|
|
593
|
+
same object; render `fs.list` yourself or pass it to `<QuestionList questions={…} filter={fs} toolbar={false} />`.
|
|
594
|
+
On the server (no React): `filterQuestions(questions, answers, resolveFilters(filters, questions), values, search)`.
|
|
595
|
+
|
|
596
|
+
### 5. Headless: hooks only
|
|
597
|
+
|
|
598
|
+
`useFormFiller` and `useFormEditor` hold everything the built-in screens do (answers, conditions, validation, queued
|
|
599
|
+
saves and autosave, uploads; the document with undo / redo, selection, drag / resize, pages, auto-detect, save). Draw
|
|
600
|
+
whatever you like around them. `PdfPages` renders the pages and gives you an overlay per page.
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import { boxStyle } from 'formlift'
|
|
604
|
+
import { PdfPages, useFormFiller } from 'formlift/react'
|
|
605
|
+
|
|
606
|
+
function MyFiller({ pdfUrl, fields }) {
|
|
607
|
+
const f = useFormFiller({ fields, onSave: (answers, { submit }) => api.save(answers, submit) })
|
|
608
|
+
return (
|
|
609
|
+
<>
|
|
610
|
+
<header>
|
|
611
|
+
{f.saveState} <button onClick={() => f.submit()}>Send</button>
|
|
612
|
+
</header>
|
|
613
|
+
<PdfPages
|
|
614
|
+
load={pdfUrl}
|
|
615
|
+
zoom={1}
|
|
616
|
+
onLoaded={f.onPdfLoaded}
|
|
617
|
+
overlay={(page) =>
|
|
618
|
+
fields
|
|
619
|
+
.filter((x) => x.position.page === page && f.states[x.key].visible)
|
|
620
|
+
.map((x) => (
|
|
621
|
+
<input
|
|
622
|
+
key={x.id}
|
|
623
|
+
style={{ position: 'absolute', ...boxStyle(x.position) }}
|
|
624
|
+
value={String(f.answers[x.key] ?? '')}
|
|
625
|
+
onChange={(e) => f.change(x, e.target.value)}
|
|
626
|
+
/>
|
|
627
|
+
))
|
|
628
|
+
}
|
|
629
|
+
/>
|
|
630
|
+
</>
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Or skip the PDF and show `f.questions` (fields grouped into questions, tables and option groups) as a plain list.
|
|
636
|
+
For the editor, `useFormEditor` gives `fields`, `layout`, `selection`, `placeNew`, `startDrag` / `onDrag` /
|
|
637
|
+
`endDrag`, `updateField`, `undo` / `redo`, `detect`, `save` and the rest. Hover the hook in your editor for the
|
|
638
|
+
full list.
|
|
639
|
+
|
|
436
640
|
## Security
|
|
437
641
|
|
|
438
642
|
- Run `validateFields` / `validateLayout` when a form is saved, and `cleanAnswers` + `validateAnswers` when
|
|
@@ -997,7 +997,7 @@ function headerRow(lines, first) {
|
|
|
997
997
|
});
|
|
998
998
|
return names.every(Boolean) ? names : null;
|
|
999
999
|
}
|
|
1000
|
-
function toQuestions(fields, textOf = () => void 0) {
|
|
1000
|
+
function toQuestions(fields, textOf = () => void 0, { categoryCode = CODE } = {}) {
|
|
1001
1001
|
const sorted = [...fields].sort(
|
|
1002
1002
|
(a, b) => a.position.page - b.position.page || a.position.y - b.position.y || a.position.x - b.position.x
|
|
1003
1003
|
);
|
|
@@ -1165,7 +1165,7 @@ function toQuestions(fields, textOf = () => void 0) {
|
|
|
1165
1165
|
});
|
|
1166
1166
|
const at = (q) => top(q.parts.map((p) => p.field).filter((f) => f.position.page === q.page));
|
|
1167
1167
|
out.sort((a, b) => a.page - b.page || at(a) - at(b));
|
|
1168
|
-
setCategories(out, sorted, textOf);
|
|
1168
|
+
if (categoryCode) setCategories(out, sorted, textOf, categoryCode);
|
|
1169
1169
|
setSections(out, sorted, textOf);
|
|
1170
1170
|
setScales(out, textOf);
|
|
1171
1171
|
return mergeBlocks(out);
|
|
@@ -1270,10 +1270,10 @@ function mergeBlocks(out) {
|
|
|
1270
1270
|
return res;
|
|
1271
1271
|
}
|
|
1272
1272
|
var CODE = /^[A-Z]{2,4}$/;
|
|
1273
|
-
function setCategories(out, fields, textOf) {
|
|
1273
|
+
function setCategories(out, fields, textOf, code) {
|
|
1274
1274
|
const last = Math.max(0, ...fields.map((f) => f.position.page));
|
|
1275
1275
|
const found = [];
|
|
1276
|
-
const isCode = (t) =>
|
|
1276
|
+
const isCode = (t) => code.test(t.trim()) && !COLUMN.test(t.trim());
|
|
1277
1277
|
for (let page = 1; page <= last; page++) {
|
|
1278
1278
|
const lines = _nullishCoalesce(textOf(page), () => ( []));
|
|
1279
1279
|
for (const l of lines) {
|
|
@@ -1431,25 +1431,7 @@ function tagsOf(text) {
|
|
|
1431
1431
|
if (pairs.some((m) => !m || m[1].trim().split(/\s+/).length > 3)) return null;
|
|
1432
1432
|
return Object.fromEntries(pairs.map((m) => [m[1].trim(), m[2].trim()]));
|
|
1433
1433
|
}
|
|
1434
|
-
var NO_FILTER = {
|
|
1435
|
-
search: "",
|
|
1436
|
-
answered: "all",
|
|
1437
|
-
remarks: "all",
|
|
1438
|
-
comments: "all",
|
|
1439
|
-
type: "all",
|
|
1440
|
-
category: "all",
|
|
1441
|
-
tags: {},
|
|
1442
|
-
choices: "all",
|
|
1443
|
-
picked: "all"
|
|
1444
|
-
};
|
|
1445
1434
|
var isFilled = (v) => v != null && v !== "" && v !== false;
|
|
1446
|
-
var partsLike = (q, re) => q.parts.filter((p) => re.test(p.name) || re.test(p.field.label));
|
|
1447
|
-
function check(parts, answers, want) {
|
|
1448
|
-
if (want === "all") return true;
|
|
1449
|
-
if (!parts.length) return false;
|
|
1450
|
-
const any = parts.some((p) => isFilled(answers[p.field.key]));
|
|
1451
|
-
return want === "filled" ? any : !any;
|
|
1452
|
-
}
|
|
1453
1435
|
function choiceParts(q) {
|
|
1454
1436
|
if (q.columns) return [];
|
|
1455
1437
|
const size = _nullishCoalesce(_optionalChain([q, 'access', _30 => _30.rows, 'optionalAccess', _31 => _31.reduce, 'call', _32 => _32((a, b) => a + b, 0)]), () => ( 0));
|
|
@@ -1470,17 +1452,157 @@ function pickedOf(q, answers) {
|
|
|
1470
1452
|
const v = radio && answers[radio.key];
|
|
1471
1453
|
return typeof v === "string" && v ? [v] : [];
|
|
1472
1454
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1455
|
+
|
|
1456
|
+
// src/core/filters.ts
|
|
1457
|
+
var counted = (questions, values, label = (v) => v) => {
|
|
1458
|
+
const m = /* @__PURE__ */ new Map();
|
|
1459
|
+
for (const q of questions) for (const v of new Set(values(q))) m.set(v, (_nullishCoalesce(m.get(v), () => ( 0))) + 1);
|
|
1460
|
+
return [...m].map(([value, count]) => ({ value, label: label(value), count }));
|
|
1461
|
+
};
|
|
1462
|
+
var filledOptions = (yes, no) => [
|
|
1463
|
+
{ value: "filled", label: yes },
|
|
1464
|
+
{ value: "empty", label: no }
|
|
1465
|
+
];
|
|
1466
|
+
function filledTest(parts, value, answers) {
|
|
1467
|
+
if (!parts.length) return false;
|
|
1468
|
+
const any = parts.some((p) => isFilled(answers[p.field.key]));
|
|
1469
|
+
return value === "filled" ? any : !any;
|
|
1470
|
+
}
|
|
1471
|
+
var answeredFilter = ({
|
|
1472
|
+
id = "answered",
|
|
1473
|
+
title = "Answered",
|
|
1474
|
+
yes = "Answered",
|
|
1475
|
+
no = "Not answered",
|
|
1476
|
+
section = "Progress"
|
|
1477
|
+
} = {}) => ({
|
|
1478
|
+
id,
|
|
1479
|
+
title,
|
|
1480
|
+
section,
|
|
1481
|
+
options: (qs) => qs.length ? filledOptions(yes, no) : [],
|
|
1482
|
+
test: (q, v, a) => filledTest(q.parts, v, a)
|
|
1483
|
+
});
|
|
1484
|
+
function partFilter({
|
|
1485
|
+
id,
|
|
1486
|
+
title,
|
|
1487
|
+
match,
|
|
1488
|
+
yes = "Written",
|
|
1489
|
+
no = "Empty",
|
|
1490
|
+
section = "Progress"
|
|
1491
|
+
}) {
|
|
1492
|
+
const parts = (q) => q.parts.filter((p) => match.test(p.name) || match.test(p.field.label));
|
|
1493
|
+
return {
|
|
1494
|
+
id,
|
|
1495
|
+
title,
|
|
1496
|
+
section,
|
|
1497
|
+
options: (qs) => qs.some((q) => parts(q).length) ? filledOptions(yes, no) : [],
|
|
1498
|
+
test: (q, v, a) => filledTest(parts(q), v, a)
|
|
1499
|
+
};
|
|
1483
1500
|
}
|
|
1501
|
+
var choiceFilter = ({
|
|
1502
|
+
id = "choices",
|
|
1503
|
+
title = "Has option",
|
|
1504
|
+
section = "Answers",
|
|
1505
|
+
min = 2,
|
|
1506
|
+
max = 40
|
|
1507
|
+
} = {}) => ({
|
|
1508
|
+
id,
|
|
1509
|
+
title,
|
|
1510
|
+
section,
|
|
1511
|
+
options: (qs) => counted(qs, choicesOf).filter((o) => o.count >= min).slice(0, max),
|
|
1512
|
+
test: (q, v) => choicesOf(q).includes(v)
|
|
1513
|
+
});
|
|
1514
|
+
var pickedFilter = ({
|
|
1515
|
+
id = "picked",
|
|
1516
|
+
title = "Answered with",
|
|
1517
|
+
section = "Answers",
|
|
1518
|
+
min = 2,
|
|
1519
|
+
max = 40
|
|
1520
|
+
} = {}) => ({
|
|
1521
|
+
id,
|
|
1522
|
+
title,
|
|
1523
|
+
section,
|
|
1524
|
+
options: (qs, answers) => {
|
|
1525
|
+
const offered = new Set(
|
|
1526
|
+
choiceFilter({ min, max }).options(qs, answers).map((o) => o.value)
|
|
1527
|
+
);
|
|
1528
|
+
const picked = new Map(counted(qs, (q) => pickedOf(q, answers)).map((o) => [o.value, o.count]));
|
|
1529
|
+
return [...offered].map((c) => ({ value: c, label: c, count: _nullishCoalesce(picked.get(c), () => ( 0)) }));
|
|
1530
|
+
},
|
|
1531
|
+
test: (q, v, a) => pickedOf(q, a).includes(v)
|
|
1532
|
+
});
|
|
1533
|
+
var categoryFilter = ({ id = "category", title = "Category", section = "bar" } = {}) => ({
|
|
1534
|
+
id,
|
|
1535
|
+
title,
|
|
1536
|
+
section,
|
|
1537
|
+
options: (qs) => counted(qs, (q) => q.category ? [q.category] : []),
|
|
1538
|
+
test: (q, v) => q.category === v
|
|
1539
|
+
});
|
|
1540
|
+
var typeFilter = ({
|
|
1541
|
+
id = "type",
|
|
1542
|
+
title = "Answer type",
|
|
1543
|
+
section = "Answer type",
|
|
1544
|
+
label = (t) => t
|
|
1545
|
+
} = {}) => ({
|
|
1546
|
+
id,
|
|
1547
|
+
title,
|
|
1548
|
+
section,
|
|
1549
|
+
options: (qs) => {
|
|
1550
|
+
const opts = counted(
|
|
1551
|
+
qs,
|
|
1552
|
+
(q) => q.parts.map((p) => p.field.type),
|
|
1553
|
+
(t) => label(t)
|
|
1554
|
+
);
|
|
1555
|
+
return opts.length > 1 ? opts : [];
|
|
1556
|
+
},
|
|
1557
|
+
test: (q, v) => q.parts.some((p) => p.field.type === v)
|
|
1558
|
+
});
|
|
1559
|
+
var tagFilter = (key, { title = key, section = "Question details" } = {}) => ({
|
|
1560
|
+
id: `tag:${key}`,
|
|
1561
|
+
title,
|
|
1562
|
+
section,
|
|
1563
|
+
options: (qs) => counted(qs, (q) => _optionalChain([q, 'access', _42 => _42.tags, 'optionalAccess', _43 => _43[key]]) ? [q.tags[key]] : []).sort(
|
|
1564
|
+
(a, b) => a.value.localeCompare(b.value, void 0, { numeric: true })
|
|
1565
|
+
),
|
|
1566
|
+
test: (q, v) => _optionalChain([q, 'access', _44 => _44.tags, 'optionalAccess', _45 => _45[key]]) === v
|
|
1567
|
+
});
|
|
1568
|
+
var tagFilters = ({ section = "Question details" } = {}) => (qs) => [...new Set(qs.flatMap((q) => Object.keys(_nullishCoalesce(q.tags, () => ( {})))))].map((k) => tagFilter(k, { section }));
|
|
1569
|
+
var customFilter = (id, title, valuesOf, section) => ({
|
|
1570
|
+
id,
|
|
1571
|
+
title,
|
|
1572
|
+
section,
|
|
1573
|
+
options: (qs, answers) => counted(qs, (q) => valuesOf(q, answers)),
|
|
1574
|
+
test: (q, v, a) => valuesOf(q, a).includes(v)
|
|
1575
|
+
});
|
|
1576
|
+
var builtinFilters = (labels = {}) => [
|
|
1577
|
+
answeredFilter(),
|
|
1578
|
+
partFilter({ id: "remarks", title: "Remarks", match: /remark/i }),
|
|
1579
|
+
partFilter({ id: "comments", title: "Comments", match: /comment/i }),
|
|
1580
|
+
choiceFilter(),
|
|
1581
|
+
pickedFilter(),
|
|
1582
|
+
tagFilters(),
|
|
1583
|
+
typeFilter({ label: labels.type }),
|
|
1584
|
+
categoryFilter()
|
|
1585
|
+
];
|
|
1586
|
+
var resolveFilters = (sources, questions) => sources.flatMap((s) => typeof s === "function" ? s(questions) : [s]);
|
|
1587
|
+
var searchText = (q) => `${_nullishCoalesce(q.section, () => ( ""))} ${_nullishCoalesce(q.intro, () => ( ""))} ${q.title} ${q.parts.map((p) => p.field.label).join(" ")}`;
|
|
1588
|
+
function filterQuestions(questions, answers, filters, values, search = "") {
|
|
1589
|
+
const text = search.trim().toLowerCase();
|
|
1590
|
+
const active = filters.filter((f) => values[f.id] != null);
|
|
1591
|
+
return questions.filter(
|
|
1592
|
+
(q) => (!text || searchText(q).toLowerCase().includes(text)) && active.every((f) => f.test(q, values[f.id], answers))
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
|
|
1484
1606
|
|
|
1485
1607
|
|
|
1486
1608
|
|
|
@@ -1513,5 +1635,5 @@ function matches(q, answers, f) {
|
|
|
1513
1635
|
|
|
1514
1636
|
|
|
1515
1637
|
|
|
1516
|
-
exports.snap = snap; exports.toPage = toPage; exports.fitInPage = fitInPage; exports.moveBy = moveBy; exports.resizeBy = resizeBy; exports.boxStyle = boxStyle; exports.uniqueKey = uniqueKey; exports.keyFor = keyFor; exports.withField = withField; exports.newField = newField; exports.duplicate = duplicate; exports.reorder = reorder; exports.movePage = movePage; exports.deletePage = deletePage; exports.insertBlankPage = insertBlankPage; exports.ELEMENT_TOOLS = ELEMENT_TOOLS; exports.newElement = newElement; exports.replaceLine = replaceLine; exports.fitTextWidth = fitTextWidth; exports.duplicateElement = duplicateElement; exports.toLines = toLines; exports.extractShapes = extractShapes; exports.detectFields = detectFields; exports.detectDocument = detectDocument; exports.toQuestions = toQuestions; exports.
|
|
1517
|
-
//# sourceMappingURL=chunk-
|
|
1638
|
+
exports.snap = snap; exports.toPage = toPage; exports.fitInPage = fitInPage; exports.moveBy = moveBy; exports.resizeBy = resizeBy; exports.boxStyle = boxStyle; exports.uniqueKey = uniqueKey; exports.keyFor = keyFor; exports.withField = withField; exports.newField = newField; exports.duplicate = duplicate; exports.reorder = reorder; exports.movePage = movePage; exports.deletePage = deletePage; exports.insertBlankPage = insertBlankPage; exports.ELEMENT_TOOLS = ELEMENT_TOOLS; exports.newElement = newElement; exports.replaceLine = replaceLine; exports.fitTextWidth = fitTextWidth; exports.duplicateElement = duplicateElement; exports.toLines = toLines; exports.extractShapes = extractShapes; exports.detectFields = detectFields; exports.detectDocument = detectDocument; exports.toQuestions = toQuestions; exports.isFilled = isFilled; exports.choicesOf = choicesOf; exports.pickedOf = pickedOf; exports.answeredFilter = answeredFilter; exports.partFilter = partFilter; exports.choiceFilter = choiceFilter; exports.pickedFilter = pickedFilter; exports.categoryFilter = categoryFilter; exports.typeFilter = typeFilter; exports.tagFilter = tagFilter; exports.tagFilters = tagFilters; exports.customFilter = customFilter; exports.builtinFilters = builtinFilters; exports.resolveFilters = resolveFilters; exports.searchText = searchText; exports.filterQuestions = filterQuestions;
|
|
1639
|
+
//# sourceMappingURL=chunk-33XWO4KB.cjs.map
|