Skip to content

Commit

Permalink
add fallback behavior for showOpenFilePicker and `showSaveFilePicke…
Browse files Browse the repository at this point in the history
…r` (#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>
  • Loading branch information
josxha authored Jan 16, 2025
1 parent d50ea76 commit 405b8aa
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 26 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 27 additions & 21 deletions src/components/ModalExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -15,6 +15,7 @@ import fieldSpecAdditional from '../libs/field-spec-additional'


const MAPLIBRE_GL_VERSION = version;
const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "function";


type ModalExportInternalProps = {
Expand All @@ -29,16 +30,16 @@ type ModalExportInternalProps = {

class ModalExportInternal extends React.Component<ModalExportInternalProps> {

tokenizedStyle () {
tokenizedStyle() {
return format(
style.stripAccessTokens(
style.replaceAccessTokens(this.props.mapStyle)
)
);
}

exportName () {
if(this.props.mapStyle.name) {
exportName() {
if (this.props.mapStyle.name) {
return Slugify(this.props.mapStyle.name, {
replacement: '_',
remove: /[*\-+~.()'"!:]/g,
Expand Down Expand Up @@ -86,6 +87,15 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
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();
Expand All @@ -112,12 +122,12 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
this.props.onOpenToggle();
}

async createFileHandle() : Promise<FileSystemFileHandle | null> {
async createFileHandle(): Promise<FileSystemFileHandle | null> {
const pickerOpts: SaveFilePickerOptions = {
types: [
{
description: "json",
accept: { "application/json": [".json"] },
accept: {"application/json": [".json"]},
},
],
suggestedName: this.exportName(),
Expand Down Expand Up @@ -179,23 +189,19 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
</div>

<div className="maputnik-modal-export-buttons">
<InputButton
onClick={this.saveStyle.bind(this)}
>
<MdSave />
<InputButton onClick={this.saveStyle.bind(this)}>
<MdSave/>
{t("Save")}
</InputButton>
<InputButton
onClick={this.saveStyleAs.bind(this)}
>
<MdSave />
{t("Save as")}
</InputButton>

<InputButton
onClick={this.createHtml.bind(this)}
>
<MdMap />
{showSaveFilePickerAvailable && (
<InputButton onClick={this.saveStyleAs.bind(this)}>
<MdSave/>
{t("Save as")}
</InputButton>
)}

<InputButton onClick={this.createHtml.bind(this)}>
<MdMap/>
{t("Create HTML")}
</InputButton>
</div>
Expand Down
41 changes: 37 additions & 4 deletions src/components/ModalOpen.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -168,6 +169,32 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
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() {
this.setState({
styleUrl: ""
Expand Down Expand Up @@ -217,10 +244,16 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<h1>{t("Open local Style")}</h1>
<p>{t("Open a local JSON style from your computer.")}</p>
<div>
<InputButton
className="maputnik-big-button"
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
</InputButton>
{typeof window.showOpenFilePicker === "function" ? (
<InputButton
className="maputnik-big-button"
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>
</section>

Expand Down

0 comments on commit 405b8aa

Please sign in to comment.