diff --git a/how-to/web-layout/client/src/provider.ts b/how-to/web-layout/client/src/provider.ts index f40006b..41bd125 100644 --- a/how-to/web-layout/client/src/provider.ts +++ b/how-to/web-layout/client/src/provider.ts @@ -1,6 +1,7 @@ +/* eslint-disable jsdoc/require-param */ import type OpenFin from "@openfin/core"; import { type WebLayoutSnapshot, connect } from "@openfin/core-web"; -import { getDefaultLayout, getSettings } from "./platform/settings"; +import { getDefaultLayout, getSecondLayout, getSettings } from "./platform/settings"; import type { LayoutManager, LayoutManagerConstructor, LayoutManagerItem } from "./shapes/layout-shapes"; import type { Settings } from "./shapes/setting-shapes"; @@ -39,12 +40,131 @@ function setupPanels(settings: Settings): void { * Attach listeners to elements. */ async function attachListeners(): Promise { - const swapButton = document.querySelector("#swap-layouts"); - swapButton?.addEventListener("click", async () => { - await swapLayout(); + const addLayoutBtn = document.querySelector("#add-layout"); + addLayoutBtn?.addEventListener("click", async () => { + await addLayout(); }); } +/** + * Attaches Listeners to Tab Click Events. + * @param tabName the name of the tab to add the event to. + */ +async function attachTabListener(tabName: string): Promise { + const tabBtn = document.querySelector(`#${tabName}`); + tabBtn?.addEventListener("click", async () => { + await selectTab(tabName); + }); +} + +/** + * Creates a new tab in the tab row given a specific tab/layout name. + */ +async function createTabBtn(tabName: string): Promise { + const tabRow = document.querySelector("#tabs"); + const newTab = document.createElement("div"); + newTab.id = `tab-${tabName}`; + newTab.className = "tab"; + newTab.style.display = "block"; + newTab.append(document.createTextNode(`${tabName}`)); + const closeBtn = document.createElement("span"); + closeBtn.className = "close-btn"; + closeBtn.innerHTML = "X"; + closeBtn.addEventListener("click", async (e) => { + await removeTab(tabName); + e.stopPropagation(); + }); + newTab.append(closeBtn); + if (tabRow) { + tabRow.append(newTab); + if (document.querySelector(`#tab-${tabName}`)) { + await attachTabListener(newTab.id); + await selectTab(tabName); + } + } +} + +/** + * Makes a layout and tab active. + */ +async function selectTab(tabName: string, removedTabName?: string): Promise { + console.log(`Tab ${tabName} selected`); + let actualName = tabName; + if (tabName.includes("tab")) { + const split = tabName.split("-"); + actualName = split[1]; + } + const currentOrder = window.localStorage.getItem("order"); + if (currentOrder !== "") { + const layoutsArr = currentOrder?.split(","); + if (layoutsArr) { + for (const tab of layoutsArr) { + if (actualName !== removedTabName) { + if (tab === actualName) { + await showTab(tab); + } else { + await hideTab(tab); + } + } + } + } + } +} + +/** + * Makes a layout and tab hidden. + */ +async function showTab(tabName: string): Promise { + console.log(`Tab ${tabName} showing...`); + const currentTab = document.querySelector(`#${tabName}`); + if (currentTab) { + currentTab.style.display = "block"; + } +} + +/** + * Makes a layout and tab hidden. + */ +async function hideTab(tabName: string): Promise { + console.log(`Tab ${tabName} hiding...`); + const currentTab = document.querySelector(`#${tabName}`); + if (currentTab) { + currentTab.style.display = "none"; + } +} + +/** + * Removes a layout & tab from the page. + */ +async function removeTab(tabName: string): Promise { + console.log(`Removing Tab & Layout ${tabName}`); + const lm = window.fin?.Platform.Layout.getCurrentLayoutManagerSync(); + await lm?.removeLayout({ layoutName: tabName } as OpenFin.LayoutIdentity); + const tabToRemove = document.querySelector(`#tab-${tabName}`); + tabToRemove?.remove(); + + const currentOrder = window.localStorage.getItem("order"); + if (currentOrder !== "") { + const layouts = currentOrder?.split(","); + const newOrder = layouts?.filter((e) => e !== tabName); + if (newOrder && newOrder.length > 0) { + window.localStorage.setItem("order", newOrder.toString()); + } else { + window.localStorage.setItem("order", ""); + } + + if (newOrder) { + if (newOrder.length > 0) { + await selectTab(newOrder[0], tabName); + } else { + console.log("There are no layouts loaded."); + // eslint-disable-next-line no-alert + alert("There are no layouts loaded. Please add one."); + } + } + } +} + /** * A Create function for layouts. * @param fin the fin object. @@ -67,16 +187,14 @@ async function createLayout( // Normally you can use state here, but just tracking the order of layouts in localStorage. const currentOrder = window.localStorage.getItem("order"); - if (!currentOrder) { - window.localStorage.setItem("order", ""); - } let newOrder = ""; - if (order === 0) { + if (!currentOrder || currentOrder === "") { newOrder = layoutName; } else { - newOrder = currentOrder ? currentOrder.concat(",", layoutName) : ""; + newOrder = currentOrder?.concat(",", layoutName); } window.localStorage.setItem("order", newOrder); + // Finally, call the Layout.create() function to apply the snapshot layout to the div we just created. await fin.Platform.Layout.create({ layoutName, layout, container }); } @@ -103,44 +221,104 @@ function makeOverride(fin: OpenFin.Fin, layoutContainerId: s * @param snapshot The layouts object containing the fixed set of available layouts. */ public async applyLayoutSnapshot(snapshot: WebLayoutSnapshot): Promise { - console.log(`Does this exist? ${Boolean(this.layoutContainer)}`); + console.log(`[Apply Layout] Does this exist? ${Boolean(this.layoutContainer)}`); if (this.layoutContainer !== null && this.layoutContainer !== undefined) { + for (const [key, value] of Object.entries(snapshot.layouts)) { + this.layoutMapArray.push({ layoutName: key, layout: value, container: this.layoutContainer }); + } setTimeout( () => - Object.entries(snapshot.layouts).map(async ([layoutName, layout], i) => - createLayout(fin, layoutName, layout, i) - ), + Object.entries(snapshot.layouts).map(async ([layoutName, layout], i) => { + await createLayout(fin, layoutName, layout, i); + await createTabBtn(layoutName); + }), 1000 ); - console.log("Layouts loaded"); + console.log("[Apply Layout] Layouts loaded"); + console.log(`[Apply Layout] Layouts are: ${JSON.stringify(this.layoutMapArray)}`); + window.localStorage.setItem("currentLayout", JSON.stringify(this.layoutMapArray)); } } + + /** + * Remove Layout - You guessed it, it removes a layout from the existing array of layouts. + * @param id The name of the layout you want removed. + */ + public async removeLayout(id: OpenFin.LayoutIdentity): Promise { + const index = this.layoutMapArray.findIndex((x) => x.layoutName === id.layoutName); + console.log(`[LM Override] Removing Layout ${id.layoutName}`); + console.log(`[LM Override] Found layout at index ${index}`); + await removeThisLayout(id.layoutName); + } }; }; } /** - * Returns a layout from the settings with a provided name. - * @returns The default layout from the settings. + * Saves the list of layout items to Local Storage. + * @param updatedLayoutContents List of Layouts to save. */ -export async function swapLayout(): Promise { - // Get that order of created div ids from storage, or state, or wherever you want to save them. - const currentOrder = window.localStorage.getItem("order"); - const layouts = currentOrder?.split(","); - // This is a simple swap between two, but you can do this anyway you like. - const firstLayout = document.querySelector(`#${layouts ? layouts[0] : null}`); - const secondLayout = document.querySelector(`#${layouts ? layouts[1] : null}`); - if (firstLayout && secondLayout) { - if (secondLayout.style.display === "block") { - firstLayout.style.display = "block"; - secondLayout.style.display = "none"; - } else { - firstLayout.style.display = "none"; - secondLayout.style.display = "block"; - } +export async function saveLayout(updatedLayoutContents: LayoutManagerItem[]): Promise { + window.localStorage.setItem("currentLayout", JSON.stringify(updatedLayoutContents)); +} + +/** + * Reads a list of layouts from Local Storage. + * @returns List of Layouts. + */ +export function readLayouts(): LayoutManagerItem[] { + const currentLayouts = window.localStorage.getItem("currentLayout"); + if (currentLayouts) { + return JSON.parse(currentLayouts) as LayoutManagerItem[]; + } + + return []; +} + +/** + * Adds another layout. + */ +export async function addLayout(): Promise { + const secondLayoutToAdd = await getSecondLayout(); + console.log("[Add Layout] Grabbing Secondary layout file..."); + if (secondLayoutToAdd !== undefined) { + const lm = window.fin?.Platform.Layout.getCurrentLayoutManagerSync(); + console.log("[Add Layout] Adding layout"); + await lm?.applyLayoutSnapshot(secondLayoutToAdd); + } else { + console.log("[Add Layout] Error adding Layout. No Secondary Layout exists."); + } + const addBtn = document.querySelector("#add-layout"); + if (addBtn) { + addBtn.setAttribute("disabled", "disabled"); } } +/** + * Click function to remove a layout by name. + * @param layoutName the name of a layout. + */ +export async function removeThisLayout(layoutName: string): Promise { + // remove layout from state. + const layoutsBefore = readLayouts(); + let layoutsRemoved: LayoutManagerItem[] = []; + const layoutNameElement = document.querySelector(`#${layoutName}`); + if (layoutsBefore.length > 0 && layoutNameElement !== null) { + const idx = layoutsBefore.findIndex((x) => x.layoutName === layoutName); + layoutsRemoved = layoutsBefore.splice(idx, 1); + console.log(`[Remove Layout] Removed this layout: ${JSON.stringify(layoutsRemoved)}`); + await saveLayout(layoutsBefore); + console.log(`[Remove Layout] Layouts After Removal: ${JSON.stringify(layoutsBefore)}`); + layoutNameElement.remove(); + await fin.Platform.Layout.destroy({ layoutName, uuid: fin.me.uuid, name: fin.me.name }); + if (layoutName === "new") { + const addBtn = document.querySelector("#add-layout"); + if (addBtn) { + addBtn.removeAttribute("disabled"); + } + } + } +} /** * Initializes the OpenFin Web Broker connection. */ @@ -182,7 +360,7 @@ async function init(): Promise { connectionInheritance: "enabled", platform: { layoutSnapshot } }); - + window.fin = fin; if (fin) { const layoutManagerOverride = makeOverride(fin, settings.platform.layout.layoutContainerId); // You may now use the `fin` object to initialize the broker and the layout. diff --git a/how-to/web-layout/public/common/style/app.css b/how-to/web-layout/public/common/style/app.css index cf6a78d..94c2abf 100644 --- a/how-to/web-layout/public/common/style/app.css +++ b/how-to/web-layout/public/common/style/app.css @@ -71,6 +71,7 @@ body { margin: 0; background-color: var(--brand-background); color: var(--brand-text); + width: 100%; } body.border { @@ -705,6 +706,10 @@ em { flex: 2; } +.fill_3 { + flex: 3; +} + .scroll { overflow: auto; } @@ -943,3 +948,38 @@ td, .nowrap { white-space: nowrap; } + +.tabs { + display: inline-flex; + height: 30px; + width: 100%; +} + +.tab { + min-width: 120px; + min-height: 30px; + padding: 5px; + border: 2px solid var(--brand-border); + font-weight: 300; + border-top: 2px solid var(--tab-border-top-color); + background-color: var(--tab-background-active-color); + opacity: 100%; + cursor: pointer; +} + +.close-btn { + float: right; + font-size: 1em; + max-height: 18px; + content: var(--tab-close-button-url); + color: var(--brand-text-secondary); + cursor: pointer; +} + +.main-container { + display: flex; + justify-content: stretch; + align-items: stretch; + height: 100%; + width: 100%; +} diff --git a/how-to/web-layout/public/layouts/secondary.layout.fin.json b/how-to/web-layout/public/layouts/secondary.layout.fin.json index fdc40c4..67bf81e 100644 --- a/how-to/web-layout/public/layouts/secondary.layout.fin.json +++ b/how-to/web-layout/public/layouts/secondary.layout.fin.json @@ -1,6 +1,6 @@ { "layouts": { - "default": { + "new": { "content": [ { "type": "row", @@ -18,10 +18,10 @@ "type": "component", "componentName": "view", "componentState": { - "url": "http://localhost:6060/views/fdc3-view.html", - "name": "internal-generated-view-secondary-1" + "url": "http://localhost:6060/views/fdc3-view.html" }, - "title": "FDC3 Same Domain" + "title": "FDC3 Same Domain", + "isClosable": false } ] }, @@ -33,10 +33,10 @@ "type": "component", "componentName": "view", "componentState": { - "url": "https://built-on-openfin.github.io/dev-extensions/extensions/v19.0.0/interop/fdc3/context/2-0/fdc3-broadcast-view.html", - "name": "internal-generated-view-secondary-2" + "url": "https://built-on-openfin.github.io/dev-extensions/extensions/v19.0.0/interop/fdc3/context/2-0/fdc3-broadcast-view.html" }, - "title": "FDC3 Different Domain" + "title": "FDC3 Different Domain", + "isClosable": true } ] } diff --git a/how-to/web-layout/public/platform/provider.html b/how-to/web-layout/public/platform/provider.html index c08fd37..fc91b44 100644 --- a/how-to/web-layout/public/platform/provider.html +++ b/how-to/web-layout/public/platform/provider.html @@ -19,14 +19,19 @@

Web Multiple Layout Example

Demonstrate web interop usage with multiple layouts

- + OpenFin
-
- -
+
+ +
+
+
+
diff --git a/package-lock.json b/package-lock.json index 27b0449..7d5b0b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openfin-web-starter", - "version": "18.0.0", + "version": "19.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openfin-web-starter", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "workspaces": [ "how-to/*" @@ -20,7 +20,7 @@ }, "how-to/cloud-interop": { "name": "openfin-web--cloud-interop", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -50,7 +50,7 @@ }, "how-to/cloud-interop-basic": { "name": "openfin-web--cloud-interop-basic", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -81,7 +81,7 @@ }, "how-to/web-client-api": { "name": "openfin-web--web-client-api", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -110,7 +110,7 @@ }, "how-to/web-interop": { "name": "openfin-web--web-interop", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -139,7 +139,7 @@ }, "how-to/web-interop-basic": { "name": "openfin-web--web-interop-basic", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -168,7 +168,7 @@ }, "how-to/web-interop-support-context-and-intents": { "name": "openfin-web--web-interop-support-context-and-intents", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -198,7 +198,7 @@ }, "how-to/web-layout": { "name": "openfin-web--web-layout", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -227,7 +227,7 @@ }, "how-to/web-layout-basic": { "name": "openfin-web--web-layout-basic", - "version": "18.0.0", + "version": "19.0.0", "license": "SEE LICENSE IN LICENSE.MD", "dependencies": { "@finos/fdc3": "2.0.3", @@ -363,9 +363,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2315,9 +2315,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.825", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.825.tgz", - "integrity": "sha512-OCcF+LwdgFGcsYPYC5keEEFC2XT0gBhrYbeGzHCx7i9qRFbzO/AqTmc/C/1xNhJj+JA7rzlN7mpBuStshh96Cg==", + "version": "1.4.827", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", + "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==", "dev": true }, "node_modules/emoji-regex": { @@ -6882,9 +6882,9 @@ } }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3",