mirror of
https://github.com/maputnik/editor.git
synced 2026-08-25 14:37:27 +00:00
feat: add drag and drop support to Upload Style area (#1852)
## Feat: Add drag and drop support to Upload Style area Closes #911 --- ### Problem The Upload Style area only supported click-to-upload. Users could not drag and drop a JSON file directly onto it. ### Fix Added `onDragOver` and `onDrop` event handlers to the Upload Style area. The dropped file is passed to the existing file reading logic already used by the Upload button. A visual highlight is shown when dragging over the area.
This commit is contained in:
committed by
GitHub
parent
a588f3e08c
commit
93596b3540
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|
||||||
|
- Improved the local style open modal and local file upload.
|
||||||
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
||||||
- Fixed an issue when clicking on a popup and then clicking on the map again
|
- Fixed an issue when clicking on a popup and then clicking on the map again
|
||||||
- Fix modal close button position
|
- Fix modal close button position
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ export default class MaputnikCypressHelper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
|
||||||
|
this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
|
||||||
|
action: "drag-drop",
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,12 @@ export class MaputnikDriver {
|
|||||||
},
|
},
|
||||||
chooseExampleFile: () => {
|
chooseExampleFile: () => {
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
this.helper.given.fixture("example-style.json", "example-style.json");
|
||||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
|
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
|
||||||
|
this.helper.when.wait(200);
|
||||||
|
},
|
||||||
|
dropExampleFile: () => {
|
||||||
|
this.helper.given.fixture("example-style.json", "example-style.json");
|
||||||
|
this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
|
||||||
this.helper.when.wait(200);
|
this.helper.when.wait(200);
|
||||||
},
|
},
|
||||||
setStyle: (
|
setStyle: (
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ describe("modals", () => {
|
|||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("upload via drag and drop", () => {
|
||||||
|
when.dropExampleFile();
|
||||||
|
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
||||||
|
});
|
||||||
|
|
||||||
describe("when click open url", () => {
|
describe("when click open url", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const styleFileUrl = get.exampleFileUrl();
|
const styleFileUrl = get.exampleFileUrl();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { type FormEvent } from "react";
|
import React, { type DragEvent, type 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 { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
||||||
@@ -51,16 +51,20 @@ type ModalOpenInternalProps = {
|
|||||||
|
|
||||||
type ModalOpenState = {
|
type ModalOpenState = {
|
||||||
styleUrl: string
|
styleUrl: string
|
||||||
|
isDragOver: boolean
|
||||||
error?: string | null
|
error?: string | null
|
||||||
activeRequest?: any
|
activeRequest?: any
|
||||||
activeRequestUrl?: string | null
|
activeRequestUrl?: string | null
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
||||||
|
private fileInputRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
constructor(props: ModalOpenInternalProps) {
|
constructor(props: ModalOpenInternalProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
styleUrl: ""
|
styleUrl: "",
|
||||||
|
isDragOver: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +202,45 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
|
|
||||||
onOpenToggle() {
|
onOpenToggle() {
|
||||||
this.setState({
|
this.setState({
|
||||||
styleUrl: ""
|
styleUrl: "",
|
||||||
|
isDragOver: false,
|
||||||
});
|
});
|
||||||
this.clearError();
|
this.clearError();
|
||||||
this.props.onOpenToggle();
|
this.props.onOpenToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBrowseClick = async () => {
|
||||||
|
if (typeof window.showOpenFilePicker === "function") {
|
||||||
|
await this.onOpenFile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
onFileDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.state.isDragOver) {
|
||||||
|
this.setState({ isDragOver: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onFileDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({ isDragOver: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFileDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.setState({ isDragOver: false });
|
||||||
|
this.onFileChanged(e.dataTransfer.files);
|
||||||
|
};
|
||||||
|
|
||||||
onChangeUrl = (url: string) => {
|
onChangeUrl = (url: string) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
styleUrl: url,
|
styleUrl: url,
|
||||||
@@ -244,19 +281,35 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
<section className="maputnik-modal-section">
|
<section className="maputnik-modal-section">
|
||||||
<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
|
||||||
{typeof window.showOpenFilePicker === "function" ? (
|
data-wd-key="modal:open.dropzone"
|
||||||
<InputButton
|
className={`maputnik-upload-dropzone${this.state.isDragOver ? " maputnik-upload-dropzone--active" : ""}`}
|
||||||
data-wd-key="modal:open.file.button"
|
role="button"
|
||||||
className="maputnik-big-button"
|
tabIndex={0}
|
||||||
onClick={this.onOpenFile}><MdFileUpload /> {t("Open Style")}
|
onDragOver={this.onFileDragOver}
|
||||||
</InputButton>
|
onDragLeave={this.onFileDragLeave}
|
||||||
) : (
|
onDrop={this.onFileDrop}
|
||||||
<label>
|
onClick={() => void this.onBrowseClick()}
|
||||||
<a className="maputnik-button maputnik-upload-button" aria-label={t("Open Style")}><MdFileUpload /> {t("Open Style")}</a>
|
onKeyDown={(e) => {
|
||||||
<input data-wd-key="modal:open.file.input" type="file" style={{ display: "none" }} onChange={(e) => this.onFileChanged(e.target.files)} />
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
</label>
|
e.preventDefault();
|
||||||
)}
|
void this.onBrowseClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="maputnik-upload-dropzone-content">
|
||||||
|
<MdFileUpload className="maputnik-upload-dropzone-icon" aria-hidden="true" />
|
||||||
|
<p className="maputnik-upload-dropzone-text">
|
||||||
|
{t("Drag and drop a style JSON file here or click to browse")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={this.fileInputRef}
|
||||||
|
data-wd-key="modal:open.file.input"
|
||||||
|
type="file"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => this.onFileChanged(e.target.files)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "Video-URL",
|
"Video URL": "Video-URL",
|
||||||
"View": "Ansicht",
|
"View": "Ansicht",
|
||||||
"You've entered an old style filter.": "Du hast einen alten Filter-Stil eingegeben.",
|
"You've entered an old style filter.": "Du hast einen alten Filter-Stil eingegeben.",
|
||||||
"Zoom": "Zoom"
|
"Zoom": "Zoom",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "Ziehen Sie eine Style-JSON-Datei hierher oder klicken Sie, um zu durchsuchen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "Drag and drop a style JSON file here or click to browse"
|
||||||
|
}
|
||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "URL de la vidéo",
|
"Video URL": "URL de la vidéo",
|
||||||
"View": "Vue",
|
"View": "Vue",
|
||||||
"You've entered an old style filter.": "Vous avez entré un ancien style de filtre.",
|
"You've entered an old style filter.": "Vous avez entré un ancien style de filtre.",
|
||||||
"Zoom": "Zoom"
|
"Zoom": "Zoom",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "Faites glisser un fichier JSON de style ici ou cliquez pour parcourir"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "כתובת וידאו",
|
"Video URL": "כתובת וידאו",
|
||||||
"View": "תצוגה",
|
"View": "תצוגה",
|
||||||
"You've entered an old style filter.": "הכנסתם סינון מסוג ישן,",
|
"You've entered an old style filter.": "הכנסתם סינון מסוג ישן,",
|
||||||
"Zoom": "זום"
|
"Zoom": "זום",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "גרור ושחרר כאן קובץ JSON של סגנון או לחץ כדי לעיין"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "Indirizzo video",
|
"Video URL": "Indirizzo video",
|
||||||
"View": "Vista",
|
"View": "Vista",
|
||||||
"You've entered an old style filter.": "Hai inserito uno stile filtro obsoleto.",
|
"You've entered an old style filter.": "Hai inserito uno stile filtro obsoleto.",
|
||||||
"Zoom": "Zoom"
|
"Zoom": "Zoom",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "Trascina e rilascia qui un file JSON dello stile o fai clic per sfogliare"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "動画URL",
|
"Video URL": "動画URL",
|
||||||
"View": "表示",
|
"View": "表示",
|
||||||
"You've entered an old style filter.": "旧型フィルタを使用しております。",
|
"You've entered an old style filter.": "旧型フィルタを使用しております。",
|
||||||
"Zoom": "ズーム"
|
"Zoom": "ズーム",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "ここにスタイルのJSONファイルをドラッグ&ドロップするか、クリックして参照してください"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "비디오 URL",
|
"Video URL": "비디오 URL",
|
||||||
"View": "보기",
|
"View": "보기",
|
||||||
"You've entered an old style filter.": "이전 스타일 필터를 입력했습니다.",
|
"You've entered an old style filter.": "이전 스타일 필터를 입력했습니다.",
|
||||||
"Zoom": "줌"
|
"Zoom": "줌",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "여기에 스타일 JSON 파일을 끌어다 놓거나 클릭하여 찾아보세요"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,5 +199,6 @@
|
|||||||
"Video URL": "视频URL",
|
"Video URL": "视频URL",
|
||||||
"View": "视图",
|
"View": "视图",
|
||||||
"You've entered an old style filter.": "您输入了一个旧风格的过滤器。",
|
"You've entered an old style filter.": "您输入了一个旧风格的过滤器。",
|
||||||
"Zoom": "缩放"
|
"Zoom": "缩放",
|
||||||
|
"Drag and drop a style JSON file here or click to browse": "将样式 JSON 文件拖放到此处或点击以浏览"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,47 @@
|
|||||||
@extend .maputnik-big-button !optional; /* stylelint-disable-line */
|
@extend .maputnik-big-button !optional; /* stylelint-disable-line */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-upload-dropzone {
|
||||||
|
border: 2px dashed vars.$color-midgray;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: vars.$margin-4 vars.$margin-3;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 120ms ease, background-color 120ms ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: vars.$color-lowgray;
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid vars.$color-lowgray;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-upload-dropzone--active {
|
||||||
|
border-color: vars.$color-lowgray;
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-upload-dropzone-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-upload-dropzone-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: vars.$color-midgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-upload-dropzone-text {
|
||||||
|
margin: vars.$margin-2 0 0;
|
||||||
|
color: vars.$color-lowgray;
|
||||||
|
}
|
||||||
|
|
||||||
.maputnik-style-gallery-container {
|
.maputnik-style-gallery-container {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user