The 2021 edition of dealing with files on the Web

Thomas Steiner: Hi, my name is Thomas Steiner.

I'm a developer relations engineer.

And today I want to talk about modern ways of dealing with files in the browser.

This is Web Directions Code.

It's a virtual event as always these days.

And I thought, well, let's make the best out of it.

So I developed this little application that has a virtual badge application, where you can create your own badge.

So you can see, this is my badge, Thomas Steiner developer relations engineer at Google.

My Twitter handle @tomayac and some Web Directions branding down there, and the world has a background.

Nice deep pink holder, neck holder and a little reset button.

But Of course, this is a dynamic thing.

So let me actually just edit it and turn this into Alex Russell's badge-Russell.

Here we go.

Alex.

Now is a partner PM Microsoft.

And Alex's Twitter handle is @slightlylate-here we go.

So this is Alex's badge now.

You can see there's a little reset button.

So if I reset the batch boom, it's back to being my badge.

Yeah, this is a dynamic application.

So let me quickly show you how all of this works.

We don't need this here.

We have the preview over there.

So let me get intro into the code here real quick.

Yeah, we have a pretty small HTML file that you can see.

We have a main element.

We have the section for the lanyard.

We have some visual sections here that will be later converted into the holder up here.

And then we have, of course the name I can see my name here.

We have the role.

You can see, there is an image that we don't see yet, because it's still hidden.

There's lots of button and you can see here.

There's a couple of more hidden things here.

We will release them in the coming minutes and we have a link down there.

That is my hard-coded twitter handle.

And yeah, you can see there's the logo that is in the CSS.

So the logo gets inserted by CSS.

And I, may have a couple more still hidden things.

As I said, we will reveal them a little later.

That's also a footer.

And you can see everything that is editable is content editable, which means I can just go there.

To the application, modify all of it, delete stuff, enter new stuff.

And that's it.

So this is application, and you will notice up here in the browser, it's an installablr progressive web app.

It's called.

"Hello my name is".

The browser asks me if I want to install this.

You can see there's my picture here, my avatar.

Let's not install this for now, but just keep in mind,this is a progressive web app.

So let me quickly show you that the service worker of this.

So you can see it's super short.

It's obviously not really production ready, but it's definitely working as is.

I have an install handler.

We have an activate handler.

And we have a fetch handler.

The install handler essentially just skips waiting.

The activate handler claims all the clients off this app.

And then we just activate navigationPreload if it is supported by the browser and yeah, enable it here and that's the activate handler.

And then finally we have the fetch handler, and you can see here, this fetch handler responds with the asynchronous preload response if it exists.

So you can see that here, if the response exists, I respond with it.

And if not, it just goes to the network and fetches the request and it catches the request failing with just a static response, that is the word offline.

It's definitely not super nice, but it gives me this offline enabled-ness that is required for making this application installable.

So, yeah, that's that.

That's good enough for my small use case here and yeah, the CSS is not really interesting.

It's just a couple of classes to make the badge look like a badge and some super basic stuff.

But let me actually then go into the very first file here, which is script.JS.

That does all of the logic.

So you can see at the beginning, there's a couple of DOM references to the link, which is the Twitter handle here.

That's a link actually internally.

Okay.

We have the reset button.

We have the image that we still can't see.

We have the icon.

Which is the fav icon that you can see up there.

So it's my face again.

And we have the name and we have the role.

So names, obviously Thomas Steiner and the complete case and role is developer relations engineer.

So that's the name and the role here.

And let's see what's happening.

So we have the link and I attached an input listener to it.

And this Linked then just gets it's a href dynamically set whenever someone inputs any kind of text into the field and you can see here, all I'm doing is I'm replacing the @ with nothing.

So remove it and prepend the whole thing with twitter.com.

So if you actually look at this, now you can see if I go into dev tools, I inspect this and I edit, you can see whatever I'm typing gets reflected.

So let's quickly just reset the thing and you can see the actual link is going to my Twtter.

So that's that, but because it's content editable when I click it sort of this comes first.

I can't really edit sorry.

I can't really click through to the length because I can't edit it first, but we we'll see how about we'll make this into an actually clickable link in the next couple of minutes.

Okay.

So that's that.

The reset button is really just setting back everything to the hard-coded defaults.

So not really very interesting.

We have a function that we will need a little later called blobToDataURL.

And we'll talk about this when you need it.

We have the ServiceWorker registration here.

So dynamically only if ServiceWorker exists.

It's still a good idea to feature detect this because we have a lot of browsers, for example web views on iOS that not necessarily are capable of being ServiceWorker enabled.

So it still a good idea even even in 2021 to prepend this with feature detection and then we have the window load event listener that then registers the ServiceWorker that I've just shown you down there.

All right.

So let me go back.

Then I can see there's a whole bunch of things that are commented out so they can see.

Yeah.

There is this, and I want to one by one, go through all of these features and make this application a little bit more rich, a little bit more worthwhile your time, because be fair to be fair right now, it's not a super amazing app, but we will make this a little bit more advanced in the next couple of minutes.

So let me first show you what we can do with this.

So here I do feature detection.

If clipboard is in navigator, and if writeText is then in navigator.clipboard, I dynamically import a file, copy dot text.

So if I uncomment this now and readload my application here, you can see that.

Now we can add a little copy button and you guessed it already.

What it does, if I click it, it copies the Twitter handle.

So that's just check this works.

I open a new tab and paste in what I have on my clipboard, and you can see it goes to my Twitter.

So to show you that this is not scripted, I can just enter it anything here.

So sligh, oops, sightlylate.

Typing is hard, especially while recording.

Copy this.

Open-in in a new tab, and it goes to Alex's Twitter.

So that's working as intended.

That's cool.

So let's have a look at how it works.

Let me find the file "copy text".

Here it is.

You can see it's super small.

It's pretty short.

What it does is it gets DOM references to button and link it.

Unhides the button because now we can use it.

Attaches a click listener.

That is an asynchronous navigator.clipboard.writeText of the link's href and that's essentially it.

So now we have the link on the clipboard and the rest here is just essentially from here to here, this little outline that I'm painting.

Whenever I press the copy button so you can see here highlights here what I have copied.

So that visually the user gets a clue of what is going on and you can see I'm saving the before state of the links, style style.boxShadow, which is essentially nothing.

But to be future-proof, it might, at some point becomes something by default.

So I copy that as the before state, and then I set the new style.boxShadow, and then set a timeout to reset the thing back to the previous initial state.

And you can see, I have wrapped the entire thing into a try/catch block.

My, error handling strategy.

is essentially just logging the arrow to the console.

That's a very poor error handling strategy, but it works for my small toy example.

So this is this, this is this, this, that, and whatever.

Now we have a copy button that does something, which is good, but yeah, it's not super amazing yet.

But still it's a small convenience function that makes people's lives a little easier.

Let's dive next into the more interesting case.

We have here feature detection for showSaveFilePicker in window.

If showSaveFilePicker exists on window, I include a new file called open JS.

So let's see what this does.

If I reload the app.

You can see that all of a sudden I get my picture here, so that's great.

And I think most, all right, you can see this looks like I can click it.

So if I actually do that, if I click that.

You can see that we have a file open dialog, and it starts in my pictures folder here.

And you can see I've already prepared that.

So let me just open Alex Russell and boom.

We have Alex Russell on this badge.

Now you can also see it's pretty small, but you can see the favicon has changed.

So let's just quickly make this Alex Russell's badge again.

So yeah, you can see this is a new file that is here.

I can click it again and change to something else and you get a fugu fish.

So now I can see it's the fruga fish pretty clear now that it's also in the favicon.

So let me show you what is going on in open.js.

If I go there, you can see it.

It's also pretty small.

Again, DOM references for img and icon.

No big surprises.

I unhide the image because now I can actually set it and yeah, make this, not just statically my image, but something that is dynamic.

And then on this image, I can attach an event listener that listens for click and you can see here calling window.showOpenFilePicker that I pass an object.

Sorry.

An options object too, with a types, key and this types key then has an array and I pass it a description of image files, and then accept.

And then an object with image/* as the key, add an array of extensions that I want to accept.

So you can see a JPG, JPEG with an E, WebP, blah, blah, blah.

So all these different file types.

And you can see I'm storing the result of this in a handle variable, and you can see it's destructured because this gives me back an array, but in this case, I'm only interested in the very first file, because in theory, I could open multiple files in this case, I'm really just interested in one image.

So I destructure it and just get the first one.

Up next on this file handle I call getFile, which returns me a file.

As you have guessed.

From this file, I can then create an object URL and set it as the source variable that I then pass to the image and to the favicon.

So that's it essentially super small, the whole thing again, wrapped in a try/catch block with the error handling strategy from before.

So that's proof that this actually does what it tells us on the tin.

So let me inspect this and I can see the source now is indeed a blob URL.

If we go up to the head into the icon, you can see it.

It's also a blob URL.

That is the same as this blob URL.

So we have opened a file now and dynamically set the image empty icon to the new files funds contents here, which are which is just an image that we have open here.

The description "image files" is essentially just what you see when you open here.

So you can see if I click options, they can see there's image files.

I can still select all files.

If I wanted to in the future a couple more minutes, I will show you how you can also prevent this.

But for this case, I also wanted to allow other formats just in case someone has a really I don't know, old bitmap image, for example, this would also work in this case I haven't really explicitly listed it, but you can do it.

So, yeah, that's that's opening a file.

So let me next go into the clipboard again.

So you can see feature detection for clipboard in navigator and this case.

And in this case, I'm checking for if write is in navigator.clipboard.

Before it was writeText, now I'm looking for, write.

And I'm also looking for showSaveFilePicker.

So before we had showOpenFilePicker, in this case, I'm looking for showSaveFilePicker.

And if it exists, if all these feature detection does work, I'm including a file copy-image.js.

So let me show you what this does.

And we can now see that next to the avatar.

We do have a copy button.

Let me click it.

You can see it, it highlights the thing that I've copied and now let me launch the preview application and in preview I can say, oh, I don't want the default opening dialog in preview.

You can see, I can open new from clipboard.

And it will create a new file based on my clipboard content.

Let me show you how this works.

So we have seen copy-text.

So let's next look at copy-image.

As you can see, I'm getting DOM references for button and for image.

I'm unhiding the button.

If the showOpenFilePicker is in window.

So this is again, feature detection.

Here just in case.

And next we have the click listener on the button that then does the following-it fetches the image.

So the image that you can see here and gets the response and from the response gets the blob, which now means we have to blob of the image.

Next I'm creating an array and you can see the only entry in this array is a new clipboard item that I pass an object with the key of the blobs type and then the actual blob as the value, and I'm storing this whole thing, this one element array in a variable called data that then write onto the clipboard.

The rest is just the same code as you've seen before.

So just a highlighting of the little box here off the ring and the whole thing wrapped in, try/catch just in case something goes wrong.

And that's it essentially, so now we have copied an image to the clipboard and we can work with it.

We can paste it into an application iike I've shown as I've shown you.

And this is just working fine.

So copy of text and images.

All a solved problem now, thanks to that asynchronous clipboard API.

So let me go to my next feature here.

So you can see up next, I'm doing feature detection for showSaveFilePicker.

And if, showSaveFilePicker is on window, I'm loading save.js.

So let me first show you the effect of this.

So up next, you can see there's now a safe badge to file button, which does what you would expect.

If I save this now I get a dialog that asks me if I want to save this.

I can store it in my pictures folder.

That's fine for now.

And.

Yeah, I can replace this.

That's good because I've done some tests before, so this file existed.

And if I know now open this file from my pictures folder, you can see here it is.

I now have my badge as before, but this time when I click around it's static, so I can't do anything anymore.

If I click the image, it doesn't do anything.

And if I clicked the Twitter link here, it actually is a link.

So it goes to my Twitter as I would expect.

And if you inspect this whole thing, we can see that it's the entire badge wrapped in just one HTML file.

You can see here.

These are all data URLs, so everything is dynamically or statically rather statically encoded in the HTML file here.

It's just one static file of the badge.

Okay.

So let me quickly recreate Alex Russell's badge.

Alex Rusell.

Partner PM at Microsoft, choose his picture, Set his Twitter handle slightlylate and now save the badge.

It asked me, do you want to save this as slightlylater.html?

And now, by the way you can see there's no way to choose something else.

So this time I am forced to store this as an HTML document because my application doesn't support export in another format.

So I can save this file.

And if I open it now, so let's just go here.

File open.

Check for slightlylate.

You can see it now goes to, oh, I made a typo.

Yep.

Well, you've got the point.

You can see the typo and you can see the image here, so let's actually just go back and fix it.

slightlylate Just re type again, slightlylate.

Just copy it.

And test of this works yet at this time it works.

Okay.

So now save the thing again And you can see there's no dialogue.

I'm clicking, but nothing happens.

So let's actually just check what happens if I reload now.

You can see that now I'm landing on Alex's Twitter.

So.

Well, I didn't have to go through another save dialogue.

It was just automatically done for me.

So how did this work?

And also if I go to my finder, I just have, let's see pictures here.

I don't have a slightlylate1 or slightlylate2 file.

It's just the same file from before you can see here's the wrong file that I can just remove.

So what's going on?

Why, why is this, why is this happening?

It's a good thing, but why is it happening?

What's going on here?

So let me show you the contents of save.js.

This is actually the longest of all files, but I will break it down and make it clear what everything does.

So first I'm importing get set and del from Jake Archibald's idb key value store library, which just is a key value store on top of indexedDB to make it a little saner for simple use cases like mine.

So you can just get and set items that yeah, you can then store in a similar way as the local storage API would work.

Okay.

Would work just backed by indexed DB.

So that's that.

Let me go back to here.

Next.

I am importing my blobToDataURL function from script.js.

You will remember from the very beginning.

It was here somewhere.

Yeah, here it is.

blobToDataURL.

So let me quickly walk you through it, what it does.

So blobToDataURL it does exactly what it says on the tin.

It returns a new promise and resolves a reader that we get, that onload resolves with the reader's result.

And then on the reader, we have the readAsDataURL convenience function here that takes a blob as an input.

So essentially we have a blob that gets then converted into a data URL, that we then return in this promise.

Let's go back to here.

We will use this in the future.

And let me walk you through the rest.

We have a couple of DOM references as before button, reset, main, image and link.

And we have this funny global variable here so we can see window.businessCard is window.businessCard if it exists and else it creates a new object.

And what I do here.

I check if window.businessCard.handle exists on the key value store, so on indexedDB and set it to undefined if it non-exists else, I set it to the value that I receive from my key value store here.

And we'll talk about this in a couple more minutes Up next.

I unhide my button.

So that's a button down here, the save badge to a file button.

And let me go back.

So we have a click listener on this button that gets a suggested name, which is essentially just the link's href's path name, minus the @, plus html.

So very quickly, what this does is it takes the link that you can see here and removes everything up until the @ and only leaves slightlylate and adds a dot HTML suffix here.

So that's what this does.

And then we have the handle that either will be set to the business card handle that we have in the global variable.

Or, and here it comes the interesting part, the window.ShowSaveFilePicker's result.

I've it a big options object here, so I can see the suggested name from above and then similar to before a types object here that is an array of objects in this case description, HTML document you have maybe remembered.

If I click save file here to actually reset.

If I click save file, you can see an HTML document that's the description that you can see here.

And this time I'm accepting only text/HTML with the extension .html.

You've seen, I couldn't switch to anything else because in this case I've said the excludeAcceptAllOption to true.

All right.

So I have now the handle that is either the previously stored handle or the dynamically obtained handle from the window.showSaveFilePicker method.

What I do next is I, as similar to before get the image, the response will then be converted into the blob.

I store the blob, this time I'm converting the whole thing, the blob to a dataURL.

So with my convenience function that I have from before, and then I do something horrible.

I have worked with regular expressions and parse it and use it to parse HTML.

So first what I do is I get my style CSS, just copy everything in a variable and then create an HTML file that then has all the buttons replaced by nothing.

So I remove all the buttons.

I remove all the content editable attributes so that they go away and I rewrite the source to be...so the source of the image to be, the dataURL that I have created dynamically based on my blob image that I obtained from that user's avatar.

Up next I have the file handle.

And from this file handle, I get a writeable which you can see here-createWritable.

I call createWritable.

I get a writable in this writable I then write the HTML file.

I need to close it so that it's persistent on disk actually.

And then I set my global variable window.businessCard.handle to the obtained handle that I have from this operation, or just from the previous operation that it was the same, same one.

And also set on indexedDB, under the key file, the new handle or the old handle if it was reduced.

The whole thing wrapped in try-catch as usual.

And we've can actually now inspect what is going on here.

So if you go to indexedDB and check here, the key value store, we can now see nothing.

But if I save this badge now, save...replace...

and I reload, you can now see I have on indexedDB, the file system file handle that points to my file on disk.

I can also show you on.

My global isn't...this it...not windows or window window.

window.businessCard.handle.

I can show you that online global variable.

This is now the same fat handle that points to tomoyac.HTML that I have here on indexedDB.

But what I do next is I have another event listener here.

It's registered on document and it's registered on keydown and I'm checking here-if someone is pressing the S key combined with a meta-key.

So essentially on windows, this will be the control key, on Mac this would be the command key.

So I'm checking if someone presses command S.

So command-S by default is the shortcut for a save page as, so you can see here command S on the neck, but in this case, I'm preventing the default from happening.

So I'm preventing the default from happening and manually trigger instead a clicking of my button, which means the save button will be clicked programmatically whenever I press command-S.

The rest of this code is just highlighting that we've seen before.

So setting the box shadow la la la.

So let me now actually just do this.

I press command S and you can see my save button gets quickly highlighted so that I can see that something is happening.

Let me open here.

My file, tomoyac..html and now make some changes to it.

So now remember I have this pointer here, so I can now change this to Thomas Steiner 2, press command-S and if I go here now, and I refresh, you can see that now it's Thomas Steiner 2.

If I go back and enter this time 3, press save.

I go back, reload and I can see it's Thomas Steiner 3.

So I don't have to go through a new file save dialogue every time I make an edit this is just reusing the existing file, but remember one thing in the very beginning of my file here, onload I checked at this stage if I already have a file handle in my key value store, which means if I reload this application now entirely, so I reload the entire App.

You can see it gets reset to the default state because it didn't restore from my saved file, but I can now make some changes and I can now say this is number five.

And if I press F now, save S now, you can see that this time I get a dialog.

It asks me if the site, after the reload should still edit tomoyac.html.

And in this case, I say, yes, this is fine.

You can see I get a little icon up here that tells me yep.

this app now has access to tomoyac.html.

And let's actually just refresh here to see if it's actually still the same and you can see, yes, it did make the change to Thomas Steiner 5.

So this is the same application access that you have had before.

Now, of course, if I reset the badge, You can see it says your data, maybe stale.

So indeed, if I reload, now I have reset my file handle.

It's no longer there because if I reset the badge, of course, I want to make the badge go away.

And just for the sake of clarity, if I also look at my global variable, now it's undefined and I have done this in the final piece of the code here, when the reset button is clicked, I'm adding undefined and delete the file.

And this is actually the same reset button that we've seen before that resets the badge.

That's just another event listener that I've registered on top of the already existing event linsteners, that resets to the default state, but here I just also do the resetting of the file states.

So super cool, super useful application.

You can imagine already if you have a real application, but you have, let's say a graphics editor, for example you make some changes.

You can just naturally press command-S.

Everything will be saved.

You don't need to go through another file, save dialogue every single time you make an edit.

Okay.

Up next.

Dragging and dropping.

So you can see this time I'm checking for getAsFileSystemHandle.

And if it exists on DataTransferItem.prototype.

Let me show you first the effective of this.

So if I reload the page now, and you can see nothing has changed apart from now we have a tiny footer here that tells me that I can drag and drop an image onto this document.

So let's actually just try this now.

I go to my pictures folder.

I take Alex Russell's picture and I drag it and you can see, the moment I drag something onto the application, it gets this funny frame here.

And if I release, you can see that now the avatar has been replaced by Alex's picture.

You also briefly saw this pink ring around Alex's avatar and yeah, by that we have now the drag and drop operation that lets us drop and drag and drop any kind of image into this application.

And you can also see it as updated the favicon here-now it's Alex again.

So, let me show you the code for this.

And it's also a little long, but it's not complex.

Most of it is just DOM references at the beginning as always.

We have the footer, that we unhide.

You have the image, that we unhide.

We have the default before style.

That is for the document element.

So the outline that by default will be nothing.

And then I have three different, actually four different event listeners on body.

dragenter, dragleave, dragover and drop.

dragenter, dragleave, dragover, essentially do what you expect.

All they do is they set and unset the border here.

So if I take my file.

They just set and unset the border, depending if I have something over the drag area or not.

So not very interesting, but then the most interesting part here is that drop handler.

So for drop, it's important to prevent the default because else, you would just open the image in the entire tab, kick, kick you out completely of your flow.

This is not what you want.

We need to reset the style to the default.

And then I can iterate over all of the transfer items that I have on my event here.

I get an item out of it.

I check if the items kind is file because we also could drasg other things on top of it.

But what we want is we want to drag a file.

We get from this file the handle using the getAsFileSystemHandle function here from the item.

And now we have a handle that has the same handle, same quality as before with the openFilePicker.

And so I can then check if the file that I obtained from this, so handle.getFile is a file.

I can then check if the file's type starts with image.

If this is a case I can then set the source to a dynamically created blob URL that I then use on my image.,and on my icon.

The rest of the code is just resetting the default styling of the of the ring.

So I briefly highlight the avatar, go back to the default styling.

And then I break out of the loop because all I want is the very first item of those.

If it's an item that I can deal with.

So one thing you will notice, I'm not really doing anything with this handle, but in theory, I could use this handle and before store it to indexedDB, make make it persistent, use it to make any kind of changes.

So if I had let's say a graphics editor, and I would drag an image onto this graphics editor, I could use this handle that I get from drag and drop and then store the file changes directly back to the handle.

Super powerful API.

And in this case, I'm not doing anything with it, as I said, but it could be done very easily.

Okay.

You're getting closer to the end.

Now we have a paste operation, so clipboardData in ClipboardEvent.prototype.

And if showSaveFilePicker in window, we now import pasting.

So let me show you what this does first, as you can see these.

Let's just actually reset the badge.

You can see it's me now and if I now go back to the finder and I copy Alex Russell and I paste Alex here by pressing command-v you can see Alex is now on this page, so I've taken the file, copied it from the finder and pasted it right into my applicaiton.

Let me reset the badge one more time.

So that it's me again, then go back to here.

Copy the thing again.

And then I go to the browser's paste style up here-so I can see I click 'paste', and it's the same.

This is reacting on paste at the global level.

I had pasted Alex Russell's picture here.

Alex is there pasted onto my application using the paste dot.js function.

You can see this time.

It's a shorter one.

So we've been through a lot of longer versions, but this time it's a short one.

DOM references, DOM references to image and icon.

And then we have at the document level an event listener for paste.

We prevent the default because yeah, we don't want the default to happen because in this case we're dealing with the clipboard on our own.

So first I check if at least something off files is on the clipboard.

If not, I return early, but if I have files on the clipboard, I take the first one.

And then I check if the file's type starts with image slash.

So if it's any kind of image if this is the case, you know this code by now, I just create an object URL, save it to source, set the image and the icon to this, and then briefly highlight the avatar.

As you've seen before.

The next thing that I want to show you is a really powerful new addition to the platform.

You can see here, I'm checking if launchQueue is on window.

If launchQueue is on window, I import file.js.

For this we will actually now need to install the application.

So let me quickly do that.

I installed the app.

So now we have it running here stand alone.

In our yeah.

Nice stand-alone window, but let me close it for now.

And now let me show you what this does.

So the app is closed, but I can go to the finder now go to my pictures folder select my own image and say open with, and I can see I have: hello my name is.

That that is one of the entries here in applications that can handle image files.

So if I click that now you can see that now the application launches and now at this stage it asks me if this app should be able to open AVIF, GIF, JPG, JPEG, PNG, SVG, webP files, whatever.

Which is fine.

I allow it and now you can quickly see, well, I opened my own picture, so you couldn't really see a change, but let's actually run through this again, but Alex's picture in a second.

Let's close the app again and I'll take Alex's picture open with and choose this app.

And now you can see Alex is there.

So I've created now an application that is able to open files directly from the file picker from, from the file handler off my Mac OS operating system.

And you can imagine if you have any kind of image editor or any kind of productivity app that needs its own file type, you can register and have your own file types here directly integrated in the finder.

So, how does this work?

Let me go back to here and show you the contents of file.js.

It's a really short one again.

So this is very positive.

We have an image, we have an icon DOM reference.

We unhide the image and then we do the following.

I go to the window launchQueue and set a consumer, which receives launch parameters.

On these launch parameters I check if there's a files array that has a length.

If this is not the case I return early, but if this is the case, I can then iterate over all the launch parameter files and get for each of them the handle.

From each of the handles, I can get the file.

And then I just check if the file's type starts with image.

And if this is, if this is the case, I do what you've now seen a million times, I create an objectURL, set it to the image and the icon and return.

So super sharp, super easy to do.

And you know, seeing the power of this, if you have an application that has its own files or can edit known file set of an operating system, you can directly right click, or double click here.

If you have just one application that registers certain files, you can double click those ... those files and open your app directly from the Finder or from the Windows Explorer i f you are And with that we are close to the end.

We have one more thing to do.

We have next storageFoundation in window.

If I uncomment this now and reload the thing-I've closed it actually it's here now.

It's its own application so I can just quickly reload.

You can now see that now I have a table down here and in this table I have something that looks and feels like files, but it's not file.

So remember when i said in the very beginning, not all files are created equal.

These are some files that are not created equal.

So first let me just quickly delete them.

Now I can see there's a new button-store badge in browser.

So if I do this now, if I start a badge in the browser, you can see now I have a file that has a length of whatever bytes I can read its contents.

So it is just a CSS in this case.

Plus the HTML, blah, blah.

Not really interesting.

But if I change this back now to Alex Russell,'s badge, and for the sake of time, I will only changed the name and a Twitter handle in this case.

slightlylate.

Slightly late.

I think this is right.

It doesn't really matter.

I can store the badge now and I can see, oh wait, I think I made a typo anyway.

It's not really important.

You can see you, we have a file that has a different length.

You will not find these files on the file system.

So if you go to the Finder they're not button says these files are stop in the browser.

These files are also not files in a regular sense.

They are files that are optimized for certain operations.

So let's imagine you have an SQL database that you need to run on the web.

And people have done that people have compiled SQL to WASM for example, Web Assembly.

And they need a backend to store the files actually.

And today typically people do this in indexedDB, but where this kind of high-performance files that are not files in a regular sense you can use these different files as a backend.

There's a project called absurd SQL.

I don't know about Absurd SQL.

This one where someone has created SQL Lite 3 in your indexedDB.

And you can see in this comment, hopefully a better backend soon, and this better backend, hopefully soon could be something like storage foundation, the author's looking into converting the backend from indexedDB to storage foundation so that you can store your.

your...on this high-performance files.

If you want to know more about this go to dev.Web/storagefoundation.

There's an article that I've written about these files and what makes them special and why they're faster than regular files.

They have a couple of optimizations, read your article to get the full picture about these kinds of files.

In my toy sample that I can quickly go back to.

You can see, well, I've just, I'm just storing the HTML files here.

It's not really making yeah.

I've used it as super useful in a sense of being very performant in the need of much performance.

But nevertheless, I can just do that.

So if I delete the files, I can make the things go away.

If I reload the application now you can see the files ar ereally gone they're of course the system.

So if a store store the file again, I can now read out the the app and the file is still there.

So they're private to the origin of this application.

You can see, I have this hilarious scratch.wax.gym.glitch.me origin origin here.

So in this case the file survives the reload.

Okay, let me quickly go back This is all of the things that you can do with files on the web.

Let's create a quickly recap what you've seen.

We've seen copy and paste integration.

So we copied files.

We pasted files onto it.

We dragged and dropped files.

We have seen the storagefoundation files we've seen just regular open and saving of files.

And one last thing that it didn't show you.

Well, why all this feature detection, right?

All this funny, if blah, blah, import.

Let me show you this same application in Safari.

If I reload this app, now you can see it's the same application as before.

Alex Russell, you can still edit stuff.

You can still change the title and so on.

You can copy the the link here.

So if I open a new tab in Safari, I can paste the copy link here and it still works.

But a couple of the other features like the avatar image is not shown.

The storagefoundation stuff is not ownd there.

I can't open an image.

I cannot drag and drop an image.

Because while my future detection doesn't happen, but what is good about this?

Look at my actually loaded files in this case, I'm only loading the stuff that I actually need that I can use in this browser.

Nothing more, nothing less.

So everything gets dynamically loaded.

This is progressive enhancement at its best.

And making sure that I don't punish people on browsers that don't support certain features that some browsers may support, some browsers may not support.

And I think with this pattern by dynamically importnt based on feature detection you're making everything right.

You're setting up yourself for success so that it can make sure that this application runs and perform The best of Ken on each browser and question connection And with that thank you very much for listening.

I hope you had a little bit of fun.

I hope you learned something about files and how you can work with with them in the Rosa in 2021.

My name is Thomas Steiner.

You have seen my badge a million times now reach me @tomonayac Twitter and on anything on the internet in general.

Thank you very much for listening and see you next time.

Cheers.

Thomas does a better job describing what is happening on the screen a lot of the time than these text descriptions could provide. As a result, we'll put the code Thomas is referring to in these slides when it appears.

self.addEventListener('install', (event) => {
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  self.clients.claim();
  event.waitUntil(
    (async () => {
      if ('navigationPreload' in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })(),
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    (async () => {
      const response = await event.preloadResponse;
      if (response) {
        return response;
      }
      return fetch(event.request).catch(() => new Response('Offline'));
    })(),
  );
});

const link = document.querySelector('.twitter-link');
const reset = document.querySelector('.reset-card');
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');
const name = document.querySelector('.name');
const role = document.querySelector('.role');

link.addEventListener('input', () => {
  link.href = `https://twitter.com/${link.textContent.replace('@', '')}`;
});

reset.addEventListener('click', async () => {
  img.src = './avatar.png';
  icon.href = './avatar.png';
  name.textContent = 'Thomas Steiner';
  role.textContent = 'Developer Relations Engineer at Google';
  link.textContent = '@tomayac';
  link.href = 'https://twitter.com/tomayac';
});

const blobToDataURL = async (blob) => {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
};

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    await navigator.serviceWorker.register('./sw.js');
  });
}

if ('clipboard' in navigator && 'writeText' in navigator.clipboard) {
  import('./copy-text.js');
}

if ('showOpenFilePicker' in window) {
  import('./open.js');
}

if (
  'clipboard' in navigator &&
  'write' in navigator.clipboard &&
  'showSaveFilePicker' in window
) {
  import('./copy-image.js');
}

if ('showSaveFilePicker' in window) {
  //import('./save.js');
}

if ('getAsFileSystemHandle' in DataTransferItem.prototype) {
  //import('./drag.js');
}

if (
  'clipboardData' in ClipboardEvent.prototype &&
  'showSaveFilePicker' in window
) {
  //import('./paste.js');
}

if ('launchQueue' in window) {
  import('./file.js');
}

if ('storageFoundation' in window) {
  import('./storage.js');
}

export { blobToDataURL };
const link = document.querySelector('.twitter-link');
const reset = document.querySelector('.reset-card');
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');
const name = document.querySelector('.name');
const role = document.querySelector('.role');
link.addEventListener('input', () => {
  link.href = `https://twitter.com/${link.textContent.replace('@', '')}`;
});
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    await navigator.serviceWorker.register('./sw.js');
  });
}
if (
  'clipboard' in navigator &&
  'write' in navigator.clipboard &&
  'showSaveFilePicker' in window
) {
  //import('./copy-image.js');
}

if ('showSaveFilePicker' in window) {
  //import('./save.js');
}

if ('getAsFileSystemHandle' in DataTransferItem.prototype) {
  //import('./drag.js');
}

if (
  'clipboardData' in ClipboardEvent.prototype &&
  'showSaveFilePicker' in window
) {
  //import('./paste.js');
}
if ('clipboard' in navigator && 'writeText' in navigator.clipboard) {
  import('./copy-text.js');
}
const button = document.querySelector('.copy-twitter');
const link = document.querySelector('.twitter-link');

button.hidden = false;
button.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(link.href);

    const before = link.style.boxShadow;
    link.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      link.style.boxShadow = before;
    }, 1000);
  } catch (err) {
    console.error(err.name, err.message);
  }
});

if ('showOpenFilePicker' in window) {
  import('./open.js');
}
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');

img.hidden = false;
img.addEventListener('click', async () => {
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Image files',
          accept: {
            'image/*': [
              '.jpg',
              '.jpeg',
              '.webp',
              '.png',
              '.avif',
              '.gif',
              '.svg',
            ],
          },
        },
      ],
    });
    const file = await handle.getFile();
    const src = URL.createObjectURL(file);
    img.src = src;
    icon.href = src;
  } catch (err) {
    console.error(err.name, err.message);
  }
});

const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');

img.hidden = false;
img.addEventListener('click', async () => {
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Image files',
          accept: {
            'image/*': [
              '.jpg',
              '.jpeg',
              '.webp',
              '.png',
              '.avif',
              '.gif',
              '.svg',
            ],
          },
        },
      ],
    });
    const file = await handle.getFile();
    const src = URL.createObjectURL(file);
    img.src = src;
    icon.href = src;
  } catch (err) {
    console.error(err.name, err.message);
  }
});

if (
  'clipboard' in navigator &&
  'write' in navigator.clipboard &&
  'showSaveFilePicker' in window
) {
  //import('./copy-image.js');
}

const button = document.querySelector('.copy-avatar');
const img = document.querySelector('.avatar');

button.hidden = !('showOpenFilePicker' in window);
button.addEventListener('click', async () => {
  try {
    const blob = await fetch(img.src).then((response) => response.blob());
    const data = [new window.ClipboardItem({ [blob.type]: blob })];
    await navigator.clipboard.write(data);

    const before = img.style.boxShadow;
    img.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      img.style.boxShadow = before;
    }, 1000);
  } catch (err) {
    console.error(err.name, err.message);
  }
});

const button = document.querySelector('.copy-avatar');
const img = document.querySelector('.avatar');

button.hidden = !('showOpenFilePicker' in window);
button.addEventListener('click', async () => {
  try {
    const blob = await fetch(img.src).then((response) => response.blob());
    const data = [new window.ClipboardItem({ [blob.type]: blob })];
    await navigator.clipboard.write(data);

    const before = img.style.boxShadow;
    img.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      img.style.boxShadow = before;
    }, 1000);
  } catch (err) {
    console.error(err.name, err.message);
  }
});

if ('getAsFileSystemHandle' in DataTransferItem.prototype) {
  import('./drag.js');
}
if ('showSaveFilePicker' in window) { //import('./save.js'); }
import {
  get,
  set,
  del,
} from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

import { blobToDataURL } from './script.js';

const button = document.querySelector('.save-card');
const reset = document.querySelector('.reset-card');
const main = document.querySelector('main');
const img = document.querySelector('.avatar');
const link = document.querySelector('.twitter-link');

window.businessCard = window.businessCard || {};

(async () => {
  window.businessCard.handle = (await get('file')) || undefined;
})();

button.hidden = false;
button.addEventListener('click', async () => {
  try {
    const suggestedName = `${new URL(link.href).pathname.substr(1)}.html`;
    const handle =
      window.businessCard.handle ||
      (await window.showSaveFilePicker({
        suggestedName,
        types: [
          {
            description: 'HTML document',
            accept: {
              'text/html': '.html',
            },
          },
        ],
        excludeAcceptAllOption: true,
      }));

    const blob = await fetch(img.src).then((response) => response.blob());
    const dataURL = await blobToDataURL(blob);
    const css = await fetch('./style.css').then((response) => response.text());
    const html = `<style>${css}</style>${main.outerHTML
      .replace(/<button[^>]+.*?<\/button>/g, '')
      .replace(/\s+contenteditable=""/g, '')
      .replace(/src="[^"]+"/g, `src="${dataURL}"`)}`;

    const writable = await handle.createWritable();
    await writable.write(html);
    await writable.close();
    window.businessCard.handle = handle;
    await set('file', handle);
  } catch (err) {
    console.error(err.name, err.message);
  }
});

document.addEventListener('keydown', (e) => {
  if (e.key === 's' && e.metaKey) {
    e.preventDefault();
    button.click();
    const before = button.style.boxShadow;
    button.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      button.style.boxShadow = before;
    }, 1000);
  }
});

reset.addEventListener('click', async () => {
  window.businessCard.handle = undefined;
  await del('file');
});

const blobToDataURL = async (blob) => {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
};
import {
  get,
  set,
  del,
} from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

import { blobToDataURL } from './script.js';

const button = document.querySelector('.save-card');
const reset = document.querySelector('.reset-card');
const main = document.querySelector('main');
const img = document.querySelector('.avatar');
const link = document.querySelector('.twitter-link');

window.businessCard = window.businessCard || {};

(async () => {
  window.businessCard.handle = (await get('file')) || undefined;
})();

button.hidden = false;
button.addEventListener('click', async () => {
  try {
    const suggestedName = `${new URL(link.href).pathname.substr(1)}.html`;
    const handle =
      window.businessCard.handle ||
      (await window.showSaveFilePicker({
        suggestedName,
        types: [
          {
            description: 'HTML document',
            accept: {
              'text/html': '.html',
            },
          },
        ],
        excludeAcceptAllOption: true,
      }));

    const blob = await fetch(img.src).then((response) => response.blob());
    const dataURL = await blobToDataURL(blob);
    const css = await fetch('./style.css').then((response) => response.text());
    const html = `<style>${css}</style>${main.outerHTML
      .replace(/<button[^>]+.*?<\/button>/g, '')
      .replace(/\s+contenteditable=""/g, '')
      .replace(/src="[^"]+"/g, `src="${dataURL}"`)}`;

    const writable = await handle.createWritable();
    await writable.write(html);
    await writable.close();
    window.businessCard.handle = handle;
    await set('file', handle);
  } catch (err) {
    console.error(err.name, err.message);
  }
});

document.addEventListener('keydown', (e) => {
  if (e.key === 's' && e.metaKey) {
    e.preventDefault();
    button.click();
    const before = button.style.boxShadow;
    button.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      button.style.boxShadow = before;
    }, 1000);
  }
});

reset.addEventListener('click', async () => {
  window.businessCard.handle = undefined;
  await del('file');
});
if ('getAsFileSystemHandle' in DataTransferItem.prototype) {
  import('./drag.js');
}
const body = document.body;
const documentElement = document.documentElement;
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');
const footer = document.querySelector('footer');

footer.hidden = false;
img.hidden = false;

const before = documentElement.style.outline;

body.addEventListener('dragenter', (e) => {
  e.preventDefault();
  documentElement.style.outline = 'solid var(--border) var(--accent-color)';
});

body.addEventListener('dragleave', (e) => {
  e.preventDefault();
  documentElement.style.outline = before;
});

body.addEventListener('dragover', (e) => {
  e.preventDefault();
});

body.addEventListener('drop', async (e) => {
  e.preventDefault();
  documentElement.style.outline = before;
  for (const item of e.dataTransfer.items) {
    if (item.kind === 'file') {
      const handle = await item.getAsFileSystemHandle();
      if (handle.kind === 'file') {
        const file = await handle.getFile();
        if (file.type.startsWith('image/')) {
          const src = URL.createObjectURL(file);
          img.src = src;
          icon.href = src;
          const before = img.style.boxShadow;
          img.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
          setTimeout(() => {
            img.style.boxShadow = before;
          }, 1000);
          return;
        }
      }
    }
  }
});

if (
  'clipboardData' in ClipboardEvent.prototype &&
  'showSaveFilePicker' in window
) {
  //import('./paste.js');
}
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  if (file.type.startsWith('image/')) {
    const src = URL.createObjectURL(file);
    img.src = src;
    icon.href = src;
    const before = img.style.boxShadow;
    img.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      img.style.boxShadow = before;
    }, 1000);
    return;
  }
});
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  if (file.type.startsWith('image/')) {
    const src = URL.createObjectURL(file);
    img.src = src;
    icon.href = src;
    const before = img.style.boxShadow;
    img.style.boxShadow = '0px 0px 0px var(--border) var(--accent-color)';
    setTimeout(() => {
      img.style.boxShadow = before;
    }, 1000);
    return;
  }
});

if ('launchQueue' in window) {
  import('./file.js');
}
const img = document.querySelector('.avatar');
const icon = document.querySelector('link[rel=icon]');

img.hidden = false;

window.launchQueue.setConsumer(async (launchParams) => {
  if (!launchParams.files.length) {
    return;
  }
  for (const handle of launchParams.files) {
    const file = await handle.getFile();
    if (file.type.startsWith('image/')) {
      const src = URL.createObjectURL(file);
      img.src = src;
      icon.href = src;
      return;
    }
  }
});
if ('storageFoundation' in window) {
  import('./storage.js');
}