mirror of
https://github.com/maputnik/editor.git
synced 2026-08-27 23:47:27 +00:00
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 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. <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>
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
- Add scheme type options for vector/raster tile
|
- Add scheme type options for vector/raster tile
|
||||||
- Add `tileSize` field for raster and raster-dem tile sources
|
- Add `tileSize` field for raster and raster-dem tile sources
|
||||||
- Update Protomaps Light gallery style to v4
|
- 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..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {version} from 'maplibre-gl/package.json'
|
|||||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import type {StyleSpecification} from 'maplibre-gl'
|
import type {StyleSpecification} from 'maplibre-gl'
|
||||||
import {MdMap, MdSave} from 'react-icons/md'
|
import {MdMap, MdSave} from 'react-icons/md'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import {WithTranslation, withTranslation} from 'react-i18next';
|
||||||
|
|
||||||
import FieldString from './FieldString'
|
import FieldString from './FieldString'
|
||||||
import InputButton from './InputButton'
|
import InputButton from './InputButton'
|
||||||
@@ -15,6 +15,7 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
|
|||||||
|
|
||||||
|
|
||||||
const MAPLIBRE_GL_VERSION = version;
|
const MAPLIBRE_GL_VERSION = version;
|
||||||
|
const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "function";
|
||||||
|
|
||||||
|
|
||||||
type ModalExportInternalProps = {
|
type ModalExportInternalProps = {
|
||||||
@@ -29,7 +30,7 @@ type ModalExportInternalProps = {
|
|||||||
|
|
||||||
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||||
|
|
||||||
tokenizedStyle () {
|
tokenizedStyle() {
|
||||||
return format(
|
return format(
|
||||||
style.stripAccessTokens(
|
style.stripAccessTokens(
|
||||||
style.replaceAccessTokens(this.props.mapStyle)
|
style.replaceAccessTokens(this.props.mapStyle)
|
||||||
@@ -37,8 +38,8 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportName () {
|
exportName() {
|
||||||
if(this.props.mapStyle.name) {
|
if (this.props.mapStyle.name) {
|
||||||
return Slugify(this.props.mapStyle.name, {
|
return Slugify(this.props.mapStyle.name, {
|
||||||
replacement: '_',
|
replacement: '_',
|
||||||
remove: /[*\-+~.()'"!:]/g,
|
remove: /[*\-+~.()'"!:]/g,
|
||||||
@@ -86,6 +87,15 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
async saveStyle() {
|
async saveStyle() {
|
||||||
const tokenStyle = this.tokenizedStyle();
|
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;
|
let fileHandle = this.props.fileHandle;
|
||||||
if (fileHandle == null) {
|
if (fileHandle == null) {
|
||||||
fileHandle = await this.createFileHandle();
|
fileHandle = await this.createFileHandle();
|
||||||
@@ -112,12 +122,12 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
this.props.onOpenToggle();
|
this.props.onOpenToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFileHandle() : Promise<FileSystemFileHandle | null> {
|
async createFileHandle(): Promise<FileSystemFileHandle | null> {
|
||||||
const pickerOpts: SaveFilePickerOptions = {
|
const pickerOpts: SaveFilePickerOptions = {
|
||||||
types: [
|
types: [
|
||||||
{
|
{
|
||||||
description: "json",
|
description: "json",
|
||||||
accept: { "application/json": [".json"] },
|
accept: {"application/json": [".json"]},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
suggestedName: this.exportName(),
|
suggestedName: this.exportName(),
|
||||||
@@ -179,23 +189,19 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="maputnik-modal-export-buttons">
|
<div className="maputnik-modal-export-buttons">
|
||||||
<InputButton
|
<InputButton onClick={this.saveStyle.bind(this)}>
|
||||||
onClick={this.saveStyle.bind(this)}
|
<MdSave/>
|
||||||
>
|
|
||||||
<MdSave />
|
|
||||||
{t("Save")}
|
{t("Save")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
<InputButton
|
{showSaveFilePickerAvailable && (
|
||||||
onClick={this.saveStyleAs.bind(this)}
|
<InputButton onClick={this.saveStyleAs.bind(this)}>
|
||||||
>
|
<MdSave/>
|
||||||
<MdSave />
|
{t("Save as")}
|
||||||
{t("Save as")}
|
</InputButton>
|
||||||
</InputButton>
|
)}
|
||||||
|
|
||||||
<InputButton
|
<InputButton onClick={this.createHtml.bind(this)}>
|
||||||
onClick={this.createHtml.bind(this)}
|
<MdMap/>
|
||||||
>
|
|
||||||
<MdMap />
|
|
||||||
{t("Create HTML")}
|
{t("Create HTML")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { FormEvent } from 'react'
|
import React, { FormEvent } from 'react'
|
||||||
import {MdFileUpload} from 'react-icons/md'
|
import {MdFileUpload} from 'react-icons/md'
|
||||||
import {MdAddCircleOutline} 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 { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ModalLoading from './ModalLoading'
|
import ModalLoading from './ModalLoading'
|
||||||
@@ -168,6 +169,32 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
onFileChanged = async (_: any, files: Result[]) => {
|
||||||
|
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() {
|
onOpenToggle() {
|
||||||
this.setState({
|
this.setState({
|
||||||
styleUrl: ""
|
styleUrl: ""
|
||||||
@@ -217,10 +244,16 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
<h1>{t("Open local Style")}</h1>
|
<h1>{t("Open local Style")}</h1>
|
||||||
<p>{t("Open a local JSON style from your computer.")}</p>
|
<p>{t("Open a local JSON style from your computer.")}</p>
|
||||||
<div>
|
<div>
|
||||||
<InputButton
|
{typeof window.showOpenFilePicker === "function" ? (
|
||||||
className="maputnik-big-button"
|
<InputButton
|
||||||
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
|
className="maputnik-big-button"
|
||||||
</InputButton>
|
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
|
||||||
|
</InputButton>
|
||||||
|
) : (
|
||||||
|
<FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}>
|
||||||
|
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton>
|
||||||
|
</FileReaderInput>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user