vue-legal-docs-import 1.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 +147 -0
- package/dist/index.d.ts +73 -0
- package/dist/style.css +1 -0
- package/dist/vue-legal-docs-import.es.js +234 -0
- package/dist/vue-legal-docs-import.umd.js +3 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# vue-legal-docs-import
|
|
2
|
+
|
|
3
|
+
Bringing documents into a platform: pick or drop files, read them in the
|
|
4
|
+
browser, hand them to whoever mounted the component.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install vue-legal-docs-import
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```vue
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { LegalDocsImport } from 'vue-legal-docs-import'
|
|
13
|
+
import type { ImportedDocument } from 'vue-legal-docs-import'
|
|
14
|
+
import 'vue-legal-docs-import/style.css'
|
|
15
|
+
|
|
16
|
+
async function save(documents: ImportedDocument[]) {
|
|
17
|
+
await fetch('/api/datasets', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ name: 'My documents', documents }),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<LegalDocsImport :on-import="save" />
|
|
27
|
+
</template>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Each document comes back as `{ name, source, full_text }` — the filename
|
|
31
|
+
without its extension, the filename, and the text.
|
|
32
|
+
|
|
33
|
+
## It stores nothing and sends nothing
|
|
34
|
+
|
|
35
|
+
Files are read in the browser, because a file somebody picked is already
|
|
36
|
+
theirs: no upload, no round trip, nothing to authenticate. What happens next
|
|
37
|
+
is the host's decision, and this component does not know or care whether the
|
|
38
|
+
documents become rows in a database, a session in a tab, or a download.
|
|
39
|
+
|
|
40
|
+
That is the same arrangement `vue-legal-query-builder` uses for search. A
|
|
41
|
+
component that talks to a server needs to know which server and often needs a
|
|
42
|
+
credential, and neither belongs in a package that renders a file picker.
|
|
43
|
+
|
|
44
|
+
## Doing the work on a backend
|
|
45
|
+
|
|
46
|
+
There are two places a server belongs, and they are not the same place.
|
|
47
|
+
|
|
48
|
+
**Extracting text from a format** is a reader. `read` is async and the
|
|
49
|
+
component never calls anything itself, so a reader is free to post the file to
|
|
50
|
+
an endpoint you own and let a real parser handle it — a Go or Python library
|
|
51
|
+
will do more with a PDF than anything that fits in a page, and OCR is not
|
|
52
|
+
happening in a browser at all:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const pdfReader: FormatReader = {
|
|
56
|
+
extensions: ['.pdf'],
|
|
57
|
+
label: 'PDF',
|
|
58
|
+
async read(file) {
|
|
59
|
+
const body = new FormData()
|
|
60
|
+
body.append('file', file)
|
|
61
|
+
const res = await fetch('/api/services/docs/extract', { method: 'POST', body })
|
|
62
|
+
if (!res.ok) throw new Error('could not read this PDF')
|
|
63
|
+
// Return metadata alongside the text when the parser knows more.
|
|
64
|
+
return res.json() as Promise<{ text: string; metadata: { pages: number } }>
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Doing something to the text** is `onPrepare`. Summarising, cleaning
|
|
70
|
+
boilerplate, splitting a long judgment into sections, detecting a language,
|
|
71
|
+
redacting names. It receives every document at once, so a backend can do it in
|
|
72
|
+
one request rather than one per file — which matters most for exactly the
|
|
73
|
+
expensive steps worth sending away:
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<LegalDocsImport :readers="[...defaultReaders, pdfReader]" :on-prepare="summarise" :on-import="save" />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
async function summarise(documents: ImportedDocument[]) {
|
|
81
|
+
const res = await fetch('/api/services/docs/summarise', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ documents }),
|
|
85
|
+
})
|
|
86
|
+
if (!res.ok) throw new Error('the summariser is not answering')
|
|
87
|
+
return res.json() as Promise<ImportedDocument[]>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It returns documents rather than changing them in place, so it may return fewer
|
|
92
|
+
— dropping what turned out to be unusable — or more, one file becoming a
|
|
93
|
+
document per section.
|
|
94
|
+
|
|
95
|
+
The result is shown in the list *before* anything is imported, which is why
|
|
96
|
+
this is separate from `onImport`. Whoever is importing gets to see what the
|
|
97
|
+
backend made of their files and remove what came back wrong, while it is still
|
|
98
|
+
a decision rather than a correction.
|
|
99
|
+
|
|
100
|
+
If `onPrepare` throws, the documents are kept as they were read and the reason
|
|
101
|
+
is shown. A summariser being down is not a reason to lose somebody's upload.
|
|
102
|
+
|
|
103
|
+
**The component still makes no requests.** Every one of these is a function you
|
|
104
|
+
write, pointed at an endpoint you own — the same arrangement
|
|
105
|
+
`vue-legal-query-builder` uses for search, and for the same reason: a package
|
|
106
|
+
that renders a file picker should not be choosing a server or holding a
|
|
107
|
+
credential.
|
|
108
|
+
|
|
109
|
+
## Adding a format
|
|
110
|
+
|
|
111
|
+
Only plain text is read today. The formats people actually have are PDF and
|
|
112
|
+
DOCX, and each needs a parser large enough that it should not load until a file
|
|
113
|
+
of that kind turns up — so the component takes readers rather than knowing how
|
|
114
|
+
to read anything itself:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import type { FormatReader } from 'vue-legal-docs-import'
|
|
118
|
+
import { defaultReaders } from 'vue-legal-docs-import'
|
|
119
|
+
|
|
120
|
+
const pdfReader: FormatReader = {
|
|
121
|
+
extensions: ['.pdf'],
|
|
122
|
+
label: 'PDF',
|
|
123
|
+
async read(file) {
|
|
124
|
+
const { extractText } = await import('./my-pdf-parser') // loaded on demand
|
|
125
|
+
return extractText(await file.arrayBuffer())
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```vue
|
|
131
|
+
<LegalDocsImport :readers="[...defaultReaders, pdfReader]" :on-import="save" />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A file nothing can read is listed as skipped with the reason, rather than
|
|
135
|
+
disappearing — files vanishing without explanation is what makes an import feel
|
|
136
|
+
broken.
|
|
137
|
+
|
|
138
|
+
## Props
|
|
139
|
+
|
|
140
|
+
| Prop | Type | Description |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `readers` | `FormatReader[]` | Formats to accept. Defaults to plain text. A reader may call your server. |
|
|
143
|
+
| `onPrepare` | `(docs) => Promise<docs>` | Runs over everything read, before it is shown as ready. Where a backend step goes. |
|
|
144
|
+
| `onImport` | `(docs) => Promise<void>` | Called with everything read. Without it, no button is shown and `v-model:documents` is how the host reads the list. |
|
|
145
|
+
| `importLabel` | `string` | The button's text. |
|
|
146
|
+
| `clearOnImport` | `boolean` | Empty the list once the host has taken them. Default `true`. |
|
|
147
|
+
| `v-model:documents` | `ImportedDocument[]` | The list, for a host that would rather drive it. |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DefineComponent, Plugin } from "vue";
|
|
2
|
+
|
|
3
|
+
/** A document read from a file, ready for the host to store. */
|
|
4
|
+
export interface ImportedDocument {
|
|
5
|
+
name: string;
|
|
6
|
+
source: string;
|
|
7
|
+
full_text: string;
|
|
8
|
+
/** Whatever a reader or a preparation step attached. Passed through. */
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** What a reader produces. A bare string is the common case. */
|
|
13
|
+
export interface ReadResult {
|
|
14
|
+
text: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A file that could not be read, kept so the person importing can see why. */
|
|
19
|
+
export interface ImportFailure {
|
|
20
|
+
name: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How one kind of file becomes a document. Adding PDF or DOCX means adding a
|
|
26
|
+
* reader, not changing the component.
|
|
27
|
+
*/
|
|
28
|
+
export interface FormatReader {
|
|
29
|
+
extensions: string[];
|
|
30
|
+
label: string;
|
|
31
|
+
read(file: File): Promise<string | ReadResult>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Works over everything that was read, before any of it is imported — where a
|
|
36
|
+
* backend step goes when it is about the text rather than the file format.
|
|
37
|
+
* Given every document at once so it can be done in one request.
|
|
38
|
+
*/
|
|
39
|
+
export type PrepareDocuments = (
|
|
40
|
+
documents: ImportedDocument[],
|
|
41
|
+
) => Promise<ImportedDocument[]>;
|
|
42
|
+
|
|
43
|
+
export declare const textReader: FormatReader;
|
|
44
|
+
export declare const defaultReaders: FormatReader[];
|
|
45
|
+
export declare function readerFor(readers: FormatReader[], filename: string): FormatReader | undefined;
|
|
46
|
+
export declare function extensionOf(filename: string): string;
|
|
47
|
+
export declare function baseName(filename: string): string;
|
|
48
|
+
/** Strips a BOM and makes line endings LF, so offsets are stable. */
|
|
49
|
+
export declare function normalise(text: string): string;
|
|
50
|
+
|
|
51
|
+
export interface LegalDocsImportProps {
|
|
52
|
+
/** Formats to accept. Defaults to plain text. */
|
|
53
|
+
readers?: FormatReader[];
|
|
54
|
+
/**
|
|
55
|
+
* Runs over everything read, before it is shown as ready. A failure here
|
|
56
|
+
* keeps the documents as they were read rather than losing the upload.
|
|
57
|
+
*/
|
|
58
|
+
onPrepare?: PrepareDocuments;
|
|
59
|
+
/**
|
|
60
|
+
* Called with everything read. The host decides what to keep — this
|
|
61
|
+
* component stores nothing and sends nothing anywhere.
|
|
62
|
+
*/
|
|
63
|
+
onImport?: (documents: ImportedDocument[]) => Promise<void> | void;
|
|
64
|
+
importLabel?: string;
|
|
65
|
+
clearOnImport?: boolean;
|
|
66
|
+
/** v-model:documents, for a host that would rather drive the list itself. */
|
|
67
|
+
documents?: ImportedDocument[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export declare const LegalDocsImport: DefineComponent<LegalDocsImportProps, {}, any>;
|
|
71
|
+
|
|
72
|
+
export declare const VueLegalDocsImportPlugin: Plugin;
|
|
73
|
+
export default VueLegalDocsImportPlugin;
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.ldi[data-v-df8ee0fe]{font-family:inherit;color-scheme:light;color:#1f2937}.ldi-drop[data-v-df8ee0fe]{border:2px dashed #d1d5db;border-radius:8px;padding:28px 16px;text-align:center;background:#f9fafb;transition:border-color .15s,background-color .15s}.ldi-drop--over[data-v-df8ee0fe]{border-color:#3b82f6;background:#eff6ff}.ldi-drop--busy[data-v-df8ee0fe]{opacity:.7}.ldi-picker[data-v-df8ee0fe]{display:none}.ldi-drop-title[data-v-df8ee0fe]{margin:0;font-size:14px;color:#1f2937}.ldi-drop-hint[data-v-df8ee0fe]{margin:6px 0 0;font-size:12px;color:#6b7280}.ldi-link[data-v-df8ee0fe]{background:none;border:none;padding:0;font:inherit;color:#2563eb;cursor:pointer;text-decoration:underline}.ldi-status[data-v-df8ee0fe]{margin:8px 0 0;font-size:13px;color:#6b7280}.ldi-failures[data-v-df8ee0fe]{margin:10px 0 0;padding:10px 12px;list-style:none;border:1px solid #fecaca;background:#fef2f2;border-radius:6px;font-size:13px;color:#991b1b}.ldi-list[data-v-df8ee0fe]{margin-top:14px;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;background:#fff}.ldi-list ul[data-v-df8ee0fe]{margin:0;padding:0;list-style:none;max-height:260px;overflow-y:auto}.ldi-list-head[data-v-df8ee0fe]{color:#1f2937;display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#f3f4f6;font-size:13px;font-weight:600}.ldi-item[data-v-df8ee0fe]{color:#1f2937;display:flex;align-items:center;gap:10px;padding:8px 12px;border-top:1px solid #e5e7eb;font-size:13px}.ldi-item-name[data-v-df8ee0fe]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ldi-item-size[data-v-df8ee0fe]{color:#6b7280;font-size:12px}.ldi-remove[data-v-df8ee0fe]{background:none;border:none;cursor:pointer;font-size:18px;line-height:1;color:#9ca3af;padding:0 4px}.ldi-remove[data-v-df8ee0fe]:hover{color:#dc2626}.ldi-actions[data-v-df8ee0fe]{display:flex;align-items:center;gap:12px;margin-top:14px}.ldi-button[data-v-df8ee0fe]{background:#2563eb;color:#fff;border:none;border-radius:6px;padding:8px 16px;font:inherit;font-size:14px;cursor:pointer}.ldi-button[data-v-df8ee0fe]:disabled{opacity:.6;cursor:default}.ldi-error[data-v-df8ee0fe]{color:#b91c1c;font-size:13px}.ldi-error--standalone[data-v-df8ee0fe]{margin:10px 0 0}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { defineComponent as z, useModel as F, ref as v, computed as x, openBlock as r, createElementBlock as i, createElementVNode as l, withModifiers as I, normalizeClass as S, createTextVNode as P, toDisplayString as u, createCommentVNode as h, Fragment as E, renderList as M, mergeModels as q } from "vue";
|
|
2
|
+
const G = {
|
|
3
|
+
extensions: [".txt", ".text", ".md"],
|
|
4
|
+
label: "Text files",
|
|
5
|
+
async read(e) {
|
|
6
|
+
return H(await e.text());
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function H(e) {
|
|
10
|
+
return e.replace(/^\ufeff/, "").replace(/\r\n/g, `
|
|
11
|
+
`).replace(/\r/g, `
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
const J = [G];
|
|
15
|
+
function K(e, o) {
|
|
16
|
+
const s = O(o);
|
|
17
|
+
return e.find((f) => f.extensions.includes(s));
|
|
18
|
+
}
|
|
19
|
+
function O(e) {
|
|
20
|
+
const o = e.lastIndexOf(".");
|
|
21
|
+
return o === -1 ? "" : e.slice(o).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function Q(e) {
|
|
24
|
+
const o = O(e);
|
|
25
|
+
return o ? e.slice(0, -o.length) : e;
|
|
26
|
+
}
|
|
27
|
+
function U(e) {
|
|
28
|
+
return e.flatMap((o) => o.extensions).join(",");
|
|
29
|
+
}
|
|
30
|
+
const W = { class: "ldi" }, X = ["accept"], Y = { class: "ldi-drop-title" }, Z = { class: "ldi-drop-hint" }, ee = {
|
|
31
|
+
key: 0,
|
|
32
|
+
class: "ldi-status"
|
|
33
|
+
}, te = {
|
|
34
|
+
key: 1,
|
|
35
|
+
class: "ldi-status"
|
|
36
|
+
}, ne = {
|
|
37
|
+
key: 2,
|
|
38
|
+
class: "ldi-failures"
|
|
39
|
+
}, ae = {
|
|
40
|
+
key: 3,
|
|
41
|
+
class: "ldi-list"
|
|
42
|
+
}, oe = { class: "ldi-list-head" }, se = { class: "ldi-item-name" }, le = { class: "ldi-item-size" }, re = ["aria-label", "onClick"], ie = {
|
|
43
|
+
key: 4,
|
|
44
|
+
class: "ldi-error ldi-error--standalone"
|
|
45
|
+
}, ue = {
|
|
46
|
+
key: 5,
|
|
47
|
+
class: "ldi-actions"
|
|
48
|
+
}, ce = ["disabled"], de = {
|
|
49
|
+
key: 0,
|
|
50
|
+
class: "ldi-error"
|
|
51
|
+
}, pe = /* @__PURE__ */ z({
|
|
52
|
+
__name: "LegalDocsImport",
|
|
53
|
+
props: /* @__PURE__ */ q({
|
|
54
|
+
readers: {},
|
|
55
|
+
onPrepare: {},
|
|
56
|
+
onImport: {},
|
|
57
|
+
importLabel: { default: "Import documents" },
|
|
58
|
+
clearOnImport: { type: Boolean, default: !0 }
|
|
59
|
+
}, {
|
|
60
|
+
documents: { default: () => [] },
|
|
61
|
+
documentsModifiers: {}
|
|
62
|
+
}),
|
|
63
|
+
emits: ["update:documents"],
|
|
64
|
+
setup(e) {
|
|
65
|
+
const o = e, s = F(e, "documents"), f = v(null), g = v(!1), y = v(""), k = v(!1), _ = v(!1), c = v(""), m = v([]), b = x(() => o.readers ?? J), $ = x(() => U(b.value)), N = x(() => b.value.map((n) => n.label).join(", "));
|
|
66
|
+
function R(n) {
|
|
67
|
+
return n.trim() ? n.trim().split(/\s+/).length : 0;
|
|
68
|
+
}
|
|
69
|
+
async function C(n) {
|
|
70
|
+
m.value = [], c.value = "";
|
|
71
|
+
const a = [];
|
|
72
|
+
for (const t of n) {
|
|
73
|
+
const p = K(b.value, t.name);
|
|
74
|
+
if (!p) {
|
|
75
|
+
m.value.push({ name: t.name, reason: "not a kind of file this can read" });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
y.value = t.name;
|
|
79
|
+
try {
|
|
80
|
+
const d = await p.read(t), D = typeof d == "string" ? d : d.text, L = typeof d == "string" ? void 0 : d.metadata;
|
|
81
|
+
if (!D.trim()) {
|
|
82
|
+
m.value.push({ name: t.name, reason: "the file is empty" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
a.push({
|
|
86
|
+
name: Q(t.name),
|
|
87
|
+
source: t.name,
|
|
88
|
+
full_text: D,
|
|
89
|
+
...L ? { metadata: L } : {}
|
|
90
|
+
});
|
|
91
|
+
} catch (d) {
|
|
92
|
+
m.value.push({
|
|
93
|
+
name: t.name,
|
|
94
|
+
reason: d instanceof Error ? d.message : "could not be read"
|
|
95
|
+
});
|
|
96
|
+
} finally {
|
|
97
|
+
y.value = "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
s.value = [...s.value, ...await V(a)];
|
|
101
|
+
}
|
|
102
|
+
async function V(n) {
|
|
103
|
+
if (!o.onPrepare || n.length === 0) return n;
|
|
104
|
+
k.value = !0;
|
|
105
|
+
try {
|
|
106
|
+
return await o.onPrepare(n);
|
|
107
|
+
} catch (a) {
|
|
108
|
+
return c.value = (a instanceof Error ? a.message : "preparing these documents failed") + " — importing them as they were read", n;
|
|
109
|
+
} finally {
|
|
110
|
+
k.value = !1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function A(n) {
|
|
114
|
+
const a = n.target;
|
|
115
|
+
C(Array.from(a.files ?? [])), a.value = "";
|
|
116
|
+
}
|
|
117
|
+
function B(n) {
|
|
118
|
+
var a;
|
|
119
|
+
g.value = !1, C(Array.from(((a = n.dataTransfer) == null ? void 0 : a.files) ?? []));
|
|
120
|
+
}
|
|
121
|
+
function T(n) {
|
|
122
|
+
s.value = s.value.filter((a, t) => t !== n);
|
|
123
|
+
}
|
|
124
|
+
function w() {
|
|
125
|
+
s.value = [], m.value = [], c.value = "";
|
|
126
|
+
}
|
|
127
|
+
async function j() {
|
|
128
|
+
if (o.onImport) {
|
|
129
|
+
_.value = !0, c.value = "";
|
|
130
|
+
try {
|
|
131
|
+
await o.onImport(s.value), o.clearOnImport && w();
|
|
132
|
+
} catch (n) {
|
|
133
|
+
c.value = n instanceof Error ? n.message : "could not import these documents";
|
|
134
|
+
} finally {
|
|
135
|
+
_.value = !1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return (n, a) => (r(), i("div", W, [
|
|
140
|
+
l("div", {
|
|
141
|
+
class: S(["ldi-drop", { "ldi-drop--over": g.value, "ldi-drop--busy": y.value }]),
|
|
142
|
+
onDragover: a[1] || (a[1] = I((t) => g.value = !0, ["prevent"])),
|
|
143
|
+
onDragleave: a[2] || (a[2] = I((t) => g.value = !1, ["prevent"])),
|
|
144
|
+
onDrop: I(B, ["prevent"])
|
|
145
|
+
}, [
|
|
146
|
+
l("input", {
|
|
147
|
+
ref_key: "picker",
|
|
148
|
+
ref: f,
|
|
149
|
+
type: "file",
|
|
150
|
+
multiple: "",
|
|
151
|
+
class: "ldi-picker",
|
|
152
|
+
accept: $.value,
|
|
153
|
+
onChange: A
|
|
154
|
+
}, null, 40, X),
|
|
155
|
+
l("p", Y, [
|
|
156
|
+
l("button", {
|
|
157
|
+
type: "button",
|
|
158
|
+
class: "ldi-link",
|
|
159
|
+
onClick: a[0] || (a[0] = (t) => {
|
|
160
|
+
var p;
|
|
161
|
+
return (p = f.value) == null ? void 0 : p.click();
|
|
162
|
+
})
|
|
163
|
+
}, " Choose files "),
|
|
164
|
+
a[3] || (a[3] = P(" or drop them here ", -1))
|
|
165
|
+
]),
|
|
166
|
+
l("p", Z, u(N.value), 1)
|
|
167
|
+
], 34),
|
|
168
|
+
y.value ? (r(), i("p", ee, "Reading " + u(y.value) + "…", 1)) : k.value ? (r(), i("p", te, "Preparing documents…")) : h("", !0),
|
|
169
|
+
m.value.length ? (r(), i("ul", ne, [
|
|
170
|
+
(r(!0), i(E, null, M(m.value, (t) => (r(), i("li", {
|
|
171
|
+
key: t.name
|
|
172
|
+
}, [
|
|
173
|
+
l("strong", null, u(t.name), 1),
|
|
174
|
+
P(" — " + u(t.reason), 1)
|
|
175
|
+
]))), 128))
|
|
176
|
+
])) : h("", !0),
|
|
177
|
+
s.value.length ? (r(), i("div", ae, [
|
|
178
|
+
l("div", oe, [
|
|
179
|
+
l("span", null, u(s.value.length) + " document" + u(s.value.length === 1 ? "" : "s") + " ready", 1),
|
|
180
|
+
l("button", {
|
|
181
|
+
type: "button",
|
|
182
|
+
class: "ldi-link",
|
|
183
|
+
onClick: w
|
|
184
|
+
}, "Clear")
|
|
185
|
+
]),
|
|
186
|
+
l("ul", null, [
|
|
187
|
+
(r(!0), i(E, null, M(s.value, (t, p) => (r(), i("li", {
|
|
188
|
+
key: t.source + p,
|
|
189
|
+
class: "ldi-item"
|
|
190
|
+
}, [
|
|
191
|
+
l("span", se, u(t.name), 1),
|
|
192
|
+
l("span", le, u(R(t.full_text)) + " words", 1),
|
|
193
|
+
l("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
class: "ldi-remove",
|
|
196
|
+
"aria-label": `Remove ${t.name}`,
|
|
197
|
+
onClick: (d) => T(p)
|
|
198
|
+
}, " × ", 8, re)
|
|
199
|
+
]))), 128))
|
|
200
|
+
])
|
|
201
|
+
])) : h("", !0),
|
|
202
|
+
c.value && !s.value.length ? (r(), i("p", ie, u(c.value), 1)) : h("", !0),
|
|
203
|
+
s.value.length && e.onImport ? (r(), i("div", ue, [
|
|
204
|
+
l("button", {
|
|
205
|
+
type: "button",
|
|
206
|
+
class: "ldi-button",
|
|
207
|
+
disabled: _.value,
|
|
208
|
+
onClick: j
|
|
209
|
+
}, u(_.value ? "Importing…" : e.importLabel), 9, ce),
|
|
210
|
+
c.value ? (r(), i("span", de, u(c.value), 1)) : h("", !0)
|
|
211
|
+
])) : h("", !0)
|
|
212
|
+
]));
|
|
213
|
+
}
|
|
214
|
+
}), me = (e, o) => {
|
|
215
|
+
const s = e.__vccOpts || e;
|
|
216
|
+
for (const [f, g] of o)
|
|
217
|
+
s[f] = g;
|
|
218
|
+
return s;
|
|
219
|
+
}, ve = /* @__PURE__ */ me(pe, [["__scopeId", "data-v-df8ee0fe"]]), ge = {
|
|
220
|
+
install(e) {
|
|
221
|
+
e.component("LegalDocsImport", ve);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
export {
|
|
225
|
+
ve as LegalDocsImport,
|
|
226
|
+
ge as VueLegalDocsImportPlugin,
|
|
227
|
+
Q as baseName,
|
|
228
|
+
ge as default,
|
|
229
|
+
J as defaultReaders,
|
|
230
|
+
O as extensionOf,
|
|
231
|
+
H as normalise,
|
|
232
|
+
K as readerFor,
|
|
233
|
+
G as textReader
|
|
234
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
(function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.VueLegalDocsImport={},s.Vue))})(this,function(s,e){"use strict";const _={extensions:[".txt",".text",".md"],label:"Text files",async read(t){return E(await t.text())}};function E(t){return t.replace(/^\ufeff/,"").replace(/\r\n/g,`
|
|
2
|
+
`).replace(/\r/g,`
|
|
3
|
+
`)}const V=[_];function b(t,a){const r=y(a);return t.find(p=>p.extensions.includes(r))}function y(t){const a=t.lastIndexOf(".");return a===-1?"":t.slice(a).toLowerCase()}function B(t){const a=y(t);return a?t.slice(0,-a.length):t}function w(t){return t.flatMap(a=>a.extensions).join(",")}const L={class:"ldi"},M=["accept"],P={class:"ldi-drop-title"},O={class:"ldi-drop-hint"},T={key:0,class:"ldi-status"},R={key:1,class:"ldi-status"},$={key:2,class:"ldi-failures"},j={key:3,class:"ldi-list"},F={class:"ldi-list-head"},A={class:"ldi-item-name"},v={class:"ldi-item-size"},z=["aria-label","onClick"],q={key:4,class:"ldi-error ldi-error--standalone"},G={key:5,class:"ldi-actions"},H=["disabled"],J={key:0,class:"ldi-error"},N=((t,a)=>{const r=t.__vccOpts||t;for(const[p,u]of a)r[p]=u;return r})(e.defineComponent({__name:"LegalDocsImport",props:e.mergeModels({readers:{},onPrepare:{},onImport:{},importLabel:{default:"Import documents"},clearOnImport:{type:Boolean,default:!0}},{documents:{default:()=>[]},documentsModifiers:{}}),emits:["update:documents"],setup(t){const a=t,r=e.useModel(t,"documents"),p=e.ref(null),u=e.ref(!1),f=e.ref(""),k=e.ref(!1),g=e.ref(!1),i=e.ref(""),m=e.ref([]),h=e.computed(()=>a.readers??V),K=e.computed(()=>w(h.value)),Q=e.computed(()=>h.value.map(o=>o.label).join(", "));function U(o){return o.trim()?o.trim().split(/\s+/).length:0}async function C(o){m.value=[],i.value="";const l=[];for(const n of o){const d=b(h.value,n.name);if(!d){m.value.push({name:n.name,reason:"not a kind of file this can read"});continue}f.value=n.name;try{const c=await d.read(n),x=typeof c=="string"?c:c.text,S=typeof c=="string"?void 0:c.metadata;if(!x.trim()){m.value.push({name:n.name,reason:"the file is empty"});continue}l.push({name:B(n.name),source:n.name,full_text:x,...S?{metadata:S}:{}})}catch(c){m.value.push({name:n.name,reason:c instanceof Error?c.message:"could not be read"})}finally{f.value=""}}r.value=[...r.value,...await W(l)]}async function W(o){if(!a.onPrepare||o.length===0)return o;k.value=!0;try{return await a.onPrepare(o)}catch(l){return i.value=(l instanceof Error?l.message:"preparing these documents failed")+" — importing them as they were read",o}finally{k.value=!1}}function X(o){const l=o.target;C(Array.from(l.files??[])),l.value=""}function Y(o){var l;u.value=!1,C(Array.from(((l=o.dataTransfer)==null?void 0:l.files)??[]))}function Z(o){r.value=r.value.filter((l,n)=>n!==o)}function I(){r.value=[],m.value=[],i.value=""}async function ee(){if(a.onImport){g.value=!0,i.value="";try{await a.onImport(r.value),a.clearOnImport&&I()}catch(o){i.value=o instanceof Error?o.message:"could not import these documents"}finally{g.value=!1}}}return(o,l)=>(e.openBlock(),e.createElementBlock("div",L,[e.createElementVNode("div",{class:e.normalizeClass(["ldi-drop",{"ldi-drop--over":u.value,"ldi-drop--busy":f.value}]),onDragover:l[1]||(l[1]=e.withModifiers(n=>u.value=!0,["prevent"])),onDragleave:l[2]||(l[2]=e.withModifiers(n=>u.value=!1,["prevent"])),onDrop:e.withModifiers(Y,["prevent"])},[e.createElementVNode("input",{ref_key:"picker",ref:p,type:"file",multiple:"",class:"ldi-picker",accept:K.value,onChange:X},null,40,M),e.createElementVNode("p",P,[e.createElementVNode("button",{type:"button",class:"ldi-link",onClick:l[0]||(l[0]=n=>{var d;return(d=p.value)==null?void 0:d.click()})}," Choose files "),l[3]||(l[3]=e.createTextVNode(" or drop them here ",-1))]),e.createElementVNode("p",O,e.toDisplayString(Q.value),1)],34),f.value?(e.openBlock(),e.createElementBlock("p",T,"Reading "+e.toDisplayString(f.value)+"…",1)):k.value?(e.openBlock(),e.createElementBlock("p",R,"Preparing documents…")):e.createCommentVNode("",!0),m.value.length?(e.openBlock(),e.createElementBlock("ul",$,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.value,n=>(e.openBlock(),e.createElementBlock("li",{key:n.name},[e.createElementVNode("strong",null,e.toDisplayString(n.name),1),e.createTextVNode(" — "+e.toDisplayString(n.reason),1)]))),128))])):e.createCommentVNode("",!0),r.value.length?(e.openBlock(),e.createElementBlock("div",j,[e.createElementVNode("div",F,[e.createElementVNode("span",null,e.toDisplayString(r.value.length)+" document"+e.toDisplayString(r.value.length===1?"":"s")+" ready",1),e.createElementVNode("button",{type:"button",class:"ldi-link",onClick:I},"Clear")]),e.createElementVNode("ul",null,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(r.value,(n,d)=>(e.openBlock(),e.createElementBlock("li",{key:n.source+d,class:"ldi-item"},[e.createElementVNode("span",A,e.toDisplayString(n.name),1),e.createElementVNode("span",v,e.toDisplayString(U(n.full_text))+" words",1),e.createElementVNode("button",{type:"button",class:"ldi-remove","aria-label":`Remove ${n.name}`,onClick:c=>Z(d)}," × ",8,z)]))),128))])])):e.createCommentVNode("",!0),i.value&&!r.value.length?(e.openBlock(),e.createElementBlock("p",q,e.toDisplayString(i.value),1)):e.createCommentVNode("",!0),r.value.length&&t.onImport?(e.openBlock(),e.createElementBlock("div",G,[e.createElementVNode("button",{type:"button",class:"ldi-button",disabled:g.value,onClick:ee},e.toDisplayString(g.value?"Importing…":t.importLabel),9,H),i.value?(e.openBlock(),e.createElementBlock("span",J,e.toDisplayString(i.value),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0)]))}}),[["__scopeId","data-v-df8ee0fe"]]),D={install(t){t.component("LegalDocsImport",N)}};s.LegalDocsImport=N,s.VueLegalDocsImportPlugin=D,s.baseName=B,s.default=D,s.defaultReaders=V,s.extensionOf=y,s.normalise=E,s.readerFor=b,s.textReader=_,Object.defineProperties(s,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-legal-docs-import",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A Vue component for bringing documents into a platform: pick or drop files, read them in the browser, hand them to the host.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/MaastrichtU-BISS/vue-legal-docs-import.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/MaastrichtU-BISS/vue-legal-docs-import#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/MaastrichtU-BISS/vue-legal-docs-import/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/vue-legal-docs-import.umd.js",
|
|
15
|
+
"module": "./dist/vue-legal-docs-import.es.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/vue-legal-docs-import.es.js",
|
|
21
|
+
"require": "./dist/vue-legal-docs-import.umd.js"
|
|
22
|
+
},
|
|
23
|
+
"./style.css": "./dist/style.css"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "vite build && node scripts/copy-types.mjs",
|
|
30
|
+
"dev": "vite build --watch",
|
|
31
|
+
"type-check": "vue-tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"legal",
|
|
36
|
+
"vue",
|
|
37
|
+
"import",
|
|
38
|
+
"upload"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vue": "^3.4.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vite": "^5.2.0",
|
|
48
|
+
"vue": "^3.4.0",
|
|
49
|
+
"vue-tsc": "^2.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|