@matdata/yasqe 5.3.0 → 5.5.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/__tests__/basicauth-test.d.ts +1 -0
- package/build/ts/src/__tests__/basicauth-test.js +52 -0
- package/build/ts/src/__tests__/basicauth-test.js.map +1 -0
- package/build/ts/src/defaults.d.ts +2 -0
- package/build/ts/src/defaults.js +29 -0
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/imgs.d.ts +2 -0
- package/build/ts/src/imgs.js +2 -0
- package/build/ts/src/imgs.js.map +1 -1
- package/build/ts/src/index.d.ts +21 -0
- package/build/ts/src/index.js +157 -2
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/sparql.js +47 -1
- package/build/ts/src/sparql.js.map +1 -1
- package/build/yasqe.min.css +1 -1
- package/build/yasqe.min.css.map +3 -3
- package/build/yasqe.min.js +83 -72
- package/build/yasqe.min.js.map +4 -4
- package/package.json +1 -1
- package/src/__tests__/basicauth-test.ts +66 -0
- package/src/autocompleters/show-hint.scss +1 -2
- package/src/defaults.ts +29 -0
- package/src/imgs.ts +4 -0
- package/src/index.ts +213 -2
- package/src/scss/buttons.scss +106 -0
- package/src/sparql.ts +61 -1
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic Authentication Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "mocha";
|
|
6
|
+
import { expect } from "chai";
|
|
7
|
+
|
|
8
|
+
describe("Basic Authentication", () => {
|
|
9
|
+
describe("Base64 Encoding", () => {
|
|
10
|
+
it("should encode credentials correctly", () => {
|
|
11
|
+
const username = "testuser";
|
|
12
|
+
const password = "testpass";
|
|
13
|
+
const credentials = `${username}:${password}`;
|
|
14
|
+
const encoded = btoa(credentials);
|
|
15
|
+
const expected = "dGVzdHVzZXI6dGVzdHBhc3M=";
|
|
16
|
+
|
|
17
|
+
expect(encoded).to.equal(expected);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should handle special characters", () => {
|
|
21
|
+
const username = "user@example.com";
|
|
22
|
+
const password = "p@ss:word!";
|
|
23
|
+
const credentials = `${username}:${password}`;
|
|
24
|
+
const encoded = btoa(credentials);
|
|
25
|
+
|
|
26
|
+
// Verify it can be decoded back
|
|
27
|
+
const decoded = atob(encoded);
|
|
28
|
+
expect(decoded).to.equal(credentials);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("Authorization Header Format", () => {
|
|
33
|
+
it("should create proper Basic auth header", () => {
|
|
34
|
+
const username = "admin";
|
|
35
|
+
const password = "secret";
|
|
36
|
+
const credentials = `${username}:${password}`;
|
|
37
|
+
const encoded = btoa(credentials);
|
|
38
|
+
const header = `Basic ${encoded}`;
|
|
39
|
+
|
|
40
|
+
expect(header).to.equal("Basic YWRtaW46c2VjcmV0");
|
|
41
|
+
expect(header).to.match(/^Basic [A-Za-z0-9+/=]+$/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("Empty Credentials", () => {
|
|
46
|
+
it("should handle empty username", () => {
|
|
47
|
+
const username = "";
|
|
48
|
+
const password = "password";
|
|
49
|
+
const credentials = `${username}:${password}`;
|
|
50
|
+
const encoded = btoa(credentials);
|
|
51
|
+
|
|
52
|
+
expect(encoded).to.be.a("string");
|
|
53
|
+
expect(encoded.length).to.be.greaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle empty password", () => {
|
|
57
|
+
const username = "user";
|
|
58
|
+
const password = "";
|
|
59
|
+
const credentials = `${username}:${password}`;
|
|
60
|
+
const encoded = btoa(credentials);
|
|
61
|
+
|
|
62
|
+
expect(encoded).to.be.a("string");
|
|
63
|
+
expect(encoded.length).to.be.greaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/defaults.ts
CHANGED
|
@@ -132,6 +132,34 @@ SELECT * WHERE {
|
|
|
132
132
|
prefixCcApi: prefixCcApi,
|
|
133
133
|
showFormatButton: true,
|
|
134
134
|
checkConstructVariables: true,
|
|
135
|
+
snippets: [
|
|
136
|
+
{
|
|
137
|
+
label: "SELECT",
|
|
138
|
+
code: "SELECT * WHERE {\n ?s ?p ?o .\n} LIMIT 10",
|
|
139
|
+
group: "Query Types",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: "CONSTRUCT",
|
|
143
|
+
code: "CONSTRUCT {\n ?s ?p ?o .\n} WHERE {\n ?s ?p ?o .\n} LIMIT 10",
|
|
144
|
+
group: "Query Types",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: "ASK",
|
|
148
|
+
code: "ASK {\n ?s ?p ?o .\n}",
|
|
149
|
+
group: "Query Types",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: "FILTER",
|
|
153
|
+
code: "FILTER (?var > 100)",
|
|
154
|
+
group: "Patterns",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: "OPTIONAL",
|
|
158
|
+
code: "OPTIONAL {\n ?s ?p ?o .\n}",
|
|
159
|
+
group: "Patterns",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
showSnippetsBar: true,
|
|
135
163
|
};
|
|
136
164
|
const requestConfig: PlainRequestConfig = {
|
|
137
165
|
queryArgument: undefined, //undefined means: get query argument based on query mode
|
|
@@ -146,6 +174,7 @@ SELECT * WHERE {
|
|
|
146
174
|
headers: {},
|
|
147
175
|
withCredentials: false,
|
|
148
176
|
adjustQueryBeforeRequest: false,
|
|
177
|
+
basicAuth: undefined,
|
|
149
178
|
};
|
|
150
179
|
return { ...config, requestConfig };
|
|
151
180
|
}
|
package/src/imgs.ts
CHANGED
|
@@ -14,3 +14,7 @@ export var fullscreenExit =
|
|
|
14
14
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>';
|
|
15
15
|
export var format =
|
|
16
16
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 3v18h18v-2H5V3H3zm14 4V5l-4 4 4 4V9h4V7h-4zM7 9v2h10V9H7zm0 4v2h10v-2H7zm0 4v2h10v-2H7z"/></svg>';
|
|
17
|
+
export var snippet =
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>';
|
|
19
|
+
export var chevronDown =
|
|
20
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>';
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { drawSvgStringAsElement, addClass, removeClass } from "@matdata/yasgui-u
|
|
|
11
11
|
import * as Sparql from "./sparql";
|
|
12
12
|
import * as imgs from "./imgs";
|
|
13
13
|
import * as Autocompleter from "./autocompleters";
|
|
14
|
-
import { merge, escape } from "lodash-es";
|
|
14
|
+
import { merge, mergeWith, escape } from "lodash-es";
|
|
15
15
|
|
|
16
16
|
import getDefaults from "./defaults";
|
|
17
17
|
import CodeMirror from "./CodeMirror";
|
|
@@ -56,6 +56,8 @@ export class Yasqe extends CodeMirror {
|
|
|
56
56
|
private fullscreenBtn: HTMLButtonElement | undefined;
|
|
57
57
|
private isFullscreen: boolean = false;
|
|
58
58
|
private resizeWrapper?: HTMLDivElement;
|
|
59
|
+
private snippetsBar?: HTMLDivElement;
|
|
60
|
+
private snippetsClickHandler?: (e: MouseEvent) => void;
|
|
59
61
|
public rootEl: HTMLDivElement;
|
|
60
62
|
public storage: YStorage;
|
|
61
63
|
public config: Config;
|
|
@@ -66,7 +68,13 @@ export class Yasqe extends CodeMirror {
|
|
|
66
68
|
this.rootEl = document.createElement("div");
|
|
67
69
|
this.rootEl.className = "yasqe";
|
|
68
70
|
parent.appendChild(this.rootEl);
|
|
69
|
-
|
|
71
|
+
// Use mergeWith to replace arrays instead of merging them by index
|
|
72
|
+
// This ensures that snippets: [] properly overrides default snippets
|
|
73
|
+
this.config = mergeWith({}, Yasqe.defaults, conf, (objValue: any, srcValue: any) => {
|
|
74
|
+
if (Array.isArray(srcValue)) {
|
|
75
|
+
return srcValue;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
70
78
|
//inherit codemirror props
|
|
71
79
|
const cm = (CodeMirror as any)(this.rootEl, this.config);
|
|
72
80
|
//Assign our functions to the cm object. This is needed, as some functions (like the ctrl-enter callback)
|
|
@@ -80,6 +88,7 @@ export class Yasqe extends CodeMirror {
|
|
|
80
88
|
//Do some post processing
|
|
81
89
|
this.storage = new YStorage(Yasqe.storageNamespace);
|
|
82
90
|
this.drawButtons();
|
|
91
|
+
this.drawSnippetsBar();
|
|
83
92
|
const storageId = this.getStorageId();
|
|
84
93
|
// this.getWrapperElement
|
|
85
94
|
if (storageId) {
|
|
@@ -417,6 +426,165 @@ export class Yasqe extends CodeMirror {
|
|
|
417
426
|
public getIsFullscreen() {
|
|
418
427
|
return this.isFullscreen;
|
|
419
428
|
}
|
|
429
|
+
|
|
430
|
+
private insertSnippet(code: string) {
|
|
431
|
+
const doc = this.getDoc();
|
|
432
|
+
const cursor = doc.getCursor();
|
|
433
|
+
doc.replaceRange(code, cursor);
|
|
434
|
+
// Move cursor to end of inserted code
|
|
435
|
+
const lines = code.split("\n");
|
|
436
|
+
const lastLine = lines[lines.length - 1];
|
|
437
|
+
doc.setCursor({
|
|
438
|
+
line: cursor.line + lines.length - 1,
|
|
439
|
+
ch: lines.length === 1 ? cursor.ch + lastLine.length : lastLine.length,
|
|
440
|
+
});
|
|
441
|
+
this.focus();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private drawSnippetsBar() {
|
|
445
|
+
// Check if snippets bar should be shown
|
|
446
|
+
const shouldShow =
|
|
447
|
+
this.config.showSnippetsBar &&
|
|
448
|
+
(this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
|
|
449
|
+
this.config.snippets.length > 0;
|
|
450
|
+
|
|
451
|
+
if (!shouldShow) {
|
|
452
|
+
// Remove existing bar if present
|
|
453
|
+
if (this.snippetsBar) {
|
|
454
|
+
this.snippetsBar.remove();
|
|
455
|
+
this.snippetsBar = undefined;
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Create snippets bar if it doesn't exist
|
|
461
|
+
if (!this.snippetsBar) {
|
|
462
|
+
this.snippetsBar = document.createElement("div");
|
|
463
|
+
addClass(this.snippetsBar, "yasqe_snippetsBar");
|
|
464
|
+
// Insert before the CodeMirror wrapper element
|
|
465
|
+
const cmWrapper = this.getWrapperElement();
|
|
466
|
+
cmWrapper.parentElement?.insertBefore(this.snippetsBar, cmWrapper);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Clear existing content
|
|
470
|
+
this.snippetsBar.innerHTML = "";
|
|
471
|
+
|
|
472
|
+
const snippets = this.config.snippets;
|
|
473
|
+
|
|
474
|
+
// If 10 or fewer snippets, show all as buttons
|
|
475
|
+
if (snippets.length <= 10) {
|
|
476
|
+
snippets.forEach((snippet) => {
|
|
477
|
+
const btn = document.createElement("button");
|
|
478
|
+
addClass(btn, "yasqe_snippetButton");
|
|
479
|
+
btn.textContent = snippet.label;
|
|
480
|
+
btn.title = snippet.code;
|
|
481
|
+
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
|
|
482
|
+
btn.onclick = () => this.insertSnippet(snippet.code);
|
|
483
|
+
this.snippetsBar!.appendChild(btn);
|
|
484
|
+
});
|
|
485
|
+
} else {
|
|
486
|
+
// Group snippets by group property
|
|
487
|
+
const grouped: { [group: string]: Snippet[] } = {};
|
|
488
|
+
const ungrouped: Snippet[] = [];
|
|
489
|
+
|
|
490
|
+
snippets.forEach((snippet) => {
|
|
491
|
+
if (snippet.group) {
|
|
492
|
+
if (!grouped[snippet.group]) grouped[snippet.group] = [];
|
|
493
|
+
grouped[snippet.group].push(snippet);
|
|
494
|
+
} else {
|
|
495
|
+
ungrouped.push(snippet);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Create dropdown for each group
|
|
500
|
+
Object.keys(grouped).forEach((groupName) => {
|
|
501
|
+
const dropdown = document.createElement("div");
|
|
502
|
+
addClass(dropdown, "yasqe_snippetDropdown");
|
|
503
|
+
|
|
504
|
+
const dropdownBtn = document.createElement("button");
|
|
505
|
+
addClass(dropdownBtn, "yasqe_snippetDropdownButton");
|
|
506
|
+
dropdownBtn.textContent = groupName + " ";
|
|
507
|
+
const chevron = drawSvgStringAsElement(imgs.chevronDown);
|
|
508
|
+
addClass(chevron, "chevronIcon");
|
|
509
|
+
dropdownBtn.appendChild(chevron);
|
|
510
|
+
dropdownBtn.setAttribute("aria-label", `${groupName} snippets`);
|
|
511
|
+
dropdownBtn.setAttribute("aria-expanded", "false");
|
|
512
|
+
|
|
513
|
+
const dropdownContent = document.createElement("div");
|
|
514
|
+
addClass(dropdownContent, "yasqe_snippetDropdownContent");
|
|
515
|
+
dropdownContent.setAttribute("role", "menu");
|
|
516
|
+
|
|
517
|
+
grouped[groupName].forEach((snippet) => {
|
|
518
|
+
const item = document.createElement("button");
|
|
519
|
+
addClass(item, "yasqe_snippetDropdownItem");
|
|
520
|
+
item.textContent = snippet.label;
|
|
521
|
+
item.title = snippet.code;
|
|
522
|
+
item.setAttribute("role", "menuitem");
|
|
523
|
+
item.onclick = () => {
|
|
524
|
+
this.insertSnippet(snippet.code);
|
|
525
|
+
dropdownContent.style.display = "none";
|
|
526
|
+
dropdownBtn.setAttribute("aria-expanded", "false");
|
|
527
|
+
};
|
|
528
|
+
dropdownContent.appendChild(item);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
dropdownBtn.onclick = (e) => {
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
const isOpen = dropdownContent.style.display === "block";
|
|
534
|
+
// Close all other dropdowns
|
|
535
|
+
const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
|
|
536
|
+
allDropdowns.forEach((dd) => {
|
|
537
|
+
(dd as HTMLElement).style.display = "none";
|
|
538
|
+
});
|
|
539
|
+
const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
|
|
540
|
+
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
|
|
541
|
+
|
|
542
|
+
// Toggle this dropdown
|
|
543
|
+
if (!isOpen) {
|
|
544
|
+
dropdownContent.style.display = "block";
|
|
545
|
+
dropdownBtn.setAttribute("aria-expanded", "true");
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
dropdown.appendChild(dropdownBtn);
|
|
550
|
+
dropdown.appendChild(dropdownContent);
|
|
551
|
+
this.snippetsBar!.appendChild(dropdown);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Add ungrouped snippets as individual buttons
|
|
555
|
+
ungrouped.forEach((snippet) => {
|
|
556
|
+
const btn = document.createElement("button");
|
|
557
|
+
addClass(btn, "yasqe_snippetButton");
|
|
558
|
+
btn.textContent = snippet.label;
|
|
559
|
+
btn.title = snippet.code;
|
|
560
|
+
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
|
|
561
|
+
btn.onclick = () => this.insertSnippet(snippet.code);
|
|
562
|
+
this.snippetsBar!.appendChild(btn);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Set up click handler for closing dropdowns when clicking outside
|
|
567
|
+
// Remove any existing handler first
|
|
568
|
+
if (this.snippetsClickHandler) {
|
|
569
|
+
document.removeEventListener("click", this.snippetsClickHandler);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Create and store the handler
|
|
573
|
+
this.snippetsClickHandler = (e: MouseEvent) => {
|
|
574
|
+
if (this.snippetsBar && !this.snippetsBar.contains(e.target as Node)) {
|
|
575
|
+
const allDropdowns = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownContent");
|
|
576
|
+
allDropdowns.forEach((dd) => {
|
|
577
|
+
(dd as HTMLElement).style.display = "none";
|
|
578
|
+
});
|
|
579
|
+
const allButtons = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownButton");
|
|
580
|
+
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Add the handler
|
|
585
|
+
document.addEventListener("click", this.snippetsClickHandler);
|
|
586
|
+
}
|
|
587
|
+
|
|
420
588
|
private drawResizer() {
|
|
421
589
|
if (this.resizeWrapper) return;
|
|
422
590
|
this.resizeWrapper = document.createElement("div");
|
|
@@ -954,6 +1122,31 @@ export class Yasqe extends CodeMirror {
|
|
|
954
1122
|
}
|
|
955
1123
|
}
|
|
956
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* Snippets management
|
|
1127
|
+
*/
|
|
1128
|
+
public setSnippetsBarVisible(visible: boolean) {
|
|
1129
|
+
if (!this.persistentConfig) {
|
|
1130
|
+
this.persistentConfig = {
|
|
1131
|
+
query: this.getValue(),
|
|
1132
|
+
editorHeight: this.config.editorHeight,
|
|
1133
|
+
showSnippetsBar: visible,
|
|
1134
|
+
};
|
|
1135
|
+
} else {
|
|
1136
|
+
this.persistentConfig.showSnippetsBar = visible;
|
|
1137
|
+
}
|
|
1138
|
+
this.saveQuery();
|
|
1139
|
+
this.drawSnippetsBar();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
public getSnippetsBarVisible(): boolean {
|
|
1143
|
+
return (
|
|
1144
|
+
this.config.showSnippetsBar &&
|
|
1145
|
+
(this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
|
|
1146
|
+
this.config.snippets.length > 0
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
957
1150
|
/**
|
|
958
1151
|
* Autocompleter management
|
|
959
1152
|
*/
|
|
@@ -1069,6 +1262,10 @@ export class Yasqe extends CodeMirror {
|
|
|
1069
1262
|
this.unregisterEventListeners();
|
|
1070
1263
|
this.resizeWrapper?.removeEventListener("mousedown", this.initDrag, false);
|
|
1071
1264
|
this.resizeWrapper?.removeEventListener("dblclick", this.expandEditor);
|
|
1265
|
+
if (this.snippetsClickHandler) {
|
|
1266
|
+
document.removeEventListener("click", this.snippetsClickHandler);
|
|
1267
|
+
this.snippetsClickHandler = undefined;
|
|
1268
|
+
}
|
|
1072
1269
|
for (const autocompleter in this.autocompleters) {
|
|
1073
1270
|
this.disableCompleter(autocompleter);
|
|
1074
1271
|
}
|
|
@@ -1164,6 +1361,10 @@ export interface HintConfig {
|
|
|
1164
1361
|
) => void;
|
|
1165
1362
|
};
|
|
1166
1363
|
}
|
|
1364
|
+
export interface BasicAuthConfig {
|
|
1365
|
+
username: string;
|
|
1366
|
+
password: string;
|
|
1367
|
+
}
|
|
1167
1368
|
export interface RequestConfig<Y> {
|
|
1168
1369
|
queryArgument: string | ((yasqe: Y) => string) | undefined;
|
|
1169
1370
|
endpoint: string | ((yasqe: Y) => string);
|
|
@@ -1177,6 +1378,7 @@ export interface RequestConfig<Y> {
|
|
|
1177
1378
|
headers: { [key: string]: string } | ((yasqe: Y) => { [key: string]: string });
|
|
1178
1379
|
withCredentials: boolean | ((yasqe: Y) => boolean);
|
|
1179
1380
|
adjustQueryBeforeRequest: ((yasqe: Y) => string) | false;
|
|
1381
|
+
basicAuth: BasicAuthConfig | ((yasqe: Y) => BasicAuthConfig | undefined) | undefined;
|
|
1180
1382
|
}
|
|
1181
1383
|
export type PlainRequestConfig = {
|
|
1182
1384
|
[K in keyof RequestConfig<any>]: Exclude<RequestConfig<any>[K], Function>;
|
|
@@ -1184,6 +1386,12 @@ export type PlainRequestConfig = {
|
|
|
1184
1386
|
export type PartialConfig = {
|
|
1185
1387
|
[P in keyof Config]?: Config[P] extends object ? Partial<Config[P]> : Config[P];
|
|
1186
1388
|
};
|
|
1389
|
+
|
|
1390
|
+
export interface Snippet {
|
|
1391
|
+
label: string;
|
|
1392
|
+
code: string;
|
|
1393
|
+
group?: string;
|
|
1394
|
+
}
|
|
1187
1395
|
export interface Config extends Partial<CodeMirror.EditorConfiguration> {
|
|
1188
1396
|
mode: string;
|
|
1189
1397
|
collapsePrefixesOnLoad: boolean;
|
|
@@ -1223,12 +1431,15 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
|
|
|
1223
1431
|
prefixCcApi: string; // the suggested default prefixes URL API getter
|
|
1224
1432
|
showFormatButton: boolean; // Show a button to format the query
|
|
1225
1433
|
checkConstructVariables: boolean; // Check for undefined variables in CONSTRUCT queries
|
|
1434
|
+
snippets: Snippet[]; // Code snippets to show in the snippets bar
|
|
1435
|
+
showSnippetsBar: boolean; // Show the snippets bar
|
|
1226
1436
|
}
|
|
1227
1437
|
export interface PersistentConfig {
|
|
1228
1438
|
query: string;
|
|
1229
1439
|
editorHeight: string;
|
|
1230
1440
|
formatterType?: "sparql-formatter" | "legacy"; // Which formatter to use
|
|
1231
1441
|
autoformatOnQuery?: boolean; // Auto-format query on execution
|
|
1442
|
+
showSnippetsBar?: boolean; // User preference for snippets bar visibility
|
|
1232
1443
|
}
|
|
1233
1444
|
// export var _Yasqe = _Yasqe;
|
|
1234
1445
|
|
package/src/scss/buttons.scss
CHANGED
|
@@ -292,4 +292,110 @@
|
|
|
292
292
|
height: calc(100vh - 150px) !important;
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
.yasqe_snippetsBar {
|
|
297
|
+
position: relative;
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-wrap: wrap;
|
|
300
|
+
gap: 5px;
|
|
301
|
+
padding: 5px 10px;
|
|
302
|
+
background-color: var(--yasgui-bg-primary, #fff);
|
|
303
|
+
border-bottom: 1px solid var(--yasgui-border-color, #e3e3e3);
|
|
304
|
+
order: -1; // Place before CodeMirror in flex layout
|
|
305
|
+
|
|
306
|
+
.yasqe_snippetButton {
|
|
307
|
+
padding: 4px 10px;
|
|
308
|
+
font-size: 12px;
|
|
309
|
+
border: 1px solid var(--yasgui-border-color, #ccc);
|
|
310
|
+
background-color: var(--yasgui-bg-primary, #fff);
|
|
311
|
+
border-radius: 3px;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
white-space: nowrap;
|
|
314
|
+
color: var(--yasgui-text-primary, #333);
|
|
315
|
+
|
|
316
|
+
&:hover {
|
|
317
|
+
background-color: var(--yasgui-bg-secondary, #f0f0f0);
|
|
318
|
+
border-color: var(--yasgui-text-muted, #999);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
&:active {
|
|
322
|
+
background-color: var(--yasgui-bg-tertiary, #e0e0e0);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.yasqe_snippetDropdown {
|
|
327
|
+
position: relative;
|
|
328
|
+
display: inline-block;
|
|
329
|
+
|
|
330
|
+
.yasqe_snippetDropdownButton {
|
|
331
|
+
padding: 4px 10px;
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
border: 1px solid var(--yasgui-border-color, #ccc);
|
|
334
|
+
background-color: var(--yasgui-bg-primary, #fff);
|
|
335
|
+
border-radius: 3px;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
color: var(--yasgui-text-primary, #333);
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 4px;
|
|
342
|
+
|
|
343
|
+
.chevronIcon {
|
|
344
|
+
width: 16px;
|
|
345
|
+
height: 16px;
|
|
346
|
+
svg {
|
|
347
|
+
width: 16px;
|
|
348
|
+
height: 16px;
|
|
349
|
+
fill: var(--yasgui-text-secondary, #505050);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
&:hover {
|
|
354
|
+
background-color: var(--yasgui-bg-secondary, #f0f0f0);
|
|
355
|
+
border-color: var(--yasgui-text-muted, #999);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
&[aria-expanded="true"] {
|
|
359
|
+
background-color: var(--yasgui-bg-tertiary, #e0e0e0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.yasqe_snippetDropdownContent {
|
|
364
|
+
display: none;
|
|
365
|
+
position: absolute;
|
|
366
|
+
top: 100%;
|
|
367
|
+
left: 0;
|
|
368
|
+
margin-top: 2px;
|
|
369
|
+
background-color: var(--yasgui-bg-primary, #fff);
|
|
370
|
+
border: 1px solid var(--yasgui-border-color, #ccc);
|
|
371
|
+
border-radius: 3px;
|
|
372
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
373
|
+
z-index: 100;
|
|
374
|
+
min-width: 150px;
|
|
375
|
+
max-height: 300px;
|
|
376
|
+
overflow-y: auto;
|
|
377
|
+
|
|
378
|
+
.yasqe_snippetDropdownItem {
|
|
379
|
+
display: block;
|
|
380
|
+
width: 100%;
|
|
381
|
+
padding: 6px 12px;
|
|
382
|
+
border: none;
|
|
383
|
+
background: none;
|
|
384
|
+
text-align: left;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
color: var(--yasgui-text-primary, #333);
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
|
|
390
|
+
&:hover {
|
|
391
|
+
background-color: var(--yasgui-bg-secondary, #f0f0f0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
&:active {
|
|
395
|
+
background-color: var(--yasgui-bg-tertiary, #e0e0e0);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
295
401
|
}
|
package/src/sparql.ts
CHANGED
|
@@ -16,6 +16,47 @@ function getRequestConfigSettings(yasqe: Yasqe, conf?: Partial<Config["requestCo
|
|
|
16
16
|
}
|
|
17
17
|
return (conf ?? {}) as RequestConfig<Yasqe>;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a Basic Authentication header value
|
|
22
|
+
*/
|
|
23
|
+
function createBasicAuthHeader(username: string, password: string): string {
|
|
24
|
+
const credentials = `${username}:${password}`;
|
|
25
|
+
const encoded = base64EncodeUnicode(credentials);
|
|
26
|
+
return `Basic ${encoded}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base64-encode a Unicode string using UTF-8 encoding.
|
|
31
|
+
* This avoids errors with btoa() and supports all Unicode characters.
|
|
32
|
+
*/
|
|
33
|
+
function base64EncodeUnicode(str: string): string {
|
|
34
|
+
if (typeof window !== "undefined" && typeof window.TextEncoder !== "undefined") {
|
|
35
|
+
const utf8Bytes = new window.TextEncoder().encode(str);
|
|
36
|
+
let binary = "";
|
|
37
|
+
for (let i = 0; i < utf8Bytes.length; i++) {
|
|
38
|
+
binary += String.fromCharCode(utf8Bytes[i]);
|
|
39
|
+
}
|
|
40
|
+
return btoa(binary);
|
|
41
|
+
} else if (typeof TextEncoder !== "undefined") {
|
|
42
|
+
// For environments where TextEncoder is global (e.g., Node.js)
|
|
43
|
+
const utf8Bytes = new TextEncoder().encode(str);
|
|
44
|
+
let binary = "";
|
|
45
|
+
for (let i = 0; i < utf8Bytes.length; i++) {
|
|
46
|
+
binary += String.fromCharCode(utf8Bytes[i]);
|
|
47
|
+
}
|
|
48
|
+
return btoa(binary);
|
|
49
|
+
} else {
|
|
50
|
+
// Fallback: try btoa directly, but warn about possible errors
|
|
51
|
+
try {
|
|
52
|
+
return btoa(str);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Basic authentication credentials contain unsupported Unicode characters. Please use a modern browser or restrict credentials to Latin1 characters.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
19
60
|
// type callback = AjaxConfig.callbacks['complete'];
|
|
20
61
|
export function getAjaxConfig(
|
|
21
62
|
yasqe: Yasqe,
|
|
@@ -38,11 +79,30 @@ export function getAjaxConfig(
|
|
|
38
79
|
const headers = isFunction(config.headers) ? config.headers(yasqe) : config.headers;
|
|
39
80
|
// console.log({headers})
|
|
40
81
|
const withCredentials = isFunction(config.withCredentials) ? config.withCredentials(yasqe) : config.withCredentials;
|
|
82
|
+
|
|
83
|
+
// Add Basic Authentication header if configured
|
|
84
|
+
const finalHeaders = { ...headers };
|
|
85
|
+
try {
|
|
86
|
+
const basicAuth = isFunction(config.basicAuth) ? config.basicAuth(yasqe) : config.basicAuth;
|
|
87
|
+
if (basicAuth && basicAuth.username && basicAuth.password) {
|
|
88
|
+
if (finalHeaders["Authorization"] !== undefined) {
|
|
89
|
+
console.warn(
|
|
90
|
+
"Authorization header already exists in request headers; skipping Basic Auth header to avoid overwrite.",
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
finalHeaders["Authorization"] = createBasicAuthHeader(basicAuth.username, basicAuth.password);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn("Failed to configure basic authentication:", error);
|
|
98
|
+
// Continue without authentication if there's an error
|
|
99
|
+
}
|
|
100
|
+
|
|
41
101
|
return {
|
|
42
102
|
reqMethod,
|
|
43
103
|
url: endpoint,
|
|
44
104
|
args: getUrlArguments(yasqe, config),
|
|
45
|
-
headers:
|
|
105
|
+
headers: finalHeaders,
|
|
46
106
|
accept: getAcceptHeader(yasqe, config),
|
|
47
107
|
withCredentials,
|
|
48
108
|
};
|