@matdata/yasqe 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 +121 -0
- package/build/ts/grammar/tokenizer.d.ts +37 -0
- package/build/ts/src/CodeMirror.d.ts +21 -0
- package/build/ts/src/autocompleters/classes.d.ts +3 -0
- package/build/ts/src/autocompleters/index.d.ts +39 -0
- package/build/ts/src/autocompleters/prefixes.d.ts +3 -0
- package/build/ts/src/autocompleters/properties.d.ts +3 -0
- package/build/ts/src/autocompleters/variables.d.ts +3 -0
- package/build/ts/src/defaults.d.ts +75 -0
- package/build/ts/src/imgs.d.ts +7 -0
- package/build/ts/src/index.d.ts +303 -0
- package/build/ts/src/prefixFold.d.ts +8 -0
- package/build/ts/src/prefixUtils.d.ts +9 -0
- package/build/ts/src/sparql.d.ts +20 -0
- package/build/ts/src/tokenUtils.d.ts +4 -0
- package/build/ts/src/tooltip.d.ts +2 -0
- package/build/ts/src/trie.d.ts +13 -0
- package/build/yasqe.html +108 -0
- package/build/yasqe.min.css +2 -0
- package/build/yasqe.min.css.map +1 -0
- package/build/yasqe.min.js +3 -0
- package/build/yasqe.min.js.LICENSE.txt +3 -0
- package/build/yasqe.min.js.map +1 -0
- package/grammar/README.md +12 -0
- package/grammar/_tokenizer-table.js +4776 -0
- package/grammar/build.sh +2 -0
- package/grammar/sparql11-grammar.pl +834 -0
- package/grammar/sparqljs-browser-min.js +4535 -0
- package/grammar/tokenizer.ts +729 -0
- package/grammar/util/gen_ll1.pl +37 -0
- package/grammar/util/gen_sparql11.pl +11 -0
- package/grammar/util/ll1.pl +175 -0
- package/grammar/util/output_to_javascript.pl +75 -0
- package/grammar/util/prune.pl +49 -0
- package/grammar/util/rewrite.pl +104 -0
- package/package.json +40 -0
- package/src/CodeMirror.ts +54 -0
- package/src/autocompleters/classes.ts +32 -0
- package/src/autocompleters/index.ts +346 -0
- package/src/autocompleters/prefixes.ts +130 -0
- package/src/autocompleters/properties.ts +28 -0
- package/src/autocompleters/show-hint.scss +38 -0
- package/src/autocompleters/variables.ts +52 -0
- package/src/defaults.ts +149 -0
- package/src/imgs.ts +14 -0
- package/src/index.ts +1089 -0
- package/src/prefixFold.ts +93 -0
- package/src/prefixUtils.ts +65 -0
- package/src/scss/buttons.scss +275 -0
- package/src/scss/codemirrorMods.scss +36 -0
- package/src/scss/yasqe.scss +89 -0
- package/src/sparql.ts +215 -0
- package/src/tokenUtils.ts +121 -0
- package/src/tooltip.ts +31 -0
- package/src/trie.ts +238 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
require("./scss/yasqe.scss");
|
|
2
|
+
require("./scss/buttons.scss");
|
|
3
|
+
import { findFirstPrefixLine } from "./prefixFold";
|
|
4
|
+
import { getPrefixesFromQuery, addPrefixes, removePrefixes, Prefixes } from "./prefixUtils";
|
|
5
|
+
import { getPreviousNonWsToken, getNextNonWsToken, getCompleteToken } from "./tokenUtils";
|
|
6
|
+
import * as sparql11Mode from "../grammar/tokenizer";
|
|
7
|
+
import { Storage as YStorage } from "@matdata/yasgui-utils";
|
|
8
|
+
import * as queryString from "query-string";
|
|
9
|
+
import tooltip from "./tooltip";
|
|
10
|
+
import { drawSvgStringAsElement, addClass, removeClass } from "@matdata/yasgui-utils";
|
|
11
|
+
import * as Sparql from "./sparql";
|
|
12
|
+
import * as imgs from "./imgs";
|
|
13
|
+
import * as Autocompleter from "./autocompleters";
|
|
14
|
+
import { merge, escape } from "lodash-es";
|
|
15
|
+
|
|
16
|
+
import getDefaults from "./defaults";
|
|
17
|
+
import CodeMirror from "./CodeMirror";
|
|
18
|
+
import { YasqeAjaxConfig } from "./sparql";
|
|
19
|
+
|
|
20
|
+
export interface Yasqe {
|
|
21
|
+
on(eventName: "query", handler: (instance: Yasqe, req: Request, abortController?: AbortController) => void): void;
|
|
22
|
+
off(eventName: "query", handler: (instance: Yasqe, req: Request, abortController?: AbortController) => void): void;
|
|
23
|
+
on(eventName: "queryAbort", handler: (instance: Yasqe, req: Request) => void): void;
|
|
24
|
+
off(eventName: "queryAbort", handler: (instance: Yasqe, req: Request) => void): void;
|
|
25
|
+
on(eventName: "queryResponse", handler: (instance: Yasqe, response: any, duration: number) => void): void;
|
|
26
|
+
off(eventName: "queryResponse", handler: (instance: Yasqe, response: any, duration: number) => void): void;
|
|
27
|
+
showHint: (conf: HintConfig) => void;
|
|
28
|
+
on(eventName: "error", handler: (instance: Yasqe) => void): void;
|
|
29
|
+
off(eventName: "error", handler: (instance: Yasqe) => void): void;
|
|
30
|
+
on(eventName: "blur", handler: (instance: Yasqe) => void): void;
|
|
31
|
+
off(eventName: "blur", handler: (instance: Yasqe) => void): void;
|
|
32
|
+
on(eventName: "queryBefore", handler: (instance: Yasqe, config: YasqeAjaxConfig) => void): void;
|
|
33
|
+
off(eventName: "queryBefore", handler: (instance: Yasqe, config: YasqeAjaxConfig) => void): void;
|
|
34
|
+
on(eventName: "queryResults", handler: (instance: Yasqe, results: any, duration: number) => void): void;
|
|
35
|
+
off(eventName: "queryResults", handler: (instance: Yasqe, results: any, duration: number) => void): void;
|
|
36
|
+
on(eventName: "autocompletionShown", handler: (instance: Yasqe, widget: any) => void): void;
|
|
37
|
+
off(eventName: "autocompletionShown", handler: (instance: Yasqe, widget: any) => void): void;
|
|
38
|
+
on(eventName: "autocompletionClose", handler: (instance: Yasqe) => void): void;
|
|
39
|
+
off(eventName: "autocompletionClose", handler: (instance: Yasqe) => void): void;
|
|
40
|
+
on(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
|
|
41
|
+
off(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
|
|
42
|
+
on(eventName: string, handler: () => void): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class Yasqe extends CodeMirror {
|
|
46
|
+
private static storageNamespace = "triply";
|
|
47
|
+
public autocompleters: { [name: string]: Autocompleter.Completer | undefined } = {};
|
|
48
|
+
private prevQueryValid = false;
|
|
49
|
+
public queryValid = true;
|
|
50
|
+
public lastQueryDuration: number | undefined;
|
|
51
|
+
private req: Request | undefined;
|
|
52
|
+
private abortController: AbortController | undefined;
|
|
53
|
+
private queryStatus: "valid" | "error" | undefined;
|
|
54
|
+
private queryBtn: HTMLButtonElement | undefined;
|
|
55
|
+
private fullscreenBtn: HTMLButtonElement | undefined;
|
|
56
|
+
private isFullscreen: boolean = false;
|
|
57
|
+
private resizeWrapper?: HTMLDivElement;
|
|
58
|
+
public rootEl: HTMLDivElement;
|
|
59
|
+
public storage: YStorage;
|
|
60
|
+
public config: Config;
|
|
61
|
+
public persistentConfig: PersistentConfig | undefined;
|
|
62
|
+
constructor(parent: HTMLElement, conf: PartialConfig = {}) {
|
|
63
|
+
super();
|
|
64
|
+
if (!parent) throw new Error("No parent passed as argument. Dont know where to draw YASQE");
|
|
65
|
+
this.rootEl = document.createElement("div");
|
|
66
|
+
this.rootEl.className = "yasqe";
|
|
67
|
+
parent.appendChild(this.rootEl);
|
|
68
|
+
this.config = merge({}, Yasqe.defaults, conf);
|
|
69
|
+
//inherit codemirror props
|
|
70
|
+
const cm = (CodeMirror as any)(this.rootEl, this.config);
|
|
71
|
+
//Assign our functions to the cm object. This is needed, as some functions (like the ctrl-enter callback)
|
|
72
|
+
//get the original cm as argument, and not yasqe
|
|
73
|
+
for (const key of Object.getOwnPropertyNames(Yasqe.prototype)) {
|
|
74
|
+
cm[key] = (<any>Yasqe.prototype)[key].bind(this);
|
|
75
|
+
}
|
|
76
|
+
//Also assign the codemirror functions to our object, so we can easily use those
|
|
77
|
+
Object.assign(this, CodeMirror.prototype, cm);
|
|
78
|
+
|
|
79
|
+
//Do some post processing
|
|
80
|
+
this.storage = new YStorage(Yasqe.storageNamespace);
|
|
81
|
+
this.drawButtons();
|
|
82
|
+
const storageId = this.getStorageId();
|
|
83
|
+
// this.getWrapperElement
|
|
84
|
+
if (storageId) {
|
|
85
|
+
const persConf = this.storage.get<any>(storageId);
|
|
86
|
+
if (persConf && typeof persConf === "string") {
|
|
87
|
+
this.persistentConfig = { query: persConf, editorHeight: this.config.editorHeight }; // Migrate to object based localstorage
|
|
88
|
+
} else {
|
|
89
|
+
this.persistentConfig = persConf;
|
|
90
|
+
}
|
|
91
|
+
if (!this.persistentConfig)
|
|
92
|
+
this.persistentConfig = { query: this.getValue(), editorHeight: this.config.editorHeight };
|
|
93
|
+
if (this.persistentConfig && this.persistentConfig.query) this.setValue(this.persistentConfig.query);
|
|
94
|
+
}
|
|
95
|
+
this.config.autocompleters.forEach((c) => this.enableCompleter(c).then(() => {}, console.warn));
|
|
96
|
+
if (this.config.consumeShareLink) {
|
|
97
|
+
this.config.consumeShareLink(this);
|
|
98
|
+
//and: add a hash listener!
|
|
99
|
+
window.addEventListener("hashchange", this.handleHashChange);
|
|
100
|
+
}
|
|
101
|
+
this.checkSyntax();
|
|
102
|
+
// Size codemirror to the
|
|
103
|
+
if (this.persistentConfig && this.persistentConfig.editorHeight) {
|
|
104
|
+
this.getWrapperElement().style.height = this.persistentConfig.editorHeight;
|
|
105
|
+
} else if (this.config.editorHeight) {
|
|
106
|
+
this.getWrapperElement().style.height = this.config.editorHeight;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.config.resizeable) this.drawResizer();
|
|
110
|
+
if (this.config.collapsePrefixesOnLoad) this.collapsePrefixes(true);
|
|
111
|
+
this.registerEventListeners();
|
|
112
|
+
}
|
|
113
|
+
private handleHashChange = () => {
|
|
114
|
+
this.config.consumeShareLink?.(this);
|
|
115
|
+
};
|
|
116
|
+
private handleChange() {
|
|
117
|
+
this.checkSyntax();
|
|
118
|
+
this.updateQueryButton();
|
|
119
|
+
}
|
|
120
|
+
private handleBlur() {
|
|
121
|
+
this.saveQuery();
|
|
122
|
+
}
|
|
123
|
+
private handleChanges() {
|
|
124
|
+
// e.g. handle blur
|
|
125
|
+
this.checkSyntax();
|
|
126
|
+
this.updateQueryButton();
|
|
127
|
+
}
|
|
128
|
+
private handleCursorActivity() {
|
|
129
|
+
this.autocomplete(true);
|
|
130
|
+
}
|
|
131
|
+
private handleQuery(_yasqe: Yasqe, req: Request, abortController?: AbortController) {
|
|
132
|
+
this.req = req;
|
|
133
|
+
this.abortController = abortController;
|
|
134
|
+
this.updateQueryButton();
|
|
135
|
+
}
|
|
136
|
+
private handleQueryResponse(_yasqe: Yasqe, _response: any, duration: number) {
|
|
137
|
+
this.lastQueryDuration = duration;
|
|
138
|
+
this.req = undefined;
|
|
139
|
+
this.updateQueryButton();
|
|
140
|
+
}
|
|
141
|
+
private handleQueryAbort(_yasqe: Yasqe, _req: Request) {
|
|
142
|
+
this.req = undefined;
|
|
143
|
+
this.updateQueryButton();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private registerEventListeners() {
|
|
147
|
+
/**
|
|
148
|
+
* Register listeners
|
|
149
|
+
*/
|
|
150
|
+
this.on("change", this.handleChange);
|
|
151
|
+
this.on("blur", this.handleBlur);
|
|
152
|
+
this.on("changes", this.handleChanges);
|
|
153
|
+
this.on("cursorActivity", this.handleCursorActivity);
|
|
154
|
+
|
|
155
|
+
this.on("query", this.handleQuery);
|
|
156
|
+
this.on("queryResponse", this.handleQueryResponse);
|
|
157
|
+
this.on("queryAbort", this.handleQueryAbort);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private unregisterEventListeners() {
|
|
161
|
+
this.off("change" as any, this.handleChange);
|
|
162
|
+
this.off("blur", this.handleBlur);
|
|
163
|
+
this.off("changes" as any, this.handleChanges);
|
|
164
|
+
this.off("cursorActivity" as any, this.handleCursorActivity);
|
|
165
|
+
|
|
166
|
+
this.off("query", this.handleQuery);
|
|
167
|
+
this.off("queryResponse", this.handleQueryResponse);
|
|
168
|
+
this.off("queryAbort", this.handleQueryAbort);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generic IDE functions
|
|
172
|
+
*/
|
|
173
|
+
public emit(event: string, ...data: any[]) {
|
|
174
|
+
CodeMirror.signal(this, event, this, ...data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public getStorageId(getter?: Config["persistenceId"]): string | undefined {
|
|
178
|
+
const persistenceId = getter || this.config.persistenceId;
|
|
179
|
+
if (!persistenceId) return undefined;
|
|
180
|
+
if (typeof persistenceId === "string") return persistenceId;
|
|
181
|
+
return persistenceId(this);
|
|
182
|
+
}
|
|
183
|
+
private drawButtons() {
|
|
184
|
+
const buttons = document.createElement("div");
|
|
185
|
+
buttons.className = "yasqe_buttons";
|
|
186
|
+
this.getWrapperElement().appendChild(buttons);
|
|
187
|
+
|
|
188
|
+
if (this.config.pluginButtons) {
|
|
189
|
+
const pluginButtons = this.config.pluginButtons();
|
|
190
|
+
if (!pluginButtons) return;
|
|
191
|
+
if (Array.isArray(pluginButtons)) {
|
|
192
|
+
for (const button of pluginButtons) {
|
|
193
|
+
buttons.append(button);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
buttons.appendChild(pluginButtons);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* draw share link button
|
|
202
|
+
*/
|
|
203
|
+
if (this.config.createShareableLink) {
|
|
204
|
+
var svgShare = drawSvgStringAsElement(imgs.share);
|
|
205
|
+
const shareLinkWrapper = document.createElement("button");
|
|
206
|
+
shareLinkWrapper.className = "yasqe_share";
|
|
207
|
+
shareLinkWrapper.title = "Share query";
|
|
208
|
+
shareLinkWrapper.setAttribute("aria-label", "Share query");
|
|
209
|
+
shareLinkWrapper.appendChild(svgShare);
|
|
210
|
+
buttons.appendChild(shareLinkWrapper);
|
|
211
|
+
shareLinkWrapper.addEventListener("click", (event: MouseEvent) => showSharePopup(event));
|
|
212
|
+
shareLinkWrapper.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
213
|
+
if (event.code === "Enter") {
|
|
214
|
+
showSharePopup(event);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const showSharePopup = (event: MouseEvent | KeyboardEvent) => {
|
|
219
|
+
event.stopPropagation();
|
|
220
|
+
let popup: HTMLDivElement | undefined = document.createElement("div");
|
|
221
|
+
popup.className = "yasqe_sharePopup";
|
|
222
|
+
buttons.appendChild(popup);
|
|
223
|
+
document.body.addEventListener(
|
|
224
|
+
"click",
|
|
225
|
+
(event) => {
|
|
226
|
+
if (popup && event.target !== popup && !popup.contains(<any>event.target)) {
|
|
227
|
+
popup.remove();
|
|
228
|
+
popup = undefined;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
true,
|
|
232
|
+
);
|
|
233
|
+
var input = document.createElement("input");
|
|
234
|
+
input.type = "text";
|
|
235
|
+
input.value = this.config.createShareableLink(this);
|
|
236
|
+
|
|
237
|
+
input.onfocus = function () {
|
|
238
|
+
input.select();
|
|
239
|
+
};
|
|
240
|
+
// Work around Chrome's little problem
|
|
241
|
+
input.onmouseup = function () {
|
|
242
|
+
// $this.unbind("mouseup");
|
|
243
|
+
return false;
|
|
244
|
+
};
|
|
245
|
+
popup.innerHTML = "";
|
|
246
|
+
|
|
247
|
+
var inputWrapper = document.createElement("div");
|
|
248
|
+
inputWrapper.className = "inputWrapper";
|
|
249
|
+
|
|
250
|
+
inputWrapper.appendChild(input);
|
|
251
|
+
|
|
252
|
+
popup.appendChild(inputWrapper);
|
|
253
|
+
|
|
254
|
+
// We need to track which buttons are drawn here since the two implementations don't play nice together
|
|
255
|
+
const popupInputButtons: HTMLButtonElement[] = [];
|
|
256
|
+
const createShortLink = this.config.createShortLink;
|
|
257
|
+
if (createShortLink) {
|
|
258
|
+
popup.className = popup.className += " enableShort";
|
|
259
|
+
const shortBtn = document.createElement("button");
|
|
260
|
+
popupInputButtons.push(shortBtn);
|
|
261
|
+
shortBtn.innerHTML = "Shorten";
|
|
262
|
+
shortBtn.className = "yasqe_btn yasqe_btn-sm shorten";
|
|
263
|
+
popup.appendChild(shortBtn);
|
|
264
|
+
shortBtn.onclick = () => {
|
|
265
|
+
popupInputButtons.forEach((button) => (button.disabled = true));
|
|
266
|
+
createShortLink(this, input.value).then(
|
|
267
|
+
(value) => {
|
|
268
|
+
input.value = value;
|
|
269
|
+
input.focus();
|
|
270
|
+
},
|
|
271
|
+
(err) => {
|
|
272
|
+
const errSpan = document.createElement("span");
|
|
273
|
+
errSpan.className = "shortlinkErr";
|
|
274
|
+
// Throwing a string or an object should work
|
|
275
|
+
let textContent = "An error has occurred";
|
|
276
|
+
if (typeof err === "string" && err.length !== 0) {
|
|
277
|
+
textContent = err;
|
|
278
|
+
} else if (err.message && err.message.length !== 0) {
|
|
279
|
+
textContent = err.message;
|
|
280
|
+
}
|
|
281
|
+
errSpan.textContent = textContent;
|
|
282
|
+
input.replaceWith(errSpan);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const curlBtn = document.createElement("button");
|
|
289
|
+
popupInputButtons.push(curlBtn);
|
|
290
|
+
curlBtn.innerText = "cURL";
|
|
291
|
+
curlBtn.className = "yasqe_btn yasqe_btn-sm curl";
|
|
292
|
+
popup.appendChild(curlBtn);
|
|
293
|
+
curlBtn.onclick = () => {
|
|
294
|
+
popupInputButtons.forEach((button) => (button.disabled = true));
|
|
295
|
+
input.value = this.getAsCurlString();
|
|
296
|
+
input.focus();
|
|
297
|
+
popup?.appendChild(curlBtn);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const svgPos = svgShare.getBoundingClientRect();
|
|
301
|
+
popup.style.top = svgShare.offsetTop + svgPos.height + "px";
|
|
302
|
+
popup.style.left = svgShare.offsetLeft + svgShare.clientWidth - popup.clientWidth + "px";
|
|
303
|
+
input.focus();
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Draw query btn
|
|
308
|
+
*/
|
|
309
|
+
if (this.config.showQueryButton) {
|
|
310
|
+
this.queryBtn = document.createElement("button");
|
|
311
|
+
addClass(this.queryBtn, "yasqe_queryButton");
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Add busy/valid/error btns
|
|
315
|
+
*/
|
|
316
|
+
const queryEl = drawSvgStringAsElement(imgs.query);
|
|
317
|
+
addClass(queryEl, "queryIcon");
|
|
318
|
+
this.queryBtn.appendChild(queryEl);
|
|
319
|
+
|
|
320
|
+
const warningIcon = drawSvgStringAsElement(imgs.warning);
|
|
321
|
+
addClass(warningIcon, "warningIcon");
|
|
322
|
+
this.queryBtn.appendChild(warningIcon);
|
|
323
|
+
|
|
324
|
+
this.queryBtn.onclick = () => {
|
|
325
|
+
if (this.config.queryingDisabled) return; // Don't do anything
|
|
326
|
+
if (this.req) {
|
|
327
|
+
this.abortQuery();
|
|
328
|
+
} else {
|
|
329
|
+
this.query().catch(() => {}); //catch this to avoid unhandled rejection
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
this.queryBtn.title = "Run query";
|
|
333
|
+
this.queryBtn.setAttribute("aria-label", "Run query");
|
|
334
|
+
|
|
335
|
+
buttons.appendChild(this.queryBtn);
|
|
336
|
+
this.updateQueryButton();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Draw fullscreen btn
|
|
341
|
+
*/
|
|
342
|
+
this.fullscreenBtn = document.createElement("button");
|
|
343
|
+
addClass(this.fullscreenBtn, "yasqe_fullscreenButton");
|
|
344
|
+
const fullscreenIcon = drawSvgStringAsElement(imgs.fullscreen);
|
|
345
|
+
addClass(fullscreenIcon, "fullscreenIcon");
|
|
346
|
+
this.fullscreenBtn.appendChild(fullscreenIcon);
|
|
347
|
+
const fullscreenExitIcon = drawSvgStringAsElement(imgs.fullscreenExit);
|
|
348
|
+
addClass(fullscreenExitIcon, "fullscreenExitIcon");
|
|
349
|
+
this.fullscreenBtn.appendChild(fullscreenExitIcon);
|
|
350
|
+
this.fullscreenBtn.onclick = () => {
|
|
351
|
+
this.toggleFullscreen();
|
|
352
|
+
};
|
|
353
|
+
this.fullscreenBtn.title = "Toggle fullscreen (F11)";
|
|
354
|
+
this.fullscreenBtn.setAttribute("aria-label", "Toggle fullscreen");
|
|
355
|
+
buttons.appendChild(this.fullscreenBtn);
|
|
356
|
+
}
|
|
357
|
+
public toggleFullscreen() {
|
|
358
|
+
this.isFullscreen = !this.isFullscreen;
|
|
359
|
+
if (this.isFullscreen) {
|
|
360
|
+
addClass(this.rootEl, "fullscreen");
|
|
361
|
+
this.fullscreenBtn?.setAttribute("title", "Exit fullscreen (F11)");
|
|
362
|
+
} else {
|
|
363
|
+
removeClass(this.rootEl, "fullscreen");
|
|
364
|
+
this.fullscreenBtn?.setAttribute("title", "Toggle fullscreen (F11)");
|
|
365
|
+
}
|
|
366
|
+
this.refresh();
|
|
367
|
+
}
|
|
368
|
+
public getIsFullscreen() {
|
|
369
|
+
return this.isFullscreen;
|
|
370
|
+
}
|
|
371
|
+
private drawResizer() {
|
|
372
|
+
if (this.resizeWrapper) return;
|
|
373
|
+
this.resizeWrapper = document.createElement("div");
|
|
374
|
+
addClass(this.resizeWrapper, "resizeWrapper");
|
|
375
|
+
const chip = document.createElement("div");
|
|
376
|
+
addClass(chip, "resizeChip");
|
|
377
|
+
this.resizeWrapper.appendChild(chip);
|
|
378
|
+
this.resizeWrapper.addEventListener("mousedown", this.initDrag, false);
|
|
379
|
+
this.resizeWrapper.addEventListener("dblclick", this.expandEditor);
|
|
380
|
+
this.rootEl.appendChild(this.resizeWrapper);
|
|
381
|
+
}
|
|
382
|
+
private initDrag() {
|
|
383
|
+
document.documentElement.addEventListener("mousemove", this.doDrag, false);
|
|
384
|
+
document.documentElement.addEventListener("mouseup", this.stopDrag, false);
|
|
385
|
+
}
|
|
386
|
+
private calculateDragOffset(event: MouseEvent, rootEl: HTMLElement) {
|
|
387
|
+
let parentOffset = 0;
|
|
388
|
+
// offsetParent is, at the time of writing, a working draft. see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
389
|
+
if (rootEl.offsetParent) parentOffset = (rootEl.offsetParent as HTMLElement).offsetTop;
|
|
390
|
+
let scrollOffset = 0;
|
|
391
|
+
let parentElement = rootEl.parentElement;
|
|
392
|
+
while (parentElement) {
|
|
393
|
+
scrollOffset += parentElement.scrollTop;
|
|
394
|
+
parentElement = parentElement.parentElement;
|
|
395
|
+
}
|
|
396
|
+
return event.clientY - parentOffset - this.rootEl.offsetTop + scrollOffset;
|
|
397
|
+
}
|
|
398
|
+
private doDrag(event: MouseEvent) {
|
|
399
|
+
this.getWrapperElement().style.height = this.calculateDragOffset(event, this.rootEl) + "px";
|
|
400
|
+
}
|
|
401
|
+
private stopDrag() {
|
|
402
|
+
document.documentElement.removeEventListener("mousemove", this.doDrag, false);
|
|
403
|
+
document.documentElement.removeEventListener("mouseup", this.stopDrag, false);
|
|
404
|
+
this.emit("resize", this.getWrapperElement().style.height);
|
|
405
|
+
if (this.getStorageId() && this.persistentConfig) {
|
|
406
|
+
// If there is no storage id there is no persistency wanted
|
|
407
|
+
this.persistentConfig.editorHeight = this.getWrapperElement().style.height;
|
|
408
|
+
this.saveQuery();
|
|
409
|
+
}
|
|
410
|
+
// Refresh the editor to make sure the 'hidden' lines are rendered
|
|
411
|
+
this.refresh();
|
|
412
|
+
}
|
|
413
|
+
public duplicateLine() {
|
|
414
|
+
const cur = this.getDoc().getCursor();
|
|
415
|
+
if (cur) {
|
|
416
|
+
const line = this.getDoc().getLine(cur.line);
|
|
417
|
+
this.getDoc().replaceRange(line + "\n" + line, { ch: 0, line: cur.line }, { ch: line.length, line: cur.line });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
private updateQueryButton(status?: "valid" | "error") {
|
|
421
|
+
if (!this.queryBtn) return;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Set query status (valid vs invalid)
|
|
425
|
+
*/
|
|
426
|
+
if (this.config.queryingDisabled) {
|
|
427
|
+
addClass(this.queryBtn, "query_disabled");
|
|
428
|
+
this.queryBtn.title = this.config.queryingDisabled;
|
|
429
|
+
} else {
|
|
430
|
+
removeClass(this.queryBtn, "query_disabled");
|
|
431
|
+
this.queryBtn.title = "Run query";
|
|
432
|
+
this.queryBtn.setAttribute("aria-label", "Run query");
|
|
433
|
+
}
|
|
434
|
+
if (!status) {
|
|
435
|
+
status = this.queryValid ? "valid" : "error";
|
|
436
|
+
}
|
|
437
|
+
if (status != this.queryStatus) {
|
|
438
|
+
//reset query status classnames
|
|
439
|
+
removeClass(this.queryBtn, "query_" + this.queryStatus);
|
|
440
|
+
addClass(this.queryBtn, "query_" + status);
|
|
441
|
+
this.queryStatus = status;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Set/remove spinner if needed
|
|
446
|
+
*/
|
|
447
|
+
if (this.req && this.queryBtn.className.indexOf("busy") < 0) {
|
|
448
|
+
this.queryBtn.className = this.queryBtn.className += " busy";
|
|
449
|
+
}
|
|
450
|
+
if (!this.req && this.queryBtn.className.indexOf("busy") >= 0) {
|
|
451
|
+
this.queryBtn.className = this.queryBtn.className.replace("busy", "");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
public handleLocalStorageQuotaFull(_e: any) {
|
|
455
|
+
console.warn("Localstorage quota exceeded. Clearing all queries");
|
|
456
|
+
Yasqe.clearStorage();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public saveQuery() {
|
|
460
|
+
const storageId = this.getStorageId();
|
|
461
|
+
if (!storageId || !this.persistentConfig) return;
|
|
462
|
+
this.persistentConfig.query = this.getValue();
|
|
463
|
+
this.storage.set(storageId, this.persistentConfig, this.config.persistencyExpire, this.handleLocalStorageQuotaFull);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get SPARQL query props
|
|
468
|
+
*/
|
|
469
|
+
public getQueryType() {
|
|
470
|
+
return this.getOption("queryType");
|
|
471
|
+
}
|
|
472
|
+
public getQueryMode(): "update" | "query" {
|
|
473
|
+
switch (this.getQueryType()) {
|
|
474
|
+
case "INSERT":
|
|
475
|
+
case "DELETE":
|
|
476
|
+
case "LOAD":
|
|
477
|
+
case "CLEAR":
|
|
478
|
+
case "CREATE":
|
|
479
|
+
case "DROP":
|
|
480
|
+
case "COPY":
|
|
481
|
+
case "MOVE":
|
|
482
|
+
case "ADD":
|
|
483
|
+
return "update";
|
|
484
|
+
default:
|
|
485
|
+
return "query";
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
public getVariablesFromQuery() {
|
|
489
|
+
//Use precise here. We want to be sure we use the most up to date state. If we're
|
|
490
|
+
//not, we might get outdated info from the current query (creating loops such
|
|
491
|
+
//as https://github.com/TriplyDB/YASGUI/issues/84)
|
|
492
|
+
//on caveat: this function won't work when query is invalid (i.e. when typing)
|
|
493
|
+
const token: Token = this.getTokenAt(
|
|
494
|
+
{ line: this.getDoc().lastLine(), ch: this.getDoc().getLine(this.getDoc().lastLine()).length },
|
|
495
|
+
true,
|
|
496
|
+
);
|
|
497
|
+
const vars: string[] = [];
|
|
498
|
+
for (var v in token.state.variables) {
|
|
499
|
+
vars.push(v);
|
|
500
|
+
}
|
|
501
|
+
return vars.sort();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Sparql-related tasks
|
|
506
|
+
*/
|
|
507
|
+
private autoformatSelection(start: number, end: number): string {
|
|
508
|
+
var text = this.getValue();
|
|
509
|
+
text = text.substring(start, end);
|
|
510
|
+
return Yasqe.autoformatString(text);
|
|
511
|
+
}
|
|
512
|
+
public static autoformatString(text: string): string {
|
|
513
|
+
var breakAfterArray = [
|
|
514
|
+
["keyword", "ws", "string-2", "ws", "variable-3"], // i.e. prefix declaration
|
|
515
|
+
["keyword", "ws", "variable-3"], // i.e. base
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
var breakBeforeCharacters = ["}"];
|
|
519
|
+
|
|
520
|
+
var getBreakType = function (stringVal: string) {
|
|
521
|
+
//first check the characters to break after
|
|
522
|
+
if (stringVal === "{") return 1;
|
|
523
|
+
if (stringVal === ".") return 1;
|
|
524
|
+
if (stringVal === ";") {
|
|
525
|
+
//it shouldnt be part of a group concat though.
|
|
526
|
+
//To check this case, we need to check the previous char type in the stacktrace
|
|
527
|
+
if (stackTrace.length > 2 && stackTrace[stackTrace.length - 2] === "punc") return 0;
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
//Now check which arrays to break after
|
|
532
|
+
for (var i = 0; i < breakAfterArray.length; i++) {
|
|
533
|
+
if (stackTrace.valueOf().toString() === breakAfterArray[i].valueOf().toString()) {
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
for (var i = 0; i < breakBeforeCharacters.length; i++) {
|
|
538
|
+
// don't want to issue 'breakbefore' AND 'breakafter', so check
|
|
539
|
+
// current line
|
|
540
|
+
if (currentLine.trim() !== "" && stringVal == breakBeforeCharacters[i]) {
|
|
541
|
+
return -1;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return 0;
|
|
545
|
+
};
|
|
546
|
+
var formattedQuery = "";
|
|
547
|
+
var currentLine = "";
|
|
548
|
+
var stackTrace: string[] = [];
|
|
549
|
+
(<any>Yasqe).runMode(text, "sparql11", function (stringVal: string, type: string) {
|
|
550
|
+
stackTrace.push(type);
|
|
551
|
+
var breakType = getBreakType(stringVal);
|
|
552
|
+
if (breakType != 0) {
|
|
553
|
+
if (breakType == 1) {
|
|
554
|
+
formattedQuery += stringVal + "\n";
|
|
555
|
+
currentLine = "";
|
|
556
|
+
} else {
|
|
557
|
+
// (-1)
|
|
558
|
+
formattedQuery += "\n" + stringVal;
|
|
559
|
+
currentLine = stringVal;
|
|
560
|
+
}
|
|
561
|
+
stackTrace = [];
|
|
562
|
+
} else {
|
|
563
|
+
currentLine += stringVal;
|
|
564
|
+
formattedQuery += stringVal;
|
|
565
|
+
}
|
|
566
|
+
if (stackTrace.length == 1 && stackTrace[0] == "sp-ws") stackTrace = [];
|
|
567
|
+
});
|
|
568
|
+
return formattedQuery.replace(/\n\s*\n/g, "\n").trim();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
public commentLines() {
|
|
572
|
+
var startLine = this.getDoc().getCursor("start").line;
|
|
573
|
+
var endLine = this.getDoc().getCursor("end").line;
|
|
574
|
+
var min = Math.min(startLine, endLine);
|
|
575
|
+
var max = Math.max(startLine, endLine);
|
|
576
|
+
|
|
577
|
+
// if all lines start with #, remove this char. Otherwise add this char
|
|
578
|
+
var linesAreCommented = true;
|
|
579
|
+
for (var i = min; i <= max; i++) {
|
|
580
|
+
var line = this.getDoc().getLine(i);
|
|
581
|
+
if (line.length == 0 || line.substring(0, 1) != "#") {
|
|
582
|
+
linesAreCommented = false;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
for (var i = min; i <= max; i++) {
|
|
587
|
+
if (linesAreCommented) {
|
|
588
|
+
// lines are commented, so remove comments
|
|
589
|
+
this.getDoc().replaceRange(
|
|
590
|
+
"",
|
|
591
|
+
{
|
|
592
|
+
line: i,
|
|
593
|
+
ch: 0,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
line: i,
|
|
597
|
+
ch: 1,
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
} else {
|
|
601
|
+
// Not all lines are commented, so add comments
|
|
602
|
+
this.getDoc().replaceRange("#", {
|
|
603
|
+
line: i,
|
|
604
|
+
ch: 0,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
public autoformat() {
|
|
611
|
+
if (!this.getDoc().somethingSelected()) this.execCommand("selectAll");
|
|
612
|
+
const from = this.getDoc().getCursor("start");
|
|
613
|
+
|
|
614
|
+
var to: Position = {
|
|
615
|
+
line: this.getDoc().getCursor("end").line,
|
|
616
|
+
ch: this.getDoc().getSelection().length,
|
|
617
|
+
};
|
|
618
|
+
var absStart = this.getDoc().indexFromPos(from);
|
|
619
|
+
var absEnd = this.getDoc().indexFromPos(to);
|
|
620
|
+
// Insert additional line breaks where necessary according to the
|
|
621
|
+
// mode's syntax
|
|
622
|
+
|
|
623
|
+
const res = this.autoformatSelection(absStart, absEnd);
|
|
624
|
+
|
|
625
|
+
// Replace and auto-indent the range
|
|
626
|
+
this.operation(() => {
|
|
627
|
+
this.getDoc().replaceRange(res, from, to);
|
|
628
|
+
var startLine = this.getDoc().posFromIndex(absStart).line;
|
|
629
|
+
var endLine = this.getDoc().posFromIndex(absStart + res.length).line;
|
|
630
|
+
for (var i = startLine; i <= endLine; i++) {
|
|
631
|
+
this.indentLine(i, "smart");
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
//values in the form of {?var: 'value'}, or [{?var: 'value'}]
|
|
636
|
+
public getQueryWithValues(values: string | { [varName: string]: string } | Array<{ [varName: string]: string }>) {
|
|
637
|
+
if (!values) return this.getValue();
|
|
638
|
+
var injectString: string;
|
|
639
|
+
if (typeof values === "string") {
|
|
640
|
+
injectString = values;
|
|
641
|
+
} else {
|
|
642
|
+
//start building inject string
|
|
643
|
+
if (!(values instanceof Array)) values = [values];
|
|
644
|
+
var variables = values.reduce(function (vars, valueObj) {
|
|
645
|
+
for (var v in valueObj) {
|
|
646
|
+
vars[v] = v;
|
|
647
|
+
}
|
|
648
|
+
return vars;
|
|
649
|
+
}, {});
|
|
650
|
+
var varArray: string[] = [];
|
|
651
|
+
for (var v in variables) {
|
|
652
|
+
varArray.push(v);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!varArray.length) return this.getValue();
|
|
656
|
+
//ok, we've got enough info to start building the string now
|
|
657
|
+
injectString = "VALUES (" + varArray.join(" ") + ") {\n";
|
|
658
|
+
values.forEach(function (valueObj) {
|
|
659
|
+
injectString += "( ";
|
|
660
|
+
varArray.forEach(function (variable) {
|
|
661
|
+
injectString += valueObj[variable] || "UNDEF";
|
|
662
|
+
});
|
|
663
|
+
injectString += " )\n";
|
|
664
|
+
});
|
|
665
|
+
injectString += "}\n";
|
|
666
|
+
}
|
|
667
|
+
if (!injectString) return this.getValue();
|
|
668
|
+
|
|
669
|
+
var newQuery = "";
|
|
670
|
+
var injected = false;
|
|
671
|
+
var gotSelect = false;
|
|
672
|
+
(<any>Yasqe).runMode(
|
|
673
|
+
this.getValue(),
|
|
674
|
+
"sparql11",
|
|
675
|
+
function (stringVal: string, className: string, _row: number, _col: number, _state: TokenizerState) {
|
|
676
|
+
if (className === "keyword" && stringVal.toLowerCase() === "select") gotSelect = true;
|
|
677
|
+
newQuery += stringVal;
|
|
678
|
+
if (gotSelect && !injected && className === "punc" && stringVal === "{") {
|
|
679
|
+
injected = true;
|
|
680
|
+
//start injecting
|
|
681
|
+
newQuery += "\n" + injectString;
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
return newQuery;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
public getValueWithoutComments() {
|
|
689
|
+
var cleanedQuery = "";
|
|
690
|
+
(<any>Yasqe).runMode(this.getValue(), "sparql11", function (stringVal: string, className: string) {
|
|
691
|
+
if (className != "comment") {
|
|
692
|
+
cleanedQuery += stringVal;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
return cleanedQuery;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
public setCheckSyntaxErrors(isEnabled: boolean) {
|
|
699
|
+
this.config.syntaxErrorCheck = isEnabled;
|
|
700
|
+
this.checkSyntax();
|
|
701
|
+
}
|
|
702
|
+
public checkSyntax() {
|
|
703
|
+
this.queryValid = true;
|
|
704
|
+
|
|
705
|
+
this.clearGutter("gutterErrorBar");
|
|
706
|
+
|
|
707
|
+
var state: TokenizerState;
|
|
708
|
+
for (var l = 0; l < this.getDoc().lineCount(); ++l) {
|
|
709
|
+
var precise = false;
|
|
710
|
+
if (!this.prevQueryValid) {
|
|
711
|
+
// we don't want cached information in this case, otherwise the
|
|
712
|
+
// previous error sign might still show up,
|
|
713
|
+
// even though the syntax error might be gone already
|
|
714
|
+
precise = true;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
var token: Token = this.getTokenAt(
|
|
718
|
+
{
|
|
719
|
+
line: l,
|
|
720
|
+
ch: this.getDoc().getLine(l).length,
|
|
721
|
+
},
|
|
722
|
+
precise,
|
|
723
|
+
);
|
|
724
|
+
var state = token.state;
|
|
725
|
+
this.setOption("queryType", state.queryType);
|
|
726
|
+
if (state.OK == false) {
|
|
727
|
+
if (!this.config.syntaxErrorCheck) {
|
|
728
|
+
//the library we use already marks everything as being an error. Overwrite this class attribute.
|
|
729
|
+
const els = this.getWrapperElement().querySelectorAll(".sp-error");
|
|
730
|
+
for (let i = 0; i < els.length; i++) {
|
|
731
|
+
var el: any = els[i];
|
|
732
|
+
if (el.style) el.style.color = "black";
|
|
733
|
+
}
|
|
734
|
+
//we don't want the gutter error, so return
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const warningEl = drawSvgStringAsElement(imgs.warning);
|
|
738
|
+
if (state.errorMsg) {
|
|
739
|
+
tooltip(this, warningEl, escape(token.state.errorMsg));
|
|
740
|
+
} else if (state.possibleCurrent && state.possibleCurrent.length > 0) {
|
|
741
|
+
var expectedEncoded: string[] = [];
|
|
742
|
+
state.possibleCurrent.forEach(function (expected) {
|
|
743
|
+
expectedEncoded.push("<strong style='text-decoration:underline'>" + escape(expected) + "</strong>");
|
|
744
|
+
});
|
|
745
|
+
tooltip(this, warningEl, "This line is invalid. Expected: " + expectedEncoded.join(", "));
|
|
746
|
+
}
|
|
747
|
+
// warningEl.style.marginTop = "2px";
|
|
748
|
+
// warningEl.style.marginLeft = "2px";
|
|
749
|
+
warningEl.className = "parseErrorIcon";
|
|
750
|
+
this.setGutterMarker(l, "gutterErrorBar", warningEl);
|
|
751
|
+
|
|
752
|
+
this.queryValid = false;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Token management
|
|
759
|
+
*/
|
|
760
|
+
|
|
761
|
+
public getCompleteToken(token?: Token, cur?: Position): Token {
|
|
762
|
+
return getCompleteToken(this, token, cur);
|
|
763
|
+
}
|
|
764
|
+
public getPreviousNonWsToken(line: number, token: Token): Token {
|
|
765
|
+
return getPreviousNonWsToken(this, line, token);
|
|
766
|
+
}
|
|
767
|
+
public getNextNonWsToken(lineNumber: number, charNumber?: number): Token | undefined {
|
|
768
|
+
return getNextNonWsToken(this, lineNumber, charNumber);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Notification management
|
|
772
|
+
*/
|
|
773
|
+
private notificationEls: { [key: string]: HTMLDivElement } = {};
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Shows notification
|
|
777
|
+
* @param key reference to the notification
|
|
778
|
+
* @param message the message to display
|
|
779
|
+
*/
|
|
780
|
+
public showNotification(key: string, message: string) {
|
|
781
|
+
if (!this.notificationEls[key]) {
|
|
782
|
+
// We create one wrapper for each notification, since there is no interactivity with the container (yet) we don't need to keep a reference
|
|
783
|
+
const notificationContainer = document.createElement("div");
|
|
784
|
+
addClass(notificationContainer, "notificationContainer");
|
|
785
|
+
this.getWrapperElement().appendChild(notificationContainer);
|
|
786
|
+
|
|
787
|
+
// Create the actual notification element
|
|
788
|
+
this.notificationEls[key] = document.createElement("div");
|
|
789
|
+
addClass(this.notificationEls[key], "notification", "notif_" + key);
|
|
790
|
+
notificationContainer.appendChild(this.notificationEls[key]);
|
|
791
|
+
}
|
|
792
|
+
// Hide others
|
|
793
|
+
for (const notificationId in this.notificationEls) {
|
|
794
|
+
if (notificationId !== key) this.hideNotification(notificationId);
|
|
795
|
+
}
|
|
796
|
+
const el = this.notificationEls[key];
|
|
797
|
+
addClass(el, "active");
|
|
798
|
+
el.innerText = message;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Hides notification
|
|
802
|
+
* @param key the identifier of the notification to hide
|
|
803
|
+
*/
|
|
804
|
+
public hideNotification(key: string) {
|
|
805
|
+
if (this.notificationEls[key]) {
|
|
806
|
+
removeClass(this.notificationEls[key], "active");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Autocompleter management
|
|
812
|
+
*/
|
|
813
|
+
public enableCompleter(name: string): Promise<void> {
|
|
814
|
+
if (!Yasqe.Autocompleters[name])
|
|
815
|
+
return Promise.reject(new Error("Autocompleter " + name + " is not a registered autocompleter"));
|
|
816
|
+
if (this.config.autocompleters.indexOf(name) < 0) this.config.autocompleters.push(name);
|
|
817
|
+
const autocompleter = (this.autocompleters[name] = new Autocompleter.Completer(this, Yasqe.Autocompleters[name]));
|
|
818
|
+
return autocompleter.initialize();
|
|
819
|
+
}
|
|
820
|
+
public disableCompleter(name: string) {
|
|
821
|
+
this.config.autocompleters = this.config.autocompleters.filter((a) => a !== name);
|
|
822
|
+
this.autocompleters[name] = undefined;
|
|
823
|
+
}
|
|
824
|
+
public autocomplete(fromAutoShow = false) {
|
|
825
|
+
if (this.getDoc().somethingSelected()) return;
|
|
826
|
+
|
|
827
|
+
for (let i in this.config.autocompleters) {
|
|
828
|
+
const autocompleter = this.autocompleters[this.config.autocompleters[i]];
|
|
829
|
+
if (!autocompleter || !autocompleter.autocomplete(fromAutoShow)) continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Prefix management
|
|
835
|
+
*/
|
|
836
|
+
public collapsePrefixes(collapse = true) {
|
|
837
|
+
const firstPrefixLine = findFirstPrefixLine(this);
|
|
838
|
+
if (firstPrefixLine === undefined) return; //nothing to collapse
|
|
839
|
+
this.foldCode(firstPrefixLine, (<any>CodeMirror).fold.prefix, collapse ? "fold" : "unfold");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
public getPrefixesFromQuery(): Prefixes {
|
|
843
|
+
return getPrefixesFromQuery(this);
|
|
844
|
+
}
|
|
845
|
+
public addPrefixes(prefixes: string | Prefixes): void {
|
|
846
|
+
return addPrefixes(this, prefixes);
|
|
847
|
+
}
|
|
848
|
+
public removePrefixes(prefixes: Prefixes): void {
|
|
849
|
+
return removePrefixes(this, prefixes);
|
|
850
|
+
}
|
|
851
|
+
public updateWidget() {
|
|
852
|
+
if (
|
|
853
|
+
(this as any).cursorCoords &&
|
|
854
|
+
(this as any).state.completionActive &&
|
|
855
|
+
(this as any).state.completionActive.widget
|
|
856
|
+
) {
|
|
857
|
+
const newTop: string = (this as any).cursorCoords(null).bottom;
|
|
858
|
+
(this as any).state.completionActive.widget.hints.style.top = newTop + "px";
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Querying
|
|
864
|
+
*/
|
|
865
|
+
public query(config?: Sparql.YasqeAjaxConfig) {
|
|
866
|
+
if (this.config.queryingDisabled) return Promise.reject("Querying is disabled.");
|
|
867
|
+
// Abort previous request
|
|
868
|
+
this.abortQuery();
|
|
869
|
+
return Sparql.executeQuery(this, config);
|
|
870
|
+
}
|
|
871
|
+
public getUrlParams() {
|
|
872
|
+
//first try hash
|
|
873
|
+
let urlParams: queryString.ParsedQuery = {};
|
|
874
|
+
if (window.location.hash.length > 1) {
|
|
875
|
+
//firefox does some decoding if we're using window.location.hash (e.g. the + sign in contentType settings)
|
|
876
|
+
//Don't want this. So simply get the hash string ourselves
|
|
877
|
+
urlParams = queryString.parse(location.hash);
|
|
878
|
+
}
|
|
879
|
+
if ((!urlParams || !("query" in urlParams)) && window.location.search.length > 1) {
|
|
880
|
+
//ok, then just try regular url params
|
|
881
|
+
urlParams = queryString.parse(window.location.search);
|
|
882
|
+
}
|
|
883
|
+
return urlParams;
|
|
884
|
+
}
|
|
885
|
+
public configToQueryParams(): queryString.ParsedQuery {
|
|
886
|
+
//extend existing link, so first fetch current arguments
|
|
887
|
+
var urlParams: any = {};
|
|
888
|
+
if (window.location.hash.length > 1) urlParams = queryString.parse(window.location.hash);
|
|
889
|
+
urlParams["query"] = this.getValue();
|
|
890
|
+
return urlParams;
|
|
891
|
+
}
|
|
892
|
+
public queryParamsToConfig(params: queryString.ParsedQuery) {
|
|
893
|
+
if (params && params.query && typeof params.query === "string") {
|
|
894
|
+
this.setValue(params.query);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
public getAsCurlString(config?: Sparql.YasqeAjaxConfig): string {
|
|
899
|
+
return Sparql.getAsCurlString(this, config);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
public abortQuery() {
|
|
903
|
+
if (this.req) {
|
|
904
|
+
if (this.abortController) {
|
|
905
|
+
this.abortController.abort();
|
|
906
|
+
}
|
|
907
|
+
this.emit("queryAbort", this, this.req);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
public expandEditor() {
|
|
912
|
+
this.setSize(null, "100%");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
public destroy() {
|
|
916
|
+
// Abort running query
|
|
917
|
+
this.abortQuery();
|
|
918
|
+
this.unregisterEventListeners();
|
|
919
|
+
this.resizeWrapper?.removeEventListener("mousedown", this.initDrag, false);
|
|
920
|
+
this.resizeWrapper?.removeEventListener("dblclick", this.expandEditor);
|
|
921
|
+
for (const autocompleter in this.autocompleters) {
|
|
922
|
+
this.disableCompleter(autocompleter);
|
|
923
|
+
}
|
|
924
|
+
window.removeEventListener("hashchange", this.handleHashChange);
|
|
925
|
+
this.rootEl.remove();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Statics
|
|
930
|
+
*/
|
|
931
|
+
static Sparql = Sparql;
|
|
932
|
+
static runMode = (<any>CodeMirror).runMode;
|
|
933
|
+
static clearStorage() {
|
|
934
|
+
const storage = new YStorage(Yasqe.storageNamespace);
|
|
935
|
+
storage.removeNamespace();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
static Autocompleters: { [name: string]: Autocompleter.CompleterConfig } = {};
|
|
939
|
+
static registerAutocompleter(value: Autocompleter.CompleterConfig, enable = true) {
|
|
940
|
+
const name = value.name;
|
|
941
|
+
Yasqe.Autocompleters[name] = value;
|
|
942
|
+
if (enable && Yasqe.defaults.autocompleters.indexOf(name) < 0) Yasqe.defaults.autocompleters.push(name);
|
|
943
|
+
}
|
|
944
|
+
static defaults = getDefaults();
|
|
945
|
+
static forkAutocompleter(
|
|
946
|
+
fromCompleter: string,
|
|
947
|
+
newCompleter: { name: string } & Partial<Autocompleter.CompleterConfig>,
|
|
948
|
+
enable = true,
|
|
949
|
+
) {
|
|
950
|
+
if (!Yasqe.Autocompleters[fromCompleter]) throw new Error("Autocompleter " + fromCompleter + " does not exist");
|
|
951
|
+
if (!newCompleter?.name) {
|
|
952
|
+
throw new Error("Expected a name for newly registered autocompleter");
|
|
953
|
+
}
|
|
954
|
+
const name = newCompleter.name;
|
|
955
|
+
Yasqe.Autocompleters[name] = { ...Yasqe.Autocompleters[fromCompleter], ...newCompleter };
|
|
956
|
+
|
|
957
|
+
if (enable && Yasqe.defaults.autocompleters.indexOf(name) < 0) Yasqe.defaults.autocompleters.push(name);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
(<any>Object).assign(CodeMirror.prototype, Yasqe.prototype);
|
|
961
|
+
|
|
962
|
+
export type TokenizerState = sparql11Mode.State;
|
|
963
|
+
export type Position = CodeMirror.Position;
|
|
964
|
+
export type Token = CodeMirror.Token;
|
|
965
|
+
|
|
966
|
+
export interface HintList {
|
|
967
|
+
list: Hint[];
|
|
968
|
+
from: Position;
|
|
969
|
+
to: Position;
|
|
970
|
+
}
|
|
971
|
+
export interface Hint {
|
|
972
|
+
text: string;
|
|
973
|
+
displayText?: string;
|
|
974
|
+
className?: string;
|
|
975
|
+
render?: (el: HTMLElement, self: Hint, data: any) => void;
|
|
976
|
+
from?: Position;
|
|
977
|
+
to?: Position;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
export type HintFn = { async?: boolean } & (() => Promise<HintList> | HintList);
|
|
981
|
+
export interface HintConfig {
|
|
982
|
+
completeOnSingleClick?: boolean;
|
|
983
|
+
container?: HTMLElement;
|
|
984
|
+
closeCharacters?: RegExp;
|
|
985
|
+
completeSingle?: boolean;
|
|
986
|
+
// A hinting function, as specified above. It is possible to set the async property on a hinting function to true, in which case it will be called with arguments (cm, callback, ?options), and the completion interface will only be popped up when the hinting function calls the callback, passing it the object holding the completions. The hinting function can also return a promise, and the completion interface will only be popped when the promise resolves. By default, hinting only works when there is no selection. You can give a hinting function a supportsSelection property with a truthy value to indicate that it supports selections.
|
|
987
|
+
hint: HintFn;
|
|
988
|
+
|
|
989
|
+
// Whether the pop-up should be horizontally aligned with the start of the word (true, default), or with the cursor (false).
|
|
990
|
+
alignWithWord?: boolean;
|
|
991
|
+
// When enabled (which is the default), the pop-up will close when the editor is unfocused.
|
|
992
|
+
closeOnUnfocus?: boolean;
|
|
993
|
+
// Allows you to provide a custom key map of keys to be active when the pop-up is active. The handlers will be called with an extra argument, a handle to the completion menu, which has moveFocus(n), setFocus(n), pick(), and close() methods (see the source for details), that can be used to change the focused element, pick the current element or close the menu. Additionally menuSize() can give you access to the size of the current dropdown menu, length give you the number of available completions, and data give you full access to the completion returned by the hinting function.
|
|
994
|
+
customKeys?: any;
|
|
995
|
+
|
|
996
|
+
// Like customKeys above, but the bindings will be added to the set of default bindings, instead of replacing them.
|
|
997
|
+
extraKeys?: {
|
|
998
|
+
[key: string]: (
|
|
999
|
+
yasqe: Yasqe,
|
|
1000
|
+
event: {
|
|
1001
|
+
close: () => void;
|
|
1002
|
+
data: {
|
|
1003
|
+
from: Position;
|
|
1004
|
+
to: Position;
|
|
1005
|
+
list: Hint[];
|
|
1006
|
+
};
|
|
1007
|
+
length: number;
|
|
1008
|
+
menuSize: () => void;
|
|
1009
|
+
moveFocus: (movement: number) => void;
|
|
1010
|
+
pick: () => void;
|
|
1011
|
+
setFocus: (index: number) => void;
|
|
1012
|
+
},
|
|
1013
|
+
) => void;
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
export interface RequestConfig<Y> {
|
|
1017
|
+
queryArgument: string | ((yasqe: Y) => string) | undefined;
|
|
1018
|
+
endpoint: string | ((yasqe: Y) => string);
|
|
1019
|
+
method: "POST" | "GET" | ((yasqe: Y) => "POST" | "GET");
|
|
1020
|
+
acceptHeaderGraph: string | ((yasqe: Y) => string);
|
|
1021
|
+
acceptHeaderSelect: string | ((yasqe: Y) => string);
|
|
1022
|
+
acceptHeaderUpdate: string | ((yasqe: Y) => string);
|
|
1023
|
+
namedGraphs: string[] | ((yasqe: Y) => string[]);
|
|
1024
|
+
defaultGraphs: string[] | ((yasqe: Y) => []);
|
|
1025
|
+
args: Array<{ name: string; value: string }> | ((yasqe: Y) => Array<{ name: string; value: string }>);
|
|
1026
|
+
headers: { [key: string]: string } | ((yasqe: Y) => { [key: string]: string });
|
|
1027
|
+
withCredentials: boolean | ((yasqe: Y) => boolean);
|
|
1028
|
+
adjustQueryBeforeRequest: ((yasqe: Y) => string) | false;
|
|
1029
|
+
}
|
|
1030
|
+
export type PlainRequestConfig = {
|
|
1031
|
+
[K in keyof RequestConfig<any>]: Exclude<RequestConfig<any>[K], Function>;
|
|
1032
|
+
};
|
|
1033
|
+
export type PartialConfig = {
|
|
1034
|
+
[P in keyof Config]?: Config[P] extends object ? Partial<Config[P]> : Config[P];
|
|
1035
|
+
};
|
|
1036
|
+
export interface Config extends Partial<CodeMirror.EditorConfiguration> {
|
|
1037
|
+
mode: string;
|
|
1038
|
+
collapsePrefixesOnLoad: boolean;
|
|
1039
|
+
syntaxErrorCheck: boolean;
|
|
1040
|
+
/**
|
|
1041
|
+
* Show a button with which users can create a link to this query. Set this value to null to disable this functionality.
|
|
1042
|
+
* By default, this feature is enabled, and the only the query value is appended to the link.
|
|
1043
|
+
* ps. This function should return an object which is parseable by jQuery.param (http://api.jquery.com/jQuery.param/)
|
|
1044
|
+
*/
|
|
1045
|
+
createShareableLink: (yasqe: Yasqe) => string;
|
|
1046
|
+
createShortLink: ((yasqe: Yasqe, longLink: string) => Promise<string>) | undefined;
|
|
1047
|
+
consumeShareLink: ((yasqe: Yasqe) => void) | undefined | null;
|
|
1048
|
+
/**
|
|
1049
|
+
* Change persistency settings for the YASQE query value. Setting the values
|
|
1050
|
+
* to null, will disable persistancy: nothing is stored between browser
|
|
1051
|
+
* sessions Setting the values to a string (or a function which returns a
|
|
1052
|
+
* string), will store the query in localstorage using the specified string.
|
|
1053
|
+
* By default, the ID is dynamically generated using the closest dom ID, to avoid collissions when using multiple YASQE items on one
|
|
1054
|
+
* page
|
|
1055
|
+
*/
|
|
1056
|
+
persistenceId: ((yasqe: Yasqe) => string) | string | undefined | null;
|
|
1057
|
+
persistencyExpire: number; //seconds
|
|
1058
|
+
showQueryButton: boolean;
|
|
1059
|
+
requestConfig: RequestConfig<Yasqe> | ((yasqe: Yasqe) => RequestConfig<Yasqe>);
|
|
1060
|
+
pluginButtons: (() => HTMLElement[] | HTMLElement) | undefined;
|
|
1061
|
+
//Addon specific addon ts defs, or missing props from codemirror conf
|
|
1062
|
+
highlightSelectionMatches: { showToken?: RegExp; annotateScrollbar?: boolean };
|
|
1063
|
+
tabMode: string;
|
|
1064
|
+
foldGutter: any; //This should be of type boolean, or an object. However, setting it to any to avoid
|
|
1065
|
+
//ts complaining about incorrectly extending, as the cm def only defined it has having a boolean type.
|
|
1066
|
+
matchBrackets: boolean;
|
|
1067
|
+
autocompleters: string[];
|
|
1068
|
+
hintConfig: Partial<HintConfig>;
|
|
1069
|
+
resizeable: boolean;
|
|
1070
|
+
editorHeight: string;
|
|
1071
|
+
queryingDisabled: string | undefined; // The string will be the message displayed when hovered
|
|
1072
|
+
prefixCcApi: string; // the suggested default prefixes URL API getter
|
|
1073
|
+
}
|
|
1074
|
+
export interface PersistentConfig {
|
|
1075
|
+
query: string;
|
|
1076
|
+
editorHeight: string;
|
|
1077
|
+
}
|
|
1078
|
+
// export var _Yasqe = _Yasqe;
|
|
1079
|
+
|
|
1080
|
+
//add missing static functions, added by e.g. addons
|
|
1081
|
+
// declare function runMode(text:string, mode:any, out:any):void
|
|
1082
|
+
|
|
1083
|
+
//Need to assign our prototype to codemirror's, as some of the callbacks (e.g. the keymap opts)
|
|
1084
|
+
//give us a cm doc, instead of a yasqe + cm doc
|
|
1085
|
+
Autocompleter.completers.forEach((c) => {
|
|
1086
|
+
Yasqe.registerAutocompleter(c);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
export default Yasqe;
|