From d85e1c1f686e46da422e570845f7076e9442a847 Mon Sep 17 00:00:00 2001 From: Chris Mills Date: Tue, 6 Aug 2024 13:20:43 +0100 Subject: [PATCH 1/4] Add information on file system access locking modes --- .../createsyncaccesshandle/index.md | 149 +++++++++++++++++- .../createwritable/index.md | 124 ++++++++++++++- 2 files changed, 270 insertions(+), 3 deletions(-) diff --git a/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md b/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md index 7ee8bfed17b8a30..dd757dc919d9745 100644 --- a/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md +++ b/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md @@ -19,11 +19,24 @@ Creating a {{domxref('FileSystemSyncAccessHandle')}} takes an exclusive lock on ```js-nolint createSyncAccessHandle() +createSyncAccessHandle(options) ``` ### Parameters -None. +- `options` {{optional_inline}} + + - : An object with the following properties: + + - `mode` {{optional_inline}} {{non-standard_inline}} + - : A string specifying the locking mode for the access handle. The default value is `"readwrite"`. + Possible values are: + - `"read-only"` + - : Multiple `FileSystemSyncAccessHandle`s can be opened simultaneously on a file (for example when using the same app in multiple tabs), provided they are all opened in `"read-only"` mode. Once opened, read-like methods can be called on the handles — {{domxref("FileSystemSyncAccessHandle.read", "read()")}}, {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}, and {{domxref("FileSystemSyncAccessHandle.close", "close()")}}. + - `"readwrite"` + - : Only one `FileSystemSyncAccessHandle` can be opened simultaneously. Attempting to open subsequent handles before the first handle is closed results in a `NoModificationAllowedError` exception being thrown. Once opened, any available method can be called on the handle. + - `"readwrite-unsafe"` + - : Multiple `FileSystemSyncAccessHandle`s can be opened simultaneously on a file, provided they are all opened in `"readwrite-unsafe"` mode. Once opened, any available method can be called on the handles. ### Return value @@ -38,10 +51,12 @@ A {{jsxref('Promise')}} which resolves to a {{domxref('FileSystemSyncAccessHandl - `NotFoundError` {{domxref("DOMException")}} - : Thrown if current entry is not found. - `NoModificationAllowedError` {{domxref("DOMException")}} - - : Thrown if the browser is not able to acquire a lock on the file associated with the file handle. + - : Thrown if the browser is not able to acquire a lock on the file associated with the file handle. This could be because `mode` is set to `readwrite` and an attempt is made to open multiple handles simultaneously. ## Examples +### Basic usage + The following asynchronous event handler function is contained inside a Web Worker. The snippet inside it creates a synchronous file access handle. ```js @@ -62,6 +77,136 @@ onmessage = async (e) => { }; ``` +### Complete example with `mode` option + +Our [createSyncAccessHandle() mode test](https://createsyncaccesshandle-mode-test.glitch.me/) example provides an {{htmlelement("input")}} field to enter text into, and two buttons — one to write entered text to the end of a file in the origin private file system, and one to empty the file when it becomes too full. + +Try exploring the demo above, with the browser developer console open so you can see what is happening. If you try opening the demo in multiple browsers (get some friends to help!) you will find that multiple handles can be opened at once to write to the file at the same time. This is because `mode: "readwrite-unsafe"` is set on the `createSyncAccessHandle()` calls. + +Below we'll explore the code. + +#### HTML + +The two {{htmlelement("button")}} elements and text {{htmlelement("input")}} field look like this: + +```html +
    +
  1. + + +
  2. +
  3. + Write your text to the file: +
  4. +
  5. + Empty the file if it gets too full: + +
  6. +
+``` + +#### Main JavaScript + +The main thread JavaScript inside the HTML file is shown below. We grab references to the write text button, empty file button, and text input field, then we create a new web worker using the {{domxref("Worker.Worker", "Worker()")}} constructor. We then define two functions and set them as event handlers on the buttons: + +- `writeToOPFS()` is run when the write text button is clicked. This function posts the entered value of the text field to the worker using the {{domxref("Worker.postMessage()")}} method, then empties the text field, ready for the next addition. +- `emptyOPFS()` is run when the empty file button is clicked. This posts a very specific string to the worker that we will use to identify when the file is to be emptied rather then written to. + +```js +const writeBtn = document.querySelector(".write"); +const emptyBtn = document.querySelector(".empty"); +const fileText = document.querySelector("#filetext"); + +const opfsWorker = new Worker("worker.js"); + +function writeToOPFS() { + opfsWorker.postMessage(fileText.value); + console.log("Text posted to worker"); + fileText.value = ""; +} + +function emptyOPFS() { + opfsWorker.postMessage("!!empty!!"); +} + +writeBtn.addEventListener("click", writeToOPFS); +emptyBtn.addEventListener("click", emptyOPFS); +``` + +#### Worker JavaScript + +The worker JavaScript is shown below. + +First, we run a function called `initOPFS()` that gets a reference to the OPFS root using {{domxref("StorageManager.getDirectory()")}}, creates a file and returns its handle using {{domxref("FileSystemDirectoryHandle.getFileHandle()")}}, and then returns an accessHandle using `createSyncAccessHandle()` with `mode: "readwrite-unsafe"` set to allow multiple handles to access the same file simultaneously. + +```js +let accessHandle; + +async function initOPFS() { + const opfsRoot = await navigator.storage.getDirectory(); + const fileHandle = await opfsRoot.getFileHandle("file.txt", { create: true }); + accessHandle = await fileHandle.createSyncAccessHandle({ + mode: "readwrite-unsafe", + }); +} + +initOPFS(); +``` + +Inside the worker's [message event](/en-US/docs/Web/API/Worker/message_event) handler function, we first get the size of the file using {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}. We then check to see whether the data sent in the message is the specific string sent by the `emptyOPFS()` method — `"!!empty!!"`. If so, we empty the file using {{domxref("FileSystemSyncAccessHandle.truncate", "truncate()")}} with a vlaue of `0`, and update the file size contained in the `size` variable. + +If the message data is something else, we: + +- Create a new {{domxref("TextEncoder")}} and {{domxref("TextDecoder")}} to handle encoding and decoding the text content later on. +- Encode the message data and write the result to the end of the file using {{domxref("FileSystemSyncAccessHandle.write", "write()")}}, then update the file size contained in the `size` variable. +- Create a {{domxref("DataView")}} to contain the file contents, and read the content into it using {{domxref("FileSystemSyncAccessHandle.read", "read()")}}. +- Decode the `DataView` contents and log it to the console. + +```js +onmessage = function (e) { + console.log("Worker: Message received from main script"); + + // Initialize this variable for the size of the file + let size; + // Get the current size of the file + size = accessHandle.getSize(); + + if (e.data === "!!empty!!") { + // Truncate the file to 0 bytes + accessHandle.truncate(0); + + // Get the current size of the file + size = accessHandle.getSize(); + } else { + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); + + // Encode content to write to the file + const content = textEncoder.encode(e.data); + // Write the content at the end of the file + accessHandle.write(content, { at: size }); + + // Get the current size of the file + size = accessHandle.getSize(); + + // Prepare a data view of the length of the file + const dataView = new DataView(new ArrayBuffer(size)); + + // Read the entire file into the data view + accessHandle.read(dataView); + + // Log the current file contents to the console + console.log("File contents:" + textDecoder.decode(dataView)); + + // Flush the changes + accessHandle.flush(); + } + + // Log the size of the file to the console + console.log("Size: " + size); +}; +``` + ## Specifications {{Specifications}} diff --git a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md index b2f134bf2ceb623..01151e2b57bcbeb 100644 --- a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md +++ b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md @@ -31,6 +31,13 @@ createWritable(options) - : A {{jsxref('Boolean')}}. Default `false`. When set to `true` if the file exists, the existing file is first copied to the temporary file. Otherwise the temporary file starts out empty. + - `mode` {{optional_inline}} {{non-standard_inline}} + - : A string specifying the locking mode for the writable file stream. The default value is `"siloed"`. + Possible values are: + - `"exclusive"` + - : Only one `FileSystemWritableFileStream` writer can be opened. Attempting to open subsequent writers before the first writer is closed results in a `NoModificationAllowedError` exception being thrown. + - `"siloed"` + - : Multiple `FileSystemWritableFileStream` writers can be opened at the same time, each with its own swap file, for example when using the same app in multiple tabs. The last writer opened has its data written, as the data gets flushed when each writer is closed. ### Return value @@ -43,12 +50,14 @@ A {{jsxref('Promise')}} which resolves to a {{domxref('FileSystemWritableFileStr - `NotFoundError` {{domxref("DOMException")}} - : Thrown if current entry is not found. - `NoModificationAllowedError` {{domxref("DOMException")}} - - : Thrown if the browser is not able to acquire a lock on the file associated with the file handle. + - : Thrown if the browser is not able to acquire a lock on the file associated with the file handle. This could be because `mode` is set to `exclusive` and an attempt is made to open multiple writers simultaneously. - `AbortError` {{domxref("DOMException")}} - : Thrown if implementation-defined malware scans and safe-browsing checks fails. ## Examples +### Basic usage + The following asynchronous function writes the given contents to the file handle, and thus to disk. ```js @@ -64,6 +73,119 @@ async function writeFile(fileHandle, contents) { } ``` +### Expanded usage with options + +In this example, we provide a button to select a file to write to, a text input field into which you can enter some text to write to the file, and a second button to write the text to the file. + +#### HTML + +The two {{htmlelement("button")}} elements and text {{htmlelement("input")}} field look like this: + +```html +
    +
  1. + Select a file to write to: +
  2. +
  3. + + +
  4. +
  5. + Write your text to the file: + +
  6. +
+``` + +The text input field and the write text button are set to be disabled initially via the [`disabled`](/en-US/docs/Web/HTML/Attributes/disabled) attribute — they shouldn't be used until the user has selected a file to write to. + +```css hidden +li { + margin-bottom: 10px; +} +``` + +#### JavaScript + +In the JavaScript, we start by grabbing references to the select file button, the the write text button, and the text input field. We also declare a global variable `writableStream`, which will store a reference to the writeable stream for writing the text to the file, once created. We initially set it to `null`. + +```js +const selectBtn = document.querySelector(".select"); +const writeBtn = document.querySelector(".write"); +const fileText = document.querySelector("#filetext"); + +let writableStream = null; +``` + +Next, we create an async function called `selectFile()`, which we'll invoke when the select button is pressed. This uses the {{domxref("Window.showSaveFilePicker()")}} method to show the user a file picker dialog and create a file handle to the file they choose. On that handle, we invoke the `createWritable()` method to create a stream to write the text to the selected file. If the call fails, we log an error to the console. + +We pass `createWritable()` an options object containing the following options: + +- `keepExistingData: true`: If the selected file already exists, and data contained within it is copied to the temporary file before writing commences. +- `mode: "exclusive"`: States that only one writer can be open on the file handle simultneously. If a second user loads the example and tries to select a file, they will get an error. + +Last of all, we enable the input field and the write text button, as they are needed for the next step, and disable the select file button (this is not currently needed). + +```js +async function selectFile() { + // create a new handle + const handle = await window.showSaveFilePicker(); + + // create a FileSystemWritableFileStream to write to + try { + writableStream = await handle.createWritable({ + keepExistingData: true, + mode: "exclusive", + }); + } catch (e) { + console.log( + `You can't access that file right now; someone else is probably trying to modify it at the same time. Try again later. ${e}`, + ); + } + + // Enable text field and write buton, disable select button + fileText.disabled = false; + writeBtn.disabled = false; + selectBtn.disabled = true; +} +``` + +Our next function, `writeFile()`, writes the text entered into the input field to the chosen file using {{domxref("FileSystemWritableFileStream.write()")}}, then empties out the input field. We then close the writable stream using {{domxref("WritableStream.close()")}}, and reset the demo so it can be run again — the `disabled` states of the controls are toggled back to their original states, and the `writableStream` variable is set back to `null`. + +```js +async function writeFile() { + // write text to our file and empty out the text field + await writableStream.write(fileText.value); + fileText.value = ""; + + // close the file and write the contents to disk. + await writableStream.close(); + + // Disable text field and write button, enable select button + fileText.disabled = true; + writeBtn.disabled = true; + selectBtn.disabled = false; + + // Set writableStream back to null + writableStream = null; +} +``` + +To get the demo running, we set event listeners on the buttons so that the relevant function is run when each one is clicked. + +```js +selectBtn.addEventListener("click", selectFile); +writeBtn.addEventListener("click", writeFile); +``` + +#### Result + +The demo is rendered as follows. Try selecting a text file on your file system (or entering a new file name), entering some text into the input field, and writing the text to the file. Open the file on your file system to check whether the write was successful. + +{{ EmbedLiveSample("Expanded usage with options", "100%", "200") }} + +Also try opening the page in two browser tabs simultaneously. Select a file to write to in the first tab, and then immediately try selecting the same file to write to in the second tab. You should get an error mesage because we set `mode: "exclusive"` in the `createWritable()` call. + ## Specifications {{Specifications}} From 7b78bf4fe19742f57c20ea2bcf269ceb6e1d0c70 Mon Sep 17 00:00:00 2001 From: Chris Mills Date: Wed, 7 Aug 2024 12:41:40 +0100 Subject: [PATCH 2/4] Fixes for tomayac and nathanmemmott review comments --- .../createsyncaccesshandle/index.md | 37 ++++++++++--------- .../createwritable/index.md | 24 +++++++----- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md b/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md index dd757dc919d9745..29a7b5a7921a24e 100644 --- a/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md +++ b/files/en-us/web/api/filesystemfilehandle/createsyncaccesshandle/index.md @@ -32,11 +32,11 @@ createSyncAccessHandle(options) - : A string specifying the locking mode for the access handle. The default value is `"readwrite"`. Possible values are: - `"read-only"` - - : Multiple `FileSystemSyncAccessHandle`s can be opened simultaneously on a file (for example when using the same app in multiple tabs), provided they are all opened in `"read-only"` mode. Once opened, read-like methods can be called on the handles — {{domxref("FileSystemSyncAccessHandle.read", "read()")}}, {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}, and {{domxref("FileSystemSyncAccessHandle.close", "close()")}}. + - : Multiple `FileSystemSyncAccessHandle` objects can be opened simultaneously on a file (for example when using the same app in multiple tabs), provided they are all opened in `"read-only"` mode. Once opened, read-like methods can be called on the handles — {{domxref("FileSystemSyncAccessHandle.read", "read()")}}, {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}, and {{domxref("FileSystemSyncAccessHandle.close", "close()")}}. - `"readwrite"` - - : Only one `FileSystemSyncAccessHandle` can be opened simultaneously. Attempting to open subsequent handles before the first handle is closed results in a `NoModificationAllowedError` exception being thrown. Once opened, any available method can be called on the handle. + - : Only one `FileSystemSyncAccessHandle` object can be opened on a file. Attempting to open subsequent handles before the first handle is closed results in a `NoModificationAllowedError` exception being thrown. Once opened, any available method can be called on the handle. - `"readwrite-unsafe"` - - : Multiple `FileSystemSyncAccessHandle`s can be opened simultaneously on a file, provided they are all opened in `"readwrite-unsafe"` mode. Once opened, any available method can be called on the handles. + - : Multiple `FileSystemSyncAccessHandle` objects can be opened simultaneously on a file, provided they are all opened in `"readwrite-unsafe"` mode. Once opened, any available method can be called on the handles. ### Return value @@ -79,9 +79,9 @@ onmessage = async (e) => { ### Complete example with `mode` option -Our [createSyncAccessHandle() mode test](https://createsyncaccesshandle-mode-test.glitch.me/) example provides an {{htmlelement("input")}} field to enter text into, and two buttons — one to write entered text to the end of a file in the origin private file system, and one to empty the file when it becomes too full. +Our [`createSyncAccessHandle()` mode test](https://createsyncaccesshandle-mode-test.glitch.me/) example provides an {{htmlelement("input")}} field to enter text into, and two buttons — one to write entered text to the end of a file in the origin private file system, and one to empty the file when it becomes too full. -Try exploring the demo above, with the browser developer console open so you can see what is happening. If you try opening the demo in multiple browsers (get some friends to help!) you will find that multiple handles can be opened at once to write to the file at the same time. This is because `mode: "readwrite-unsafe"` is set on the `createSyncAccessHandle()` calls. +Try exploring the demo above, with the browser developer console open so you can see what is happening. If you try opening the demo in multiple browser tabs, you will find that multiple handles can be opened at once to write to the file at the same time. This is because `mode: "readwrite-unsafe"` is set on the `createSyncAccessHandle()` calls. Below we'll explore the code. @@ -109,8 +109,8 @@ The two {{htmlelement("button")}} elements and text {{htmlelement("input")}} fie The main thread JavaScript inside the HTML file is shown below. We grab references to the write text button, empty file button, and text input field, then we create a new web worker using the {{domxref("Worker.Worker", "Worker()")}} constructor. We then define two functions and set them as event handlers on the buttons: -- `writeToOPFS()` is run when the write text button is clicked. This function posts the entered value of the text field to the worker using the {{domxref("Worker.postMessage()")}} method, then empties the text field, ready for the next addition. -- `emptyOPFS()` is run when the empty file button is clicked. This posts a very specific string to the worker that we will use to identify when the file is to be emptied rather then written to. +- `writeToOPFS()` is run when the write text button is clicked. This function posts the entered value of the text field to the worker inside an object using the {{domxref("Worker.postMessage()")}} method, then empties the text field, ready for the next addition. Note how the passed object also includes a `command: "write"` property to specify that we want to trigger a write action with this message. +- `emptyOPFS()` is run when the empty file button is clicked. This posts an object containing a `command: "empty"` property to the worker specifying that the file is to be emptied. ```js const writeBtn = document.querySelector(".write"); @@ -120,13 +120,18 @@ const fileText = document.querySelector("#filetext"); const opfsWorker = new Worker("worker.js"); function writeToOPFS() { - opfsWorker.postMessage(fileText.value); - console.log("Text posted to worker"); + opfsWorker.postMessage({ + command: "write", + content: fileText.value, + }); + console.log("Main script: Text posted to worker"); fileText.value = ""; } function emptyOPFS() { - opfsWorker.postMessage("!!empty!!"); + opfsWorker.postMessage({ + command: "empty", + }); } writeBtn.addEventListener("click", writeToOPFS); @@ -137,7 +142,7 @@ emptyBtn.addEventListener("click", emptyOPFS); The worker JavaScript is shown below. -First, we run a function called `initOPFS()` that gets a reference to the OPFS root using {{domxref("StorageManager.getDirectory()")}}, creates a file and returns its handle using {{domxref("FileSystemDirectoryHandle.getFileHandle()")}}, and then returns an accessHandle using `createSyncAccessHandle()` with `mode: "readwrite-unsafe"` set to allow multiple handles to access the same file simultaneously. +First, we run a function called `initOPFS()` that gets a reference to the OPFS root using {{domxref("StorageManager.getDirectory()")}}, creates a file and returns its handle using {{domxref("FileSystemDirectoryHandle.getFileHandle()")}}, and then returns a {{domxref("FileSystemSyncAccessHandle")}} using `createSyncAccessHandle()`. This call includes the `mode: "readwrite-unsafe"` property, allowing multiple handles to access the same file simultaneously. ```js let accessHandle; @@ -153,7 +158,7 @@ async function initOPFS() { initOPFS(); ``` -Inside the worker's [message event](/en-US/docs/Web/API/Worker/message_event) handler function, we first get the size of the file using {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}. We then check to see whether the data sent in the message is the specific string sent by the `emptyOPFS()` method — `"!!empty!!"`. If so, we empty the file using {{domxref("FileSystemSyncAccessHandle.truncate", "truncate()")}} with a vlaue of `0`, and update the file size contained in the `size` variable. +Inside the worker's [message event](/en-US/docs/Web/API/Worker/message_event) handler function, we first get the size of the file using {{domxref("FileSystemSyncAccessHandle.getSize", "getSize()")}}. We then check to see whether the data sent in the message includes a `command` property value of `"empty"`. If so, we empty the file using {{domxref("FileSystemSyncAccessHandle.truncate", "truncate()")}} with a value of `0`, and update the file size contained in the `size` variable. If the message data is something else, we: @@ -166,12 +171,10 @@ If the message data is something else, we: onmessage = function (e) { console.log("Worker: Message received from main script"); - // Initialize this variable for the size of the file - let size; // Get the current size of the file - size = accessHandle.getSize(); + let size = accessHandle.getSize(); - if (e.data === "!!empty!!") { + if (e.data.command === "empty") { // Truncate the file to 0 bytes accessHandle.truncate(0); @@ -182,7 +185,7 @@ onmessage = function (e) { const textDecoder = new TextDecoder(); // Encode content to write to the file - const content = textEncoder.encode(e.data); + const content = textEncoder.encode(e.data.content); // Write the content at the end of the file accessHandle.write(content, { at: size }); diff --git a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md index 01151e2b57bcbeb..cbfb5318b2f181d 100644 --- a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md +++ b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md @@ -107,7 +107,7 @@ li { #### JavaScript -In the JavaScript, we start by grabbing references to the select file button, the the write text button, and the text input field. We also declare a global variable `writableStream`, which will store a reference to the writeable stream for writing the text to the file, once created. We initially set it to `null`. +We start by grabbing references to the select file button, the the write text button, and the text input field. We also declare a global variable `writableStream`, which will store a reference to the writeable stream for writing the text to the file, once created. We initially set it to `null`. ```js const selectBtn = document.querySelector(".select"); @@ -128,22 +128,26 @@ Last of all, we enable the input field and the write text button, as they are ne ```js async function selectFile() { - // create a new handle + // Create a new handle const handle = await window.showSaveFilePicker(); - // create a FileSystemWritableFileStream to write to + // Create a FileSystemWritableFileStream to write to try { writableStream = await handle.createWritable({ keepExistingData: true, mode: "exclusive", }); } catch (e) { - console.log( - `You can't access that file right now; someone else is probably trying to modify it at the same time. Try again later. ${e}`, - ); + if (e.name === "NoModificationAllowedError") { + console.log( + `You can't access that file right now; someone else is trying to modify it. Try again later.`, + ); + } else { + console.log(e.message); + } } - // Enable text field and write buton, disable select button + // Enable text field and write button, disable select button fileText.disabled = false; writeBtn.disabled = false; selectBtn.disabled = true; @@ -154,11 +158,11 @@ Our next function, `writeFile()`, writes the text entered into the input field t ```js async function writeFile() { - // write text to our file and empty out the text field + // Write text to our file and empty out the text field await writableStream.write(fileText.value); fileText.value = ""; - // close the file and write the contents to disk. + // Close the file and write the contents to disk. await writableStream.close(); // Disable text field and write button, enable select button @@ -184,7 +188,7 @@ The demo is rendered as follows. Try selecting a text file on your file system ( {{ EmbedLiveSample("Expanded usage with options", "100%", "200") }} -Also try opening the page in two browser tabs simultaneously. Select a file to write to in the first tab, and then immediately try selecting the same file to write to in the second tab. You should get an error mesage because we set `mode: "exclusive"` in the `createWritable()` call. +Also, try opening the page in two browser tabs simultaneously. Select a file to write to in the first tab, and then immediately try selecting the same file to write to in the second tab. You should get an error mesage because we set `mode: "exclusive"` in the `createWritable()` call. ## Specifications From dada58059ab23d28a283e7f482c8d3e38ab3a143 Mon Sep 17 00:00:00 2001 From: Chris Mills Date: Wed, 7 Aug 2024 13:10:00 +0100 Subject: [PATCH 3/4] move createWritable() live example into glitch --- .../createwritable/index.md | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md index cbfb5318b2f181d..b9ec49a28d7dbb1 100644 --- a/files/en-us/web/api/filesystemfilehandle/createwritable/index.md +++ b/files/en-us/web/api/filesystemfilehandle/createwritable/index.md @@ -75,7 +75,13 @@ async function writeFile(fileHandle, contents) { ### Expanded usage with options -In this example, we provide a button to select a file to write to, a text input field into which you can enter some text to write to the file, and a second button to write the text to the file. +Our [`createWritable()` mode test](https://createwritable-mode-test.glitch.me/) example provides a {{htmlelement("button")}} to select a file to write to, a text {{htmlelement("input")}} field into which you can enter some text to write to the file, and a second `