asljs-ui-file 0.0.0-stage → 0.1.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/LICENSE.md +51 -0
- package/README.md +75 -2
- package/dist/file.d.ts +41 -0
- package/dist/file.js +315 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/File.md +69 -0
- package/package.json +61 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Permissive No-Derivatives License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software (the "Software") to:
|
|
7
|
+
|
|
8
|
+
1. Use the Software for any purpose, including commercial purposes.
|
|
9
|
+
2. Copy and redistribute the Software, including as part of another application,
|
|
10
|
+
library, or package.
|
|
11
|
+
3. Modify the Software for private or internal use.
|
|
12
|
+
|
|
13
|
+
Subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
## 1. No Distribution of Modified Versions
|
|
16
|
+
|
|
17
|
+
Modified versions of the Software, or substantial portions thereof, must not be
|
|
18
|
+
distributed without prior written permission from the copyright holder.
|
|
19
|
+
|
|
20
|
+
Routine build transformations, including minification, transpilation, and
|
|
21
|
+
bundling, are permitted for distribution, provided they do not intentionally
|
|
22
|
+
alter the Software's functionality.
|
|
23
|
+
|
|
24
|
+
## 2. Licence and Copyright Preservation
|
|
25
|
+
|
|
26
|
+
All distributed copies of the Software must retain this copyright notice and
|
|
27
|
+
licence.
|
|
28
|
+
|
|
29
|
+
The Software itself, including copies incorporated into other applications,
|
|
30
|
+
libraries, or packages, remains subject to this licence.
|
|
31
|
+
|
|
32
|
+
The Software must not be sublicensed or redistributed under a different licence.
|
|
33
|
+
|
|
34
|
+
## 3. Use in Other Software
|
|
35
|
+
|
|
36
|
+
Applications and libraries that merely use, import, link to, or bundle the
|
|
37
|
+
Software are not considered modified versions of the Software and may be
|
|
38
|
+
distributed under any licence.
|
|
39
|
+
|
|
40
|
+
Such use, incorporation, or bundling does not grant permission to relicense the
|
|
41
|
+
Software or distribute modified versions of it.
|
|
42
|
+
|
|
43
|
+
### 4. No Warranty
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
47
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
+
|
|
49
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
50
|
+
OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS
|
|
51
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# file
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality, performant
|
|
4
|
+
> JavaScript libraries and tools for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-ui-file` provides `asljs-file`, a file view that renders one file through
|
|
9
|
+
an ordered list of display handlers. A provider loads the file, and the first
|
|
10
|
+
handler that can display it renders it.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
- Handlers for PDF, image, text and editable text files, and your own.
|
|
15
|
+
- Text editing persists through the provider's optional `saveText`.
|
|
16
|
+
- A fallback "Preview unavailable" view, with an Open link when the content
|
|
17
|
+
allows one.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install asljs-ui-file
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
NPM package: [asljs-ui-file][NPM]
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Import the package to register the element and get the handlers:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createImageFileHandler,
|
|
33
|
+
createPdfFileHandler,
|
|
34
|
+
createTextFileHandler }
|
|
35
|
+
from 'asljs-ui-file';
|
|
36
|
+
|
|
37
|
+
const fileView =
|
|
38
|
+
document.createElement('asljs-file') as HTMLElement & {
|
|
39
|
+
provider: { loadFile: (fileName: string) => Promise<unknown> } | null;
|
|
40
|
+
handlers: unknown[];
|
|
41
|
+
fileName: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fileView.provider =
|
|
45
|
+
{ loadFile: async fileName => ({ name: fileName, text: 'Hello' }) };
|
|
46
|
+
|
|
47
|
+
fileView.handlers =
|
|
48
|
+
[ createPdfFileHandler(),
|
|
49
|
+
createImageFileHandler(),
|
|
50
|
+
createTextFileHandler() ];
|
|
51
|
+
|
|
52
|
+
fileView.fileName = 'notes.txt';
|
|
53
|
+
|
|
54
|
+
document.body.appendChild(fileView);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Further reading
|
|
58
|
+
|
|
59
|
+
- [File][DOC] - the provider, the handlers and the fallback view.
|
|
60
|
+
|
|
61
|
+
Questions and bugs: [asljs/issues][ISS].
|
|
62
|
+
|
|
63
|
+
## Related packages
|
|
64
|
+
|
|
65
|
+
- [ui][UI] - the shared code and the other UI packages.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT License. See [LICENSE][LIC] for details.
|
|
70
|
+
|
|
71
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
|
72
|
+
[DOC]: docs/File.md
|
|
73
|
+
[ISS]: https://github.com/AlexandriteSoftware/asljs/issues
|
|
74
|
+
[LIC]: LICENSE.md
|
|
75
|
+
[NPM]: https://www.npmjs.com/package/asljs-ui-file
|
|
76
|
+
[UI]: ../core/README.md
|
package/dist/file.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ComponentModelDefinition } from 'asljs-ui';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
export declare const FileViewModelDefinition: ComponentModelDefinition;
|
|
4
|
+
export interface FileViewData {
|
|
5
|
+
name: string;
|
|
6
|
+
mimeType?: string | null;
|
|
7
|
+
blob?: Blob | null;
|
|
8
|
+
text?: string | null;
|
|
9
|
+
dataUrl?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface FileViewProvider {
|
|
12
|
+
loadFile: (fileName: string) => Promise<FileViewData | null>;
|
|
13
|
+
saveText?: (fileName: string, text: string) => Promise<void> | void;
|
|
14
|
+
}
|
|
15
|
+
export interface FileHandlerRenderResult {
|
|
16
|
+
element: HTMLElement;
|
|
17
|
+
dispose?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface FileHandlerRenderContext {
|
|
20
|
+
file: FileViewData;
|
|
21
|
+
fileName: string;
|
|
22
|
+
provider: FileViewProvider;
|
|
23
|
+
}
|
|
24
|
+
export interface FileHandler {
|
|
25
|
+
canDisplay: (file: FileViewData) => boolean | Promise<boolean>;
|
|
26
|
+
render: (context: FileHandlerRenderContext) => Promise<FileHandlerRenderResult> | FileHandlerRenderResult;
|
|
27
|
+
}
|
|
28
|
+
export declare class FileView extends LitElement {
|
|
29
|
+
#private;
|
|
30
|
+
accessor provider: FileViewProvider | null;
|
|
31
|
+
accessor handlers: FileHandler[];
|
|
32
|
+
accessor fileName: string | null;
|
|
33
|
+
createRenderRoot(): this;
|
|
34
|
+
disconnectedCallback(): void;
|
|
35
|
+
render(): ReturnType<LitElement['render']>;
|
|
36
|
+
updated(changedProperties: Map<PropertyKey, unknown>): void;
|
|
37
|
+
}
|
|
38
|
+
export declare function createPdfFileHandler(): FileHandler;
|
|
39
|
+
export declare function createImageFileHandler(): FileHandler;
|
|
40
|
+
export declare function createTextFileHandler(): FileHandler;
|
|
41
|
+
export declare function createTextEditorFileHandler(): FileHandler;
|
package/dist/file.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { defineComponentModel } from 'asljs-ui';
|
|
8
|
+
import { html, LitElement } from 'lit';
|
|
9
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
10
|
+
export const FileViewModelDefinition = { name: 'FileViewModelDefinition',
|
|
11
|
+
title: 'File View',
|
|
12
|
+
properties: [{ name: 'provider',
|
|
13
|
+
title: 'Provider',
|
|
14
|
+
type: 'object',
|
|
15
|
+
description: 'File provider used to load and optionally save files.' },
|
|
16
|
+
{ name: 'handlers',
|
|
17
|
+
title: 'Handlers',
|
|
18
|
+
type: 'array',
|
|
19
|
+
description: 'Ordered file handlers from most specific to most general.' },
|
|
20
|
+
{ name: 'fileName',
|
|
21
|
+
title: 'File name',
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Selected file name to preview.' }] };
|
|
24
|
+
defineComponentModel('asljs-file', FileViewModelDefinition);
|
|
25
|
+
let FileView = class FileView extends LitElement {
|
|
26
|
+
#activeRenderState = null;
|
|
27
|
+
#renderRequestId = 0;
|
|
28
|
+
#provider_accessor_storage = null;
|
|
29
|
+
get provider() { return this.#provider_accessor_storage; }
|
|
30
|
+
set provider(value) { this.#provider_accessor_storage = value; }
|
|
31
|
+
#handlers_accessor_storage = [];
|
|
32
|
+
get handlers() { return this.#handlers_accessor_storage; }
|
|
33
|
+
set handlers(value) { this.#handlers_accessor_storage = value; }
|
|
34
|
+
#fileName_accessor_storage = null;
|
|
35
|
+
get fileName() { return this.#fileName_accessor_storage; }
|
|
36
|
+
set fileName(value) { this.#fileName_accessor_storage = value; }
|
|
37
|
+
createRenderRoot() {
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
disconnectedCallback() {
|
|
41
|
+
this.#disposeActiveRender();
|
|
42
|
+
super.disconnectedCallback();
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
return html `
|
|
46
|
+
<div data-role="content"></div>
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
updated(changedProperties) {
|
|
50
|
+
if (changedProperties.has('provider')
|
|
51
|
+
|| changedProperties.has('handlers')
|
|
52
|
+
|| changedProperties.has('fileName')) {
|
|
53
|
+
void this.#refresh();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async #refresh() {
|
|
57
|
+
const requestId = ++this.#renderRequestId;
|
|
58
|
+
this.#disposeActiveRender();
|
|
59
|
+
const contentHost = this.querySelector('[data-role="content"]');
|
|
60
|
+
if (contentHost === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.fileName === null
|
|
64
|
+
|| this.fileName.trim() === '') {
|
|
65
|
+
contentHost.replaceChildren(createMessageElement('Select a file to preview.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const provider = this.provider;
|
|
69
|
+
if (provider === null) {
|
|
70
|
+
contentHost.replaceChildren(createMessageElement('File provider is not configured.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
contentHost.replaceChildren(createMessageElement('Loading preview...'));
|
|
74
|
+
const file = await provider.loadFile(this.fileName);
|
|
75
|
+
if (requestId
|
|
76
|
+
!== this.#renderRequestId) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (file === null) {
|
|
80
|
+
contentHost.replaceChildren(createMessageElement('File content is not available.'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const handler = await findHandler(this.handlers, file);
|
|
84
|
+
if (requestId
|
|
85
|
+
!== this.#renderRequestId) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (handler === null) {
|
|
89
|
+
const fallback = createFallbackElement(file);
|
|
90
|
+
this.#activeRenderState =
|
|
91
|
+
{ dispose: fallback.dispose };
|
|
92
|
+
contentHost.replaceChildren(fallback.element);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const result = await handler.render({ file,
|
|
96
|
+
fileName: this.fileName,
|
|
97
|
+
provider });
|
|
98
|
+
if (requestId
|
|
99
|
+
!== this.#renderRequestId) {
|
|
100
|
+
result.dispose?.();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.#activeRenderState =
|
|
104
|
+
{ dispose: result.dispose };
|
|
105
|
+
contentHost.replaceChildren(result.element);
|
|
106
|
+
}
|
|
107
|
+
#disposeActiveRender() {
|
|
108
|
+
this.#activeRenderState?.dispose?.();
|
|
109
|
+
this.#activeRenderState = null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
__decorate([
|
|
113
|
+
property({ attribute: false })
|
|
114
|
+
], FileView.prototype, "provider", null);
|
|
115
|
+
__decorate([
|
|
116
|
+
property({ attribute: false })
|
|
117
|
+
], FileView.prototype, "handlers", null);
|
|
118
|
+
__decorate([
|
|
119
|
+
property({ attribute: false })
|
|
120
|
+
], FileView.prototype, "fileName", null);
|
|
121
|
+
FileView = __decorate([
|
|
122
|
+
customElement('asljs-file')
|
|
123
|
+
], FileView);
|
|
124
|
+
export { FileView };
|
|
125
|
+
export function createPdfFileHandler() {
|
|
126
|
+
return { canDisplay: file => isPdfFile(file),
|
|
127
|
+
render: async ({ file }) => {
|
|
128
|
+
const source = await getObjectUrlSource(file);
|
|
129
|
+
if (source === null) {
|
|
130
|
+
return createUnavailableResult();
|
|
131
|
+
}
|
|
132
|
+
const frame = document.createElement('iframe');
|
|
133
|
+
frame.src =
|
|
134
|
+
`${source.url}#toolbar=1&navpanes=0`;
|
|
135
|
+
frame.title = file.name;
|
|
136
|
+
frame.referrerPolicy = 'no-referrer';
|
|
137
|
+
frame.style.width = '100%';
|
|
138
|
+
frame.style.height = '100%';
|
|
139
|
+
frame.style.minHeight = '32rem';
|
|
140
|
+
frame.style.border = '0';
|
|
141
|
+
return { element: frame,
|
|
142
|
+
dispose: source.dispose };
|
|
143
|
+
} };
|
|
144
|
+
}
|
|
145
|
+
export function createImageFileHandler() {
|
|
146
|
+
return { canDisplay: file => isImageFile(file),
|
|
147
|
+
render: async ({ file }) => {
|
|
148
|
+
const source = await getObjectUrlSource(file);
|
|
149
|
+
if (source === null) {
|
|
150
|
+
return createUnavailableResult();
|
|
151
|
+
}
|
|
152
|
+
const image = document.createElement('img');
|
|
153
|
+
image.src = source.url;
|
|
154
|
+
image.alt = file.name;
|
|
155
|
+
image.style.display = 'block';
|
|
156
|
+
image.style.maxWidth = '100%';
|
|
157
|
+
image.style.maxHeight = '100%';
|
|
158
|
+
image.style.objectFit = 'contain';
|
|
159
|
+
return { element: image,
|
|
160
|
+
dispose: source.dispose };
|
|
161
|
+
} };
|
|
162
|
+
}
|
|
163
|
+
export function createTextFileHandler() {
|
|
164
|
+
return { canDisplay: file => isTextFile(file),
|
|
165
|
+
render: async ({ file }) => {
|
|
166
|
+
const text = await resolveText(file);
|
|
167
|
+
if (text === null) {
|
|
168
|
+
return createUnavailableResult();
|
|
169
|
+
}
|
|
170
|
+
const pre = document.createElement('pre');
|
|
171
|
+
pre.textContent = text;
|
|
172
|
+
pre.style.margin = '0';
|
|
173
|
+
pre.style.whiteSpace = 'pre-wrap';
|
|
174
|
+
return { element: pre };
|
|
175
|
+
} };
|
|
176
|
+
}
|
|
177
|
+
export function createTextEditorFileHandler() {
|
|
178
|
+
return { canDisplay: file => isTextFile(file),
|
|
179
|
+
render: async ({ file, fileName, provider }) => {
|
|
180
|
+
const text = await resolveText(file);
|
|
181
|
+
if (text === null) {
|
|
182
|
+
return createUnavailableResult();
|
|
183
|
+
}
|
|
184
|
+
const textArea = document.createElement('textarea');
|
|
185
|
+
textArea.value = text;
|
|
186
|
+
textArea.disabled = provider.saveText === undefined;
|
|
187
|
+
textArea.spellcheck = false;
|
|
188
|
+
textArea.style.width = '100%';
|
|
189
|
+
textArea.style.height = '100%';
|
|
190
|
+
textArea.style.minHeight = '16rem';
|
|
191
|
+
textArea.style.border = '0';
|
|
192
|
+
textArea.style.resize = 'none';
|
|
193
|
+
textArea.style.padding = '0.75rem';
|
|
194
|
+
textArea.style.fontFamily =
|
|
195
|
+
'SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
196
|
+
textArea.style.fontSize = '0.8rem';
|
|
197
|
+
textArea.style.lineHeight = '1.6';
|
|
198
|
+
textArea.style.boxSizing = 'border-box';
|
|
199
|
+
if (provider.saveText !== undefined) {
|
|
200
|
+
textArea.addEventListener('blur', () => {
|
|
201
|
+
void provider.saveText?.(fileName, textArea.value);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return { element: textArea };
|
|
205
|
+
} };
|
|
206
|
+
}
|
|
207
|
+
async function findHandler(handlers, file) {
|
|
208
|
+
for (const handler of handlers) {
|
|
209
|
+
if (await handler.canDisplay(file)) {
|
|
210
|
+
return handler;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function createMessageElement(text) {
|
|
216
|
+
const element = document.createElement('div');
|
|
217
|
+
element.textContent = text;
|
|
218
|
+
element.style.color =
|
|
219
|
+
'var(--bs-secondary-color, #6c757d)';
|
|
220
|
+
return element;
|
|
221
|
+
}
|
|
222
|
+
function createUnavailableResult() {
|
|
223
|
+
return { element: createMessageElement('Preview unavailable for this file type.') };
|
|
224
|
+
}
|
|
225
|
+
function createFallbackElement(file) {
|
|
226
|
+
const root = document.createElement('div');
|
|
227
|
+
root.style.display = 'flex';
|
|
228
|
+
root.style.alignItems = 'center';
|
|
229
|
+
root.style.gap = '0.5rem';
|
|
230
|
+
const message = createMessageElement('Preview unavailable for this file type.');
|
|
231
|
+
root.appendChild(message);
|
|
232
|
+
if (file.blob
|
|
233
|
+
|| file.dataUrl) {
|
|
234
|
+
const link = document.createElement('a');
|
|
235
|
+
link.textContent = 'Open';
|
|
236
|
+
link.target = '_blank';
|
|
237
|
+
link.rel =
|
|
238
|
+
'noopener noreferrer';
|
|
239
|
+
if (file.dataUrl) {
|
|
240
|
+
link.href = file.dataUrl;
|
|
241
|
+
root.appendChild(link);
|
|
242
|
+
return { element: root };
|
|
243
|
+
}
|
|
244
|
+
const url = URL.createObjectURL(file.blob);
|
|
245
|
+
link.href = url;
|
|
246
|
+
root.appendChild(link);
|
|
247
|
+
return { element: root,
|
|
248
|
+
dispose: () => {
|
|
249
|
+
URL.revokeObjectURL(url);
|
|
250
|
+
} };
|
|
251
|
+
}
|
|
252
|
+
return { element: root };
|
|
253
|
+
}
|
|
254
|
+
async function getObjectUrlSource(file) {
|
|
255
|
+
if (file.dataUrl) {
|
|
256
|
+
return { url: file.dataUrl };
|
|
257
|
+
}
|
|
258
|
+
if (file.blob) {
|
|
259
|
+
if (isPdfFile(file)
|
|
260
|
+
&& normalizeMimeType(file.mimeType ?? file.blob.type)
|
|
261
|
+
!== 'application/pdf') {
|
|
262
|
+
const pdfBlob = new Blob([await file.blob.arrayBuffer()], { type: 'application/pdf' });
|
|
263
|
+
const pdfUrl = URL.createObjectURL(pdfBlob);
|
|
264
|
+
return { url: pdfUrl,
|
|
265
|
+
dispose: () => {
|
|
266
|
+
URL.revokeObjectURL(pdfUrl);
|
|
267
|
+
} };
|
|
268
|
+
}
|
|
269
|
+
const url = URL.createObjectURL(file.blob);
|
|
270
|
+
return { url,
|
|
271
|
+
dispose: () => {
|
|
272
|
+
URL.revokeObjectURL(url);
|
|
273
|
+
} };
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
async function resolveText(file) {
|
|
278
|
+
if (typeof file.text
|
|
279
|
+
=== 'string') {
|
|
280
|
+
return file.text;
|
|
281
|
+
}
|
|
282
|
+
if (file.blob
|
|
283
|
+
&& isTextFile(file)) {
|
|
284
|
+
return await file.blob.text();
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function isPdfFile(file) {
|
|
289
|
+
return normalizeMimeType(file.mimeType ?? file.blob?.type)
|
|
290
|
+
=== 'application/pdf'
|
|
291
|
+
|| getLowerFileName(file).endsWith('.pdf');
|
|
292
|
+
}
|
|
293
|
+
function isImageFile(file) {
|
|
294
|
+
const mimeType = normalizeMimeType(file.mimeType ?? file.blob?.type);
|
|
295
|
+
return mimeType.startsWith('image/')
|
|
296
|
+
|| /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(file.name);
|
|
297
|
+
}
|
|
298
|
+
function isTextFile(file) {
|
|
299
|
+
if (typeof file.text
|
|
300
|
+
=== 'string') {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const mimeType = normalizeMimeType(file.mimeType ?? file.blob?.type);
|
|
304
|
+
return mimeType.startsWith('text/')
|
|
305
|
+
|| ['application/json',
|
|
306
|
+
'application/xml',
|
|
307
|
+
'image/svg+xml'].includes(mimeType)
|
|
308
|
+
|| /\.(txt|csv|json|xml|html|htm|md|markdown|svg)$/i.test(file.name);
|
|
309
|
+
}
|
|
310
|
+
function normalizeMimeType(mimeType) {
|
|
311
|
+
return (mimeType ?? '').trim().toLowerCase();
|
|
312
|
+
}
|
|
313
|
+
function getLowerFileName(file) {
|
|
314
|
+
return file.name.toLowerCase();
|
|
315
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createImageFileHandler, createPdfFileHandler, createTextEditorFileHandler, createTextFileHandler, FileView, FileViewModelDefinition, type FileHandler, type FileHandlerRenderContext, type FileHandlerRenderResult, type FileViewData, type FileViewProvider } from './file.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createImageFileHandler, createPdfFileHandler, createTextEditorFileHandler, createTextFileHandler, FileView, FileViewModelDefinition } from './file.js';
|
package/docs/File.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# File
|
|
2
|
+
|
|
3
|
+
`asljs-file` (`FileView`) renders one file through an ordered list of display
|
|
4
|
+
handlers.
|
|
5
|
+
|
|
6
|
+
## Properties
|
|
7
|
+
|
|
8
|
+
- `provider` — `provider.loadFile(fileName)` returns normalized file data;
|
|
9
|
+
`provider.saveText(...)` persists edits when text editing is used
|
|
10
|
+
- `handlers` — ordered from most specific to most general
|
|
11
|
+
- `fileName` — the file to show
|
|
12
|
+
|
|
13
|
+
The component asks the handlers in order whether they can display the file, and
|
|
14
|
+
the first match renders it. The component itself does not hard-code file types:
|
|
15
|
+
those decisions belong to the handlers, and providers abstract file lookup and
|
|
16
|
+
optional text persistence.
|
|
17
|
+
|
|
18
|
+
## Handlers
|
|
19
|
+
|
|
20
|
+
`asljs-ui-file` provides these handler factories:
|
|
21
|
+
|
|
22
|
+
- `createPdfFileHandler()`
|
|
23
|
+
- `createImageFileHandler()`
|
|
24
|
+
- `createTextFileHandler()`
|
|
25
|
+
- `createTextEditorFileHandler()`
|
|
26
|
+
|
|
27
|
+
If no handler matches, the component shows fallback "Preview unavailable"
|
|
28
|
+
content, and an Open link when blob or data-url content is available.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import {
|
|
34
|
+
createImageFileHandler,
|
|
35
|
+
createPdfFileHandler,
|
|
36
|
+
createTextFileHandler,
|
|
37
|
+
} from 'asljs-ui-file';
|
|
38
|
+
|
|
39
|
+
const fileView =
|
|
40
|
+
document.createElement('asljs-file') as HTMLElement & {
|
|
41
|
+
provider: {
|
|
42
|
+
loadFile: (fileName: string) => Promise<unknown>;
|
|
43
|
+
} | null;
|
|
44
|
+
handlers: unknown[];
|
|
45
|
+
fileName: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fileView.provider =
|
|
49
|
+
{ loadFile: async (fileName: string) => {
|
|
50
|
+
if (fileName === 'invoice.pdf') {
|
|
51
|
+
return {
|
|
52
|
+
name: 'invoice.pdf',
|
|
53
|
+
blob: new Blob([ 'pdf' ], { type: 'application/pdf' }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: fileName,
|
|
59
|
+
text: 'No content',
|
|
60
|
+
};
|
|
61
|
+
} };
|
|
62
|
+
|
|
63
|
+
fileView.handlers =
|
|
64
|
+
[ createPdfFileHandler(),
|
|
65
|
+
createImageFileHandler(),
|
|
66
|
+
createTextFileHandler() ];
|
|
67
|
+
|
|
68
|
+
fileView.fileName = 'invoice.pdf';
|
|
69
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-ui-file",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A file view web component with pluggable display handlers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"asljs": {
|
|
7
|
+
"type": "lib"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE.md",
|
|
21
|
+
"docs/**"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"flint": "toolkit flint",
|
|
25
|
+
"clean": "toolkit clean",
|
|
26
|
+
"build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
27
|
+
"build:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
28
|
+
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
29
|
+
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
30
|
+
"prepack": "npm run clean && npm run build:dist && npm run build && npm run test && npm run guard:clean-git",
|
|
31
|
+
"postpublish": "toolkit tag-release-revision",
|
|
32
|
+
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
33
|
+
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
34
|
+
"coverage": "npm run build && node --test --experimental-test-coverage --enable-source-maps --test-coverage-exclude=\"**/*.test.js\" \"build/**/*.test.js\"",
|
|
35
|
+
"release:patch": "toolkit release-patch",
|
|
36
|
+
"all": "npm run clean && npm run build:dist && npm run flint && npm run typecheck && npm run build && npm run test"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"web-components",
|
|
40
|
+
"lit",
|
|
41
|
+
"ui",
|
|
42
|
+
"file",
|
|
43
|
+
"viewer"
|
|
44
|
+
],
|
|
45
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
46
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
47
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"asljs-ui": "^0.1.1",
|
|
57
|
+
"lit": "^3.3.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"asljs-testing": "^0.1.1",
|
|
61
|
+
"asljs-toolkit": "^0.1.0"
|
|
62
|
+
}
|
|
63
|
+
}
|