@matdata/yasgui 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasgui",
3
3
  "description": "Yet Another SPARQL GUI",
4
- "version": "5.1.0",
4
+ "version": "5.3.0",
5
5
  "main": "build/yasgui.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Export/Import Configuration Module
3
+ * Handles serialization and deserialization of YASGUI configuration to/from RDF Turtle format
4
+ */
5
+
6
+ import { PersistedJson } from "./PersistentConfig";
7
+
8
+ // YASGUI Configuration Ontology
9
+ export const YASGUI_NS = "https://yasgui.matdata.eu/ontology#";
10
+ export const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
11
+ export const RDFS_NS = "http://www.w3.org/2000/01/rdf-schema#";
12
+ export const XSD_NS = "http://www.w3.org/2001/XMLSchema#";
13
+ export const DCTERMS_NS = "http://purl.org/dc/terms/";
14
+ export const SD_NS = "http://www.w3.org/ns/sparql-service-description#";
15
+ export const SP_NS = "http://spinrdf.org/sp#";
16
+ export const HTTP_NS = "http://www.w3.org/2011/http#";
17
+ export const SCHEMA_NS = "https://schema.org/";
18
+
19
+ /**
20
+ * Serialize configuration to Turtle format
21
+ */
22
+ export function serializeToTurtle(config: PersistedJson): string {
23
+ const lines: string[] = [];
24
+
25
+ // Prefixes
26
+ lines.push(`@prefix yasgui: <${YASGUI_NS}> .`);
27
+ lines.push(`@prefix rdf: <${RDF_NS}> .`);
28
+ lines.push(`@prefix rdfs: <${RDFS_NS}> .`);
29
+ lines.push(`@prefix xsd: <${XSD_NS}> .`);
30
+ lines.push(`@prefix dcterms: <${DCTERMS_NS}> .`);
31
+ lines.push(`@prefix sd: <${SD_NS}> .`);
32
+ lines.push(`@prefix sp: <${SP_NS}> .`);
33
+ lines.push(`@prefix http: <${HTTP_NS}> .`);
34
+ lines.push(`@prefix schema: <${SCHEMA_NS}> .`);
35
+ lines.push("");
36
+
37
+ // Main configuration node
38
+ const now = new Date().toISOString();
39
+ lines.push(`[] a yasgui:Configuration ;`);
40
+ lines.push(` dcterms:created "${now}"^^xsd:dateTime ;`);
41
+
42
+ // Theme
43
+ if (config.theme) {
44
+ lines.push(` yasgui:theme "${config.theme}" ;`);
45
+ }
46
+
47
+ // Orientation
48
+ if (config.orientation) {
49
+ lines.push(` yasgui:orientation "${config.orientation}" ;`);
50
+ }
51
+
52
+ // Endpoint history
53
+ if (config.endpointHistory && config.endpointHistory.length > 0) {
54
+ lines.push(` yasgui:endpointHistory (`);
55
+ config.endpointHistory.forEach((endpoint) => {
56
+ lines.push(` "${escapeTurtleString(endpoint)}"`);
57
+ });
58
+ lines.push(` ) ;`);
59
+ }
60
+
61
+ // Active tab
62
+ if (config.active) {
63
+ lines.push(` yasgui:activeTab "${escapeTurtleString(config.active)}" ;`);
64
+ }
65
+
66
+ // Prefixes
67
+ if (config.prefixes) {
68
+ lines.push(` yasgui:prefixesValue """${escapeTurtleString(config.prefixes)}""" ;`);
69
+ }
70
+
71
+ // Auto capture enabled
72
+ if (config.autoCaptureEnabled !== undefined) {
73
+ lines.push(` yasgui:autoCaptureEnabled "${config.autoCaptureEnabled}"^^xsd:boolean ;`);
74
+ }
75
+
76
+ // Custom endpoint buttons
77
+ if (config.customEndpointButtons && config.customEndpointButtons.length > 0) {
78
+ lines.push(` yasgui:customEndpointButton [`);
79
+ config.customEndpointButtons.forEach((button, index) => {
80
+ const isLast = index === config.customEndpointButtons!.length - 1;
81
+ lines.push(` rdfs:label "${escapeTurtleString(button.label)}" ;`);
82
+ lines.push(` sd:endpoint "${escapeTurtleString(button.endpoint)}"${isLast ? "" : " ;"}`);
83
+ if (!isLast) {
84
+ lines.push(` ] , [`);
85
+ }
86
+ });
87
+ lines.push(` ] ;`);
88
+ }
89
+
90
+ // Tabs
91
+ if (config.tabs && config.tabs.length > 0) {
92
+ lines.push(` yasgui:tab [`);
93
+ config.tabs.forEach((tabId, tabIndex) => {
94
+ const tabConfig = config.tabConfig[tabId];
95
+ const isLastTab = tabIndex === config.tabs.length - 1;
96
+
97
+ lines.push(` dcterms:identifier "${escapeTurtleString(tabId)}" ;`);
98
+ lines.push(` rdfs:label "${escapeTurtleString(tabConfig.name)}" ;`);
99
+
100
+ // Orientation (if different from default)
101
+ if (tabConfig.orientation) {
102
+ lines.push(` yasgui:orientation "${escapeTurtleString(tabConfig.orientation)}" ;`);
103
+ }
104
+
105
+ // Query
106
+ if (tabConfig.yasqe?.value) {
107
+ lines.push(` sp:text """${escapeTurtleString(tabConfig.yasqe.value)}""" ;`);
108
+ }
109
+
110
+ // Editor height
111
+ if (tabConfig.yasqe?.editorHeight) {
112
+ lines.push(` schema:height "${escapeTurtleString(tabConfig.yasqe.editorHeight)}" ;`);
113
+ }
114
+
115
+ // Request config
116
+ if (tabConfig.requestConfig) {
117
+ const reqConfig = tabConfig.requestConfig;
118
+ if (reqConfig.endpoint && typeof reqConfig.endpoint === "string") {
119
+ lines.push(` sd:endpoint "${escapeTurtleString(reqConfig.endpoint)}" ;`);
120
+ }
121
+ if (reqConfig.method && typeof reqConfig.method === "string") {
122
+ lines.push(` yasgui:requestMethod "${escapeTurtleString(reqConfig.method)}" ;`);
123
+ }
124
+ if (reqConfig.acceptHeaderSelect && typeof reqConfig.acceptHeaderSelect === "string") {
125
+ lines.push(` yasgui:acceptHeaderSelect [`);
126
+ lines.push(` http:headerName "Accept" ;`);
127
+ lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderSelect)}"`);
128
+ lines.push(` ] ;`);
129
+ }
130
+ if (reqConfig.acceptHeaderGraph && typeof reqConfig.acceptHeaderGraph === "string") {
131
+ lines.push(` yasgui:acceptHeaderGraph [`);
132
+ lines.push(` http:headerName "Accept" ;`);
133
+ lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderGraph)}"`);
134
+ lines.push(` ] ;`);
135
+ }
136
+ }
137
+
138
+ // YASR settings
139
+ if (tabConfig.yasr?.settings) {
140
+ const yasrSettings = tabConfig.yasr.settings;
141
+ if (yasrSettings.selectedPlugin) {
142
+ lines.push(` yasgui:selectedPlugin "${escapeTurtleString(yasrSettings.selectedPlugin)}" ;`);
143
+ }
144
+ }
145
+
146
+ // Remove trailing semicolon from last property
147
+ const lastLine = lines[lines.length - 1];
148
+ if (lastLine.endsWith(" ;")) {
149
+ lines[lines.length - 1] = lastLine.slice(0, -2);
150
+ }
151
+
152
+ if (!isLastTab) {
153
+ lines.push(` ] , [`);
154
+ }
155
+ });
156
+ lines.push(` ] .`);
157
+ } else {
158
+ // Remove trailing semicolon if no tabs
159
+ const lastLine = lines[lines.length - 1];
160
+ if (lastLine.endsWith(" ;")) {
161
+ lines[lines.length - 1] = lastLine.slice(0, -2) + " .";
162
+ }
163
+ }
164
+
165
+ return lines.join("\n");
166
+ }
167
+
168
+ /**
169
+ * Escape special characters in Turtle strings
170
+ */
171
+ function escapeTurtleString(str: string): string {
172
+ return str
173
+ .replace(/\\/g, "\\\\")
174
+ .replace(/"/g, '\\"')
175
+ .replace(/\n/g, "\\n")
176
+ .replace(/\r/g, "\\r")
177
+ .replace(/\t/g, "\\t");
178
+ }
179
+
180
+ /**
181
+ * Unescape Turtle string
182
+ */
183
+ function unescapeTurtleString(str: string): string {
184
+ return str
185
+ .replace(/\\n/g, "\n")
186
+ .replace(/\\r/g, "\r")
187
+ .replace(/\\t/g, "\t")
188
+ .replace(/\\"/g, '"')
189
+ .replace(/\\\\/g, "\\");
190
+ }
191
+
192
+ /**
193
+ * Parse Turtle format back to configuration
194
+ * This is a simplified parser focused on the structure we generate.
195
+ *
196
+ * Note: This parser is designed to handle the specific Turtle format
197
+ * produced by serializeToTurtle(). For production use with arbitrary
198
+ * Turtle input, consider using a robust RDF library like N3.js.
199
+ * The current implementation uses regex patterns that work well for
200
+ * our serialization output but may not handle all valid Turtle syntax.
201
+ */
202
+ export function parseFromTurtle(turtle: string): Partial<PersistedJson> {
203
+ const config: Partial<PersistedJson> = {
204
+ endpointHistory: [],
205
+ tabs: [],
206
+ tabConfig: {},
207
+ customEndpointButtons: [],
208
+ };
209
+
210
+ try {
211
+ // Extract endpoint history
212
+ const endpointHistoryMatch = turtle.match(/yasgui:endpointHistory\s*\(([\s\S]*?)\)/);
213
+ if (endpointHistoryMatch) {
214
+ const endpoints = endpointHistoryMatch[1].match(/"([^"]*)"/g);
215
+ if (endpoints) {
216
+ config.endpointHistory = endpoints.map((e) => unescapeTurtleString(e.slice(1, -1)));
217
+ }
218
+ }
219
+
220
+ // Extract active tab
221
+ const activeTabMatch = turtle.match(/yasgui:activeTab\s+"([^"]*)"/);
222
+ if (activeTabMatch) {
223
+ config.active = unescapeTurtleString(activeTabMatch[1]);
224
+ }
225
+
226
+ // Extract prefixes
227
+ const prefixesMatch = turtle.match(/yasgui:prefixesValue\s+"""([\s\S]*?)"""/);
228
+ if (prefixesMatch) {
229
+ config.prefixes = unescapeTurtleString(prefixesMatch[1]);
230
+ }
231
+
232
+ // Extract auto capture enabled
233
+ const autoCaptureMatch = turtle.match(/yasgui:autoCaptureEnabled\s+"([^"]*)"/);
234
+ if (autoCaptureMatch) {
235
+ config.autoCaptureEnabled = autoCaptureMatch[1] === "true";
236
+ }
237
+
238
+ // Extract theme
239
+ const themeMatch = turtle.match(/yasgui:theme\s+"([^"]*)"/);
240
+ if (themeMatch && (themeMatch[1] === "light" || themeMatch[1] === "dark")) {
241
+ config.theme = themeMatch[1] as "light" | "dark";
242
+ }
243
+
244
+ // Extract orientation
245
+ const orientationMatch = turtle.match(/yasgui:orientation\s+"([^"]*)"/);
246
+ if (orientationMatch && (orientationMatch[1] === "vertical" || orientationMatch[1] === "horizontal")) {
247
+ config.orientation = orientationMatch[1] as "vertical" | "horizontal";
248
+ }
249
+
250
+ // Extract custom endpoint buttons
251
+ const buttonPattern =
252
+ /yasgui:customEndpointButton\s+\[([\s\S]*?)rdfs:label\s+"([^"]*)"\s*;\s*sd:endpoint\s+"([^"]*)"/g;
253
+ let buttonMatch;
254
+ while ((buttonMatch = buttonPattern.exec(turtle)) !== null) {
255
+ config.customEndpointButtons!.push({
256
+ label: unescapeTurtleString(buttonMatch[2]),
257
+ endpoint: unescapeTurtleString(buttonMatch[3]),
258
+ });
259
+ }
260
+
261
+ // Extract tabs - simplified parsing
262
+ const tabPattern =
263
+ /dcterms:identifier\s+"([^"]*)"\s*;\s*rdfs:label\s+"([^"]*)"\s*;[\s\S]*?(?=dcterms:identifier|yasgui:customEndpointButton|\]\.|\]\s*,\s*\[)/g;
264
+ const tabBlocks = turtle.match(/yasgui:tab\s+\[([\s\S]*)\]\s*\./);
265
+
266
+ if (tabBlocks) {
267
+ const tabsContent = tabBlocks[1];
268
+ const tabIdMatches = [...tabsContent.matchAll(/dcterms:identifier\s+"([^"]*)"/g)];
269
+ const tabNameMatches = [...tabsContent.matchAll(/rdfs:label\s+"([^"]*)"/g)];
270
+ const queryMatches = [...tabsContent.matchAll(/sp:text\s+"""([\s\S]*?)"""/g)];
271
+ const endpointMatches = [...tabsContent.matchAll(/sd:endpoint\s+"([^"]*)"/g)];
272
+ const methodMatches = [...tabsContent.matchAll(/yasgui:requestMethod\s+"([^"]*)"/g)];
273
+
274
+ // Extract tab-level orientation
275
+ const tabOrientationMatches = [...tabsContent.matchAll(/yasgui:orientation\s+"([^"]*)"/g)];
276
+
277
+ tabIdMatches.forEach((match, index) => {
278
+ const tabId = unescapeTurtleString(match[1]);
279
+ const tabName = tabNameMatches[index] ? unescapeTurtleString(tabNameMatches[index][1]) : "Query";
280
+ const query = queryMatches[index] ? unescapeTurtleString(queryMatches[index][1]) : "";
281
+ const endpoint = endpointMatches[index] ? unescapeTurtleString(endpointMatches[index][1]) : "";
282
+ const method = methodMatches[index] ? unescapeTurtleString(methodMatches[index][1]) : "POST";
283
+ const orientation = tabOrientationMatches[index]
284
+ ? (unescapeTurtleString(tabOrientationMatches[index][1]) as "vertical" | "horizontal")
285
+ : undefined;
286
+
287
+ config.tabs!.push(tabId);
288
+ config.tabConfig![tabId] = {
289
+ id: tabId,
290
+ name: tabName,
291
+ yasqe: {
292
+ value: query,
293
+ },
294
+ requestConfig: {
295
+ queryArgument: undefined,
296
+ endpoint: endpoint,
297
+ method: method as "GET" | "POST",
298
+ acceptHeaderGraph: "text/turtle",
299
+ acceptHeaderSelect: "application/sparql-results+json",
300
+ acceptHeaderUpdate: "application/sparql-results+json",
301
+ namedGraphs: [],
302
+ defaultGraphs: [],
303
+ args: [],
304
+ headers: {},
305
+ withCredentials: false,
306
+ adjustQueryBeforeRequest: false,
307
+ },
308
+ yasr: {
309
+ settings: {},
310
+ response: undefined,
311
+ },
312
+ orientation: orientation,
313
+ };
314
+ });
315
+ }
316
+ } catch (error) {
317
+ console.error("Error parsing Turtle configuration:", error);
318
+ throw new Error("Failed to parse configuration. Please check the format.");
319
+ }
320
+
321
+ return config;
322
+ }
323
+
324
+ /**
325
+ * Download configuration as a file
326
+ */
327
+ export function downloadConfigAsFile(config: PersistedJson, filename: string = "yasgui-config.ttl") {
328
+ const turtle = serializeToTurtle(config);
329
+ const blob = new Blob([turtle], { type: "text/turtle;charset=utf-8" });
330
+ const url = URL.createObjectURL(blob);
331
+
332
+ const link = document.createElement("a");
333
+ link.href = url;
334
+ link.download = filename;
335
+ document.body.appendChild(link);
336
+ link.click();
337
+ document.body.removeChild(link);
338
+ URL.revokeObjectURL(url);
339
+ }
340
+
341
+ /**
342
+ * Copy configuration to clipboard
343
+ */
344
+ export async function copyConfigToClipboard(config: PersistedJson): Promise<void> {
345
+ const turtle = serializeToTurtle(config);
346
+ try {
347
+ await navigator.clipboard.writeText(turtle);
348
+ } catch (error) {
349
+ // Fallback for older browsers
350
+ const textArea = document.createElement("textarea");
351
+ textArea.value = turtle;
352
+ textArea.style.position = "fixed";
353
+ textArea.style.left = "-999999px";
354
+ document.body.appendChild(textArea);
355
+ textArea.select();
356
+ try {
357
+ document.execCommand("copy");
358
+ } finally {
359
+ document.body.removeChild(textArea);
360
+ }
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Read configuration from file
366
+ */
367
+ export function readConfigFromFile(file: File): Promise<string> {
368
+ return new Promise((resolve, reject) => {
369
+ const reader = new FileReader();
370
+ reader.onload = (e) => {
371
+ if (e.target?.result) {
372
+ resolve(e.target.result as string);
373
+ } else {
374
+ reject(new Error("Failed to read file"));
375
+ }
376
+ };
377
+ reader.onerror = () => reject(new Error("Failed to read file"));
378
+ reader.readAsText(file);
379
+ });
380
+ }
381
+
382
+ /**
383
+ * Read configuration from clipboard
384
+ */
385
+ export async function readConfigFromClipboard(): Promise<string> {
386
+ try {
387
+ return await navigator.clipboard.readText();
388
+ } catch (error) {
389
+ throw new Error("Failed to read from clipboard. Please paste the content manually.");
390
+ }
391
+ }
@@ -1,5 +1,5 @@
1
1
  import { Storage as YStorage } from "@matdata/yasgui-utils";
2
- import Yasgui from "./";
2
+ import Yasgui, { EndpointButton } from "./";
3
3
  import * as Tab from "./Tab";
4
4
  export var storageNamespace = "triply";
5
5
  export interface PersistedJson {
@@ -10,6 +10,9 @@ export interface PersistedJson {
10
10
  lastClosedTab: { index: number; tab: Tab.PersistedJson } | undefined;
11
11
  prefixes?: string;
12
12
  autoCaptureEnabled?: boolean;
13
+ customEndpointButtons?: EndpointButton[];
14
+ theme?: "light" | "dark";
15
+ orientation?: "vertical" | "horizontal";
13
16
  }
14
17
  function getDefaults(): PersistedJson {
15
18
  return {
@@ -20,6 +23,7 @@ function getDefaults(): PersistedJson {
20
23
  lastClosedTab: undefined,
21
24
  prefixes: "",
22
25
  autoCaptureEnabled: true,
26
+ customEndpointButtons: [],
23
27
  };
24
28
  }
25
29
 
@@ -152,8 +156,30 @@ export default class PersistentConfig {
152
156
  this.persistedJson.autoCaptureEnabled = enabled;
153
157
  this.toStorage();
154
158
  }
159
+ public getCustomEndpointButtons(): EndpointButton[] {
160
+ return this.persistedJson.customEndpointButtons || [];
161
+ }
162
+ public setCustomEndpointButtons(buttons: EndpointButton[]) {
163
+ this.persistedJson.customEndpointButtons = buttons;
164
+ this.toStorage();
165
+ }
155
166
  public static clear() {
156
167
  const storage = new YStorage(storageNamespace);
157
168
  storage.removeNamespace();
158
169
  }
170
+
171
+ /**
172
+ * Get the current persisted configuration (for export purposes)
173
+ */
174
+ public getPersistedConfig(): PersistedJson {
175
+ return this.persistedJson;
176
+ }
177
+
178
+ /**
179
+ * Update the persisted configuration (for import purposes)
180
+ */
181
+ public updatePersistedConfig(config: Partial<PersistedJson>) {
182
+ Object.assign(this.persistedJson, config);
183
+ this.toStorage();
184
+ }
159
185
  }
package/src/Tab.ts CHANGED
@@ -9,6 +9,17 @@ import * as shareLink from "./linkUtils";
9
9
  import EndpointSelect from "./endpointSelect";
10
10
  import "./tab.scss";
11
11
  import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
12
+
13
+ // Layout orientation toggle icons
14
+ const HORIZONTAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
15
+ <rect x="2" y="4" width="9" height="16" stroke="currentColor" stroke-width="2" fill="none"/>
16
+ <rect x="13" y="4" width="9" height="16" stroke="currentColor" stroke-width="2" fill="none"/>
17
+ </svg>`;
18
+
19
+ const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
20
+ <rect x="2" y="2" width="20" height="8" stroke="currentColor" stroke-width="2" fill="none"/>
21
+ <rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
22
+ </svg>`;
12
23
  export interface PersistedJsonYasr extends YasrPersistentConfig {
13
24
  responseSummary: Parser.ResponseSummary;
14
25
  }
@@ -24,6 +35,7 @@ export interface PersistedJson {
24
35
  response: Parser.ResponseSummary | undefined;
25
36
  };
26
37
  requestConfig: YasguiRequestConfig;
38
+ orientation?: "vertical" | "horizontal";
27
39
  }
28
40
  export interface Tab {
29
41
  on(event: string | symbol, listener: (...args: any[]) => void): this;
@@ -57,12 +69,16 @@ export class Tab extends EventEmitter {
57
69
  private yasqeWrapperEl: HTMLDivElement | undefined;
58
70
  private yasrWrapperEl: HTMLDivElement | undefined;
59
71
  private endpointSelect: EndpointSelect | undefined;
72
+ private endpointButtonsContainer: HTMLDivElement | undefined;
60
73
  private settingsModal?: TabSettingsModal;
74
+ private currentOrientation: "vertical" | "horizontal";
75
+ private orientationToggleButton?: HTMLButtonElement;
61
76
  constructor(yasgui: Yasgui, conf: PersistedJson) {
62
77
  super();
63
78
  if (!conf || conf.id === undefined) throw new Error("Expected a valid configuration to initialize tab with");
64
79
  this.yasgui = yasgui;
65
80
  this.persistentJson = conf;
81
+ this.currentOrientation = this.yasgui.config.orientation || "vertical";
66
82
  }
67
83
  public name() {
68
84
  return this.persistentJson.name;
@@ -81,6 +97,9 @@ export class Tab extends EventEmitter {
81
97
  this.rootEl.setAttribute("role", "tabpanel");
82
98
  this.rootEl.setAttribute("aria-labelledby", "tab-" + this.persistentJson.id);
83
99
 
100
+ // Apply orientation class
101
+ addClass(this.rootEl, `orientation-${this.currentOrientation}`);
102
+
84
103
  // We group controlbar and Yasqe, so that users can easily .appendChild() to the .editorwrapper div
85
104
  // to add a div that goes alongside the controlbar and editor, while YASR still goes full width
86
105
  // Useful for adding an infos div that goes alongside the editor without needing to rebuild the whole Yasgui class
@@ -99,6 +118,7 @@ export class Tab extends EventEmitter {
99
118
 
100
119
  //yasr
101
120
  this.yasrWrapperEl = document.createElement("div");
121
+ this.yasrWrapperEl.className = "yasrWrapperEl";
102
122
 
103
123
  this.initTabSettingsMenu();
104
124
  this.rootEl.appendChild(editorWrapper);
@@ -151,8 +171,8 @@ export class Tab extends EventEmitter {
151
171
  }
152
172
  }
153
173
  }
154
- // Ctrl+Shift+F - Switch between fullscreen modes
155
- else if (event.ctrlKey && event.shiftKey && event.key === "F") {
174
+ // F9 - Switch between fullscreen modes
175
+ else if (event.key === "F9") {
156
176
  event.preventDefault();
157
177
  const yasqeFullscreen = this.yasqe?.getIsFullscreen();
158
178
  const yasrFullscreen = this.yasr?.getIsFullscreen();
@@ -222,10 +242,63 @@ export class Tab extends EventEmitter {
222
242
  }
223
243
  private initControlbar() {
224
244
  this.initEndpointSelectField();
245
+ this.initOrientationToggle();
246
+ this.initEndpointButtons();
225
247
  if (this.yasgui.config.endpointInfo && this.controlBarEl) {
226
248
  this.controlBarEl.appendChild(this.yasgui.config.endpointInfo());
227
249
  }
228
250
  }
251
+
252
+ private initOrientationToggle() {
253
+ if (!this.controlBarEl) return;
254
+
255
+ this.orientationToggleButton = document.createElement("button");
256
+ this.orientationToggleButton.className = "tabContextButton orientationToggle";
257
+ this.orientationToggleButton.setAttribute("aria-label", "Toggle layout orientation");
258
+ this.orientationToggleButton.title = "Toggle layout orientation";
259
+
260
+ this.updateOrientationToggleIcon();
261
+
262
+ this.orientationToggleButton.addEventListener("click", () => {
263
+ this.toggleOrientation();
264
+ });
265
+
266
+ this.controlBarEl.appendChild(this.orientationToggleButton);
267
+ }
268
+
269
+ private updateOrientationToggleIcon() {
270
+ if (!this.orientationToggleButton) return;
271
+
272
+ // Show the icon for the layout we'll switch TO (not the current layout)
273
+ this.orientationToggleButton.innerHTML =
274
+ this.currentOrientation === "vertical" ? HORIZONTAL_LAYOUT_ICON : VERTICAL_LAYOUT_ICON;
275
+ this.orientationToggleButton.title =
276
+ this.currentOrientation === "vertical" ? "Switch to horizontal layout" : "Switch to vertical layout";
277
+ }
278
+
279
+ public toggleOrientation() {
280
+ if (!this.rootEl) return;
281
+
282
+ // Remove old orientation class
283
+ removeClass(this.rootEl, `orientation-${this.currentOrientation}`);
284
+
285
+ // Toggle orientation
286
+ this.currentOrientation = this.currentOrientation === "vertical" ? "horizontal" : "vertical";
287
+
288
+ // Add new orientation class
289
+ addClass(this.rootEl, `orientation-${this.currentOrientation}`);
290
+
291
+ // Update button icon
292
+ this.updateOrientationToggleIcon();
293
+
294
+ // Refresh components to adjust to new layout
295
+ if (this.yasqe) {
296
+ this.yasqe.refresh();
297
+ }
298
+ if (this.yasr) {
299
+ this.yasr.refresh();
300
+ }
301
+ }
229
302
  public getYasqe() {
230
303
  return this.yasqe;
231
304
  }
@@ -253,6 +326,54 @@ export class Tab extends EventEmitter {
253
326
  });
254
327
  }
255
328
 
329
+ private initEndpointButtons() {
330
+ if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing endpoint buttons");
331
+
332
+ // Create container if it doesn't exist
333
+ if (!this.endpointButtonsContainer) {
334
+ this.endpointButtonsContainer = document.createElement("div");
335
+ addClass(this.endpointButtonsContainer, "endpointButtonsContainer");
336
+ this.controlBarEl.appendChild(this.endpointButtonsContainer);
337
+ }
338
+
339
+ this.refreshEndpointButtons();
340
+ }
341
+
342
+ public refreshEndpointButtons() {
343
+ if (!this.endpointButtonsContainer) return;
344
+
345
+ // Clear existing buttons
346
+ this.endpointButtonsContainer.innerHTML = "";
347
+
348
+ // Merge config buttons with custom user buttons
349
+ const configButtons = this.yasgui.config.endpointButtons || [];
350
+ const customButtons = this.yasgui.persistentConfig.getCustomEndpointButtons();
351
+ const allButtons = [...configButtons, ...customButtons];
352
+
353
+ if (allButtons.length === 0) {
354
+ // Hide container if no buttons
355
+ this.endpointButtonsContainer.style.display = "none";
356
+ return;
357
+ }
358
+
359
+ // Show container
360
+ this.endpointButtonsContainer.style.display = "flex";
361
+
362
+ allButtons.forEach((buttonConfig) => {
363
+ const button = document.createElement("button");
364
+ addClass(button, "endpointButton");
365
+ button.textContent = buttonConfig.label;
366
+ button.title = `Set endpoint to ${buttonConfig.endpoint}`;
367
+ button.setAttribute("aria-label", `Set endpoint to ${buttonConfig.endpoint}`);
368
+
369
+ button.addEventListener("click", () => {
370
+ this.setEndpoint(buttonConfig.endpoint);
371
+ });
372
+
373
+ this.endpointButtonsContainer!.appendChild(button);
374
+ });
375
+ }
376
+
256
377
  private checkEndpointForCors(endpoint: string) {
257
378
  if (this.yasgui.config.corsProxy && !(endpoint in Yasgui.corsEnabled)) {
258
379
  const askUrl = new URL(endpoint);