Skip to content

Commit 32d0343

Browse files
committed
Merge branch 'main' of github.com:Matdata-eu/Yasgui
2 parents de9580c + 2f43ff5 commit 32d0343

4 files changed

Lines changed: 142 additions & 45 deletions

File tree

packages/yasgui/src/Tab.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,42 @@ export class Tab extends EventEmitter {
175175
return this.persistentJson.yasqe.value;
176176
}
177177

178+
public async downloadAsRqFile() {
179+
const query = this.getQueryTextForSave();
180+
const tabName = this.name() || "query";
181+
const filename = tabName.endsWith(".rq") || tabName.endsWith(".sparql") ? tabName : `${tabName}.rq`;
182+
const blob = new Blob([query], { type: "application/sparql-query" });
183+
184+
// Use File System Access API if available so the user can choose the save location
185+
if ("showSaveFilePicker" in window) {
186+
try {
187+
const showSaveFilePicker = (
188+
window as Window & { showSaveFilePicker: (...args: unknown[]) => Promise<FileSystemFileHandle> }
189+
).showSaveFilePicker;
190+
const fileHandle = await showSaveFilePicker({
191+
suggestedName: filename,
192+
types: [{ description: "SPARQL Query", accept: { "application/sparql-query": [".rq", ".sparql"] } }],
193+
});
194+
const writable = await fileHandle.createWritable();
195+
await writable.write(blob);
196+
await writable.close();
197+
return;
198+
} catch (e: any) {
199+
// User cancelled the picker – abort silently
200+
if (e?.name === "AbortError") return;
201+
// Other errors fall through to the legacy download below
202+
}
203+
}
204+
205+
// Fallback for browsers without File System Access API
206+
const url = URL.createObjectURL(blob);
207+
const a = document.createElement("a");
208+
a.href = url;
209+
a.download = filename;
210+
a.click();
211+
URL.revokeObjectURL(url);
212+
}
213+
178214
public async saveManagedQueryOrSaveAsManagedQuery(): Promise<void> {
179215
const meta = this.getManagedQueryMetadata();
180216
if (!meta) {
@@ -1306,6 +1342,11 @@ export class Tab extends EventEmitter {
13061342
void this.saveManagedQueryOrSaveAsManagedQuery();
13071343
});
13081344

1345+
// Hook up download as .rq file
1346+
this.yasqe.on("downloadRqFile", () => {
1347+
void this.downloadAsRqFile();
1348+
});
1349+
13091350
// Show/hide save button based on workspace configuration
13101351
this.updateSaveButtonVisibility();
13111352

packages/yasgui/src/TabContextMenu.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default class TabContextMenu {
1616
private renameTabEl!: HTMLElement;
1717
private copyTabEl!: HTMLElement;
1818
private saveManagedQueryEl!: HTMLElement;
19+
private saveAsRqFileEl!: HTMLElement;
1920
private closeTabEl!: HTMLElement;
2021
private closeOtherTabsEl!: HTMLElement;
2122
private reOpenOldTab!: HTMLElement;
@@ -52,6 +53,8 @@ export default class TabContextMenu {
5253

5354
this.saveManagedQueryEl = this.getMenuItemEl("Save as managed query");
5455

56+
this.saveAsRqFileEl = this.getMenuItemEl("Save as .rq file");
57+
5558
this.closeTabEl = this.getMenuItemEl("Close Tab");
5659

5760
this.closeOtherTabsEl = this.getMenuItemEl("Close other tabs");
@@ -63,6 +66,7 @@ export default class TabContextMenu {
6366
dropDownList.appendChild(this.renameTabEl);
6467
dropDownList.appendChild(this.copyTabEl);
6568
dropDownList.appendChild(this.saveManagedQueryEl);
69+
dropDownList.appendChild(this.saveAsRqFileEl);
6670
// Add divider
6771
dropDownList.appendChild(document.createElement("hr"));
6872
dropDownList.appendChild(this.closeTabEl);
@@ -115,6 +119,12 @@ export default class TabContextMenu {
115119
this.closeConfigMenu();
116120
};
117121

122+
this.saveAsRqFileEl.onclick = () => {
123+
if (!tab) return;
124+
tab.downloadAsRqFile();
125+
this.closeConfigMenu();
126+
};
127+
118128
// Close tab functionality
119129
this.closeTabEl.onclick = () => tab?.close();
120130

packages/yasqe/src/index.ts

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export interface Yasqe {
4646
off(eventName: "autocompletionClose", handler: (instance: Yasqe) => void): void;
4747
on(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
4848
off(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
49+
on(eventName: "saveManagedQuery", handler: () => void): void;
50+
off(eventName: "saveManagedQuery", handler: () => void): void;
51+
on(eventName: "downloadRqFile", handler: () => void): void;
52+
off(eventName: "downloadRqFile", handler: () => void): void;
4953
on(eventName: string, handler: () => void): void;
5054
}
5155

@@ -59,7 +63,7 @@ export class Yasqe extends CodeMirror {
5963
private abortController: AbortController | undefined;
6064
private queryStatus: "valid" | "error" | undefined;
6165
private queryBtn: HTMLButtonElement | undefined;
62-
private saveBtn: HTMLButtonElement | undefined;
66+
private saveBtnWrapper: HTMLDivElement | undefined;
6367
private fullscreenBtn: HTMLButtonElement | undefined;
6468
private hamburgerBtn: HTMLButtonElement | undefined;
6569
private hamburgerMenu: HTMLDivElement | undefined;
@@ -570,24 +574,42 @@ export class Yasqe extends CodeMirror {
570574
}
571575

572576
/**
573-
* Draw save button (THIRD)
577+
* Draw save buttons (THIRD)
574578
*/
575-
const saveBtn = document.createElement("button");
576-
addClass(saveBtn, "yasqe_saveButton");
577-
const saveIcon = document.createElement("i");
578-
addClass(saveIcon, "fas");
579-
addClass(saveIcon, "fa-save");
580-
saveIcon.setAttribute("aria-hidden", "true");
581-
saveBtn.appendChild(saveIcon);
582-
saveBtn.onclick = () => {
583-
// Call the managed query save function if available
579+
const saveBtnWrapper = document.createElement("div");
580+
addClass(saveBtnWrapper, "yasqe_saveWrapper");
581+
saveBtnWrapper.style.display = "none"; // Hidden by default, shown when workspace is configured
582+
this.saveBtnWrapper = saveBtnWrapper;
583+
584+
const saveManagedBtn = document.createElement("button");
585+
addClass(saveManagedBtn, "yasqe_saveManagedButton");
586+
const saveManagedIcon = document.createElement("i");
587+
addClass(saveManagedIcon, "fas");
588+
addClass(saveManagedIcon, "fa-database");
589+
saveManagedIcon.setAttribute("aria-hidden", "true");
590+
saveManagedBtn.appendChild(saveManagedIcon);
591+
saveManagedBtn.title = "Save as managed query";
592+
saveManagedBtn.setAttribute("aria-label", "Save as managed query");
593+
saveManagedBtn.onclick = () => {
584594
this.emit("saveManagedQuery");
585595
};
586-
saveBtn.title = "Save managed query (Ctrl+S)";
587-
saveBtn.setAttribute("aria-label", "Save managed query");
588-
saveBtn.style.display = "none"; // Hidden by default, shown when workspace is configured
589-
this.saveBtn = saveBtn;
590-
buttons.appendChild(saveBtn);
596+
saveBtnWrapper.appendChild(saveManagedBtn);
597+
598+
const saveRqBtn = document.createElement("button");
599+
addClass(saveRqBtn, "yasqe_saveRqButton");
600+
const saveRqIcon = document.createElement("i");
601+
addClass(saveRqIcon, "fas");
602+
addClass(saveRqIcon, "fa-file-download");
603+
saveRqIcon.setAttribute("aria-hidden", "true");
604+
saveRqBtn.appendChild(saveRqIcon);
605+
saveRqBtn.title = "Save as .rq file";
606+
saveRqBtn.setAttribute("aria-label", "Save as .rq file");
607+
saveRqBtn.onclick = () => {
608+
this.emit("downloadRqFile");
609+
};
610+
saveBtnWrapper.appendChild(saveRqBtn);
611+
612+
buttons.appendChild(saveBtnWrapper);
591613

592614
/**
593615
* Draw format btn (FOURTH)
@@ -671,21 +693,37 @@ export class Yasqe extends CodeMirror {
671693
this.hamburgerMenu.appendChild(shareItem);
672694
}
673695

674-
const saveItem = document.createElement("button");
675-
saveItem.className = "yasqe_hamburgerMenuItem";
676-
const saveIconMenu = document.createElement("i");
677-
addClass(saveIconMenu, "fas");
678-
addClass(saveIconMenu, "fa-save");
679-
saveIconMenu.setAttribute("aria-hidden", "true");
680-
saveItem.appendChild(saveIconMenu);
681-
const saveLabel = document.createElement("span");
682-
saveLabel.textContent = "Save";
683-
saveItem.appendChild(saveLabel);
684-
saveItem.onclick = () => {
696+
const saveManagedItem = document.createElement("button");
697+
saveManagedItem.className = "yasqe_hamburgerMenuItem";
698+
const saveManagedIconMenu = document.createElement("i");
699+
addClass(saveManagedIconMenu, "fas");
700+
addClass(saveManagedIconMenu, "fa-database");
701+
saveManagedIconMenu.setAttribute("aria-hidden", "true");
702+
saveManagedItem.appendChild(saveManagedIconMenu);
703+
const saveManagedLabel = document.createElement("span");
704+
saveManagedLabel.textContent = "Save as managed query";
705+
saveManagedItem.appendChild(saveManagedLabel);
706+
saveManagedItem.onclick = () => {
685707
this.closeHamburgerMenu();
686708
this.emit("saveManagedQuery");
687709
};
688-
this.hamburgerMenu.appendChild(saveItem);
710+
this.hamburgerMenu.appendChild(saveManagedItem);
711+
712+
const saveRqItem = document.createElement("button");
713+
saveRqItem.className = "yasqe_hamburgerMenuItem";
714+
const saveRqIconMenu = document.createElement("i");
715+
addClass(saveRqIconMenu, "fas");
716+
addClass(saveRqIconMenu, "fa-file-download");
717+
saveRqIconMenu.setAttribute("aria-hidden", "true");
718+
saveRqItem.appendChild(saveRqIconMenu);
719+
const saveRqLabel = document.createElement("span");
720+
saveRqLabel.textContent = "Save as .rq file";
721+
saveRqItem.appendChild(saveRqLabel);
722+
saveRqItem.onclick = () => {
723+
this.closeHamburgerMenu();
724+
this.emit("downloadRqFile");
725+
};
726+
this.hamburgerMenu.appendChild(saveRqItem);
689727

690728
if (this.config.showFormatButton) {
691729
const formatItem = document.createElement("button");
@@ -1553,8 +1591,8 @@ export class Yasqe extends CodeMirror {
15531591
}
15541592

15551593
public setSaveButtonVisible(visible: boolean) {
1556-
if (this.saveBtn) {
1557-
this.saveBtn.style.display = visible ? "inline-flex" : "none";
1594+
if (this.saveBtnWrapper) {
1595+
this.saveBtnWrapper.style.display = visible ? "inline-flex" : "none";
15581596
}
15591597
}
15601598

packages/yasqe/src/scss/buttons.scss

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -293,25 +293,33 @@
293293
}
294294
}
295295

296-
.yasqe_saveButton {
296+
.yasqe_saveWrapper {
297+
position: relative;
297298
display: inline-flex;
298299
align-items: center;
299-
justify-content: center;
300-
border: none;
301-
background: none;
302-
cursor: pointer;
303-
padding: 6px;
300+
vertical-align: middle;
304301
margin-left: 8px;
305-
height: 36px;
306-
width: 36px;
307-
color: var(--yasgui-text-secondary, #505050);
308302

309-
i {
310-
font-size: 20px;
311-
}
303+
.yasqe_saveManagedButton,
304+
.yasqe_saveRqButton {
305+
display: inline-flex;
306+
align-items: center;
307+
justify-content: center;
308+
border: none;
309+
background: none;
310+
cursor: pointer;
311+
padding: 6px;
312+
height: 36px;
313+
width: 36px;
314+
color: var(--yasgui-text-secondary, #505050);
312315

313-
&:hover {
314-
color: #337ab7;
316+
i {
317+
font-size: 20px;
318+
}
319+
320+
&:hover {
321+
color: #337ab7;
322+
}
315323
}
316324
}
317325

@@ -441,7 +449,7 @@
441449
@media (max-width: 768px) {
442450
.yasqe_buttons {
443451
.yasqe_share,
444-
.yasqe_saveButton,
452+
.yasqe_saveWrapper,
445453
.yasqe_formatButton,
446454
.yasqe_fullscreenButton {
447455
display: none !important;

0 commit comments

Comments
 (0)