@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/CHANGELOG.md +175 -0
- package/build/ts/src/PersistentConfig.d.ts +49 -0
- package/build/ts/src/Tab.d.ts +105 -0
- package/build/ts/src/TabContextMenu.d.ts +29 -0
- package/build/ts/src/TabElements.d.ts +45 -0
- package/build/ts/src/TabSettingsModal.d.ts +28 -0
- package/build/ts/src/defaults.d.ts +3 -0
- package/build/ts/src/endpointSelect.d.ts +44 -0
- package/build/ts/src/index.d.ts +104 -0
- package/build/ts/src/linkUtils.d.ts +43 -0
- package/build/yasgui.html +24 -0
- package/build/yasgui.min.css +2 -0
- package/build/yasgui.min.css.map +1 -0
- package/build/yasgui.min.js +3 -0
- package/build/yasgui.min.js.LICENSE.txt +59 -0
- package/build/yasgui.min.js.map +1 -0
- package/package.json +48 -0
- package/src/PersistentConfig.ts +159 -0
- package/src/Tab.ts +775 -0
- package/src/TabContextMenu.scss +42 -0
- package/src/TabContextMenu.ts +143 -0
- package/src/TabElements.scss +134 -0
- package/src/TabElements.ts +336 -0
- package/src/TabSettingsModal.scss +226 -0
- package/src/TabSettingsModal.ts +424 -0
- package/src/defaults.ts +64 -0
- package/src/endpointSelect.scss +122 -0
- package/src/endpointSelect.ts +339 -0
- package/src/index.scss +97 -0
- package/src/index.ts +373 -0
- package/src/linkUtils.ts +234 -0
- package/src/tab.scss +61 -0
- package/static/yasgui.bootstrap.css +36 -0
- package/static/yasgui.polyfill.min.js +4 -0
- package/typings-custom/@tarekraafat/autocomplete.js/index.d.ts +48 -0
- package/typings-custom/main.d.ts +1 -0
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
|
+
}
|