Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add fallback behavior for showOpenFilePicker and showSaveFilePicker #967

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" ? (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use if-else here? I think it will be clearer...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the syntax you want to go with?

  <div>
      {(() => {
        if (typeof window.showOpenFilePicker === "function") {
          return (
            <InputButton
                  className="maputnik-big-button"
                  onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
                </InputButton>
          )
        } else {
          return (
            <FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}>
                  <InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton>
                </FileReaderInput>
          )
        }
      })()}
    </div>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the lambda mandatory? I recall that react has a simpler way to add if inside the render method...?

Copy link
Contributor Author

@josxha josxha Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how? I can't find anything about it in the react docs nor stackoverflow. Using a tenary operator appears to be the shortest way. Alternatively, we could move the html block before the return statement and use it from a variable.

<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
Loading