Skip to content

Commit 405b8aa

Browse files
authored
add fallback behavior for showOpenFilePicker and showSaveFilePicker (#967)
## Launch Checklist <!-- Thanks for the PR! Feel free to add or remove items from the checklist. --> - [x] Briefly describe the changes in this PR. - [x] Link to related issues. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [ ] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. ## Description `showOpenFilePicker` and `showSaveFilePicker` are undefined on Firefox. With this pr, Maputnik uses the old behavior as a fallback. It keeps the naming "open" and "save" instead of "upload" and "download" to underline that the style stays within the browser and no actual upload happens. @zstadler Could you give it a try, please? ## Related Issue - fixes #966 ## Visual Changes The "Save as" button gets hidden if `showSaveFilePicker` is not available since it would have no use. <table> <tr> <td> Chrome </td> <td> Firefox </td> </tr> <tr> <td> <img src="https://github.com/user-attachments/assets/cdc2cd4d-1c09-4dec-8c94-f8b0dd0c5b8e" /> </td> <td> <img src="https://github.com/user-attachments/assets/0763ef63-6501-4cc1-977b-94753c3008ae" /> </td> </tr> </table>
1 parent d50ea76 commit 405b8aa

File tree

3 files changed

+65
-26
lines changed

3 files changed

+65
-26
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- Add scheme type options for vector/raster tile
77
- Add `tileSize` field for raster and raster-dem tile sources
88
- Update Protomaps Light gallery style to v4
9-
- Add support to edit local files on the file system
9+
- Add support to edit local files on the file system if supported by the browser
1010
- _...Add new stuff here..._
1111

1212
### 🐞 Bug fixes

src/components/ModalExport.tsx

+27-21
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {version} from 'maplibre-gl/package.json'
55
import {format} from '@maplibre/maplibre-gl-style-spec'
66
import type {StyleSpecification} from 'maplibre-gl'
77
import {MdMap, MdSave} from 'react-icons/md'
8-
import { WithTranslation, withTranslation } from 'react-i18next';
8+
import {WithTranslation, withTranslation} from 'react-i18next';
99

1010
import FieldString from './FieldString'
1111
import InputButton from './InputButton'
@@ -15,6 +15,7 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
1515

1616

1717
const MAPLIBRE_GL_VERSION = version;
18+
const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "function";
1819

1920

2021
type ModalExportInternalProps = {
@@ -29,16 +30,16 @@ type ModalExportInternalProps = {
2930

3031
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
3132

32-
tokenizedStyle () {
33+
tokenizedStyle() {
3334
return format(
3435
style.stripAccessTokens(
3536
style.replaceAccessTokens(this.props.mapStyle)
3637
)
3738
);
3839
}
3940

40-
exportName () {
41-
if(this.props.mapStyle.name) {
41+
exportName() {
42+
if (this.props.mapStyle.name) {
4243
return Slugify(this.props.mapStyle.name, {
4344
replacement: '_',
4445
remove: /[*\-+~.()'"!:]/g,
@@ -86,6 +87,15 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
8687
async saveStyle() {
8788
const tokenStyle = this.tokenizedStyle();
8889

90+
// it is not guaranteed that the File System Access API is available on all
91+
// browsers. If the function is not available, a fallback behavior is used.
92+
if (!showSaveFilePickerAvailable) {
93+
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
94+
const exportName = this.exportName();
95+
saveAs(blob, exportName + ".json");
96+
return;
97+
}
98+
8999
let fileHandle = this.props.fileHandle;
90100
if (fileHandle == null) {
91101
fileHandle = await this.createFileHandle();
@@ -112,12 +122,12 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
112122
this.props.onOpenToggle();
113123
}
114124

115-
async createFileHandle() : Promise<FileSystemFileHandle | null> {
125+
async createFileHandle(): Promise<FileSystemFileHandle | null> {
116126
const pickerOpts: SaveFilePickerOptions = {
117127
types: [
118128
{
119129
description: "json",
120-
accept: { "application/json": [".json"] },
130+
accept: {"application/json": [".json"]},
121131
},
122132
],
123133
suggestedName: this.exportName(),
@@ -179,23 +189,19 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
179189
</div>
180190

181191
<div className="maputnik-modal-export-buttons">
182-
<InputButton
183-
onClick={this.saveStyle.bind(this)}
184-
>
185-
<MdSave />
192+
<InputButton onClick={this.saveStyle.bind(this)}>
193+
<MdSave/>
186194
{t("Save")}
187195
</InputButton>
188-
<InputButton
189-
onClick={this.saveStyleAs.bind(this)}
190-
>
191-
<MdSave />
192-
{t("Save as")}
193-
</InputButton>
194-
195-
<InputButton
196-
onClick={this.createHtml.bind(this)}
197-
>
198-
<MdMap />
196+
{showSaveFilePickerAvailable && (
197+
<InputButton onClick={this.saveStyleAs.bind(this)}>
198+
<MdSave/>
199+
{t("Save as")}
200+
</InputButton>
201+
)}
202+
203+
<InputButton onClick={this.createHtml.bind(this)}>
204+
<MdMap/>
199205
{t("Create HTML")}
200206
</InputButton>
201207
</div>

src/components/ModalOpen.tsx

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { FormEvent } from 'react'
22
import {MdFileUpload} from 'react-icons/md'
33
import {MdAddCircleOutline} from 'react-icons/md'
4+
import FileReaderInput, { Result } from 'react-file-reader-input'
45
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
56

67
import ModalLoading from './ModalLoading'
@@ -168,6 +169,32 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
168169
return file;
169170
}
170171

172+
// it is not guaranteed that the File System Access API is available on all
173+
// browsers. If the function is not available, a fallback behavior is used.
174+
onFileChanged = async (_: any, files: Result[]) => {
175+
const [, file] = files[0];
176+
const reader = new FileReader();
177+
this.clearError();
178+
179+
reader.readAsText(file, "UTF-8");
180+
reader.onload = e => {
181+
let mapStyle;
182+
try {
183+
mapStyle = JSON.parse(e.target?.result as string)
184+
}
185+
catch(err) {
186+
this.setState({
187+
error: (err as Error).toString()
188+
});
189+
return;
190+
}
191+
mapStyle = style.ensureStyleValidity(mapStyle)
192+
this.props.onStyleOpen(mapStyle);
193+
this.onOpenToggle();
194+
}
195+
reader.onerror = e => console.log(e.target);
196+
}
197+
171198
onOpenToggle() {
172199
this.setState({
173200
styleUrl: ""
@@ -217,10 +244,16 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
217244
<h1>{t("Open local Style")}</h1>
218245
<p>{t("Open a local JSON style from your computer.")}</p>
219246
<div>
220-
<InputButton
221-
className="maputnik-big-button"
222-
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
223-
</InputButton>
247+
{typeof window.showOpenFilePicker === "function" ? (
248+
<InputButton
249+
className="maputnik-big-button"
250+
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
251+
</InputButton>
252+
) : (
253+
<FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}>
254+
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton>
255+
</FileReaderInput>
256+
)}
224257
</div>
225258
</section>
226259

0 commit comments

Comments
 (0)