import { addClass, drawSvgStringAsElement, removeClass } from "@triply/yasgui-utils"; import "./TabPanel.scss"; import Tab from "./Tab"; import { RequestConfig } from "@triply/yasqe"; import { toPairs, fromPairs } from "lodash-es"; const AcceptOptionsMap: { key: string; value: string }[] = [ { key: "JSON", value: "application/sparql-results+json" }, { key: "XML", value: "application/sparql-results+xml" }, { key: "CSV", value: "text/csv" }, { key: "TSV", value: "text/tab-separated-values" }, ]; const AcceptHeaderGraphMap: { key: string; value: string }[] = [ { key: "Turtle", value: "text/turtle" }, { key: "JSON", value: "application/rdf+json" }, { key: "RDF/XML", value: "application/rdf+xml" }, { key: "TriG", value: "application/trig" }, { key: "N-Triples", value: "application/n-triples" }, { key: "N-Quads", value: "application/n-quads" }, { key: "CSV", value: "text/csv" }, { key: "TSV", value: "text/tab-separated-values" }, ]; type TextInputPair = { name: string; value: string }; export default class TabPanel { menuElement!: HTMLElement; settingsButton!: HTMLButtonElement; tab: Tab; rootEl: HTMLElement; isOpen: boolean; constructor(tab: Tab, rootEl: HTMLElement, controlBarEl: HTMLElement) { this.tab = tab; this.rootEl = rootEl; this.isOpen = false; this.init(controlBarEl); } private init(controlBarEl: HTMLElement) { this.settingsButton = document.createElement("button"); this.toggleAriaSettings(); this.settingsButton.appendChild( drawSvgStringAsElement( ` image/svg+xml Settings Settings ` ) ); addClass(this.settingsButton, "tabContextButton"); controlBarEl.appendChild(this.settingsButton); this.settingsButton.onclick = (ev) => { this.open(ev); }; this.menuElement = document.createElement("div"); addClass(this.menuElement, "tabMenu"); controlBarEl.appendChild(this.menuElement); this.menuElement.onclick = (ev) => { ev.stopImmediatePropagation(); return false; }; this.drawBody(); } private updateBody() { const reqConfig = this.tab.getRequestConfig(); if (typeof reqConfig.method !== "function") { this.setRequestMethod(reqConfig.method); } // Draw Accept headers this.setAcceptHeader_select(reqConfig.acceptHeaderSelect); this.setAcceptHeader_graph(reqConfig.acceptHeaderGraph); // console.log('setting args',reqConfig.args) if (typeof reqConfig.args !== "function") { this.setArguments([...reqConfig.args] || []); } if (typeof reqConfig.headers !== "function") { this.setHeaders(toPairs(reqConfig.headers).map(([name, value]) => ({ name, value }))); } if (typeof reqConfig.defaultGraphs !== "function") { this.setDefaultGraphs([...reqConfig.defaultGraphs] || []); } if (typeof reqConfig.namedGraphs !== "function") { this.setNamedGraphs([...reqConfig.namedGraphs] || []); } } public open(ev: MouseEvent) { if (!this.isOpen) { this.updateBody(); this.isOpen = true; addClass(this.menuElement, "open"); this.toggleAriaSettings(); const handleClick = (ev: MouseEvent) => { // Stops propagation in IE11 let parent = ev.target; while (!!(window as any).MSInputMethodContext && !!(document as any).documentMode && parent.parentElement) { if (parent.className.indexOf("tabMenu") !== -1) { return false; } parent = parent.parentElement; } this.close(ev); document.removeEventListener("click", handleClick, true); return false; }; document.addEventListener("click", handleClick, { once: true }); ev.stopImmediatePropagation(); } } public close(_event?: MouseEvent) { if (this.isOpen) { this.isOpen = false; removeClass(this.menuElement, "open"); this.toggleAriaSettings(); } } private toggleAriaSettings() { this.settingsButton.setAttribute("aria-label", this.isOpen ? "Close settings" : "Open settings"); this.settingsButton.setAttribute("aria-expanded", `${this.isOpen}`); } private setRequestMethod!: (method: Exclude["method"], Function>) => void; private drawRequestMethodSelector() { const requestTypeWrapper = document.createElement("div"); addClass(requestTypeWrapper, "requestConfigWrapper"); createLabel("Request method", requestTypeWrapper); // Create Button const getButton = document.createElement("button"); addClass(getButton, "selectorButton"); getButton.innerText = "GET"; const postButton = document.createElement("button"); addClass(postButton, "selectorButton"); postButton.innerText = "POST"; addClass(this.tab.getRequestConfig().method === "GET" ? getButton : postButton, "selected"); this.setRequestMethod = (method) => { if (method === "GET") { addClass(getButton, "selected"); removeClass(postButton, "selected"); } else if (method === "POST") { addClass(postButton, "selected"); removeClass(getButton, "selected"); } }; getButton.onclick = () => { this.tab.setRequestConfig({ method: "GET" }); this.setRequestMethod("GET"); }; postButton.onclick = () => { this.tab.setRequestConfig({ method: "POST" }); this.setRequestMethod("POST"); }; // Add elements to container requestTypeWrapper.appendChild(getButton); requestTypeWrapper.appendChild(postButton); this.menuElement.appendChild(requestTypeWrapper); } private setAcceptHeader_select!: (acceptheader: string) => void; private setAcceptHeader_graph!: (acceptheader: string) => void; private drawAcceptSelector() { const acceptWrapper = document.createElement("div"); addClass(acceptWrapper, "requestConfigWrapper", "acceptWrapper"); createLabel("Accept Headers", acceptWrapper); // Request type this.setAcceptHeader_select = createSelector( AcceptOptionsMap, (ev) => { this.tab.setRequestConfig({ acceptHeaderSelect: (ev.target).value }); }, "Ask / Select", acceptWrapper ); this.setAcceptHeader_graph = createSelector( AcceptHeaderGraphMap, (ev) => { this.tab.setRequestConfig({ acceptHeaderGraph: (ev.target).value }); }, "Construct / Describe", acceptWrapper ); this.menuElement.appendChild(acceptWrapper); } private setArguments!: (args: TextInputPair[]) => void; private drawArgumentsInput() { const onBlur = () => { const args: Exclude["args"], Function> = []; argumentsWrapper.querySelectorAll(".textRow").forEach((row) => { const [name, value] = row.children; if (name instanceof HTMLInputElement && value instanceof HTMLInputElement && name.value.length) { args.push({ name: name.value, value: value.value }); } }); this.tab.setRequestConfig({ args: args }); }; const argumentsWrapper = document.createElement("div"); addClass(argumentsWrapper, "requestConfigWrapper", "textSetting"); createLabel("Arguments", argumentsWrapper); this.menuElement.appendChild(argumentsWrapper); this.setArguments = (args) => { argumentsWrapper.querySelectorAll(".textRow").forEach((child) => { argumentsWrapper.removeChild(child); }); // Draw the arguments for (let argIndex = 0; argIndex < args.length; argIndex++) { const argRow = drawDoubleInputWhenEmpty(argumentsWrapper, argIndex, args, onBlur); getRemoveButton(() => { args.splice(argIndex, 1); this.tab.setRequestConfig({ args: args }); this.setArguments(args); }, argRow); } drawDoubleInput(argumentsWrapper, args, onBlur); }; } private setHeaders!: (headers: TextInputPair[]) => void; private drawHeaderInput() { const onBlur = () => { const headers: Exclude["headers"], Function> = {}; headerWrapper.querySelectorAll(".textRow").forEach((row) => { const [name, value] = row.children; if (name instanceof HTMLInputElement && value instanceof HTMLInputElement && name.value.length) { headers[name.value] = value.value; } }); this.tab.setRequestConfig({ headers: headers }); }; const headerWrapper = document.createElement("div"); addClass(headerWrapper, "requestConfigWrapper", "textSetting"); const URLArgLabel = createLabel("Header Arguments"); headerWrapper.appendChild(URLArgLabel); this.menuElement.appendChild(headerWrapper); this.setHeaders = (headers) => { headerWrapper.querySelectorAll(".textRow").forEach((child) => { headerWrapper.removeChild(child); }); // Draw the headers; for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) { const headerRow = drawDoubleInputWhenEmpty(headerWrapper, headerIndex, headers, onBlur); // getRemoveButton(() => (headers[headerIndex] = undefined), headerRow); getRemoveButton(() => { headers.splice(headerIndex, 1); this.tab.setRequestConfig({ headers: fromPairs(headers.map((h) => [h.name, h.value])) }); this.setHeaders(headers); }, headerRow); } drawDoubleInput(headerWrapper, headers, onBlur); }; } private setDefaultGraphs!: (defaultGraphs: Array) => void; private drawDefaultGraphInput() { const defaultGraphWrapper = document.createElement("div"); addClass(defaultGraphWrapper, "requestConfigWrapper", "textSetting"); const defaultGraphLabel = createLabel("Default Graphs"); defaultGraphWrapper.appendChild(defaultGraphLabel); this.menuElement.appendChild(defaultGraphWrapper); const onBlur = () => { const graphs: Exclude["defaultGraphs"], Function> = []; defaultGraphWrapper.querySelectorAll(".graphInput").forEach((row) => { const [el] = row.children; if (el instanceof HTMLInputElement && el.value.length) { graphs.push(el.value); } }); this.tab.setRequestConfig({ defaultGraphs: graphs }); }; this.setDefaultGraphs = (defaultGraphs) => { defaultGraphWrapper.querySelectorAll(".graphInput").forEach((child) => { defaultGraphWrapper.removeChild(child); }); for (let graphIndex = 0; graphIndex < defaultGraphs.length; graphIndex++) { const graphDiv = drawSingleInputWhenEmpty(defaultGraphWrapper, graphIndex, defaultGraphs, onBlur); getRemoveButton(() => (defaultGraphs[graphIndex] = undefined), graphDiv); } drawSingleInput(defaultGraphWrapper, defaultGraphs, onBlur); }; } private setNamedGraphs!: (defaultGraphs: Array) => void; private drawNamedGraphInput() { const namedGraphWrapper = document.createElement("div"); addClass(namedGraphWrapper, "requestConfigWrapper", "textSetting"); const namedGraphLabel = createLabel("Named Graphs"); namedGraphWrapper.appendChild(namedGraphLabel); this.menuElement.appendChild(namedGraphWrapper); const onBlur = () => { const graphs: Exclude["namedGraphs"], Function> = []; namedGraphWrapper.querySelectorAll(".graphInput").forEach((row) => { const [el] = row.children; if (el instanceof HTMLInputElement && el.value.length) { graphs.push(el.value); } }); this.tab.setRequestConfig({ namedGraphs: graphs }); }; this.setNamedGraphs = (namedGraphs) => { namedGraphWrapper.querySelectorAll(".graphInput").forEach((child) => { namedGraphWrapper.removeChild(child); }); // Draw default graphs for (let graphIndex = 0; graphIndex < namedGraphs.length; graphIndex++) { const graphDiv = drawSingleInputWhenEmpty(namedGraphWrapper, graphIndex, namedGraphs, onBlur); getRemoveButton(() => (namedGraphs[graphIndex] = undefined), graphDiv); } drawSingleInput(namedGraphWrapper, namedGraphs, onBlur); }; } private drawBody() { // Draw request Method this.drawRequestMethodSelector(); // Draw Accept headers this.drawAcceptSelector(); // Draw URL Arguments this.drawArgumentsInput(); // Draw HTTP Header body this.drawHeaderInput(); // Default graphs this.drawDefaultGraphInput(); // Named graphs this.drawNamedGraphInput(); } public destroy() { this.settingsButton.onclick = null; this.menuElement.onclick = null; while (this.menuElement.firstChild) this.menuElement.firstChild.remove(); this.menuElement.remove(); } } /** * This function returns a setter so we can easily set a new value */ function createSelector( options: { key: string; value: string }[], changeHandler: (event: Event) => void, label: string, parent: HTMLElement ): (selected: string) => void { const selectorWrapper = document.createElement("div"); addClass(selectorWrapper, "selector"); const selectorLabel = createLabel(label, selectorWrapper); addClass(selectorLabel, "selectorLabel"); const selectElement = document.createElement("select"); selectElement.onchange = changeHandler; selectorWrapper.appendChild(selectElement); const optionEls = options.map((o) => createOption(o, selectElement)); parent.appendChild(selectorWrapper); return (selected) => { if (typeof selected === "string") { for (const optionEl of optionEls) { optionEl.selected = optionEl.value === selected; } } }; } function getInputValues(div: HTMLElement) { const values = []; for (const child of div.getElementsByTagName("input")) { values.push(child.value); } return values; } function createLabel(content: string, parent?: HTMLElement) { const label = document.createElement("label"); addClass(label, "label"); label.innerText = content; if (parent) parent.appendChild(label); return label; } function createOption(content: { key: string; value: string }, parent: HTMLElement) { const option = document.createElement("option"); option.textContent = content.key; option.value = content.value; parent.appendChild(option); return option; } function createInput(content: string, parent?: HTMLElement) { const input = document.createElement("input"); input.type = "text"; input.value = content ? content : ""; if (parent) parent.appendChild(input); return input; } function getRemoveButton(deleteAction: () => void, parent?: HTMLElement) { const button = document.createElement("button"); button.textContent = "X"; addClass(button, "removeButton"); if (parent) parent.appendChild(button); button.onclick = (ev) => { deleteAction(); (ev.target).parentElement?.remove(); }; return button; } function drawSingleInput(root: HTMLElement, content: Array, onBlur: () => void) { const lastRow: HTMLDivElement | null = root.querySelector(".graphInput:last-of-type"); if (!lastRow || getInputValues(lastRow)[0] !== "" || lastRow.getElementsByTagName("button").length !== 0) { const index = content.length; drawSingleInputWhenEmpty(root, index, content, onBlur); if (lastRow && lastRow.getElementsByTagName("button").length === 0) { getRemoveButton(() => (content[index - 1] = undefined), lastRow); } } } function drawSingleInputWhenEmpty( root: HTMLElement, index: number, content: Array, onBlur: () => void ) { const namedGraphItem = document.createElement("div"); addClass(namedGraphItem, "graphInput"); const namedGraphInput = createInput(content[index] || "", namedGraphItem); namedGraphInput.onkeyup = (ev) => { const target = ev.target; content[index] ? (content[index] = target.value) : content.push(target.value); drawSingleInput(root, content, onBlur); }; namedGraphItem.onblur = onBlur; root.appendChild(namedGraphItem); return namedGraphItem; } function drawDoubleInput(root: HTMLElement, content: Array, onBlur: () => void) { const lastRow: HTMLDivElement | null = root.querySelector(".textRow:last-of-type"); // When there are no row's or the last row has values, if (!lastRow || getInputValues(lastRow).filter((value) => value).length !== 0) { const index = content.length; drawDoubleInputWhenEmpty(root, index, content, onBlur); // If there is a last row and the button is not already there if (lastRow && lastRow.getElementsByTagName("button").length === 0) { getRemoveButton(() => (content[index - 1] = undefined), lastRow); } } } function drawDoubleInputWhenEmpty( root: HTMLElement, index: number, content: Array, onBlur: () => void ) { const kvInput = document.createElement("div"); addClass(kvInput, "textRow"); const value = content[index]; const nameField = createInput(value ? value.name : "", kvInput); const valueField = createInput(value ? value.value : "", kvInput); nameField.onkeyup = (ev) => { const val = content[index]; val ? (val.name = (ev.target).value) : content.push({ name: (ev.target).value, value: "" }); drawDoubleInput(root, content, onBlur); }; nameField.onblur = onBlur; valueField.onkeyup = (ev) => { const val = content[index]; val ? (val.value = (ev.target).value) : content.push({ value: (ev.target).value, name: "" }); drawDoubleInput(root, content, onBlur); }; valueField.onblur = onBlur; root.appendChild(kvInput); return kvInput; }