@matdata/yasgui 4.6.1

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/src/Tab.ts ADDED
@@ -0,0 +1,775 @@
1
+ import { EventEmitter } from "events";
2
+ import { addClass, removeClass, getAsValue } from "@matdata/yasgui-utils";
3
+ import { TabListEl } from "./TabElements";
4
+ import TabSettingsModal from "./TabSettingsModal";
5
+ import { default as Yasqe, RequestConfig, PlainRequestConfig, PartialConfig as YasqeConfig } from "@matdata/yasqe";
6
+ import { default as Yasr, Parser, Config as YasrConfig, PersistentConfig as YasrPersistentConfig } from "@matdata/yasr";
7
+ import { mapValues, eq, mergeWith, words, deburr, invert } from "lodash-es";
8
+ import * as shareLink from "./linkUtils";
9
+ import EndpointSelect from "./endpointSelect";
10
+ require("./tab.scss");
11
+ import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
12
+ export interface PersistedJsonYasr extends YasrPersistentConfig {
13
+ responseSummary: Parser.ResponseSummary;
14
+ }
15
+ export interface PersistedJson {
16
+ name: string;
17
+ id: string;
18
+ yasqe: {
19
+ value: string;
20
+ editorHeight?: string;
21
+ };
22
+ yasr: {
23
+ settings: YasrPersistentConfig;
24
+ response: Parser.ResponseSummary | undefined;
25
+ };
26
+ requestConfig: YasguiRequestConfig;
27
+ }
28
+ export interface Tab {
29
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
30
+
31
+ on(event: "change", listener: (tab: Tab, config: PersistedJson) => void): this;
32
+ emit(event: "change", tab: Tab, config: PersistedJson): boolean;
33
+ on(event: "query", listener: (tab: Tab) => void): this;
34
+ emit(event: "query", tab: Tab): boolean;
35
+ on(event: "queryBefore", listener: (tab: Tab) => void): this;
36
+ emit(event: "queryBefore", tab: Tab): boolean;
37
+ on(event: "queryAbort", listener: (tab: Tab) => void): this;
38
+ emit(event: "queryAbort", tab: Tab): boolean;
39
+ on(event: "queryResponse", listener: (tab: Tab) => void): this;
40
+ emit(event: "queryResponse", tab: Tab): boolean;
41
+ on(event: "close", listener: (tab: Tab) => void): this;
42
+ emit(event: "close", tab: Tab): boolean;
43
+ on(event: "endpointChange", listener: (tab: Tab, endpoint: string) => void): this;
44
+ emit(event: "endpointChange", tab: Tab, endpoint: string): boolean;
45
+ on(event: "autocompletionShown", listener: (tab: Tab, widget: any) => void): this;
46
+ emit(event: "autocompletionShown", tab: Tab, widget: any): boolean;
47
+ on(event: "autocompletionClose", listener: (tab: Tab) => void): this;
48
+ emit(event: "autocompletionClose", tab: Tab): boolean;
49
+ }
50
+ export class Tab extends EventEmitter {
51
+ private persistentJson: PersistedJson;
52
+ public yasgui: Yasgui;
53
+ private yasqe: Yasqe | undefined;
54
+ private yasr: Yasr | undefined;
55
+ private rootEl: HTMLDivElement | undefined;
56
+ private controlBarEl: HTMLDivElement | undefined;
57
+ private yasqeWrapperEl: HTMLDivElement | undefined;
58
+ private yasrWrapperEl: HTMLDivElement | undefined;
59
+ private endpointSelect: EndpointSelect | undefined;
60
+ private settingsModal?: TabSettingsModal;
61
+ constructor(yasgui: Yasgui, conf: PersistedJson) {
62
+ super();
63
+ if (!conf || conf.id === undefined) throw new Error("Expected a valid configuration to initialize tab with");
64
+ this.yasgui = yasgui;
65
+ this.persistentJson = conf;
66
+ }
67
+ public name() {
68
+ return this.persistentJson.name;
69
+ }
70
+ public getPersistedJson() {
71
+ return this.persistentJson;
72
+ }
73
+ public getId() {
74
+ return this.persistentJson.id;
75
+ }
76
+ private draw() {
77
+ if (this.rootEl) return; //aready drawn
78
+ this.rootEl = document.createElement("div");
79
+ this.rootEl.className = "tabPanel";
80
+ this.rootEl.id = this.persistentJson.id;
81
+ this.rootEl.setAttribute("role", "tabpanel");
82
+ this.rootEl.setAttribute("aria-labelledby", "tab-" + this.persistentJson.id);
83
+
84
+ // We group controlbar and Yasqe, so that users can easily .appendChild() to the .editorwrapper div
85
+ // to add a div that goes alongside the controlbar and editor, while YASR still goes full width
86
+ // Useful for adding an infos div that goes alongside the editor without needing to rebuild the whole Yasgui class
87
+ const editorWrapper = document.createElement("div");
88
+ editorWrapper.className = "editorwrapper";
89
+ const controlbarAndYasqeDiv = document.createElement("div");
90
+ //controlbar
91
+ this.controlBarEl = document.createElement("div");
92
+ this.controlBarEl.className = "controlbar";
93
+ controlbarAndYasqeDiv.appendChild(this.controlBarEl);
94
+
95
+ //yasqe
96
+ this.yasqeWrapperEl = document.createElement("div");
97
+ controlbarAndYasqeDiv.appendChild(this.yasqeWrapperEl);
98
+ editorWrapper.appendChild(controlbarAndYasqeDiv);
99
+
100
+ //yasr
101
+ this.yasrWrapperEl = document.createElement("div");
102
+
103
+ this.initTabSettingsMenu();
104
+ this.rootEl.appendChild(editorWrapper);
105
+ this.rootEl.appendChild(this.yasrWrapperEl);
106
+ this.initControlbar();
107
+ this.initYasqe();
108
+ this.initYasr();
109
+ this.yasgui._setPanel(this.persistentJson.id, this.rootEl);
110
+ }
111
+ public hide() {
112
+ removeClass(this.rootEl, "active");
113
+ this.detachKeyboardListeners();
114
+ }
115
+ public show() {
116
+ this.draw();
117
+ addClass(this.rootEl, "active");
118
+ this.yasgui.tabElements.selectTab(this.persistentJson.id);
119
+ if (this.yasqe) {
120
+ this.yasqe.refresh();
121
+ if (this.yasgui.config.autofocus) this.yasqe.focus();
122
+ }
123
+ if (this.yasr) {
124
+ this.yasr.refresh();
125
+ }
126
+ //refresh, as other tabs might have changed the endpoint history
127
+ this.setEndpoint(this.getEndpoint(), this.yasgui.persistentConfig.getEndpointHistory());
128
+ this.attachKeyboardListeners();
129
+ }
130
+
131
+ private handleKeyDown = (event: KeyboardEvent) => {
132
+ // F11 - Toggle Yasqe fullscreen
133
+ if (event.key === "F11") {
134
+ event.preventDefault();
135
+ if (this.yasqe) {
136
+ this.yasqe.toggleFullscreen();
137
+ // If Yasr is fullscreen, exit it
138
+ if (this.yasr?.getIsFullscreen()) {
139
+ this.yasr.toggleFullscreen();
140
+ }
141
+ }
142
+ }
143
+ // F10 - Toggle Yasr fullscreen
144
+ else if (event.key === "F10") {
145
+ event.preventDefault();
146
+ if (this.yasr) {
147
+ this.yasr.toggleFullscreen();
148
+ // If Yasqe is fullscreen, exit it
149
+ if (this.yasqe?.getIsFullscreen()) {
150
+ this.yasqe.toggleFullscreen();
151
+ }
152
+ }
153
+ }
154
+ // Ctrl+Shift+F - Switch between fullscreen modes
155
+ else if (event.ctrlKey && event.shiftKey && event.key === "F") {
156
+ event.preventDefault();
157
+ const yasqeFullscreen = this.yasqe?.getIsFullscreen();
158
+ const yasrFullscreen = this.yasr?.getIsFullscreen();
159
+
160
+ if (yasqeFullscreen) {
161
+ // Switch from Yasqe to Yasr fullscreen
162
+ this.yasqe?.toggleFullscreen();
163
+ this.yasr?.toggleFullscreen();
164
+ } else if (yasrFullscreen) {
165
+ // Switch from Yasr to Yasqe fullscreen
166
+ this.yasr?.toggleFullscreen();
167
+ this.yasqe?.toggleFullscreen();
168
+ } else {
169
+ // If neither is fullscreen, make Yasqe fullscreen
170
+ this.yasqe?.toggleFullscreen();
171
+ }
172
+ }
173
+ };
174
+
175
+ private attachKeyboardListeners() {
176
+ if (!this.rootEl) return;
177
+ document.addEventListener("keydown", this.handleKeyDown);
178
+ }
179
+
180
+ private detachKeyboardListeners() {
181
+ document.removeEventListener("keydown", this.handleKeyDown);
182
+ }
183
+ public select() {
184
+ this.yasgui.selectTabId(this.persistentJson.id);
185
+ }
186
+ public close() {
187
+ this.detachKeyboardListeners();
188
+ if (this.yasqe) this.yasqe.abortQuery();
189
+ if (this.yasgui.getTab() === this) {
190
+ //it's the active tab
191
+ //first select other tab
192
+ const tabs = this.yasgui.persistentConfig.getTabs();
193
+ const i = tabs.indexOf(this.persistentJson.id);
194
+ if (i > -1) {
195
+ this.yasgui.selectTabId(tabs[i === tabs.length - 1 ? i - 1 : i + 1]);
196
+ }
197
+ }
198
+ this.yasgui._removePanel(this.rootEl);
199
+ this.yasgui.persistentConfig.deleteTab(this.persistentJson.id);
200
+ this.yasgui.emit("tabClose", this.yasgui, this);
201
+ this.emit("close", this);
202
+ this.yasgui.tabElements.get(this.persistentJson.id).delete();
203
+ delete this.yasgui._tabs[this.persistentJson.id];
204
+ }
205
+ public getQuery() {
206
+ if (!this.yasqe) {
207
+ throw new Error("Cannot get value from uninitialized editor");
208
+ }
209
+ return this.yasqe?.getValue();
210
+ }
211
+ public setQuery(query: string) {
212
+ if (!this.yasqe) {
213
+ throw new Error("Cannot set value for uninitialized editor");
214
+ }
215
+ this.yasqe.setValue(query);
216
+ this.persistentJson.yasqe.value = query;
217
+ this.emit("change", this, this.persistentJson);
218
+ return this;
219
+ }
220
+ public getRequestConfig() {
221
+ return this.persistentJson.requestConfig;
222
+ }
223
+ private initControlbar() {
224
+ this.initEndpointSelectField();
225
+ if (this.yasgui.config.endpointInfo && this.controlBarEl) {
226
+ this.controlBarEl.appendChild(this.yasgui.config.endpointInfo());
227
+ }
228
+ }
229
+ public getYasqe() {
230
+ return this.yasqe;
231
+ }
232
+ public getYasr() {
233
+ return this.yasr;
234
+ }
235
+ private initTabSettingsMenu() {
236
+ if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing tab settings");
237
+ this.settingsModal = new TabSettingsModal(this, this.controlBarEl);
238
+ }
239
+
240
+ private initEndpointSelectField() {
241
+ if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing endpoint field");
242
+ this.endpointSelect = new EndpointSelect(
243
+ this.getEndpoint(),
244
+ this.controlBarEl,
245
+ this.yasgui.config.endpointCatalogueOptions,
246
+ this.yasgui.persistentConfig.getEndpointHistory(),
247
+ );
248
+ this.endpointSelect.on("select", (endpoint, endpointHistory) => {
249
+ this.setEndpoint(endpoint, endpointHistory);
250
+ });
251
+ this.endpointSelect.on("remove", (endpoint, endpointHistory) => {
252
+ this.setEndpoint(endpoint, endpointHistory);
253
+ });
254
+ }
255
+
256
+ private checkEndpointForCors(endpoint: string) {
257
+ if (this.yasgui.config.corsProxy && !(endpoint in Yasgui.corsEnabled)) {
258
+ const askUrl = new URL(endpoint);
259
+ askUrl.searchParams.append("query", "ASK {?x ?y ?z}");
260
+ fetch(askUrl.toString())
261
+ .then(() => {
262
+ Yasgui.corsEnabled[endpoint] = true;
263
+ })
264
+ .catch((e) => {
265
+ // CORS error throws `TypeError: NetworkError when attempting to fetch resource.`
266
+ Yasgui.corsEnabled[endpoint] = e instanceof TypeError ? false : true;
267
+ });
268
+ }
269
+ }
270
+ public setEndpoint(endpoint: string, endpointHistory?: string[]) {
271
+ if (endpoint) endpoint = endpoint.trim();
272
+ if (endpointHistory && !eq(endpointHistory, this.yasgui.persistentConfig.getEndpointHistory())) {
273
+ this.yasgui.emit("endpointHistoryChange", this.yasgui, endpointHistory);
274
+ }
275
+ this.checkEndpointForCors(endpoint); //little cost in checking this as we're caching the check results
276
+
277
+ if (this.persistentJson.requestConfig.endpoint !== endpoint) {
278
+ this.persistentJson.requestConfig.endpoint = endpoint;
279
+ this.emit("change", this, this.persistentJson);
280
+ this.emit("endpointChange", this, endpoint);
281
+ }
282
+ if (this.endpointSelect instanceof EndpointSelect) {
283
+ this.endpointSelect.setEndpoint(endpoint, endpointHistory);
284
+ }
285
+ return this;
286
+ }
287
+ public getEndpoint(): string {
288
+ return getAsValue(this.persistentJson.requestConfig.endpoint, this.yasgui);
289
+ }
290
+ /**
291
+ * Updates the position of the Tab's contextmenu
292
+ * Useful for when being scrolled
293
+ */
294
+ public updateContextMenu(): void {
295
+ this.getTabListEl().redrawContextMenu();
296
+ }
297
+ public getShareableLink(baseURL?: string): string {
298
+ return shareLink.createShareLink(baseURL || window.location.href, this);
299
+ }
300
+ public getShareObject() {
301
+ return shareLink.createShareConfig(this);
302
+ }
303
+ private getTabListEl(): TabListEl {
304
+ return this.yasgui.tabElements.get(this.persistentJson.id);
305
+ }
306
+ public setName(newName: string) {
307
+ this.getTabListEl().rename(newName);
308
+ this.persistentJson.name = newName;
309
+ this.emit("change", this, this.persistentJson);
310
+ return this;
311
+ }
312
+ public hasResults() {
313
+ return !!this.yasr?.results;
314
+ }
315
+
316
+ public getName() {
317
+ return this.persistentJson.name;
318
+ }
319
+ public query(): Promise<any> {
320
+ if (!this.yasqe) return Promise.reject(new Error("No yasqe editor initialized"));
321
+ return this.yasqe.query();
322
+ }
323
+ public setRequestConfig(requestConfig: Partial<YasguiRequestConfig>) {
324
+ this.persistentJson.requestConfig = {
325
+ ...this.persistentJson.requestConfig,
326
+ ...requestConfig,
327
+ };
328
+
329
+ this.emit("change", this, this.persistentJson);
330
+ }
331
+
332
+ /**
333
+ * The Yasgui configuration object may contain a custom request config
334
+ * This request config object can contain getter functions, or plain json
335
+ * The plain json data is stored in persisted config, and editable via the
336
+ * tab pane.
337
+ * The getter functions are not. This function is about fetching this part of the
338
+ * request configuration, so we can merge this with the configuration from the
339
+ * persistent config and tab pane.
340
+ *
341
+ * Considering some values will never be persisted (things that should always be a function),
342
+ * we provide that as part of a whitelist called `keepDynamic`
343
+ */
344
+ private getStaticRequestConfig() {
345
+ const config: Partial<PlainRequestConfig> = {};
346
+ let key: keyof YasguiRequestConfig;
347
+ for (key in this.yasgui.config.requestConfig) {
348
+ //This config option should never be static or persisted anyway
349
+ if (key === "adjustQueryBeforeRequest") continue;
350
+ const val = this.yasgui.config.requestConfig[key];
351
+ if (typeof val === "function") {
352
+ (config[key] as any) = val(this.yasgui);
353
+ }
354
+ }
355
+ return config;
356
+ }
357
+
358
+ private initYasqe() {
359
+ const yasqeConf: Partial<YasqeConfig> = {
360
+ ...this.yasgui.config.yasqe,
361
+ value: this.persistentJson.yasqe.value,
362
+ editorHeight: this.persistentJson.yasqe.editorHeight ? this.persistentJson.yasqe.editorHeight : undefined,
363
+ persistenceId: null, //yasgui handles persistent storing
364
+ consumeShareLink: null, //not handled by this tab, but by parent yasgui instance
365
+ createShareableLink: () => this.getShareableLink(),
366
+ requestConfig: () => {
367
+ const processedReqConfig: YasguiRequestConfig = {
368
+ //setting defaults
369
+ //@ts-ignore
370
+ acceptHeaderGraph: "text/turtle",
371
+ //@ts-ignore
372
+ acceptHeaderSelect: "application/sparql-results+json",
373
+ ...mergeWith(
374
+ {},
375
+ this.persistentJson.requestConfig,
376
+ this.getStaticRequestConfig(),
377
+ function customizer(objValue, srcValue) {
378
+ if (Array.isArray(objValue) || Array.isArray(srcValue)) {
379
+ return [...(objValue || []), ...(srcValue || [])];
380
+ }
381
+ },
382
+ ),
383
+ //Passing this manually. Dont want to use our own persistentJson, as that's flattened exclude functions
384
+ //The adjustQueryBeforeRequest is meant to be a function though, so let's copy that as is
385
+ adjustQueryBeforeRequest: this.yasgui.config.requestConfig.adjustQueryBeforeRequest,
386
+ };
387
+ if (this.yasgui.config.corsProxy && !Yasgui.corsEnabled[this.getEndpoint()]) {
388
+ return {
389
+ ...processedReqConfig,
390
+ args: [
391
+ ...(Array.isArray(processedReqConfig.args) ? processedReqConfig.args : []),
392
+ { name: "endpoint", value: this.getEndpoint() },
393
+ { name: "method", value: this.persistentJson.requestConfig.method },
394
+ ],
395
+ method: "POST",
396
+ endpoint: this.yasgui.config.corsProxy,
397
+ } as PlainRequestConfig;
398
+ }
399
+ return processedReqConfig as PlainRequestConfig;
400
+ },
401
+ };
402
+ if (!yasqeConf.hintConfig) {
403
+ yasqeConf.hintConfig = {};
404
+ }
405
+ if (!yasqeConf.hintConfig.container) {
406
+ yasqeConf.hintConfig.container = this.yasgui.rootEl;
407
+ }
408
+ if (!this.yasqeWrapperEl) {
409
+ throw new Error("Expected a wrapper element before instantiating yasqe");
410
+ }
411
+ this.yasqe = new Yasqe(this.yasqeWrapperEl, yasqeConf);
412
+
413
+ this.yasqe.on("blur", this.handleYasqeBlur);
414
+ this.yasqe.on("query", this.handleYasqeQuery);
415
+ this.yasqe.on("queryBefore", this.handleYasqeQueryBefore);
416
+ this.yasqe.on("queryAbort", this.handleYasqeQueryAbort);
417
+ this.yasqe.on("resize", this.handleYasqeResize);
418
+
419
+ this.yasqe.on("autocompletionShown", this.handleAutocompletionShown);
420
+ this.yasqe.on("autocompletionClose", this.handleAutocompletionClose);
421
+
422
+ this.yasqe.on("queryResponse", this.handleQueryResponse);
423
+
424
+ // Add Ctrl+Click handler for URIs
425
+ this.attachYasqeMouseHandler();
426
+ }
427
+ private destroyYasqe() {
428
+ // As Yasqe extends of CM instead of eventEmitter, it doesn't expose the removeAllListeners function, so we should unregister all events manually
429
+ this.yasqe?.off("blur", this.handleYasqeBlur);
430
+ this.yasqe?.off("query", this.handleYasqeQuery);
431
+ this.yasqe?.off("queryAbort", this.handleYasqeQueryAbort);
432
+ this.yasqe?.off("resize", this.handleYasqeResize);
433
+ this.yasqe?.off("autocompletionShown", this.handleAutocompletionShown);
434
+ this.yasqe?.off("autocompletionClose", this.handleAutocompletionClose);
435
+ this.yasqe?.off("queryBefore", this.handleYasqeQueryBefore);
436
+ this.yasqe?.off("queryResponse", this.handleQueryResponse);
437
+ this.detachYasqeMouseHandler();
438
+ this.yasqe?.destroy();
439
+ this.yasqe = undefined;
440
+ }
441
+ handleYasqeBlur = (yasqe: Yasqe) => {
442
+ this.persistentJson.yasqe.value = yasqe.getValue();
443
+ // Capture prefixes from query if auto-capture is enabled
444
+ this.settingsModal?.capturePrefixesFromQuery();
445
+ this.emit("change", this, this.persistentJson);
446
+ };
447
+ handleYasqeQuery = (yasqe: Yasqe) => {
448
+ //the blur event might not have fired (e.g. when pressing ctrl-enter). So, we'd like to persist the query as well if needed
449
+ if (yasqe.getValue() !== this.persistentJson.yasqe.value) {
450
+ this.persistentJson.yasqe.value = yasqe.getValue();
451
+ this.emit("change", this, this.persistentJson);
452
+ }
453
+ this.emit("query", this);
454
+ };
455
+ handleYasqeQueryAbort = () => {
456
+ this.emit("queryAbort", this);
457
+ // Hide loading indicator in Yasr
458
+ if (this.yasr) {
459
+ this.yasr.hideLoading();
460
+ }
461
+ };
462
+ handleYasqeQueryBefore = () => {
463
+ this.emit("queryBefore", this);
464
+ // Show loading indicator in Yasr
465
+ if (this.yasr) {
466
+ this.yasr.showLoading();
467
+ }
468
+ };
469
+ handleYasqeResize = (_yasqe: Yasqe, newSize: string) => {
470
+ this.persistentJson.yasqe.editorHeight = newSize;
471
+ this.emit("change", this, this.persistentJson);
472
+ };
473
+ handleAutocompletionShown = (_yasqe: Yasqe, widget: string) => {
474
+ this.emit("autocompletionShown", this, widget);
475
+ };
476
+ handleAutocompletionClose = (_yasqe: Yasqe) => {
477
+ this.emit("autocompletionClose", this);
478
+ };
479
+ handleQueryResponse = (_yasqe: Yasqe, response: any, duration: number) => {
480
+ this.emit("queryResponse", this);
481
+ if (!this.yasr) throw new Error("Resultset visualizer not initialized. Cannot draw results");
482
+ this.yasr.setResponse(response, duration);
483
+ if (!this.yasr.results) return;
484
+ if (!this.yasr.results.hasError()) {
485
+ this.persistentJson.yasr.response = this.yasr.results.getAsStoreObject(
486
+ this.yasgui.config.yasr.maxPersistentResponseSize,
487
+ );
488
+ } else {
489
+ // Don't persist if there is an error and remove the previous result
490
+ this.persistentJson.yasr.response = undefined;
491
+ }
492
+ this.emit("change", this, this.persistentJson);
493
+ };
494
+
495
+ private handleYasqeMouseDown = (event: MouseEvent) => {
496
+ // Only handle Ctrl+Click
497
+ if (!event.ctrlKey || !this.yasqe) return;
498
+
499
+ const target = event.target as HTMLElement;
500
+ // Check if click is within CodeMirror editor
501
+ if (!target.closest(".CodeMirror")) return;
502
+
503
+ // Get position from mouse coordinates
504
+ const pos = this.yasqe.coordsChar({ left: event.clientX, top: event.clientY });
505
+ const token = this.yasqe.getTokenAt(pos);
506
+
507
+ // Check if token is a URI (not a variable)
508
+ // URIs typically have token.type of 'string-2' or might be in angle brackets
509
+ const tokenString = token.string.trim();
510
+
511
+ // Skip if it's a variable (starts with ? or $)
512
+ if (tokenString.startsWith("?") || tokenString.startsWith("$")) return;
513
+
514
+ // Check if it's a URI - either in angle brackets or a prefixed name
515
+ const isFullUri = tokenString.startsWith("<") && tokenString.endsWith(">");
516
+ const isPrefixedName = /^[\w-]+:[\w-]+/.test(tokenString);
517
+
518
+ if (!isFullUri && !isPrefixedName) return;
519
+
520
+ event.preventDefault();
521
+ event.stopPropagation();
522
+
523
+ // Extract the URI
524
+ let uri = tokenString;
525
+ if (isFullUri) {
526
+ // Remove angle brackets
527
+ uri = tokenString.slice(1, -1);
528
+ } else if (isPrefixedName) {
529
+ // Expand prefixed name to full URI
530
+ const prefixes = this.yasqe.getPrefixesFromQuery();
531
+ const [prefix, localName] = tokenString.split(":");
532
+ const prefixUri = prefixes[prefix];
533
+ if (prefixUri) {
534
+ uri = prefixUri + localName;
535
+ }
536
+ }
537
+
538
+ // Construct the query
539
+ const constructQuery = `CONSTRUCT {
540
+ ?s_left ?p_left ?target .
541
+ ?target ?p_right ?o_right .
542
+ }
543
+ WHERE {
544
+ BIND(<${uri}> as ?target)
545
+ {
546
+ ?s_left ?p_left ?target .
547
+ }
548
+ UNION
549
+ {
550
+ ?target ?p_right ?o_right .
551
+ }
552
+ } LIMIT 1000`;
553
+
554
+ // Execute query in background without changing editor content
555
+ this.executeBackgroundQuery(constructQuery);
556
+ };
557
+
558
+ private async executeBackgroundQuery(query: string) {
559
+ if (!this.yasqe || !this.yasr) return;
560
+
561
+ try {
562
+ // Show loading indicator
563
+ this.yasr.showLoading();
564
+ this.emit("queryBefore", this);
565
+
566
+ // Get the request configuration
567
+ const requestConfig = this.yasqe.config.requestConfig;
568
+ const config = typeof requestConfig === "function" ? requestConfig(this.yasqe) : requestConfig;
569
+
570
+ if (!config.endpoint) {
571
+ throw new Error("No endpoint configured");
572
+ }
573
+
574
+ const endpoint = typeof config.endpoint === "function" ? config.endpoint(this.yasqe) : config.endpoint;
575
+ const method = typeof config.method === "function" ? config.method(this.yasqe) : config.method || "POST";
576
+ const headers = typeof config.headers === "function" ? config.headers(this.yasqe) : config.headers || {};
577
+
578
+ // Prepare request
579
+ const searchParams = new URLSearchParams();
580
+ searchParams.append("query", query);
581
+
582
+ // Add any additional args
583
+ if (config.args && Array.isArray(config.args)) {
584
+ config.args.forEach((arg: any) => {
585
+ if (arg.name && arg.value) {
586
+ searchParams.append(arg.name, arg.value);
587
+ }
588
+ });
589
+ }
590
+
591
+ const fetchOptions: RequestInit = {
592
+ method: method,
593
+ headers: {
594
+ Accept: "text/turtle",
595
+ ...headers,
596
+ },
597
+ };
598
+
599
+ let url = endpoint;
600
+ if (method === "POST") {
601
+ fetchOptions.headers = {
602
+ ...fetchOptions.headers,
603
+ "Content-Type": "application/x-www-form-urlencoded",
604
+ };
605
+ fetchOptions.body = searchParams.toString();
606
+ } else {
607
+ const urlObj = new URL(endpoint);
608
+ searchParams.forEach((value, key) => {
609
+ urlObj.searchParams.append(key, value);
610
+ });
611
+ url = urlObj.toString();
612
+ }
613
+
614
+ const startTime = Date.now();
615
+ const response = await fetch(url, fetchOptions);
616
+ const duration = Date.now() - startTime;
617
+
618
+ if (!response.ok) {
619
+ throw new Error(`Query failed: ${response.statusText}`);
620
+ }
621
+
622
+ const result = await response.text();
623
+
624
+ // Set the response in Yasr
625
+ this.yasr.setResponse(result, duration);
626
+ this.yasr.hideLoading();
627
+ this.emit("queryResponse", this);
628
+ } catch (error) {
629
+ console.error("Background query failed:", error);
630
+ if (this.yasr) {
631
+ this.yasr.hideLoading();
632
+ // Set error response
633
+ this.yasr.setResponse(
634
+ {
635
+ error: error instanceof Error ? error.message : "Unknown error",
636
+ },
637
+ 0,
638
+ );
639
+ }
640
+ }
641
+ }
642
+
643
+ private attachYasqeMouseHandler() {
644
+ if (!this.yasqe) return;
645
+ const wrapper = this.yasqe.getWrapperElement();
646
+ if (wrapper) {
647
+ wrapper.addEventListener("mousedown", this.handleYasqeMouseDown);
648
+ }
649
+ }
650
+
651
+ private detachYasqeMouseHandler() {
652
+ if (!this.yasqe) return;
653
+ const wrapper = this.yasqe.getWrapperElement();
654
+ if (wrapper) {
655
+ wrapper.removeEventListener("mousedown", this.handleYasqeMouseDown);
656
+ }
657
+ }
658
+
659
+ private initYasr() {
660
+ if (!this.yasrWrapperEl) throw new Error("Wrapper for yasr does not exist");
661
+
662
+ const yasrConf: Partial<YasrConfig> = {
663
+ persistenceId: null, //yasgui handles persistent storing
664
+ prefixes: (yasr) => {
665
+ // Prefixes defined in YASR's config
666
+ const prefixesFromYasrConf =
667
+ typeof this.yasgui.config.yasr.prefixes === "function"
668
+ ? this.yasgui.config.yasr.prefixes(yasr)
669
+ : this.yasgui.config.yasr.prefixes;
670
+ const prefixesFromYasqe = this.yasqe?.getPrefixesFromQuery();
671
+ // Invert twice to make sure both keys and values are unique
672
+ // YASQE's prefixes should take president
673
+ return invert(invert({ ...prefixesFromYasrConf, ...prefixesFromYasqe }));
674
+ },
675
+ defaultPlugin: this.persistentJson.yasr.settings.selectedPlugin,
676
+ getPlainQueryLinkToEndpoint: () => {
677
+ if (this.yasqe) {
678
+ return shareLink.appendArgsToUrl(
679
+ this.getEndpoint(),
680
+ Yasqe.Sparql.getUrlArguments(this.yasqe, this.persistentJson.requestConfig as RequestConfig<any>),
681
+ );
682
+ }
683
+ },
684
+ plugins: mapValues(this.persistentJson.yasr.settings.pluginsConfig, (conf) => ({
685
+ dynamicConfig: conf,
686
+ })),
687
+ errorRenderers: [
688
+ // Use custom error renderer
689
+ getCorsErrorRenderer(this),
690
+ // Add default renderers to the end, to give our custom ones priority.
691
+ ...(Yasr.defaults.errorRenderers || []),
692
+ ],
693
+ };
694
+ // Allow getDownloadFilName to be overwritten by the global config
695
+ if (yasrConf.getDownloadFileName === undefined) {
696
+ yasrConf.getDownloadFileName = () => words(deburr(this.getName())).join("-");
697
+ }
698
+
699
+ this.yasr = new Yasr(this.yasrWrapperEl, yasrConf, this.persistentJson.yasr.response);
700
+
701
+ //populate our own persistent config
702
+ this.persistentJson.yasr.settings = this.yasr.getPersistentConfig();
703
+ this.yasr.on("change", () => {
704
+ if (this.yasr) {
705
+ this.persistentJson.yasr.settings = this.yasr.getPersistentConfig();
706
+ }
707
+
708
+ this.emit("change", this, this.persistentJson);
709
+ });
710
+ }
711
+ destroy() {
712
+ this.removeAllListeners();
713
+ this.settingsModal?.destroy();
714
+ this.endpointSelect?.destroy();
715
+ this.endpointSelect = undefined;
716
+ this.yasr?.destroy();
717
+ this.yasr = undefined;
718
+ this.destroyYasqe();
719
+ }
720
+ public static getDefaults(yasgui?: Yasgui): PersistedJson {
721
+ return {
722
+ yasqe: {
723
+ value: yasgui ? yasgui.config.yasqe.value : Yasgui.defaults.yasqe.value,
724
+ },
725
+ yasr: {
726
+ response: undefined,
727
+ settings: {
728
+ selectedPlugin: yasgui ? yasgui.config.yasr.defaultPlugin : "table",
729
+ pluginsConfig: {},
730
+ },
731
+ },
732
+ requestConfig: yasgui ? yasgui.config.requestConfig : { ...Yasgui.defaults.requestConfig },
733
+ id: getRandomId(),
734
+ name: yasgui ? yasgui.createTabName() : Yasgui.defaults.tabName,
735
+ };
736
+ }
737
+ }
738
+
739
+ export default Tab;
740
+
741
+ // Return a URL that is safe to display
742
+ const safeEndpoint = (endpoint: string): string => {
743
+ const url = new URL(endpoint);
744
+ return encodeURI(url.href);
745
+ };
746
+
747
+ function getCorsErrorRenderer(tab: Tab) {
748
+ return async (error: Parser.ErrorSummary): Promise<HTMLElement | undefined> => {
749
+ if (!error.status) {
750
+ // Only show this custom error if
751
+ const shouldReferToHttp =
752
+ new URL(tab.getEndpoint()).protocol === "http:" && window.location.protocol === "https:";
753
+ if (shouldReferToHttp) {
754
+ const errorEl = document.createElement("div");
755
+ const errorSpan = document.createElement("p");
756
+ errorSpan.innerHTML = `You are trying to query an HTTP endpoint (<a href="${safeEndpoint(
757
+ tab.getEndpoint(),
758
+ )}" target="_blank" rel="noopener noreferrer">${safeEndpoint(
759
+ tab.getEndpoint(),
760
+ )}</a>) from an HTTP<strong>S</strong> website (<a href="${safeEndpoint(window.location.href)}">${safeEndpoint(
761
+ window.location.href,
762
+ )}</a>).<br>This is not allowed in modern browsers, see <a target="_blank" rel="noopener noreferrer" href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy</a>.`;
763
+ if (tab.yasgui.config.nonSslDomain) {
764
+ const errorLink = document.createElement("p");
765
+ errorLink.innerHTML = `As a workaround, you can use the HTTP version of Yasgui instead: <a href="${tab.getShareableLink(
766
+ tab.yasgui.config.nonSslDomain,
767
+ )}" target="_blank">${tab.yasgui.config.nonSslDomain}</a>`;
768
+ errorSpan.appendChild(errorLink);
769
+ }
770
+ errorEl.appendChild(errorSpan);
771
+ return errorEl;
772
+ }
773
+ }
774
+ };
775
+ }