From 405b8aa9515df78acc9c21d7829129f541912319 Mon Sep 17 00:00:00 2001
From: Joscha Eckert <34318751+josxha@users.noreply.github.com>
Date: Thu, 16 Jan 2025 22:53:27 +0100
Subject: [PATCH] add fallback behavior for `showOpenFilePicker` and
`showSaveFilePicker` (#967)
## Launch 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 https://github.com/maplibre/maputnik/issues/966
## Visual Changes
The "Save as" button gets hidden if `showSaveFilePicker` is not
available since it would have no use.
|
Chrome
|
Firefox
|
|
|
---
CHANGELOG.md | 2 +-
src/components/ModalExport.tsx | 46 +++++++++++++++++++---------------
src/components/ModalOpen.tsx | 41 +++++++++++++++++++++++++++---
3 files changed, 64 insertions(+), 25 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2421aab6..9e2a1934 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@
- Add scheme type options for vector/raster tile
- Add `tileSize` field for raster and raster-dem tile sources
- Update Protomaps Light gallery style to v4
-- Add support to edit local files on the file system
+- Add support to edit local files on the file system if supported by the browser
- _...Add new stuff here..._
### 🐞 Bug fixes
diff --git a/src/components/ModalExport.tsx b/src/components/ModalExport.tsx
index 684774c4..2750afba 100644
--- a/src/components/ModalExport.tsx
+++ b/src/components/ModalExport.tsx
@@ -5,7 +5,7 @@ import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdMap, MdSave} from 'react-icons/md'
-import { WithTranslation, withTranslation } from 'react-i18next';
+import {WithTranslation, withTranslation} from 'react-i18next';
import FieldString from './FieldString'
import InputButton from './InputButton'
@@ -15,6 +15,7 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPLIBRE_GL_VERSION = version;
+const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "function";
type ModalExportInternalProps = {
@@ -29,7 +30,7 @@ type ModalExportInternalProps = {
class ModalExportInternal extends React.Component {
- tokenizedStyle () {
+ tokenizedStyle() {
return format(
style.stripAccessTokens(
style.replaceAccessTokens(this.props.mapStyle)
@@ -37,8 +38,8 @@ class ModalExportInternal extends React.Component {
);
}
- exportName () {
- if(this.props.mapStyle.name) {
+ exportName() {
+ if (this.props.mapStyle.name) {
return Slugify(this.props.mapStyle.name, {
replacement: '_',
remove: /[*\-+~.()'"!:]/g,
@@ -86,6 +87,15 @@ class ModalExportInternal extends React.Component {
async saveStyle() {
const tokenStyle = this.tokenizedStyle();
+ // it is not guaranteed that the File System Access API is available on all
+ // browsers. If the function is not available, a fallback behavior is used.
+ if (!showSaveFilePickerAvailable) {
+ const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
+ const exportName = this.exportName();
+ saveAs(blob, exportName + ".json");
+ return;
+ }
+
let fileHandle = this.props.fileHandle;
if (fileHandle == null) {
fileHandle = await this.createFileHandle();
@@ -112,12 +122,12 @@ class ModalExportInternal extends React.Component {
this.props.onOpenToggle();
}
- async createFileHandle() : Promise {
+ async createFileHandle(): Promise {
const pickerOpts: SaveFilePickerOptions = {
types: [
{
description: "json",
- accept: { "application/json": [".json"] },
+ accept: {"application/json": [".json"]},
},
],
suggestedName: this.exportName(),
@@ -179,23 +189,19 @@ class ModalExportInternal extends React.Component {
-
-
+
+
{t("Save")}
-
-
- {t("Save as")}
-
+ {showSaveFilePickerAvailable && (
+
+
+ {t("Save as")}
+
+ )}
-
-
+
+
{t("Create HTML")}
diff --git a/src/components/ModalOpen.tsx b/src/components/ModalOpen.tsx
index 2761d14e..11c84e21 100644
--- a/src/components/ModalOpen.tsx
+++ b/src/components/ModalOpen.tsx
@@ -1,6 +1,7 @@
import React, { FormEvent } from 'react'
import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md'
+import FileReaderInput, { Result } from 'react-file-reader-input'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading'
@@ -168,6 +169,32 @@ class ModalOpenInternal extends React.Component {
+ const [, file] = files[0];
+ const reader = new FileReader();
+ this.clearError();
+
+ reader.readAsText(file, "UTF-8");
+ reader.onload = e => {
+ let mapStyle;
+ try {
+ mapStyle = JSON.parse(e.target?.result as string)
+ }
+ catch(err) {
+ this.setState({
+ error: (err as Error).toString()
+ });
+ return;
+ }
+ mapStyle = style.ensureStyleValidity(mapStyle)
+ this.props.onStyleOpen(mapStyle);
+ this.onOpenToggle();
+ }
+ reader.onerror = e => console.log(e.target);
+ }
+
onOpenToggle() {
this.setState({
styleUrl: ""
@@ -217,10 +244,16 @@ class ModalOpenInternal extends React.Component{t("Open local Style")}
{t("Open a local JSON style from your computer.")}
- {t("Open Style")}
-
+ {typeof window.showOpenFilePicker === "function" ? (
+ {t("Open Style")}
+
+ ) : (
+
+ {t("Open Style")}
+
+ )}