48 |
In same-origin iframe
49 |
50 | If above it says "Using the File System Access API", then it should say
51 | so in the iframe.
52 |
53 |
54 |
In cross-origin iframe
55 |
56 | Cross-origin iframes cannot use the File System Access API, so it uses
57 | the fallback.
58 |
59 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/demo/script.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { fileOpen, directoryOpen, fileSave, supported } from '../src/index.js';
18 |
19 | import { imageToBlob } from './image-to-blob.mjs';
20 |
21 | (async () => {
22 | const openButton = document.querySelector('#open');
23 | const openMultipleButton = document.querySelector('#open-multiple');
24 | const openImageOrTextButton = document.querySelector('#open-image-or-text');
25 | const openDirectoryButton = document.querySelector('#open-directory');
26 | const saveButton = document.querySelector('#save');
27 | const saveBlobButton = document.querySelector('#save-blob');
28 | const saveResponseButton = document.querySelector('#save-response');
29 | const supportedParagraph = document.querySelector('.supported');
30 | const pre = document.querySelector('pre');
31 |
32 | const ABORT_MESSAGE = 'The user aborted a request.';
33 |
34 | if (supported) {
35 | supportedParagraph.textContent = 'Using the File System Access API.';
36 | } else {
37 | supportedParagraph.textContent = 'Using the fallback implementation.';
38 | }
39 |
40 | const appendImage = (blob) => {
41 | const img = document.createElement('img');
42 | img.src = URL.createObjectURL(blob);
43 | document.body.append(img);
44 | img.onload = img.onerror = () => URL.revokeObjectURL(img.src);
45 | };
46 |
47 | const listDirectory = (blobs) => {
48 | let fileStructure = '';
49 | if (blobs.length && !(blobs[0] instanceof File)) {
50 | return (pre.textContent += 'No files in directory.\n');
51 | }
52 | blobs
53 | .sort((a, b) => a.webkitRelativePath.localeCompare(b))
54 | .forEach((blob) => {
55 | // The File System Access API currently reports the `webkitRelativePath`
56 | // as empty string `''`.
57 | fileStructure += `${blob.webkitRelativePath}\n`;
58 | });
59 | pre.textContent += fileStructure;
60 |
61 | blobs
62 | .filter((blob) => {
63 | return blob.type.startsWith('image/');
64 | })
65 | .forEach((blob) => {
66 | appendImage(blob);
67 | });
68 | };
69 |
70 | openButton.addEventListener('click', async () => {
71 | try {
72 | const blob = await fileOpen({
73 | description: 'Image files',
74 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'],
75 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
76 | });
77 | appendImage(blob);
78 | } catch (err) {
79 | if (err.name !== 'AbortError') {
80 | return console.error(err);
81 | }
82 | console.log(ABORT_MESSAGE);
83 | }
84 | });
85 |
86 | openMultipleButton.addEventListener('click', async () => {
87 | try {
88 | const blobs = await fileOpen({
89 | description: 'Image files',
90 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'],
91 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
92 | multiple: true,
93 | });
94 | for (const blob of blobs) {
95 | appendImage(blob);
96 | }
97 | } catch (err) {
98 | if (err.name !== 'AbortError') {
99 | return console.error(err);
100 | }
101 | console.log(ABORT_MESSAGE);
102 | }
103 | });
104 |
105 | openImageOrTextButton.addEventListener('click', async () => {
106 | try {
107 | const blobs = await fileOpen([
108 | {
109 | description: 'Image files',
110 | mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'],
111 | extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
112 | multiple: true,
113 | },
114 | {
115 | description: 'Text files',
116 | mimeTypes: ['text/*'],
117 | extensions: ['.txt'],
118 | },
119 | ]);
120 | for (const blob of blobs) {
121 | if (blob.type.startsWith('image/')) {
122 | appendImage(blob);
123 | } else {
124 | document.body.append(await blob.text());
125 | }
126 | }
127 | } catch (err) {
128 | if (err.name !== 'AbortError') {
129 | return console.error(err);
130 | }
131 | console.log(ABORT_MESSAGE);
132 | }
133 | });
134 |
135 | openDirectoryButton.addEventListener('click', async () => {
136 | try {
137 | const blobs = await directoryOpen({
138 | recursive: true,
139 | });
140 | listDirectory(blobs);
141 | } catch (err) {
142 | if (err.name !== 'AbortError') {
143 | return console.error(err);
144 | }
145 | console.log(ABORT_MESSAGE);
146 | }
147 | });
148 |
149 | saveButton.addEventListener('click', async () => {
150 | const blob = await imageToBlob(document.querySelector('img'));
151 | try {
152 | await fileSave(blob, {
153 | fileName: 'floppy.png',
154 | extensions: ['.png'],
155 | });
156 | } catch (err) {
157 | if (err.name !== 'AbortError') {
158 | return console.error(err);
159 | }
160 | console.log(ABORT_MESSAGE);
161 | }
162 | });
163 |
164 | saveBlobButton.addEventListener('click', async () => {
165 | const blob = imageToBlob(document.querySelector('img'));
166 | try {
167 | await fileSave(blob, {
168 | fileName: 'floppy-blob.png',
169 | extensions: ['.png'],
170 | });
171 | } catch (err) {
172 | if (err.name !== 'AbortError') {
173 | return console.error(err);
174 | }
175 | console.log(ABORT_MESSAGE);
176 | }
177 | });
178 |
179 | saveResponseButton.addEventListener('click', async () => {
180 | const response = await fetch('./floppy.png');
181 | try {
182 | await fileSave(response, {
183 | fileName: 'floppy-response.png',
184 | extensions: ['.png'],
185 | });
186 | } catch (err) {
187 | if (err.name !== 'AbortError') {
188 | return console.error(err);
189 | }
190 | console.log(ABORT_MESSAGE);
191 | }
192 | });
193 |
194 | openButton.disabled = false;
195 | openMultipleButton.disabled = false;
196 | openImageOrTextButton.disabled = false;
197 | openDirectoryButton.disabled = false;
198 | saveButton.disabled = false;
199 | saveBlobButton.disabled = false;
200 | saveResponseButton.disabled = false;
201 | })();
202 |
203 | if (window.self !== window.top) {
204 | document.querySelector('.iframes').remove();
205 | }
206 |
--------------------------------------------------------------------------------
/demo/style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | body {
18 | font-family: helvetica, arial, sans-serif;
19 | margin: 2em;
20 | }
21 |
22 | h1 {
23 | font-style: italic;
24 | color: #373fff;
25 | }
26 |
27 | img {
28 | display: block;
29 | max-width: 100%;
30 | height: auto;
31 | margin-block: 1rem;
32 | }
33 |
34 | code {
35 | font-family: ui-monospace, monospace;
36 | }
37 |
38 | .supported {
39 | color: green;
40 | }
41 |
42 | iframe {
43 | width: 100%;
44 | height: 400px;
45 | border: solid 1px #000;
46 | }
47 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // eslint.config.js
2 | import prettierConfig from 'eslint-config-prettier';
3 |
4 | export default [
5 | {
6 | files: ['**/*.js', '**/*.mjs'],
7 | languageOptions: {
8 | ecmaVersion: 2020,
9 | sourceType: 'module',
10 | },
11 | rules: {
12 | quotes: ['error', 'single'],
13 | semi: ['error', 'always'],
14 | indent: ['error', 2],
15 | 'no-var': 'error',
16 | 'prefer-const': 'error',
17 | 'comma-dangle': ['error', 'never'],
18 | 'require-jsdoc': 'off',
19 | 'valid-jsdoc': 'off',
20 | ...prettierConfig.rules,
21 | },
22 | },
23 | ];
24 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Properties shared by all `options` provided to file save and open operations
3 | */
4 | export interface CoreFileOptions {
5 | /** Acceptable file extensions. Defaults to `[""]`. */
6 | extensions?: string[];
7 | /** Suggested file description. Defaults to `""`. */
8 | description?: string;
9 | /** Acceptable MIME types. Defaults to `[]`. */
10 | mimeTypes?: string[];
11 | }
12 |
13 | /**
14 | * Properties shared by the _first_ `options` object provided to file save and
15 | * open operations (any additional options objects provided to those operations
16 | * cannot have these properties)
17 | */
18 | export interface FirstCoreFileOptions extends CoreFileOptions {
19 | startIn?: WellKnownDirectory | FileSystemHandle;
20 | /** By specifying an ID, the user agent can remember different directories for different IDs. */
21 | id?: string;
22 | excludeAcceptAllOption?: boolean | false;
23 | }
24 |
25 | /**
26 | * The first `options` object passed to file save operations can also specify
27 | * a filename
28 | */
29 | export interface FirstFileSaveOptions extends FirstCoreFileOptions {
30 | /** Suggested file name. Defaults to `"Untitled"`. */
31 | fileName?: string;
32 | /**
33 | * Configurable cleanup and `Promise` rejector usable with legacy API for
34 | * determining when (and reacting if) a user cancels the operation. The
35 | * method will be passed a reference to the internal `rejectionHandler` that
36 | * can, e.g., be attached to/removed from the window or called after a
37 | * timeout. The method should return a function that will be called when
38 | * either the user chooses to open a file or the `rejectionHandler` is
39 | * called. In the latter case, the returned function will also be passed a
40 | * reference to the `reject` callback for the `Promise` returned by
41 | * `fileOpen`, so that developers may reject the `Promise` when desired at
42 | * that time.
43 | * Example rejector:
44 | *
45 | * const file = await fileOpen({
46 | * legacySetup: (rejectionHandler) => {
47 | * const timeoutId = setTimeout(rejectionHandler, 10_000);
48 | * return (reject) => {
49 | * clearTimeout(timeoutId);
50 | * if (reject) {
51 | * reject('My error message here.');
52 | * }
53 | * };
54 | * },
55 | * });
56 | */
57 | legacySetup?: (
58 | resolve: () => void,
59 | rejectionHandler: () => void,
60 | anchor: HTMLAnchorElement
61 | ) => () => void;
62 | }
63 |
64 | /**
65 | * The first `options` object passed to file open operations can specify
66 | * whether multiple files can be selected (the return type of the operation
67 | * will be updated appropriately).
68 | */
69 | export interface FirstFileOpenOptions