@matdata/yasgui 5.14.0 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ts/src/Tab.d.ts +2 -0
- package/build/ts/src/Tab.js +67 -5
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +2 -1
- package/build/ts/src/TabContextMenu.js +11 -3
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.js +13 -3
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +9 -0
- package/build/ts/src/TabSettingsModal.js +114 -4
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/endpointSelect.js +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +1 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +62 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -1
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +7 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +82 -4
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -1
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +185 -8
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -1
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +18 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +17 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +61 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +1 -0
- package/build/ts/src/urlUtils.d.ts +1 -0
- package/build/ts/src/urlUtils.js +21 -0
- package/build/ts/src/urlUtils.js.map +1 -0
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +245 -213
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +83 -5
- package/src/TabContextMenu.ts +15 -5
- package/src/TabElements.scss +17 -0
- package/src/TabElements.ts +17 -3
- package/src/TabSettingsModal.scss +73 -0
- package/src/TabSettingsModal.ts +152 -6
- package/src/endpointSelect.scss +0 -22
- package/src/endpointSelect.ts +1 -1
- package/src/queryManagement/QueryBrowser.ts +72 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +102 -4
- package/src/queryManagement/WorkspaceSettingsForm.ts +215 -8
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +19 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +17 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +69 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +6 -0
- package/src/tab.scss +1 -0
- package/src/themes.scss +0 -10
- package/src/urlUtils.ts +40 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Tab.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { saveManagedQuery } from "./queryManagement/saveManagedQuery";
|
|
|
17
17
|
import { getWorkspaceBackend } from "./queryManagement/backends/getWorkspaceBackend";
|
|
18
18
|
import { asWorkspaceBackendError } from "./queryManagement/backends/errors";
|
|
19
19
|
import { normalizeQueryFilename } from "./queryManagement/normalizeQueryFilename";
|
|
20
|
+
import { resolveEndpointUrl } from "./urlUtils";
|
|
20
21
|
|
|
21
22
|
export interface PersistedJsonYasr extends YasrPersistentConfig {
|
|
22
23
|
responseSummary: Parser.ResponseSummary;
|
|
@@ -174,6 +175,42 @@ export class Tab extends EventEmitter {
|
|
|
174
175
|
return this.persistentJson.yasqe.value;
|
|
175
176
|
}
|
|
176
177
|
|
|
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
|
+
|
|
177
214
|
public async saveManagedQueryOrSaveAsManagedQuery(): Promise<void> {
|
|
178
215
|
const meta = this.getManagedQueryMetadata();
|
|
179
216
|
if (!meta) {
|
|
@@ -304,7 +341,7 @@ export class Tab extends EventEmitter {
|
|
|
304
341
|
name: result.name,
|
|
305
342
|
filename: result.filename,
|
|
306
343
|
queryText: this.getQueryTextForSave(),
|
|
307
|
-
associatedEndpoint: workspace.type === "sparql" ? this.getEndpoint() : undefined,
|
|
344
|
+
associatedEndpoint: workspace.type === "sparql" ? resolveEndpointUrl(this.getEndpoint()) : undefined,
|
|
308
345
|
message: result.message,
|
|
309
346
|
expectedVersionTag,
|
|
310
347
|
});
|
|
@@ -534,7 +571,7 @@ export class Tab extends EventEmitter {
|
|
|
534
571
|
if (!this.controlBarEl) return;
|
|
535
572
|
|
|
536
573
|
this.orientationToggleButton = document.createElement("button");
|
|
537
|
-
this.orientationToggleButton.className = "tabContextButton orientationToggle";
|
|
574
|
+
this.orientationToggleButton.className = "tabContextButton orientationToggle desktopOnly";
|
|
538
575
|
this.orientationToggleButton.setAttribute("aria-label", "Toggle layout orientation");
|
|
539
576
|
this.orientationToggleButton.title = "Toggle layout orientation";
|
|
540
577
|
|
|
@@ -614,6 +651,10 @@ export class Tab extends EventEmitter {
|
|
|
614
651
|
return this.yasr;
|
|
615
652
|
}
|
|
616
653
|
|
|
654
|
+
public getCurrentOrientation() {
|
|
655
|
+
return this.currentOrientation;
|
|
656
|
+
}
|
|
657
|
+
|
|
617
658
|
private initTabSettingsMenu() {
|
|
618
659
|
if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing tab settings");
|
|
619
660
|
this.settingsModal = new TabSettingsModal(this, this.controlBarEl);
|
|
@@ -1301,6 +1342,11 @@ export class Tab extends EventEmitter {
|
|
|
1301
1342
|
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
1302
1343
|
});
|
|
1303
1344
|
|
|
1345
|
+
// Hook up download as .rq file
|
|
1346
|
+
this.yasqe.on("downloadRqFile", () => {
|
|
1347
|
+
void this.downloadAsRqFile();
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1304
1350
|
// Show/hide save button based on workspace configuration
|
|
1305
1351
|
this.updateSaveButtonVisibility();
|
|
1306
1352
|
|
|
@@ -1445,14 +1491,38 @@ export class Tab extends EventEmitter {
|
|
|
1445
1491
|
|
|
1446
1492
|
// Check if token is a URI (not a variable)
|
|
1447
1493
|
// URIs typically have token.type of 'string-2' or might be in angle brackets
|
|
1448
|
-
|
|
1494
|
+
let tokenString = token.string.trim();
|
|
1449
1495
|
|
|
1450
1496
|
// Skip if it's a variable (starts with ? or $)
|
|
1451
1497
|
if (tokenString.startsWith("?") || tokenString.startsWith("$")) return;
|
|
1452
1498
|
|
|
1499
|
+
// Handle prefixed names that may be split into multiple tokens by the tokenizer
|
|
1500
|
+
// The tokenizer splits "prefix:localname" into two tokens:
|
|
1501
|
+
// - PNAME_LN_PREFIX (e.g., "bnd:") with type "string-2"
|
|
1502
|
+
// - PNAME_LN_LOCAL (e.g., "_subnetwork_bane_KVGB") with type "string"
|
|
1503
|
+
// We need to combine them to get the full prefixed name
|
|
1504
|
+
if (token.type === "string-2" && tokenString.endsWith(":")) {
|
|
1505
|
+
// This is a prefix token, get the next token for the local name
|
|
1506
|
+
const nextToken = this.yasqe.getTokenAt({ line: pos.line, ch: token.end + 1 });
|
|
1507
|
+
if (nextToken && nextToken.type === "string" && nextToken.start === token.end) {
|
|
1508
|
+
tokenString = `${tokenString}${nextToken.string.trim()}`;
|
|
1509
|
+
}
|
|
1510
|
+
} else if (token.type === "string" && token.start > 0) {
|
|
1511
|
+
// This might be a local name token, check if previous token is a prefix
|
|
1512
|
+
const prevToken = this.yasqe.getTokenAt({ line: pos.line, ch: token.start - 1 });
|
|
1513
|
+
if (
|
|
1514
|
+
prevToken &&
|
|
1515
|
+
prevToken.type === "string-2" &&
|
|
1516
|
+
prevToken.string.trim().endsWith(":") &&
|
|
1517
|
+
prevToken.end === token.start
|
|
1518
|
+
) {
|
|
1519
|
+
tokenString = `${prevToken.string.trim()}${tokenString}`;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1453
1523
|
// Check if it's a URI - either in angle brackets or a prefixed name
|
|
1454
1524
|
const isFullUri = tokenString.startsWith("<") && tokenString.endsWith(">");
|
|
1455
|
-
const isPrefixedName = /^[\w-]
|
|
1525
|
+
const isPrefixedName = /^[\w-]+:/.test(tokenString);
|
|
1456
1526
|
|
|
1457
1527
|
if (!isFullUri && !isPrefixedName) return;
|
|
1458
1528
|
|
|
@@ -1467,7 +1537,8 @@ export class Tab extends EventEmitter {
|
|
|
1467
1537
|
} else if (isPrefixedName) {
|
|
1468
1538
|
// Expand prefixed name to full URI
|
|
1469
1539
|
const prefixes = this.yasqe.getPrefixesFromQuery();
|
|
1470
|
-
const [prefix,
|
|
1540
|
+
const [prefix, ...localParts] = tokenString.split(":");
|
|
1541
|
+
const localName = localParts.join(":");
|
|
1471
1542
|
const prefixUri = prefixes[prefix];
|
|
1472
1543
|
if (prefixUri) {
|
|
1473
1544
|
uri = prefixUri + localName;
|
|
@@ -1601,6 +1672,13 @@ WHERE {
|
|
|
1601
1672
|
// Add default renderers to the end, to give our custom ones priority.
|
|
1602
1673
|
...(Yasr.defaults.errorRenderers || []),
|
|
1603
1674
|
],
|
|
1675
|
+
executeQuery: async (query: string, options?: { acceptHeader?: string }) => {
|
|
1676
|
+
if (!this.yasqe) throw new Error("No YASQE instance available");
|
|
1677
|
+
return Yasqe.Sparql.executeQuery(this.yasqe, undefined, {
|
|
1678
|
+
customQuery: query,
|
|
1679
|
+
customAccept: options?.acceptHeader,
|
|
1680
|
+
});
|
|
1681
|
+
},
|
|
1604
1682
|
};
|
|
1605
1683
|
// Allow getDownloadFilName to be overwritten by the global config
|
|
1606
1684
|
if (yasrConf.getDownloadFileName === undefined) {
|
package/src/TabContextMenu.ts
CHANGED
|
@@ -14,8 +14,9 @@ export default class TabContextMenu {
|
|
|
14
14
|
private contextEl!: HTMLElement;
|
|
15
15
|
private newTabEl!: HTMLElement;
|
|
16
16
|
private renameTabEl!: HTMLElement;
|
|
17
|
-
private
|
|
17
|
+
private duplicateTabEl!: HTMLElement;
|
|
18
18
|
private saveManagedQueryEl!: HTMLElement;
|
|
19
|
+
private saveAsRqFileEl!: HTMLElement;
|
|
19
20
|
private closeTabEl!: HTMLElement;
|
|
20
21
|
private closeOtherTabsEl!: HTMLElement;
|
|
21
22
|
private reOpenOldTab!: HTMLElement;
|
|
@@ -48,10 +49,12 @@ export default class TabContextMenu {
|
|
|
48
49
|
|
|
49
50
|
this.renameTabEl = this.getMenuItemEl("Rename Tab");
|
|
50
51
|
|
|
51
|
-
this.
|
|
52
|
+
this.duplicateTabEl = this.getMenuItemEl("Duplicate Tab");
|
|
52
53
|
|
|
53
54
|
this.saveManagedQueryEl = this.getMenuItemEl("Save as managed query");
|
|
54
55
|
|
|
56
|
+
this.saveAsRqFileEl = this.getMenuItemEl("Save as .rq file");
|
|
57
|
+
|
|
55
58
|
this.closeTabEl = this.getMenuItemEl("Close Tab");
|
|
56
59
|
|
|
57
60
|
this.closeOtherTabsEl = this.getMenuItemEl("Close other tabs");
|
|
@@ -61,8 +64,9 @@ export default class TabContextMenu {
|
|
|
61
64
|
// Add items to list
|
|
62
65
|
dropDownList.appendChild(this.newTabEl);
|
|
63
66
|
dropDownList.appendChild(this.renameTabEl);
|
|
64
|
-
dropDownList.appendChild(this.
|
|
67
|
+
dropDownList.appendChild(this.duplicateTabEl);
|
|
65
68
|
dropDownList.appendChild(this.saveManagedQueryEl);
|
|
69
|
+
dropDownList.appendChild(this.saveAsRqFileEl);
|
|
66
70
|
// Add divider
|
|
67
71
|
dropDownList.appendChild(document.createElement("hr"));
|
|
68
72
|
dropDownList.appendChild(this.closeTabEl);
|
|
@@ -101,8 +105,8 @@ export default class TabContextMenu {
|
|
|
101
105
|
// Set rename functionality
|
|
102
106
|
this.renameTabEl.onclick = () => currentTabEl.startRename();
|
|
103
107
|
|
|
104
|
-
//
|
|
105
|
-
this.
|
|
108
|
+
// Duplicate tab functionality
|
|
109
|
+
this.duplicateTabEl.onclick = () => {
|
|
106
110
|
if (!tab) return;
|
|
107
111
|
const config = cloneDeep(tab.getPersistedJson());
|
|
108
112
|
config.id = getRandomId();
|
|
@@ -115,6 +119,12 @@ export default class TabContextMenu {
|
|
|
115
119
|
this.closeConfigMenu();
|
|
116
120
|
};
|
|
117
121
|
|
|
122
|
+
this.saveAsRqFileEl.onclick = () => {
|
|
123
|
+
if (!tab) return;
|
|
124
|
+
tab.downloadAsRqFile();
|
|
125
|
+
this.closeConfigMenu();
|
|
126
|
+
};
|
|
127
|
+
|
|
118
128
|
// Close tab functionality
|
|
119
129
|
this.closeTabEl.onclick = () => tab?.close();
|
|
120
130
|
|
package/src/TabElements.scss
CHANGED
|
@@ -94,6 +94,10 @@ $minTabHeight: 35px;
|
|
|
94
94
|
position: relative;
|
|
95
95
|
$activeColor: #337ab7;
|
|
96
96
|
$hoverColor: color.adjust($activeColor, $lightness: 30%);
|
|
97
|
+
user-select: none;
|
|
98
|
+
-webkit-user-select: none;
|
|
99
|
+
-moz-user-select: none;
|
|
100
|
+
-ms-user-select: none;
|
|
97
101
|
|
|
98
102
|
// Distinguish managed-query tabs from legacy tabs via background.
|
|
99
103
|
// Legacy tabs keep their current styling.
|
|
@@ -190,6 +194,19 @@ $minTabHeight: 35px;
|
|
|
190
194
|
padding: 0px 24px 0px 30px;
|
|
191
195
|
white-space: nowrap;
|
|
192
196
|
overflow: hidden;
|
|
197
|
+
user-select: none;
|
|
198
|
+
-webkit-user-select: none;
|
|
199
|
+
-moz-user-select: none;
|
|
200
|
+
-ms-user-select: none;
|
|
201
|
+
-webkit-user-drag: none;
|
|
202
|
+
|
|
203
|
+
span {
|
|
204
|
+
user-select: none;
|
|
205
|
+
-webkit-user-select: none;
|
|
206
|
+
-moz-user-select: none;
|
|
207
|
+
-ms-user-select: none;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
}
|
|
193
210
|
|
|
194
211
|
&:hover {
|
|
195
212
|
border-bottom-color: $hoverColor;
|
package/src/TabElements.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class TabListEl {
|
|
|
107
107
|
tabLinkEl.href = "#" + this.tabId;
|
|
108
108
|
tabLinkEl.id = "tab-" + this.tabId; // use the id for the tabpanel which is tabId to set the actual tab id
|
|
109
109
|
tabLinkEl.setAttribute("aria-controls", this.tabId); // respective tabPanel id
|
|
110
|
-
|
|
110
|
+
// Note: draggable attribute removed - CSS user-select:none prevents text selection during drag
|
|
111
111
|
tabLinkEl.addEventListener("blur", () => {
|
|
112
112
|
if (!this.tabEl) return;
|
|
113
113
|
if (this.tabEl.classList.contains("active")) {
|
|
@@ -133,6 +133,17 @@ export class TabListEl {
|
|
|
133
133
|
this.yasgui.selectTabId(this.tabId);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
// Prevent anchor default drag behavior that interferes with SortableJS in some browsers
|
|
137
|
+
tabLinkEl.addEventListener("dragstart", (e) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
});
|
|
140
|
+
tabLinkEl.addEventListener("mousedown", (e) => {
|
|
141
|
+
// Only prevent default on middle/right click to avoid navigation
|
|
142
|
+
if (e.button !== 0) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
136
147
|
//tab name
|
|
137
148
|
this.nameEl = document.createElement("span");
|
|
138
149
|
this.nameEl.textContent = name;
|
|
@@ -315,7 +326,7 @@ export class TabList {
|
|
|
315
326
|
queryBrowserButton.className = "queryBrowserToggle";
|
|
316
327
|
queryBrowserButton.setAttribute("aria-label", "Open query browser");
|
|
317
328
|
queryBrowserButton.title = "Open query browser";
|
|
318
|
-
queryBrowserButton.innerHTML = '<i class="
|
|
329
|
+
queryBrowserButton.innerHTML = '<i class="fa-regular fa-file-lines"></i>';
|
|
319
330
|
queryBrowserButton.addEventListener("click", () => {
|
|
320
331
|
this.yasgui.queryBrowser.toggle(queryBrowserButton);
|
|
321
332
|
});
|
|
@@ -326,12 +337,15 @@ export class TabList {
|
|
|
326
337
|
sortablejs.create(this._tabsListEl, {
|
|
327
338
|
group: "tabList",
|
|
328
339
|
animation: 100,
|
|
340
|
+
draggable: ".tab",
|
|
341
|
+
forceFallback: true,
|
|
342
|
+
fallbackTolerance: 3,
|
|
329
343
|
onUpdate: (_ev: any) => {
|
|
330
344
|
const tabs = this.deriveTabOrderFromEls();
|
|
331
345
|
this.yasgui.emit("tabOrderChanged", this.yasgui, tabs);
|
|
332
346
|
this.yasgui.persistentConfig.setTabOrder(tabs);
|
|
333
347
|
},
|
|
334
|
-
filter: ".queryBrowserToggle, .addTab, input, .renaming",
|
|
348
|
+
filter: ".queryBrowserToggle, .addTab, input, .renaming, .closeTab",
|
|
335
349
|
preventOnFilter: false,
|
|
336
350
|
onMove: (ev: any, _origEv: any) => {
|
|
337
351
|
return hasClass(ev.related, "tab");
|
|
@@ -1251,3 +1251,76 @@
|
|
|
1251
1251
|
transform: translateY(1px);
|
|
1252
1252
|
}
|
|
1253
1253
|
}
|
|
1254
|
+
|
|
1255
|
+
// Hamburger menu styles
|
|
1256
|
+
.hamburgerContainer {
|
|
1257
|
+
position: relative;
|
|
1258
|
+
display: none; // Hidden by default (desktop)
|
|
1259
|
+
|
|
1260
|
+
@media (max-width: 768px) {
|
|
1261
|
+
display: flex; // Show on mobile
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.hamburgerMenuButton {
|
|
1266
|
+
&.active {
|
|
1267
|
+
color: var(--yasgui-accent-color, #337ab7);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.hamburgerDropdown {
|
|
1272
|
+
position: absolute;
|
|
1273
|
+
top: 100%;
|
|
1274
|
+
left: 0;
|
|
1275
|
+
background: var(--yasgui-bg-primary, white);
|
|
1276
|
+
border: 1px solid var(--yasgui-border-color, #e0e0e0);
|
|
1277
|
+
border-radius: 4px;
|
|
1278
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1279
|
+
min-width: 200px;
|
|
1280
|
+
z-index: 1000;
|
|
1281
|
+
margin-top: 5px;
|
|
1282
|
+
overflow: hidden;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.hamburgerMenuItem {
|
|
1286
|
+
display: flex;
|
|
1287
|
+
align-items: center;
|
|
1288
|
+
gap: 12px;
|
|
1289
|
+
width: 100%;
|
|
1290
|
+
padding: 12px 16px;
|
|
1291
|
+
background: none;
|
|
1292
|
+
border: none;
|
|
1293
|
+
border-bottom: 1px solid var(--yasgui-border-color, #f0f0f0);
|
|
1294
|
+
text-align: left;
|
|
1295
|
+
cursor: pointer;
|
|
1296
|
+
color: var(--yasgui-text-primary, #333);
|
|
1297
|
+
transition: background-color 0.2s;
|
|
1298
|
+
|
|
1299
|
+
&:last-child {
|
|
1300
|
+
border-bottom: none;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
&:hover {
|
|
1304
|
+
background: var(--yasgui-bg-secondary, #f7f7f7);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
i {
|
|
1308
|
+
font-size: 16px;
|
|
1309
|
+
width: 20px;
|
|
1310
|
+
text-align: center;
|
|
1311
|
+
color: var(--yasgui-button-text, #505050);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
span {
|
|
1315
|
+
flex: 1;
|
|
1316
|
+
font-size: 14px;
|
|
1317
|
+
font-weight: 500;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Responsive visibility classes
|
|
1322
|
+
.desktopOnly {
|
|
1323
|
+
@media (max-width: 768px) {
|
|
1324
|
+
display: none !important;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
package/src/TabSettingsModal.ts
CHANGED
|
@@ -39,6 +39,10 @@ export default class TabSettingsModal {
|
|
|
39
39
|
private prefixButton!: HTMLButtonElement;
|
|
40
40
|
private prefixTextarea!: HTMLTextAreaElement;
|
|
41
41
|
private autoCaptureCheckbox!: HTMLInputElement;
|
|
42
|
+
private hamburgerMenuButton!: HTMLButtonElement;
|
|
43
|
+
private hamburgerDropdown!: HTMLElement;
|
|
44
|
+
private isHamburgerOpen = false;
|
|
45
|
+
private mouseDownOnOverlay = false;
|
|
42
46
|
private handleKeyDown = (e: KeyboardEvent) => {
|
|
43
47
|
if (e.key !== "Escape") return;
|
|
44
48
|
if (!this.modalOverlay.classList.contains("open")) return;
|
|
@@ -54,7 +58,7 @@ export default class TabSettingsModal {
|
|
|
54
58
|
private init(controlBarEl: HTMLElement) {
|
|
55
59
|
// Settings button
|
|
56
60
|
this.settingsButton = document.createElement("button");
|
|
57
|
-
addClass(this.settingsButton, "tabContextButton");
|
|
61
|
+
addClass(this.settingsButton, "tabContextButton", "desktopOnly");
|
|
58
62
|
this.settingsButton.setAttribute("aria-label", "Settings");
|
|
59
63
|
this.settingsButton.title = "Settings";
|
|
60
64
|
this.settingsButton.innerHTML = '<i class="fas fa-cog"></i>';
|
|
@@ -64,7 +68,7 @@ export default class TabSettingsModal {
|
|
|
64
68
|
// Theme toggle button (if enabled)
|
|
65
69
|
if (this.tab.yasgui.config.showThemeToggle) {
|
|
66
70
|
this.themeToggleButton = document.createElement("button");
|
|
67
|
-
addClass(this.themeToggleButton, "tabContextButton", "themeToggle");
|
|
71
|
+
addClass(this.themeToggleButton, "tabContextButton", "themeToggle", "desktopOnly");
|
|
68
72
|
this.themeToggleButton.setAttribute("aria-label", "Toggle between light and dark theme");
|
|
69
73
|
this.themeToggleButton.title = "Toggle theme";
|
|
70
74
|
this.themeToggleButton.innerHTML = this.getThemeToggleIcon();
|
|
@@ -80,19 +84,143 @@ export default class TabSettingsModal {
|
|
|
80
84
|
this.prefixButton.setAttribute("aria-label", "Insert Prefixes");
|
|
81
85
|
this.prefixButton.title = "Insert saved prefixes into query";
|
|
82
86
|
this.prefixButton.innerHTML = '<i class="fas fa-arrow-up-right-from-square"></i>';
|
|
83
|
-
addClass(this.prefixButton, "tabContextButton", "prefixButton");
|
|
87
|
+
addClass(this.prefixButton, "tabContextButton", "prefixButton", "desktopOnly");
|
|
84
88
|
controlBarEl.appendChild(this.prefixButton);
|
|
85
89
|
this.prefixButton.onclick = () => this.insertPrefixesIntoQuery();
|
|
86
90
|
|
|
91
|
+
// Hamburger menu button and dropdown (mobile only)
|
|
92
|
+
const hamburgerContainer = document.createElement("div");
|
|
93
|
+
addClass(hamburgerContainer, "hamburgerContainer", "mobileOnly");
|
|
94
|
+
|
|
95
|
+
this.hamburgerMenuButton = document.createElement("button");
|
|
96
|
+
addClass(this.hamburgerMenuButton, "tabContextButton", "hamburgerMenuButton");
|
|
97
|
+
this.hamburgerMenuButton.setAttribute("aria-label", "Menu");
|
|
98
|
+
this.hamburgerMenuButton.title = "Open menu";
|
|
99
|
+
this.hamburgerMenuButton.innerHTML = '<i class="fas fa-bars"></i>';
|
|
100
|
+
this.hamburgerMenuButton.onclick = (e) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
this.toggleHamburgerMenu();
|
|
103
|
+
};
|
|
104
|
+
hamburgerContainer.appendChild(this.hamburgerMenuButton);
|
|
105
|
+
|
|
106
|
+
// Create hamburger dropdown menu
|
|
107
|
+
this.createHamburgerDropdown(hamburgerContainer);
|
|
108
|
+
|
|
109
|
+
controlBarEl.appendChild(hamburgerContainer);
|
|
110
|
+
|
|
111
|
+
// Close hamburger menu when clicking outside
|
|
112
|
+
document.addEventListener("click", (e) => {
|
|
113
|
+
if (
|
|
114
|
+
this.isHamburgerOpen &&
|
|
115
|
+
!this.hamburgerDropdown.contains(e.target as Node) &&
|
|
116
|
+
e.target !== this.hamburgerMenuButton
|
|
117
|
+
) {
|
|
118
|
+
this.closeHamburgerMenu();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
87
122
|
this.createModal();
|
|
88
123
|
}
|
|
89
124
|
|
|
125
|
+
private createHamburgerDropdown(hamburgerContainer: HTMLElement) {
|
|
126
|
+
this.hamburgerDropdown = document.createElement("div");
|
|
127
|
+
addClass(this.hamburgerDropdown, "hamburgerDropdown");
|
|
128
|
+
this.hamburgerDropdown.style.display = "none";
|
|
129
|
+
|
|
130
|
+
// Settings menu item
|
|
131
|
+
const settingsItem = document.createElement("button");
|
|
132
|
+
addClass(settingsItem, "hamburgerMenuItem");
|
|
133
|
+
settingsItem.innerHTML = '<i class="fas fa-cog"></i><span>Settings</span>';
|
|
134
|
+
settingsItem.onclick = () => {
|
|
135
|
+
this.open();
|
|
136
|
+
this.closeHamburgerMenu();
|
|
137
|
+
};
|
|
138
|
+
this.hamburgerDropdown.appendChild(settingsItem);
|
|
139
|
+
|
|
140
|
+
// Prefix menu item
|
|
141
|
+
const prefixItem = document.createElement("button");
|
|
142
|
+
addClass(prefixItem, "hamburgerMenuItem");
|
|
143
|
+
prefixItem.innerHTML = '<i class="fas fa-arrow-up-right-from-square"></i><span>Insert Prefixes</span>';
|
|
144
|
+
prefixItem.onclick = () => {
|
|
145
|
+
this.insertPrefixesIntoQuery();
|
|
146
|
+
this.closeHamburgerMenu();
|
|
147
|
+
};
|
|
148
|
+
this.hamburgerDropdown.appendChild(prefixItem);
|
|
149
|
+
|
|
150
|
+
// Theme toggle menu item (if enabled)
|
|
151
|
+
if (this.tab.yasgui.config.showThemeToggle) {
|
|
152
|
+
const themeItem = document.createElement("button");
|
|
153
|
+
addClass(themeItem, "hamburgerMenuItem", "themeMenuItem");
|
|
154
|
+
themeItem.innerHTML = this.getThemeToggleIcon() + "<span>Toggle Theme</span>";
|
|
155
|
+
themeItem.onclick = () => {
|
|
156
|
+
this.tab.yasgui.toggleTheme();
|
|
157
|
+
themeItem.innerHTML = this.getThemeToggleIcon() + "<span>Toggle Theme</span>";
|
|
158
|
+
// Keep menu open for theme toggle so user can see the change
|
|
159
|
+
};
|
|
160
|
+
this.hamburgerDropdown.appendChild(themeItem);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Layout orientation menu item
|
|
164
|
+
const layoutItem = document.createElement("button");
|
|
165
|
+
addClass(layoutItem, "hamburgerMenuItem", "layoutMenuItem");
|
|
166
|
+
this.updateLayoutMenuItemContent(layoutItem);
|
|
167
|
+
layoutItem.onclick = () => {
|
|
168
|
+
this.tab.toggleOrientation();
|
|
169
|
+
this.updateLayoutMenuItemContent(layoutItem);
|
|
170
|
+
// Keep menu open for layout toggle so user can see the change
|
|
171
|
+
};
|
|
172
|
+
this.hamburgerDropdown.appendChild(layoutItem);
|
|
173
|
+
|
|
174
|
+
hamburgerContainer.appendChild(this.hamburgerDropdown);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private updateLayoutMenuItemContent(layoutItem: HTMLButtonElement) {
|
|
178
|
+
const orientation = this.tab.getCurrentOrientation();
|
|
179
|
+
const icon =
|
|
180
|
+
orientation === "vertical" ? '<i class="fas fa-grip-lines-vertical"></i>' : '<i class="fas fa-grip-lines"></i>';
|
|
181
|
+
const label = orientation === "vertical" ? "Horizontal Layout" : "Vertical Layout";
|
|
182
|
+
layoutItem.innerHTML = icon + "<span>" + label + "</span>";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private toggleHamburgerMenu() {
|
|
186
|
+
if (this.isHamburgerOpen) {
|
|
187
|
+
this.closeHamburgerMenu();
|
|
188
|
+
} else {
|
|
189
|
+
this.openHamburgerMenu();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private openHamburgerMenu() {
|
|
194
|
+
this.isHamburgerOpen = true;
|
|
195
|
+
this.hamburgerDropdown.style.display = "block";
|
|
196
|
+
addClass(this.hamburgerMenuButton, "active");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private closeHamburgerMenu() {
|
|
200
|
+
this.isHamburgerOpen = false;
|
|
201
|
+
this.hamburgerDropdown.style.display = "none";
|
|
202
|
+
removeClass(this.hamburgerMenuButton, "active");
|
|
203
|
+
}
|
|
204
|
+
|
|
90
205
|
private createModal() {
|
|
91
206
|
// Modal overlay
|
|
92
207
|
this.modalOverlay = document.createElement("div");
|
|
93
208
|
addClass(this.modalOverlay, "tabSettingsModalOverlay");
|
|
94
|
-
|
|
95
|
-
//
|
|
209
|
+
|
|
210
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
211
|
+
this.modalOverlay.addEventListener("mousedown", (e) => {
|
|
212
|
+
if (e.target === this.modalOverlay) {
|
|
213
|
+
this.mouseDownOnOverlay = true;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
218
|
+
this.modalOverlay.addEventListener("mouseup", (e) => {
|
|
219
|
+
if (e.target === this.modalOverlay && this.mouseDownOnOverlay) {
|
|
220
|
+
this.close();
|
|
221
|
+
}
|
|
222
|
+
this.mouseDownOnOverlay = false;
|
|
223
|
+
});
|
|
96
224
|
|
|
97
225
|
// Modal content
|
|
98
226
|
this.modalContent = document.createElement("div");
|
|
@@ -912,10 +1040,27 @@ export default class TabSettingsModal {
|
|
|
912
1040
|
const config = this.tab.yasgui.persistentConfig.getEndpointConfig(endpoint);
|
|
913
1041
|
const existingAuth = config?.authentication;
|
|
914
1042
|
|
|
1043
|
+
// Track mousedown for proper modal close behavior
|
|
1044
|
+
let mouseDownOnOverlay = false;
|
|
1045
|
+
|
|
915
1046
|
// Create modal overlay
|
|
916
1047
|
const authModalOverlay = document.createElement("div");
|
|
917
1048
|
addClass(authModalOverlay, "authModalOverlay");
|
|
918
|
-
|
|
1049
|
+
|
|
1050
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
1051
|
+
authModalOverlay.addEventListener("mousedown", (e) => {
|
|
1052
|
+
if (e.target === authModalOverlay) {
|
|
1053
|
+
mouseDownOnOverlay = true;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
1058
|
+
authModalOverlay.addEventListener("mouseup", (e) => {
|
|
1059
|
+
if (e.target === authModalOverlay && mouseDownOnOverlay) {
|
|
1060
|
+
authModalOverlay.remove();
|
|
1061
|
+
}
|
|
1062
|
+
mouseDownOnOverlay = false;
|
|
1063
|
+
});
|
|
919
1064
|
|
|
920
1065
|
// Create modal content
|
|
921
1066
|
const authModal = document.createElement("div");
|
|
@@ -1516,6 +1661,7 @@ export default class TabSettingsModal {
|
|
|
1516
1661
|
}
|
|
1517
1662
|
|
|
1518
1663
|
public close() {
|
|
1664
|
+
this.mouseDownOnOverlay = false;
|
|
1519
1665
|
removeClass(this.modalOverlay, "open");
|
|
1520
1666
|
document.removeEventListener("keydown", this.handleKeyDown);
|
|
1521
1667
|
}
|
package/src/endpointSelect.scss
CHANGED
|
@@ -99,28 +99,6 @@
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
.clearEndpointBtn {
|
|
103
|
-
border: 1px solid #d1d1d1;
|
|
104
|
-
background-color: #d1d1d1;
|
|
105
|
-
color: #505050;
|
|
106
|
-
border-radius: 3px;
|
|
107
|
-
cursor: pointer;
|
|
108
|
-
|
|
109
|
-
padding: 4px 8px;
|
|
110
|
-
margin: 4px;
|
|
111
|
-
|
|
112
|
-
display: flex;
|
|
113
|
-
align-items: center;
|
|
114
|
-
justify-content: center;
|
|
115
|
-
|
|
116
|
-
opacity: 1;
|
|
117
|
-
transition: opacity ease-in 200ms;
|
|
118
|
-
|
|
119
|
-
&:hover {
|
|
120
|
-
opacity: 0.8;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
102
|
.endpointButtonsContainer {
|
|
125
103
|
display: flex;
|
|
126
104
|
align-items: center;
|
package/src/endpointSelect.ts
CHANGED
|
@@ -92,7 +92,7 @@ export class EndpointSelect extends EventEmitter {
|
|
|
92
92
|
// Create clear button
|
|
93
93
|
const clearBtn = document.createElement("button");
|
|
94
94
|
clearBtn.title = "Clear endpoint";
|
|
95
|
-
addClass(clearBtn, "
|
|
95
|
+
addClass(clearBtn, "tabContextButton");
|
|
96
96
|
clearBtn.innerText = "✖";
|
|
97
97
|
clearBtn.addEventListener("click", () => {
|
|
98
98
|
this.inputField.value = "";
|