@matdata/yasqe 5.17.0 → 5.19.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasqe",
3
3
  "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.17.0",
4
+ "version": "5.19.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -26,12 +26,14 @@
26
26
  "dependencies": {
27
27
  "@matdata/yasgui-utils": "^4.6.1",
28
28
  "codemirror": "^5.51.0",
29
+ "leaflet": "^1.9.4",
29
30
  "lodash-es": "^4.17.15",
30
31
  "query-string": "^6.10.1",
31
32
  "sparql-formatter": "^1.0.2"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/codemirror": "0.0.100",
36
+ "@types/leaflet": "^1.9.21",
35
37
  "@types/lodash-es": "^4.17.3",
36
38
  "@types/node": "^22.5.4"
37
39
  },
package/src/defaults.ts CHANGED
@@ -135,6 +135,7 @@ SELECT * WHERE {
135
135
  queryingDisabled: undefined,
136
136
  prefixCcApi: prefixCcApi,
137
137
  showFormatButton: true,
138
+ showMapButton: true,
138
139
  checkConstructVariables: true,
139
140
  snippets: [
140
141
  {
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./scss/yasqe.scss";
2
2
  import "./scss/buttons.scss";
3
+ import "leaflet/dist/leaflet.css";
3
4
  import { findFirstPrefixLine } from "./prefixFold";
4
5
  import { getPrefixesFromQuery, addPrefixes, removePrefixes, Prefixes } from "./prefixUtils";
5
6
  import { getPreviousNonWsToken, getNextNonWsToken, getCompleteToken } from "./tokenUtils";
@@ -17,6 +18,8 @@ import getDefaults from "./defaults";
17
18
  import CodeMirror from "./CodeMirror";
18
19
  import { YasqeAjaxConfig } from "./sparql";
19
20
  import { spfmt } from "sparql-formatter";
21
+ import * as L from "leaflet";
22
+ import { coordinatesToWkt, wrapWktLiteral, WktCoordinate, WktGeometryType } from "./mapWidget";
20
23
 
21
24
  // Toast notification timing constants
22
25
  const TOAST_DEFAULT_DURATION = 3000; // 3 seconds
@@ -68,6 +71,9 @@ export class Yasqe extends CodeMirror {
68
71
  private hamburgerBtn: HTMLButtonElement | undefined;
69
72
  private hamburgerMenu: HTMLDivElement | undefined;
70
73
  private shareBtn: HTMLButtonElement | undefined;
74
+ private mapBtn: HTMLButtonElement | undefined;
75
+ private mapPopup: HTMLDivElement | undefined;
76
+ private closeMapPopupHandler: (() => void) | undefined;
71
77
  private isFullscreen: boolean = false;
72
78
  private horizontalResizeWrapper?: HTMLDivElement;
73
79
  private snippetsBar?: HTMLDivElement;
@@ -631,7 +637,26 @@ export class Yasqe extends CodeMirror {
631
637
  }
632
638
 
633
639
  /**
634
- * Draw fullscreen btn (FIFTH)
640
+ * Draw map btn (FIFTH)
641
+ */
642
+ if (this.config.showMapButton) {
643
+ this.mapBtn = document.createElement("button");
644
+ addClass(this.mapBtn, "yasqe_mapButton");
645
+ const mapIcon = document.createElement("i");
646
+ addClass(mapIcon, "fas");
647
+ addClass(mapIcon, "fa-map-location-dot");
648
+ mapIcon.setAttribute("aria-hidden", "true");
649
+ this.mapBtn.appendChild(mapIcon);
650
+ this.mapBtn.onclick = (event: MouseEvent) => {
651
+ this.toggleMapPopup(buttons, { x: event.clientX, y: event.clientY });
652
+ };
653
+ this.mapBtn.title = "Open map";
654
+ this.mapBtn.setAttribute("aria-label", "Open map");
655
+ buttons.appendChild(this.mapBtn);
656
+ }
657
+
658
+ /**
659
+ * Draw fullscreen btn (SIXTH)
635
660
  */
636
661
  this.fullscreenBtn = document.createElement("button");
637
662
  addClass(this.fullscreenBtn, "yasqe_fullscreenButton");
@@ -743,6 +768,24 @@ export class Yasqe extends CodeMirror {
743
768
  this.hamburgerMenu.appendChild(formatItem);
744
769
  }
745
770
 
771
+ if (this.config.showMapButton) {
772
+ const mapItem = document.createElement("button");
773
+ mapItem.className = "yasqe_hamburgerMenuItem";
774
+ const mapIconMenu = document.createElement("i");
775
+ addClass(mapIconMenu, "fas");
776
+ addClass(mapIconMenu, "fa-map-location-dot");
777
+ mapIconMenu.setAttribute("aria-hidden", "true");
778
+ mapItem.appendChild(mapIconMenu);
779
+ const mapLabel = document.createElement("span");
780
+ mapLabel.textContent = "Open map";
781
+ mapItem.appendChild(mapLabel);
782
+ mapItem.onclick = (event: MouseEvent) => {
783
+ this.closeHamburgerMenu();
784
+ this.toggleMapPopup(buttons, { x: event.clientX, y: event.clientY });
785
+ };
786
+ this.hamburgerMenu.appendChild(mapItem);
787
+ }
788
+
746
789
  const fullscreenItem = document.createElement("button");
747
790
  fullscreenItem.className = "yasqe_hamburgerMenuItem";
748
791
  const fullscreenIconMenu = document.createElement("i");
@@ -794,6 +837,269 @@ export class Yasqe extends CodeMirror {
794
837
  removeClass(this.hamburgerMenu, "active");
795
838
  this.hamburgerBtn.setAttribute("aria-expanded", "false");
796
839
  }
840
+ private toggleMapPopup(buttons: HTMLDivElement, anchorPoint?: { x: number; y: number }) {
841
+ if (this.mapPopup) {
842
+ this.closeMapPopupHandler?.();
843
+ return;
844
+ }
845
+ this.createMapPopup(buttons, anchorPoint);
846
+ }
847
+
848
+ private createMapPopup(buttons: HTMLDivElement, anchorPoint?: { x: number; y: number }) {
849
+ this.mapPopup = document.createElement("div");
850
+ this.mapPopup.className = "yasqe_mapPopup";
851
+ buttons.appendChild(this.mapPopup);
852
+
853
+ const header = document.createElement("div");
854
+ header.className = "yasqe_mapPopup_header";
855
+ const title = document.createElement("div");
856
+ title.className = "yasqe_mapPopup_title";
857
+ title.textContent = "Create WKT";
858
+ header.appendChild(title);
859
+
860
+ const closeBtn = document.createElement("button");
861
+ closeBtn.className = "yasqe_mapPopup_close";
862
+ closeBtn.setAttribute("aria-label", "Close map");
863
+ closeBtn.innerHTML = "×";
864
+ header.appendChild(closeBtn);
865
+ this.mapPopup.appendChild(header);
866
+
867
+ const mapContainer = document.createElement("div");
868
+ mapContainer.className = "yasqe_mapPopup_map";
869
+ this.mapPopup.appendChild(mapContainer);
870
+
871
+ const geometryControls = document.createElement("div");
872
+ geometryControls.className = "yasqe_mapPopup_geometryControls leaflet-bar";
873
+ mapContainer.appendChild(geometryControls);
874
+
875
+ const hint = document.createElement("div");
876
+ hint.className = "yasqe_mapPopup_hint";
877
+ hint.textContent = "Click the map to add coordinates";
878
+ this.mapPopup.appendChild(hint);
879
+
880
+ const preview = document.createElement("textarea");
881
+ preview.className = "yasqe_mapPopup_preview";
882
+ preview.readOnly = true;
883
+ preview.rows = 2;
884
+ this.mapPopup.appendChild(preview);
885
+
886
+ const actions = document.createElement("div");
887
+ actions.className = "yasqe_mapPopup_actions";
888
+ const undoBtn = document.createElement("button");
889
+ undoBtn.className = "yasqe_btn yasqe_btn-sm";
890
+ undoBtn.textContent = "Undo";
891
+ const clearBtn = document.createElement("button");
892
+ clearBtn.className = "yasqe_btn yasqe_btn-sm";
893
+ clearBtn.textContent = "Clear";
894
+ const insertBtn = document.createElement("button");
895
+ insertBtn.className = "yasqe_btn yasqe_btn-sm";
896
+ insertBtn.textContent = "Insert WKT";
897
+ actions.appendChild(undoBtn);
898
+ actions.appendChild(clearBtn);
899
+ actions.appendChild(insertBtn);
900
+ this.mapPopup.appendChild(actions);
901
+
902
+ let geometryType: WktGeometryType = "POINT";
903
+ let coordinates: WktCoordinate[] = [];
904
+ let marker: L.Marker | undefined;
905
+ let shape: L.Polyline | L.Polygon | undefined;
906
+ const geometryButtons: Partial<Record<WktGeometryType, HTMLButtonElement>> = {};
907
+
908
+ const getMapAccentColor = () => {
909
+ const accent = getComputedStyle(this.rootEl).getPropertyValue("--yasgui-accent-color").trim();
910
+ return accent || "#337ab7";
911
+ };
912
+
913
+ const map = L.map(mapContainer, {
914
+ zoomControl: true,
915
+ attributionControl: true,
916
+ }).setView([51.505, -0.09], 2);
917
+ map.getContainer().style.cursor = "crosshair";
918
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
919
+ attribution: "&copy; OpenStreetMap contributors",
920
+ maxZoom: 19,
921
+ }).addTo(map);
922
+ L.DomEvent.disableClickPropagation(geometryControls);
923
+ L.DomEvent.disableScrollPropagation(geometryControls);
924
+
925
+ const updatePreview = () => {
926
+ const wkt = coordinatesToWkt(geometryType, coordinates);
927
+ preview.value = wkt || "";
928
+ insertBtn.disabled = !wkt;
929
+ undoBtn.disabled = coordinates.length === 0;
930
+ clearBtn.disabled = coordinates.length === 0;
931
+ };
932
+
933
+ const redrawGeometry = () => {
934
+ if (marker) {
935
+ map.removeLayer(marker);
936
+ marker = undefined;
937
+ }
938
+ if (shape) {
939
+ map.removeLayer(shape);
940
+ shape = undefined;
941
+ }
942
+
943
+ const latLngs = coordinates.map((coord) => L.latLng(coord.lat, coord.lng));
944
+ const accentColor = getMapAccentColor();
945
+ if (geometryType === "POINT" && latLngs.length > 0) {
946
+ marker = L.marker(latLngs[latLngs.length - 1]).addTo(map);
947
+ } else if (geometryType === "LINESTRING" && latLngs.length > 0) {
948
+ shape = L.polyline(latLngs, { color: accentColor }).addTo(map);
949
+ } else if (geometryType === "POLYGON" && latLngs.length > 0) {
950
+ shape = L.polygon(latLngs, { color: accentColor }).addTo(map);
951
+ }
952
+
953
+ updatePreview();
954
+ };
955
+
956
+ const updateGeometryButtons = () => {
957
+ for (const type of ["POINT", "LINESTRING", "POLYGON"] as WktGeometryType[]) {
958
+ const button = geometryButtons[type];
959
+ if (!button) continue;
960
+ button.classList.toggle("active", type === geometryType);
961
+ button.setAttribute("aria-pressed", type === geometryType ? "true" : "false");
962
+ }
963
+ };
964
+
965
+ const createGeometryButton = (type: WktGeometryType, iconClass: string, label: string) => {
966
+ const button = document.createElement("button");
967
+ button.type = "button";
968
+ button.className = "yasqe_mapPopup_geometryBtn";
969
+ button.title = label;
970
+ button.setAttribute("aria-label", label);
971
+ button.setAttribute("aria-pressed", "false");
972
+ const icon = document.createElement("i");
973
+ addClass(icon, "fas");
974
+ addClass(icon, iconClass);
975
+ icon.setAttribute("aria-hidden", "true");
976
+ button.appendChild(icon);
977
+ button.onclick = (event: MouseEvent) => {
978
+ event.preventDefault();
979
+ event.stopPropagation();
980
+ if (geometryType === type) return;
981
+ geometryType = type;
982
+ coordinates = [];
983
+ updateGeometryButtons();
984
+ redrawGeometry();
985
+ };
986
+ geometryControls.appendChild(button);
987
+ return button;
988
+ };
989
+
990
+ geometryButtons.POINT = createGeometryButton("POINT", "fa-location-dot", "Point");
991
+ geometryButtons.LINESTRING = createGeometryButton("LINESTRING", "fa-slash", "LineString");
992
+ geometryButtons.POLYGON = createGeometryButton("POLYGON", "fa-draw-polygon", "Polygon");
993
+ updateGeometryButtons();
994
+
995
+ const closePopup = () => {
996
+ document.removeEventListener("keydown", handleKeyDown);
997
+ document.body.removeEventListener("click", closeOnOutsideClick, true);
998
+ map.remove();
999
+ this.mapPopup?.remove();
1000
+ this.mapPopup = undefined;
1001
+ this.closeMapPopupHandler = undefined;
1002
+ };
1003
+
1004
+ const handleKeyDown = (event: KeyboardEvent) => {
1005
+ if (event.key === "Escape") {
1006
+ closePopup();
1007
+ }
1008
+ };
1009
+
1010
+ const closeOnOutsideClick = (event: MouseEvent) => {
1011
+ if (!this.mapPopup) return;
1012
+ if (this.mapPopup.contains(event.target as Node) || this.mapBtn?.contains(event.target as Node)) return;
1013
+ closePopup();
1014
+ };
1015
+
1016
+ map.on("click", (event: L.LeafletMouseEvent) => {
1017
+ const coordinate: WktCoordinate = { lat: event.latlng.lat, lng: event.latlng.lng };
1018
+ if (geometryType === "POINT") {
1019
+ coordinates = [coordinate];
1020
+ } else {
1021
+ coordinates.push(coordinate);
1022
+ }
1023
+ redrawGeometry();
1024
+ });
1025
+
1026
+ undoBtn.onclick = () => {
1027
+ coordinates.pop();
1028
+ redrawGeometry();
1029
+ };
1030
+
1031
+ clearBtn.onclick = () => {
1032
+ coordinates = [];
1033
+ redrawGeometry();
1034
+ };
1035
+
1036
+ insertBtn.onclick = () => {
1037
+ const wkt = coordinatesToWkt(geometryType, coordinates);
1038
+ if (!wkt) return;
1039
+ this.replaceSelection(wrapWktLiteral(wkt));
1040
+ closePopup();
1041
+ };
1042
+
1043
+ closeBtn.onclick = () => closePopup();
1044
+ this.closeMapPopupHandler = closePopup;
1045
+
1046
+ document.addEventListener("keydown", handleKeyDown);
1047
+ document.body.addEventListener("click", closeOnOutsideClick, true);
1048
+
1049
+ const positionPopup = () => {
1050
+ if (!this.mapPopup) return;
1051
+ const popupWidth = this.mapPopup.offsetWidth || this.mapPopup.scrollWidth || 520;
1052
+ const popupHeight = this.mapPopup.offsetHeight || this.mapPopup.scrollHeight || this.mapPopup.clientHeight;
1053
+ const viewportWidth = window.innerWidth;
1054
+ const viewportHeight = window.innerHeight;
1055
+
1056
+ this.mapPopup.style.position = "fixed";
1057
+ this.mapPopup.style.bottom = "auto";
1058
+
1059
+ if (anchorPoint) {
1060
+ const padding = 20;
1061
+ let left = anchorPoint.x + 12;
1062
+ let top = anchorPoint.y + 12;
1063
+ if (left + popupWidth > viewportWidth - padding) {
1064
+ left = Math.max(padding, anchorPoint.x - popupWidth - 12);
1065
+ }
1066
+ if (top + popupHeight > viewportHeight - padding) {
1067
+ top = Math.max(padding, anchorPoint.y - popupHeight - 12);
1068
+ }
1069
+ this.mapPopup.style.left = `${Math.max(padding, left)}px`;
1070
+ this.mapPopup.style.top = `${Math.max(padding, top)}px`;
1071
+ this.mapPopup.style.right = "auto";
1072
+ return;
1073
+ }
1074
+
1075
+ const buttonsRect = buttons.getBoundingClientRect();
1076
+ const fallbackLeft = Math.min(viewportWidth - popupWidth - 20, Math.max(20, buttonsRect.right - popupWidth));
1077
+ this.mapPopup.style.left = `${Math.max(20, fallbackLeft)}px`;
1078
+ this.mapPopup.style.top = "20px";
1079
+ this.mapPopup.style.right = "auto";
1080
+ };
1081
+
1082
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
1083
+ window.requestAnimationFrame(() => {
1084
+ positionPopup();
1085
+ map.invalidateSize();
1086
+ });
1087
+ } else {
1088
+ setTimeout(() => {
1089
+ positionPopup();
1090
+ map.invalidateSize();
1091
+ }, 0);
1092
+ }
1093
+
1094
+ redrawGeometry();
1095
+ }
1096
+
1097
+ public toggleMapWidget(anchorPoint?: { x: number; y: number }) {
1098
+ const buttons = this.getWrapperElement().querySelector(".yasqe_buttons");
1099
+ if (!buttons || !(buttons instanceof HTMLDivElement)) return;
1100
+ this.toggleMapPopup(buttons, anchorPoint);
1101
+ }
1102
+
797
1103
  public toggleFullscreen() {
798
1104
  this.isFullscreen = !this.isFullscreen;
799
1105
  if (this.isFullscreen) {
@@ -1857,6 +2163,7 @@ export class Yasqe extends CodeMirror {
1857
2163
  public destroy() {
1858
2164
  // Abort running query
1859
2165
  this.abortQuery();
2166
+ this.closeMapPopupHandler?.();
1860
2167
  this.unregisterEventListeners();
1861
2168
  this.horizontalResizeWrapper?.removeEventListener("mousedown", this.initDrag, false);
1862
2169
  this.horizontalResizeWrapper?.removeEventListener("dblclick", this.expandEditor);
@@ -2043,6 +2350,7 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
2043
2350
  queryingDisabled: string | undefined; // The string will be the message displayed when hovered
2044
2351
  prefixCcApi: string; // the suggested default prefixes URL API getter
2045
2352
  showFormatButton: boolean; // Show a button to format the query
2353
+ showMapButton: boolean; // Show a button to create WKT literals from map input
2046
2354
  checkConstructVariables: boolean; // Check for undefined variables in CONSTRUCT queries
2047
2355
  snippets: Snippet[]; // Code snippets to show in the snippets bar
2048
2356
  showSnippetsBar: boolean; // Show the snippets bar
@@ -0,0 +1,53 @@
1
+ export type WktGeometryType = "POINT" | "LINESTRING" | "POLYGON";
2
+ const GEO_WKT_LITERAL_DATATYPE = "<http://www.opengis.net/ont/geosparql#wktLiteral>";
3
+
4
+ export interface WktCoordinate {
5
+ lat: number;
6
+ lng: number;
7
+ }
8
+
9
+ const MAX_DECIMALS = 6;
10
+
11
+ function normalizeCoordinate(value: number): string {
12
+ return Number(value.toFixed(MAX_DECIMALS)).toString();
13
+ }
14
+
15
+ function sameCoordinate(left: WktCoordinate, right: WktCoordinate): boolean {
16
+ return left.lat === right.lat && left.lng === right.lng;
17
+ }
18
+
19
+ function formatCoordinates(coordinates: WktCoordinate[]) {
20
+ return coordinates.map((coord) => `${normalizeCoordinate(coord.lng)} ${normalizeCoordinate(coord.lat)}`).join(", ");
21
+ }
22
+
23
+ export function coordinatesToWkt(geometryType: WktGeometryType, coordinates: WktCoordinate[]): string | undefined {
24
+ if (!Array.isArray(coordinates) || coordinates.length === 0) return undefined;
25
+
26
+ if (geometryType === "POINT") {
27
+ const point = coordinates[coordinates.length - 1];
28
+ return `POINT(${normalizeCoordinate(point.lng)} ${normalizeCoordinate(point.lat)})`;
29
+ }
30
+
31
+ if (geometryType === "LINESTRING") {
32
+ if (coordinates.length < 2) return undefined;
33
+ return `LINESTRING(${formatCoordinates(coordinates)})`;
34
+ }
35
+
36
+ if (geometryType !== "POLYGON") return undefined;
37
+ if (coordinates.length < 3) return undefined;
38
+ const uniqueCoordinates = new Set(
39
+ coordinates.map((coord) => `${normalizeCoordinate(coord.lng)} ${normalizeCoordinate(coord.lat)}`),
40
+ );
41
+ if (uniqueCoordinates.size < 3) return undefined;
42
+
43
+ const polygonCoordinates = coordinates.slice();
44
+ if (!sameCoordinate(polygonCoordinates[0], polygonCoordinates[polygonCoordinates.length - 1])) {
45
+ polygonCoordinates.push(polygonCoordinates[0]);
46
+ }
47
+
48
+ return `POLYGON((${formatCoordinates(polygonCoordinates)}))`;
49
+ }
50
+
51
+ export function wrapWktLiteral(wkt: string) {
52
+ return `"${wkt}"^^${GEO_WKT_LITERAL_DATATYPE}`;
53
+ }
@@ -292,6 +292,129 @@
292
292
  }
293
293
  }
294
294
  }
295
+ .yasqe_mapPopup {
296
+ position: absolute;
297
+ padding: 12px;
298
+ background-color: var(--yasgui-bg-primary, #fff);
299
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
300
+ border-radius: 4px;
301
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
302
+ box-sizing: border-box;
303
+ width: min(520px, calc(100vw - 40px));
304
+ max-height: calc(100vh - 40px);
305
+ overflow-y: auto;
306
+ overflow-x: hidden;
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 8px;
310
+ z-index: 10001;
311
+
312
+ .yasqe_mapPopup_header {
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: space-between;
316
+ }
317
+
318
+ .yasqe_mapPopup_title {
319
+ font-weight: 600;
320
+ font-size: 14px;
321
+ color: var(--yasgui-text-primary, #333);
322
+ }
323
+
324
+ .yasqe_mapPopup_close {
325
+ border: none;
326
+ background: none;
327
+ color: var(--yasgui-text-secondary, #505050);
328
+ font-size: 20px;
329
+ line-height: 1;
330
+ cursor: pointer;
331
+ margin-left: 0;
332
+ }
333
+
334
+ .yasqe_mapPopup_map {
335
+ position: relative;
336
+ height: 280px;
337
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
338
+ border-radius: 4px;
339
+
340
+ .leaflet-container,
341
+ .leaflet-container .leaflet-tile-pane,
342
+ .leaflet-container .leaflet-overlay-pane {
343
+ cursor: crosshair !important;
344
+ }
345
+
346
+ .leaflet-control-container,
347
+ .leaflet-control-container * {
348
+ cursor: pointer;
349
+ }
350
+ }
351
+
352
+ .yasqe_mapPopup_geometryControls {
353
+ position: absolute;
354
+ top: 10px;
355
+ right: 10px;
356
+ z-index: 500;
357
+ display: flex;
358
+ flex-direction: column;
359
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
360
+ border-radius: 4px;
361
+ overflow: hidden;
362
+ box-shadow: var(--yasgui-graph-shadow, 0 2px 4px rgba(0, 0, 0, 0.2));
363
+ }
364
+
365
+ .yasqe_mapPopup_geometryBtn {
366
+ appearance: none;
367
+ width: 30px;
368
+ height: 30px;
369
+ margin: 0;
370
+ border: none;
371
+ border-radius: 0;
372
+ border-bottom: 1px solid var(--yasgui-border-color, #e3e3e3);
373
+ background: var(--yasgui-bg-primary, #fff);
374
+ color: var(--yasgui-text-primary, #333);
375
+ cursor: pointer;
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: center;
379
+
380
+ i {
381
+ font-size: 14px;
382
+ }
383
+
384
+ &:hover {
385
+ background: var(--yasgui-bg-secondary, #f0f0f0);
386
+ }
387
+
388
+ &.active {
389
+ background: var(--yasgui-accent-color, #337ab7);
390
+ color: #fff;
391
+ }
392
+
393
+ &:last-child {
394
+ border-bottom: none;
395
+ }
396
+ }
397
+
398
+ .yasqe_mapPopup_hint {
399
+ color: var(--yasgui-text-secondary, #666);
400
+ font-size: 12px;
401
+ }
402
+
403
+ .yasqe_mapPopup_preview {
404
+ width: 100%;
405
+ box-sizing: border-box;
406
+ resize: vertical;
407
+ border: 1px solid var(--yasgui-border-color, #e3e3e3);
408
+ background-color: var(--yasgui-bg-secondary, #f8f8f8);
409
+ color: var(--yasgui-text-primary, #333);
410
+ }
411
+
412
+ .yasqe_mapPopup_actions {
413
+ display: flex;
414
+ justify-content: flex-end;
415
+ gap: 6px;
416
+ }
417
+ }
295
418
 
296
419
  .yasqe_saveWrapper {
297
420
  position: relative;
@@ -323,7 +446,8 @@
323
446
  }
324
447
  }
325
448
 
326
- .yasqe_formatButton {
449
+ .yasqe_formatButton,
450
+ .yasqe_mapButton {
327
451
  display: inline-flex;
328
452
  align-items: center;
329
453
  justify-content: center;
@@ -451,6 +575,7 @@
451
575
  .yasqe_share,
452
576
  .yasqe_saveWrapper,
453
577
  .yasqe_formatButton,
578
+ .yasqe_mapButton,
454
579
  .yasqe_fullscreenButton {
455
580
  display: none !important;
456
581
  }
package/src/sparql.ts CHANGED
@@ -251,8 +251,9 @@ export async function executeQuery(
251
251
 
252
252
  // Use custom query if provided, otherwise use the args from config
253
253
  if (options?.customQuery) {
254
- const queryArg = getQueryParameterName(populatedConfig.args);
255
- searchParams.append(queryArg, options.customQuery);
254
+ // A custom query is always a SPARQL read query (CONSTRUCT/DESCRIBE/SELECT/ASK),
255
+ // never an update — always use the "query" parameter regardless of editor mode.
256
+ searchParams.append("query", options.customQuery);
256
257
 
257
258
  // Add other args except the query/update parameter
258
259
  appendArgsToParams(populatedConfig.args, ["query", "update"]);