@matdata/yasr 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 +150 -0
- package/build/ts/src/bindingsToCsv.d.ts +2 -0
- package/build/ts/src/defaults.d.ts +2 -0
- package/build/ts/src/helpers/addCSS.d.ts +1 -0
- package/build/ts/src/helpers/addScript.d.ts +1 -0
- package/build/ts/src/helpers/index.d.ts +3 -0
- package/build/ts/src/helpers/sanitize.d.ts +2 -0
- package/build/ts/src/imgs.d.ts +4 -0
- package/build/ts/src/index.d.ts +130 -0
- package/build/ts/src/parsers/csv.d.ts +2 -0
- package/build/ts/src/parsers/index.d.ts +68 -0
- package/build/ts/src/parsers/json.d.ts +2 -0
- package/build/ts/src/parsers/tsv.d.ts +2 -0
- package/build/ts/src/parsers/turtleFamily.d.ts +4 -0
- package/build/ts/src/parsers/xml.d.ts +2 -0
- package/build/ts/src/plugins/boolean/index.d.ts +13 -0
- package/build/ts/src/plugins/error/index.d.ts +13 -0
- package/build/ts/src/plugins/index.d.ts +19 -0
- package/build/ts/src/plugins/response/index.d.ts +28 -0
- package/build/ts/src/plugins/table/index.d.ts +49 -0
- package/build/yasr.html +32 -0
- package/build/yasr.min.css +2 -0
- package/build/yasr.min.css.map +1 -0
- package/build/yasr.min.js +3 -0
- package/build/yasr.min.js.LICENSE.txt +34 -0
- package/build/yasr.min.js.map +1 -0
- package/package.json +56 -0
- package/src/bin/takeScreenshot.js +373 -0
- package/src/bindingsToCsv.ts +18 -0
- package/src/defaults.ts +28 -0
- package/src/helpers/addCSS.ts +7 -0
- package/src/helpers/addScript.ts +12 -0
- package/src/helpers/index.ts +3 -0
- package/src/helpers/sanitize.ts +11 -0
- package/src/imgs.ts +7 -0
- package/src/index.ts +688 -0
- package/src/jquery/extendJquery.js +1 -0
- package/src/jquery/tableToCsv.js +88 -0
- package/src/main.scss +229 -0
- package/src/parsers/csv.ts +30 -0
- package/src/parsers/index.ts +311 -0
- package/src/parsers/json.ts +22 -0
- package/src/parsers/tsv.ts +43 -0
- package/src/parsers/turtleFamily.ts +60 -0
- package/src/parsers/xml.ts +79 -0
- package/src/plugins/boolean/index.scss +11 -0
- package/src/plugins/boolean/index.ts +42 -0
- package/src/plugins/error/index.scss +57 -0
- package/src/plugins/error/index.ts +124 -0
- package/src/plugins/index.ts +24 -0
- package/src/plugins/response/index.scss +63 -0
- package/src/plugins/response/index.ts +170 -0
- package/src/plugins/table/index.scss +154 -0
- package/src/plugins/table/index.ts +437 -0
- package/src/scss/global.scss +10 -0
- package/src/scss/variables.scss +2 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { merge, filter, mapValues, uniqueId } from "lodash-es";
|
|
3
|
+
import getDefaults from "./defaults";
|
|
4
|
+
import { Plugin } from "./plugins";
|
|
5
|
+
import {
|
|
6
|
+
Storage as YStorage,
|
|
7
|
+
drawFontAwesomeIconAsSvg,
|
|
8
|
+
drawSvgStringAsElement,
|
|
9
|
+
removeClass,
|
|
10
|
+
addClass,
|
|
11
|
+
hasClass,
|
|
12
|
+
} from "@matdata/yasgui-utils";
|
|
13
|
+
import Parser from "./parsers";
|
|
14
|
+
export { default as Parser } from "./parsers";
|
|
15
|
+
import { addScript, addCss, sanitize } from "./helpers";
|
|
16
|
+
import * as faDownload from "@fortawesome/free-solid-svg-icons/faDownload";
|
|
17
|
+
import * as faQuestionCircle from "@fortawesome/free-solid-svg-icons/faQuestionCircle";
|
|
18
|
+
import * as imgs from "./imgs";
|
|
19
|
+
require("./main.scss");
|
|
20
|
+
|
|
21
|
+
export interface PersistentConfig {
|
|
22
|
+
selectedPlugin?: string;
|
|
23
|
+
pluginsConfig?: { [pluginName: string]: any };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Yasr {
|
|
27
|
+
on(event: "change", listener: (instance: Yasr) => void): this;
|
|
28
|
+
emit(event: "change", instance: Yasr): boolean;
|
|
29
|
+
on(event: "draw", listener: (instance: Yasr, plugin: Plugin<any>) => void): this;
|
|
30
|
+
emit(event: "draw", instance: Yasr, plugin: Plugin<any>): boolean;
|
|
31
|
+
on(event: "drawn", listener: (instance: Yasr, plugin: Plugin<any>) => void): this;
|
|
32
|
+
emit(event: "drawn", instance: Yasr, plugin: Plugin<any>): boolean;
|
|
33
|
+
on(event: "toggle-help", listener: (instance: Yasr) => void): this;
|
|
34
|
+
emit(event: "toggle-help", instance: Yasr): boolean;
|
|
35
|
+
}
|
|
36
|
+
export class Yasr extends EventEmitter {
|
|
37
|
+
public results?: Parser;
|
|
38
|
+
public rootEl: HTMLDivElement;
|
|
39
|
+
public headerEl: HTMLDivElement;
|
|
40
|
+
public fallbackInfoEl: HTMLDivElement;
|
|
41
|
+
public resultsEl: HTMLDivElement;
|
|
42
|
+
public pluginControls!: HTMLDivElement;
|
|
43
|
+
public config: Config;
|
|
44
|
+
public storage: YStorage;
|
|
45
|
+
public plugins: { [name: string]: Plugin<any> } = {};
|
|
46
|
+
// private persistentConfig: PersistentConfig;
|
|
47
|
+
public helpDrawn: Boolean = false;
|
|
48
|
+
private drawnPlugin: string | undefined;
|
|
49
|
+
private selectedPlugin: string | undefined;
|
|
50
|
+
private fullscreenBtn: HTMLButtonElement | undefined;
|
|
51
|
+
private isFullscreen: boolean = false;
|
|
52
|
+
private loadingEl: HTMLDivElement | undefined;
|
|
53
|
+
private isLoading: boolean = false;
|
|
54
|
+
|
|
55
|
+
// Utils
|
|
56
|
+
public utils = { addScript: addScript, addCSS: addCss, sanitize: sanitize };
|
|
57
|
+
|
|
58
|
+
constructor(parent: HTMLElement, conf: Partial<Config> = {}, data?: any) {
|
|
59
|
+
super();
|
|
60
|
+
if (!parent) throw new Error("No parent passed as argument. Dont know where to draw YASR");
|
|
61
|
+
this.rootEl = document.createElement("div");
|
|
62
|
+
this.rootEl.className = "yasr";
|
|
63
|
+
parent.appendChild(this.rootEl);
|
|
64
|
+
this.config = merge({}, Yasr.defaults, conf);
|
|
65
|
+
|
|
66
|
+
//Do some post processing
|
|
67
|
+
this.storage = new YStorage(Yasr.storageNamespace);
|
|
68
|
+
this.getConfigFromStorage();
|
|
69
|
+
this.headerEl = document.createElement("div");
|
|
70
|
+
this.headerEl.className = "yasr_header";
|
|
71
|
+
this.rootEl.appendChild(this.headerEl);
|
|
72
|
+
this.fallbackInfoEl = document.createElement("div");
|
|
73
|
+
this.fallbackInfoEl.className = "yasr_fallback_info";
|
|
74
|
+
this.rootEl.appendChild(this.fallbackInfoEl);
|
|
75
|
+
this.resultsEl = document.createElement("div");
|
|
76
|
+
this.resultsEl.className = "yasr_results";
|
|
77
|
+
this.resultsEl.id = uniqueId("resultsId");
|
|
78
|
+
this.rootEl.appendChild(this.resultsEl);
|
|
79
|
+
this.createLoadingElement();
|
|
80
|
+
this.initializePlugins();
|
|
81
|
+
this.drawHeader();
|
|
82
|
+
|
|
83
|
+
const resp = data || this.getResponseFromStorage();
|
|
84
|
+
if (resp) this.setResponse(resp);
|
|
85
|
+
}
|
|
86
|
+
private getConfigFromStorage() {
|
|
87
|
+
const storageId = this.getStorageId(this.config.persistenceLabelConfig);
|
|
88
|
+
if (storageId) {
|
|
89
|
+
const persistentConfig: PersistentConfig | undefined = this.storage.get(storageId);
|
|
90
|
+
if (persistentConfig) {
|
|
91
|
+
this.selectedPlugin = persistentConfig.selectedPlugin;
|
|
92
|
+
for (const pluginName in persistentConfig.pluginsConfig) {
|
|
93
|
+
const pConf = persistentConfig.pluginsConfig[pluginName];
|
|
94
|
+
if (pConf && this.config.plugins[pluginName]) this.config.plugins[pluginName].dynamicConfig = pConf;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Ask the [[ Config.errorRenderers | configured error renderers ]] for a
|
|
102
|
+
* custom rendering of [[`error`]].
|
|
103
|
+
*
|
|
104
|
+
* @param error the error for which to find a custom rendering
|
|
105
|
+
* @return the first custom rendering found, or `undefined` if none was found.
|
|
106
|
+
*/
|
|
107
|
+
public async renderError(error: Parser.ErrorSummary): Promise<HTMLElement | undefined> {
|
|
108
|
+
// Chain the errorRenderers to get the first special rendering of the error
|
|
109
|
+
// if no special rendering is found, return undefined
|
|
110
|
+
let element: HTMLElement | undefined = undefined;
|
|
111
|
+
if (this.config.errorRenderers !== undefined) {
|
|
112
|
+
for (const renderer of this.config.errorRenderers) {
|
|
113
|
+
element = await renderer(error);
|
|
114
|
+
if (element !== undefined) break; // we found the first special case, so return that!
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return element;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public getStorageId(label: string, getter?: Config["persistenceId"]): string | undefined {
|
|
121
|
+
const persistenceId = getter || this.config.persistenceId;
|
|
122
|
+
if (!persistenceId) return;
|
|
123
|
+
if (typeof persistenceId === "string") return persistenceId + "_" + label;
|
|
124
|
+
return persistenceId(this) + "_" + label;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public somethingDrawn() {
|
|
128
|
+
return !!this.resultsEl.children.length;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Empties fallback element
|
|
132
|
+
* CSS will make sure the outline is hidden when empty
|
|
133
|
+
*/
|
|
134
|
+
public emptyFallbackElement() {
|
|
135
|
+
// This is quicker than `<node>.innerHtml = ""`
|
|
136
|
+
while (this.fallbackInfoEl.firstChild) this.fallbackInfoEl.removeChild(this.fallbackInfoEl.firstChild);
|
|
137
|
+
}
|
|
138
|
+
public getSelectedPluginName(): string {
|
|
139
|
+
return this.selectedPlugin || this.config.defaultPlugin;
|
|
140
|
+
}
|
|
141
|
+
public getSelectedPlugin() {
|
|
142
|
+
if (this.plugins[this.getSelectedPluginName()]) {
|
|
143
|
+
return this.plugins[this.getSelectedPluginName()];
|
|
144
|
+
}
|
|
145
|
+
console.warn(`Tried using plugin ${this.getSelectedPluginName()}, but seems this plugin isnt registered in yasr.`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Update selectors, based on whether they can actuall draw something, and which plugin is currently selected
|
|
149
|
+
*/
|
|
150
|
+
private updatePluginSelectors(enabledPlugins?: string[]) {
|
|
151
|
+
if (!this.pluginSelectorsEl) return;
|
|
152
|
+
for (const pluginName in this.config.plugins) {
|
|
153
|
+
const plugin = this.plugins[pluginName];
|
|
154
|
+
if (plugin && !plugin.hideFromSelection) {
|
|
155
|
+
if (enabledPlugins) {
|
|
156
|
+
if (enabledPlugins.indexOf(pluginName) >= 0) {
|
|
157
|
+
//make sure enabled
|
|
158
|
+
removeClass(this.pluginSelectorsEl.querySelector(".select_" + pluginName), "disabled");
|
|
159
|
+
} else {
|
|
160
|
+
//make sure disabled
|
|
161
|
+
addClass(this.pluginSelectorsEl.querySelector(".select_" + pluginName), "disabled");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (pluginName === this.getSelectedPluginName()) {
|
|
165
|
+
addClass(this.pluginSelectorsEl.querySelector(".select_" + pluginName), "selected");
|
|
166
|
+
} else {
|
|
167
|
+
removeClass(this.pluginSelectorsEl.querySelector(".select_" + pluginName), "selected");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private getCompatiblePlugins(): string[] {
|
|
174
|
+
if (!this.results)
|
|
175
|
+
return Object.keys(
|
|
176
|
+
filter(this.config.plugins, (val) => (typeof val === "object" && val.enabled) || (val as any) === true),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const supportedPlugins: { name: string; priority: number }[] = [];
|
|
180
|
+
for (const pluginName in this.plugins) {
|
|
181
|
+
if (this.plugins[pluginName].canHandleResults()) {
|
|
182
|
+
supportedPlugins.push({ name: pluginName, priority: this.plugins[pluginName].priority });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return supportedPlugins.sort((p1, p2) => p2.priority - p1.priority).map((p) => p.name);
|
|
186
|
+
}
|
|
187
|
+
public draw() {
|
|
188
|
+
this.updateHelpButton();
|
|
189
|
+
this.updateResponseInfo();
|
|
190
|
+
if (!this.results) return;
|
|
191
|
+
const compatiblePlugins = this.getCompatiblePlugins();
|
|
192
|
+
if (this.drawnPlugin && this.getSelectedPluginName() !== this.drawnPlugin) {
|
|
193
|
+
while (this.pluginControls.firstChild) {
|
|
194
|
+
this.pluginControls.firstChild.remove();
|
|
195
|
+
}
|
|
196
|
+
this.plugins[this.drawnPlugin].destroy?.();
|
|
197
|
+
}
|
|
198
|
+
let pluginToDraw: string | undefined;
|
|
199
|
+
if (this.getSelectedPlugin() && this.getSelectedPlugin()?.canHandleResults()) {
|
|
200
|
+
pluginToDraw = this.getSelectedPluginName();
|
|
201
|
+
// When present remove fallback box
|
|
202
|
+
this.emptyFallbackElement();
|
|
203
|
+
} else if (compatiblePlugins[0]) {
|
|
204
|
+
if (this.drawnPlugin) {
|
|
205
|
+
this.plugins[this.drawnPlugin].destroy?.();
|
|
206
|
+
}
|
|
207
|
+
pluginToDraw = compatiblePlugins[0];
|
|
208
|
+
// Signal to create the plugin+
|
|
209
|
+
this.fillFallbackBox(pluginToDraw);
|
|
210
|
+
}
|
|
211
|
+
if (pluginToDraw) {
|
|
212
|
+
this.drawnPlugin = pluginToDraw;
|
|
213
|
+
|
|
214
|
+
this.emit("draw", this, this.plugins[pluginToDraw]);
|
|
215
|
+
const plugin = this.plugins[pluginToDraw];
|
|
216
|
+
const initPromise = plugin.initialize ? plugin.initialize() : Promise.resolve();
|
|
217
|
+
initPromise.then(
|
|
218
|
+
async () => {
|
|
219
|
+
if (pluginToDraw) {
|
|
220
|
+
// make sure to clear the object _here_
|
|
221
|
+
// otherwise we run into race conditions when draw is executed
|
|
222
|
+
// shortly after each other, and the plugin uses an initialize function
|
|
223
|
+
// as a result, things can be rendered _twice_
|
|
224
|
+
while (this.resultsEl.firstChild) this.resultsEl.firstChild.remove();
|
|
225
|
+
await this.plugins[pluginToDraw].draw(this.config.plugins[pluginToDraw].dynamicConfig);
|
|
226
|
+
this.emit("drawn", this, this.plugins[pluginToDraw]);
|
|
227
|
+
this.updateExportHeaders();
|
|
228
|
+
this.updatePluginSelectors(compatiblePlugins);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
(_e) => console.error,
|
|
232
|
+
);
|
|
233
|
+
} else {
|
|
234
|
+
this.resultsEl.textContent = "cannot render result";
|
|
235
|
+
this.updateExportHeaders();
|
|
236
|
+
this.updatePluginSelectors(compatiblePlugins);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//just an alias for `draw`. That way, we've got a consistent api with yasqe
|
|
240
|
+
public refresh() {
|
|
241
|
+
this.draw();
|
|
242
|
+
}
|
|
243
|
+
public destroy() {
|
|
244
|
+
if (this.drawnPlugin) this.plugins[this.drawnPlugin]?.destroy?.();
|
|
245
|
+
this.removeAllListeners();
|
|
246
|
+
this.rootEl.remove();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getPrefixes(): Prefixes {
|
|
250
|
+
if (this.config.prefixes) {
|
|
251
|
+
if (typeof this.config.prefixes === "function") {
|
|
252
|
+
return this.config.prefixes(this);
|
|
253
|
+
} else {
|
|
254
|
+
return this.config.prefixes;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
public selectPlugin(plugin: string) {
|
|
260
|
+
if (this.selectedPlugin === plugin) {
|
|
261
|
+
// Don't re-render when selecting the same plugin. Also see #1893
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (this.config.plugins[plugin]) {
|
|
265
|
+
this.selectedPlugin = plugin;
|
|
266
|
+
} else {
|
|
267
|
+
console.warn(`Plugin ${plugin} does not exist.`);
|
|
268
|
+
this.selectedPlugin = this.config.defaultPlugin;
|
|
269
|
+
}
|
|
270
|
+
this.storeConfig();
|
|
271
|
+
this.emit("change", this);
|
|
272
|
+
this.updatePluginSelectors();
|
|
273
|
+
this.draw();
|
|
274
|
+
}
|
|
275
|
+
private pluginSelectorsEl!: HTMLUListElement;
|
|
276
|
+
drawPluginSelectors() {
|
|
277
|
+
this.pluginSelectorsEl = document.createElement("ul");
|
|
278
|
+
this.pluginSelectorsEl.className = "yasr_btnGroup";
|
|
279
|
+
const pluginOrder = this.config.pluginOrder;
|
|
280
|
+
Object.keys(this.config.plugins)
|
|
281
|
+
.sort()
|
|
282
|
+
.forEach((plugin) => {
|
|
283
|
+
if (pluginOrder.indexOf(plugin) === -1) pluginOrder.push(plugin);
|
|
284
|
+
});
|
|
285
|
+
for (const pluginName of pluginOrder) {
|
|
286
|
+
if (!this.config.plugins[pluginName] || !this.config.plugins[pluginName].enabled) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const plugin = this.plugins[pluginName];
|
|
290
|
+
|
|
291
|
+
if (!plugin) continue; //plugin not loaded
|
|
292
|
+
if (plugin.hideFromSelection) continue;
|
|
293
|
+
const name = plugin.label || pluginName;
|
|
294
|
+
const button = document.createElement("button");
|
|
295
|
+
addClass(button, "yasr_btn", "select_" + pluginName);
|
|
296
|
+
button.title = name;
|
|
297
|
+
button.type = "button";
|
|
298
|
+
button.setAttribute("aria-label", `Shows ${name} view`);
|
|
299
|
+
if (plugin.getIcon) {
|
|
300
|
+
const icon = plugin.getIcon();
|
|
301
|
+
if (icon) {
|
|
302
|
+
// icon.className = '';
|
|
303
|
+
addClass(icon, "plugin_icon");
|
|
304
|
+
button.appendChild(icon);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const nameEl = document.createElement("span");
|
|
308
|
+
nameEl.textContent = name;
|
|
309
|
+
button.appendChild(nameEl);
|
|
310
|
+
button.addEventListener("click", () => this.selectPlugin(pluginName));
|
|
311
|
+
const li = document.createElement("li");
|
|
312
|
+
li.appendChild(button);
|
|
313
|
+
this.pluginSelectorsEl.appendChild(li);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (this.pluginSelectorsEl.children.length >= 1) this.headerEl.appendChild(this.pluginSelectorsEl);
|
|
317
|
+
this.updatePluginSelectors();
|
|
318
|
+
}
|
|
319
|
+
private fillFallbackBox(fallbackElement?: string) {
|
|
320
|
+
this.emptyFallbackElement();
|
|
321
|
+
|
|
322
|
+
// Do not show fallback render warnings for plugins without a selector.
|
|
323
|
+
if (this.plugins[fallbackElement || this.drawnPlugin || ""]?.hideFromSelection) return;
|
|
324
|
+
|
|
325
|
+
const selectedPlugin = this.getSelectedPlugin();
|
|
326
|
+
const fallbackPluginLabel =
|
|
327
|
+
this.plugins[fallbackElement || this.drawnPlugin || ""]?.label || fallbackElement || this.drawnPlugin;
|
|
328
|
+
const selectedPluginLabel = selectedPlugin?.label || this.getSelectedPluginName();
|
|
329
|
+
|
|
330
|
+
const textElement = document.createElement("p");
|
|
331
|
+
textElement.innerText = `Could not render results with the ${selectedPluginLabel} plugin, the results currently are rendered with the ${fallbackPluginLabel} plugin. ${
|
|
332
|
+
this.getSelectedPlugin()?.helpReference ? "See " : ""
|
|
333
|
+
}`;
|
|
334
|
+
|
|
335
|
+
if (selectedPlugin?.helpReference) {
|
|
336
|
+
const linkElement = document.createElement("a");
|
|
337
|
+
linkElement.innerText = `${selectedPluginLabel} documentation`;
|
|
338
|
+
linkElement.href = selectedPlugin.helpReference;
|
|
339
|
+
linkElement.rel = "noopener noreferrer";
|
|
340
|
+
linkElement.target = "_blank";
|
|
341
|
+
textElement.append(linkElement);
|
|
342
|
+
textElement.innerHTML += " for more information.";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
this.fallbackInfoEl.appendChild(textElement);
|
|
346
|
+
}
|
|
347
|
+
private drawPluginElement() {
|
|
348
|
+
const spaceElement = document.createElement("div");
|
|
349
|
+
addClass(spaceElement, "space_element");
|
|
350
|
+
this.headerEl.appendChild(spaceElement);
|
|
351
|
+
this.pluginControls = document.createElement("div");
|
|
352
|
+
this.pluginControls.setAttribute("id", "yasr_plugin_control");
|
|
353
|
+
addClass(this.pluginControls, "yasr_plugin_control");
|
|
354
|
+
this.pluginControls.setAttribute("aria-controls", this.resultsEl.id);
|
|
355
|
+
this.headerEl.appendChild(this.pluginControls);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private drawHeader() {
|
|
359
|
+
this.drawPluginSelectors();
|
|
360
|
+
this.drawResponseInfo();
|
|
361
|
+
this.drawPluginElement();
|
|
362
|
+
this.drawDownloadIcon();
|
|
363
|
+
this.drawFullscreenButton();
|
|
364
|
+
if (this.loadingEl) {
|
|
365
|
+
this.headerEl.appendChild(this.loadingEl);
|
|
366
|
+
}
|
|
367
|
+
this.drawDocumentationButton();
|
|
368
|
+
}
|
|
369
|
+
private downloadBtn: HTMLAnchorElement | undefined;
|
|
370
|
+
private drawDownloadIcon() {
|
|
371
|
+
this.downloadBtn = document.createElement("a");
|
|
372
|
+
addClass(this.downloadBtn, "yasr_btn", "yasr_downloadIcon", "btn_icon");
|
|
373
|
+
this.downloadBtn.download = ""; // should default to the file name of the blob
|
|
374
|
+
this.downloadBtn.setAttribute("aria-label", "Download Results");
|
|
375
|
+
this.downloadBtn.setAttribute("tabindex", "0"); // anchor elements with no href are not automatically included in the tabindex
|
|
376
|
+
this.downloadBtn.setAttribute("role", "button");
|
|
377
|
+
const iconEl = drawSvgStringAsElement(drawFontAwesomeIconAsSvg(faDownload));
|
|
378
|
+
iconEl.setAttribute("aria-hidden", "true");
|
|
379
|
+
this.downloadBtn.appendChild(iconEl);
|
|
380
|
+
this.downloadBtn.addEventListener("click", () => {
|
|
381
|
+
if (hasClass(this.downloadBtn, "disabled")) return;
|
|
382
|
+
this.download();
|
|
383
|
+
});
|
|
384
|
+
this.downloadBtn.addEventListener("keydown", (event) => {
|
|
385
|
+
// needed for accessibility
|
|
386
|
+
if (event.code === "Space" || event.code === "Enter") {
|
|
387
|
+
if (hasClass(this.downloadBtn, "disabled")) return;
|
|
388
|
+
this.download();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.headerEl.appendChild(this.downloadBtn);
|
|
393
|
+
}
|
|
394
|
+
private dataElement!: HTMLDivElement;
|
|
395
|
+
private drawResponseInfo() {
|
|
396
|
+
this.dataElement = document.createElement("div");
|
|
397
|
+
addClass(this.dataElement, "yasr_response_chip");
|
|
398
|
+
this.headerEl.appendChild(this.dataElement);
|
|
399
|
+
this.updateResponseInfo();
|
|
400
|
+
}
|
|
401
|
+
private updateResponseInfo() {
|
|
402
|
+
let innerText = "";
|
|
403
|
+
if (this.results) {
|
|
404
|
+
removeClass(this.dataElement, "empty");
|
|
405
|
+
const bindings = this.results.getBindings();
|
|
406
|
+
if (bindings) {
|
|
407
|
+
innerText += `${bindings.length} result${bindings.length === 1 ? "" : "s"}`; // Set amount of results
|
|
408
|
+
}
|
|
409
|
+
const responseTime = this.results.getResponseTime();
|
|
410
|
+
if (responseTime) {
|
|
411
|
+
if (!innerText) innerText = "Response";
|
|
412
|
+
const time = responseTime / 1000;
|
|
413
|
+
innerText += ` in ${time} second${time === 1 ? "" : "s"}`;
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
addClass(this.dataElement, "empty");
|
|
417
|
+
}
|
|
418
|
+
this.dataElement.innerText = innerText;
|
|
419
|
+
}
|
|
420
|
+
private updateHelpButton() {
|
|
421
|
+
const selectedPlugin = this.getSelectedPlugin();
|
|
422
|
+
if (selectedPlugin?.helpReference) {
|
|
423
|
+
const titleLabel = `View documentation of ${selectedPlugin.label || this.getSelectedPluginName()}`;
|
|
424
|
+
this.documentationLink.href = selectedPlugin.helpReference;
|
|
425
|
+
this.documentationLink.title = titleLabel;
|
|
426
|
+
this.documentationLink.setAttribute("aria-label", titleLabel);
|
|
427
|
+
removeClass(this.documentationLink, "disabled");
|
|
428
|
+
} else {
|
|
429
|
+
addClass(this.documentationLink, "disabled");
|
|
430
|
+
this.documentationLink.title =
|
|
431
|
+
"This plugin doesn't have a help reference yet. Please contact the maintainer to fix this";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
updateExportHeaders() {
|
|
435
|
+
if (this.downloadBtn && this.drawnPlugin) {
|
|
436
|
+
this.downloadBtn.title = "";
|
|
437
|
+
const plugin = this.plugins[this.drawnPlugin];
|
|
438
|
+
if (plugin && plugin.download) {
|
|
439
|
+
const downloadInfo = plugin.download(this.config.getDownloadFileName?.());
|
|
440
|
+
removeClass(this.downloadBtn, "disabled");
|
|
441
|
+
if (downloadInfo) {
|
|
442
|
+
if (downloadInfo.title) this.downloadBtn.title = downloadInfo.title;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.downloadBtn.title = "Download not supported";
|
|
447
|
+
addClass(this.downloadBtn, "disabled");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private drawFullscreenButton() {
|
|
452
|
+
this.fullscreenBtn = document.createElement("button");
|
|
453
|
+
addClass(this.fullscreenBtn, "yasr_btn", "yasr_fullscreenButton", "btn_icon");
|
|
454
|
+
this.fullscreenBtn.setAttribute("aria-label", "Toggle fullscreen");
|
|
455
|
+
this.fullscreenBtn.setAttribute("tabindex", "0");
|
|
456
|
+
this.fullscreenBtn.setAttribute("role", "button");
|
|
457
|
+
|
|
458
|
+
const fullscreenIcon = drawSvgStringAsElement(imgs.fullscreen);
|
|
459
|
+
addClass(fullscreenIcon, "fullscreenIcon");
|
|
460
|
+
fullscreenIcon.setAttribute("aria-hidden", "true");
|
|
461
|
+
this.fullscreenBtn.appendChild(fullscreenIcon);
|
|
462
|
+
|
|
463
|
+
const fullscreenExitIcon = drawSvgStringAsElement(imgs.fullscreenExit);
|
|
464
|
+
addClass(fullscreenExitIcon, "fullscreenExitIcon");
|
|
465
|
+
fullscreenExitIcon.setAttribute("aria-hidden", "true");
|
|
466
|
+
this.fullscreenBtn.appendChild(fullscreenExitIcon);
|
|
467
|
+
|
|
468
|
+
this.fullscreenBtn.title = "Toggle fullscreen (F10)";
|
|
469
|
+
this.fullscreenBtn.addEventListener("click", () => {
|
|
470
|
+
this.toggleFullscreen();
|
|
471
|
+
});
|
|
472
|
+
this.fullscreenBtn.addEventListener("keydown", (event) => {
|
|
473
|
+
if (event.code === "Space" || event.code === "Enter") {
|
|
474
|
+
this.toggleFullscreen();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
this.headerEl.appendChild(this.fullscreenBtn);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public toggleFullscreen() {
|
|
482
|
+
this.isFullscreen = !this.isFullscreen;
|
|
483
|
+
if (this.isFullscreen) {
|
|
484
|
+
addClass(this.rootEl, "fullscreen");
|
|
485
|
+
if (this.fullscreenBtn) this.fullscreenBtn.title = "Exit fullscreen (F10)";
|
|
486
|
+
} else {
|
|
487
|
+
removeClass(this.rootEl, "fullscreen");
|
|
488
|
+
if (this.fullscreenBtn) this.fullscreenBtn.title = "Toggle fullscreen (F10)";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public getIsFullscreen() {
|
|
493
|
+
return this.isFullscreen;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private createLoadingElement() {
|
|
497
|
+
this.loadingEl = document.createElement("div");
|
|
498
|
+
this.loadingEl.className = "yasr_loading_indicator";
|
|
499
|
+
this.loadingEl.title = "Executing query...";
|
|
500
|
+
this.loadingEl.innerHTML = `<div class="yasr_loading_spinner"></div>`;
|
|
501
|
+
this.loadingEl.style.display = "none";
|
|
502
|
+
// Will be appended to header in drawHeader
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public showLoading() {
|
|
506
|
+
this.isLoading = true;
|
|
507
|
+
if (this.loadingEl) {
|
|
508
|
+
this.loadingEl.style.display = "flex";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public hideLoading() {
|
|
513
|
+
this.isLoading = false;
|
|
514
|
+
if (this.loadingEl) {
|
|
515
|
+
this.loadingEl.style.display = "none";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private documentationLink!: HTMLAnchorElement;
|
|
520
|
+
private drawDocumentationButton() {
|
|
521
|
+
this.documentationLink = document.createElement("a");
|
|
522
|
+
addClass(this.documentationLink, "yasr_btn", "yasr_external_ref_btn");
|
|
523
|
+
this.documentationLink.appendChild(drawSvgStringAsElement(drawFontAwesomeIconAsSvg(faQuestionCircle)));
|
|
524
|
+
this.documentationLink.href = "https://docs.triply.cc/yasgui/";
|
|
525
|
+
this.documentationLink.target = "_blank";
|
|
526
|
+
this.documentationLink.rel = "noopener noreferrer";
|
|
527
|
+
this.headerEl.appendChild(this.documentationLink); // We can do this as long as the help-element is the last item in the row
|
|
528
|
+
}
|
|
529
|
+
download() {
|
|
530
|
+
if (!this.drawnPlugin) return;
|
|
531
|
+
const currentPlugin = this.plugins[this.drawnPlugin];
|
|
532
|
+
if (currentPlugin && currentPlugin.download) {
|
|
533
|
+
const downloadInfo = currentPlugin.download(this.config.getDownloadFileName?.());
|
|
534
|
+
if (!downloadInfo) return;
|
|
535
|
+
const data = downloadInfo.getData();
|
|
536
|
+
let downloadUrl: string;
|
|
537
|
+
if (data.startsWith("data:")) {
|
|
538
|
+
downloadUrl = data;
|
|
539
|
+
} else {
|
|
540
|
+
const blob = new Blob([data], { type: downloadInfo.contentType ?? "text/plain" });
|
|
541
|
+
downloadUrl = window.URL.createObjectURL(blob);
|
|
542
|
+
}
|
|
543
|
+
const mockLink = document.createElement("a");
|
|
544
|
+
mockLink.href = downloadUrl;
|
|
545
|
+
mockLink.download = downloadInfo.filename;
|
|
546
|
+
mockLink.click();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
public handleLocalStorageQuotaFull(_e: any) {
|
|
551
|
+
console.warn("Localstorage quota exceeded. Clearing all queries");
|
|
552
|
+
Yasr.clearStorage();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
public getResponseFromStorage() {
|
|
556
|
+
const storageId = this.getStorageId(this.config.persistenceLabelResponse);
|
|
557
|
+
if (storageId) {
|
|
558
|
+
return this.storage.get(storageId);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
public getPersistentConfig(): PersistentConfig {
|
|
562
|
+
return {
|
|
563
|
+
selectedPlugin: this.getSelectedPluginName(),
|
|
564
|
+
pluginsConfig: mapValues(this.config.plugins, (plugin) => plugin.dynamicConfig),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
//This doesnt store the plugin complete config. Only those configs we want persisted
|
|
568
|
+
public storePluginConfig(pluginName: string, conf: any) {
|
|
569
|
+
this.config.plugins[pluginName].dynamicConfig = conf;
|
|
570
|
+
this.storeConfig();
|
|
571
|
+
this.emit("change", this);
|
|
572
|
+
}
|
|
573
|
+
private storeConfig() {
|
|
574
|
+
const storageId = this.getStorageId(this.config.persistenceLabelConfig);
|
|
575
|
+
if (storageId) {
|
|
576
|
+
this.storage.set(
|
|
577
|
+
storageId,
|
|
578
|
+
this.getPersistentConfig(),
|
|
579
|
+
this.config.persistencyExpire,
|
|
580
|
+
this.handleLocalStorageQuotaFull,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
private storeResponse() {
|
|
585
|
+
const storageId = this.getStorageId(this.config.persistenceLabelResponse);
|
|
586
|
+
if (storageId && this.results) {
|
|
587
|
+
const storeObj = this.results.getAsStoreObject(this.config.maxPersistentResponseSize);
|
|
588
|
+
if (storeObj && !storeObj.error) {
|
|
589
|
+
this.storage.set(storageId, storeObj, this.config.persistencyExpire, this.handleLocalStorageQuotaFull);
|
|
590
|
+
} else {
|
|
591
|
+
//remove old string;
|
|
592
|
+
this.storage.remove(storageId);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
public setResponse(data: any, duration?: number) {
|
|
597
|
+
if (!data) return;
|
|
598
|
+
this.hideLoading();
|
|
599
|
+
this.results = new Parser(data, duration);
|
|
600
|
+
|
|
601
|
+
this.draw();
|
|
602
|
+
|
|
603
|
+
this.storeResponse();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private initializePlugins() {
|
|
607
|
+
for (const plugin in this.config.plugins) {
|
|
608
|
+
if (!this.config.plugins[plugin]) continue; //falsy value, so assuming it should be disabled
|
|
609
|
+
if (Yasr.plugins[plugin]) {
|
|
610
|
+
this.plugins[plugin] = new (<any>Yasr.plugins[plugin])(this);
|
|
611
|
+
} else {
|
|
612
|
+
console.warn("Wanted to initialize plugin " + plugin + " but could not find a matching registered plugin");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
static defaults: Config = getDefaults();
|
|
618
|
+
static plugins: { [key: string]: typeof Plugin & { defaults?: any } } = {};
|
|
619
|
+
static registerPlugin(name: string, plugin: typeof Plugin, enable = true) {
|
|
620
|
+
Yasr.plugins[name] = plugin;
|
|
621
|
+
if (enable) {
|
|
622
|
+
Yasr.defaults.plugins[name] = { enabled: true };
|
|
623
|
+
} else {
|
|
624
|
+
Yasr.defaults.plugins[name] = { enabled: false };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Collection of Promises to load external scripts used by Yasr Plugins
|
|
629
|
+
* That way, the plugins wont load similar scripts simultaneously
|
|
630
|
+
*/
|
|
631
|
+
static Dependencies: { [name: string]: Promise<any> } = {};
|
|
632
|
+
static storageNamespace = "triply";
|
|
633
|
+
static clearStorage() {
|
|
634
|
+
const storage = new YStorage(Yasr.storageNamespace);
|
|
635
|
+
storage.removeNamespace();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export type Prefixes = { [prefixLabel: string]: string };
|
|
640
|
+
export interface PluginConfig {
|
|
641
|
+
dynamicConfig?: any;
|
|
642
|
+
staticConfig?: any;
|
|
643
|
+
enabled?: boolean;
|
|
644
|
+
}
|
|
645
|
+
export interface Config {
|
|
646
|
+
persistenceId: ((yasr: Yasr) => string) | string | null;
|
|
647
|
+
persistenceLabelResponse: string;
|
|
648
|
+
persistenceLabelConfig: string;
|
|
649
|
+
maxPersistentResponseSize: number;
|
|
650
|
+
persistencyExpire: number;
|
|
651
|
+
getPlainQueryLinkToEndpoint: (() => string | undefined) | undefined;
|
|
652
|
+
getDownloadFileName?: () => string | undefined;
|
|
653
|
+
plugins: { [pluginName: string]: PluginConfig };
|
|
654
|
+
pluginOrder: string[];
|
|
655
|
+
defaultPlugin: string;
|
|
656
|
+
|
|
657
|
+
prefixes: Prefixes | ((yasr: Yasr) => Prefixes);
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Custom renderers for errors.
|
|
661
|
+
* Allow multiple to be able to add new custom renderers without having to
|
|
662
|
+
* overwrite or explicitly call previously added or default ones.
|
|
663
|
+
*/
|
|
664
|
+
errorRenderers?: ((error: Parser.ErrorSummary) => Promise<HTMLElement | undefined>)[];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function registerPlugin(name: string, plugin: typeof Plugin, enable = true) {
|
|
668
|
+
Yasr.plugins[name] = plugin;
|
|
669
|
+
if (enable) {
|
|
670
|
+
Yasr.defaults.plugins[name] = { enabled: true };
|
|
671
|
+
} else {
|
|
672
|
+
Yasr.defaults.plugins[name] = { enabled: false };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
import * as YasrPluginTable from "./plugins/table";
|
|
677
|
+
import * as YasrPluginBoolean from "./plugins/boolean";
|
|
678
|
+
import * as YasrPluginResponse from "./plugins/response";
|
|
679
|
+
import * as YasrPluginError from "./plugins/error";
|
|
680
|
+
|
|
681
|
+
Yasr.registerPlugin("table", YasrPluginTable.default as any);
|
|
682
|
+
Yasr.registerPlugin("boolean", YasrPluginBoolean.default as any);
|
|
683
|
+
Yasr.registerPlugin("response", YasrPluginResponse.default as any);
|
|
684
|
+
Yasr.registerPlugin("error", YasrPluginError.default as any);
|
|
685
|
+
|
|
686
|
+
export { Plugin, DownloadInfo } from "./plugins";
|
|
687
|
+
|
|
688
|
+
export default Yasr;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require("./tableToCsv.js");
|