diff --git a/packages/addon/public/channel_selector.html b/packages/addon/public/channel_selector.html index 65c82d690..0d1687c77 100644 --- a/packages/addon/public/channel_selector.html +++ b/packages/addon/public/channel_selector.html @@ -16,11 +16,16 @@ #list { background-color: white; - border: 1px solid lightgrey; - border-radius: 3px; - padding: 10px; + margin: 10px; + padding: 20px; + border-radius: 10px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; font-family: sans-serif; display: none; + position: fixed; + bottom: 4px; + right: 4px; + & > div { font-size: 18px; height: 24px; @@ -41,14 +46,20 @@ } #tokens { display: none; + cursor: pointer; } body[data-expanded="true"] #list { display: block; } + body[data-expanded="false"] #tokens { display: block; } + + .logo path { + stroke: black; + } @@ -60,6 +71,8 @@ + + diff --git a/packages/addon/public/intent_resolver.html b/packages/addon/public/intent_resolver.html index 4f71a36cf..9c56f9112 100644 --- a/packages/addon/public/intent_resolver.html +++ b/packages/addon/public/intent_resolver.html @@ -9,10 +9,15 @@ display: flex; flex-direction: column; gap: 2px; - height: 235px; + height: fit-content; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ace; + border-radius: 8px; + width: fit-content; + position: fixed; + bottom: 4px; + right: 0; & > div { font-size: 18px; @@ -105,31 +110,38 @@ box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; font-family: sans-serif; } + + dialog { + border: none; + background: transparent; + } -
-

Intent Resolver

- -
- Resolve intent for: -
-
- -
-
New Apps
-
Open Apps
-
- -
- -
- -
- + +
+

Intent Resolver

+ +
+ Resolve intent for: +
+
+ +
+
New Apps
+
Open Apps
+
+ +
+ +
+ +
+ +
-
+ diff --git a/packages/addon/src/channel_selector.ts b/packages/addon/src/channel_selector.ts index 663b96804..feeb8823c 100644 --- a/packages/addon/src/channel_selector.ts +++ b/packages/addon/src/channel_selector.ts @@ -1,10 +1,7 @@ -import { ChannelDetails, SelectorMessageChannels } from "@kite9/fdc3-common"; +import { IframeChannelsPayload, Channel } from "@kite9/fdc3-common"; -var channels: ChannelDetails[] = [] -var channelId: string | null = null - -function fillChannels(data: ChannelDetails[], selected: string | null, messageClickedChannel: (s: String) => void) { +const fillChannels = (data: Channel[], selected: string | null, messageClickedChannel: (s: string | null) => void) => { const list = document.getElementById('list')!!; list.innerHTML = ''; @@ -14,17 +11,25 @@ function fillChannels(data: ChannelDetails[], selected: string | null, messageCl const span = document.createElement('span'); span.classList.add('glyph'); - span.style.color = displayMetadata.color; - span.style.borderColor = displayMetadata.color; - span.textContent = displayMetadata.glyph ?? ''; + + if(displayMetadata?.color){ + span.style.color = displayMetadata.color; + span.style.borderColor = displayMetadata.color; + } + span.textContent = displayMetadata?.glyph ?? ''; node.appendChild(span); - const span2 = document.createElement('span'); - span2.classList.add('name'); - span2.textContent = displayMetadata.name; - node.appendChild(span2); - list.appendChild(node); - node.addEventListener('click', () => messageClickedChannel(id)); + if(displayMetadata?.name){ + const span2 = document.createElement('span'); + span2.classList.add('name'); + span2.textContent = displayMetadata.name; + node.appendChild(span2); + } + + list.appendChild(node); + node.addEventListener('click', () => { + messageClickedChannel(id) + }); if (id === selected) { node.setAttribute("aria-selected", "true"); @@ -35,40 +40,95 @@ function fillChannels(data: ChannelDetails[], selected: string | null, messageCl window.addEventListener("load", () => { const parent = window.parent; - const logo = document.getElementById("logo")!! + const logo = document.getElementById("logo")!; const mc = new MessageChannel(); - const myPort = mc.port1 - myPort.start() - - parent.postMessage({ type: "SelectorMessageInitialize" }, "*", [mc.port2]); - - function changeSize(expanded: boolean) { - document.body.setAttribute("data-expanded", "" + expanded); - myPort.postMessage({ type: "SelectorMessageResize", expanded }) - } - - myPort.addEventListener("message", (e: MessageEvent) => { - if (e.data.type == 'SelectorMessageChannels') { - const details = e.data as SelectorMessageChannels - // console.log(JSON.stringify("CHANNEL DETAILS: " + JSON.stringify(details))) - channels = details.channels - channelId = details.selected - - const selectedColor = (channelId ? (channels.find(c => c.id == channelId)?.displayMetadata?.color) : null) ?? 'black' - logo.style.fill = selectedColor + const myPort = mc.port1; + myPort.start(); + myPort.onmessage = ({data}) => { + console.debug("Received message: ", data); + switch(data.type){ + case "iframeHandshake": { + collapse(); + break; + } + case "fdc3UserInterfaceChannels": { + logo.removeEventListener("click", expand); + const {userChannels, selected} = data.payload as IframeChannelsPayload; + fillChannels(userChannels, selected, (channelStr) => { + myPort.postMessage({ + type: "fdc3UserInterfaceSelected", + payload: { + selected: channelStr || null + } + }); + collapse(); + }); + const selectedChannel = userChannels.find((c) => c.id === selected); + logo.style.fill = selectedChannel?.displayMetadata?.color ?? "white"; + logo.addEventListener("click", expand); + break; + } } - }) - - logo.addEventListener("click", () => { - fillChannels(channels, channelId, (id) => { - changeSize(false) - myPort.postMessage({ type: "SelectorMessageChoice", channelId: id }) - }) - - // ask the parent container to increase the window size - changeSize(true) - }) + }; + + parent.postMessage({ + type: "fdc3UserInterfaceHello", + payload: { + initialCSS: { + width: `${8*4}px`, + height: `${8*5}px`, + right: "2px", + bottom: "2px", + zIndex: "1000", + "z-index": "1000", + position: "fixed" + + } + } + }, "*", [mc.port2]); + + const expand = () => { + document.body.setAttribute("data-expanded", "true"); + myPort.postMessage({ + type: "fdc3UserInterfaceRestyle", + payload: { + updatedCSS: { + width: `100%`, + height: `100%`, + top: 0, + left: 0, + zIndex: "1000", + "z-index": "1000", + position: "fixed" + } + } + }); + } + const collapse = () => { + myPort.postMessage({ + type: "fdc3UserInterfaceRestyle", + payload: { + updatedCSS: { + width: `${8*4}px`, + height: `${8*5}px`, + right: "2px", + bottom: "2px", + zIndex: "1000", + "z-index": "1000", + position: "fixed" + } + } + }); + + // If you immediately change to the logo, before the iframe has a chance to finish restyling, + // you see a flicker of a giant, colored logo. + // Here, we wait a negligible amount of time, and hope that the restyling has finished. This avoids the flicker. + // It's not a *good* idea, it's just the best available, since we don't know when the restyle finishes. + setTimeout(() => { + document.body.setAttribute("data-expanded", "false"); + }, 15); + } -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/addon/src/intent_resolver.ts b/packages/addon/src/intent_resolver.ts index 550a88ebd..f8f0c92b2 100644 --- a/packages/addon/src/intent_resolver.ts +++ b/packages/addon/src/intent_resolver.ts @@ -1,23 +1,27 @@ import { Icon } from "@kite9/fdc3"; import { AppIntent } from "@kite9/fdc3"; -import { ResolverIntents, ResolverMessageChoice, SingleAppIntent } from "@kite9/fdc3-common"; +import { IframeResolveActionPayload, IframeResolvePayload } from "@kite9/fdc3-common"; -const setup = (data: ResolverIntents, callback: (s: SingleAppIntent | null) => void) => { +const setup = (data: IframeResolvePayload, callback: (s: IframeResolveActionPayload) => void) => { document.body.setAttribute("data-visible", "true"); + document.querySelector("dialog")?.showModal(); + + console.log("setup data:", data); const intentSelect = document.getElementById("displayIntent") as HTMLSelectElement - const justIntents = data.appIntents.map(ai => ai.intent) + const justIntents = data.appIntents.map(({intent}) => intent) const doneIntents = new Set() justIntents.forEach(({ name, displayName }) => { - if (!doneIntents.has(name)) { - doneIntents.add(name); - const option = document.createElement("option"); - option.textContent = displayName as string; - option.value = name; - intentSelect.appendChild(option); + if (doneIntents.has(name)) { + return; } + doneIntents.add(name); + const option = document.createElement("option"); + option.textContent = displayName as string; + option.value = name; + intentSelect.appendChild(option); }); intentSelect.addEventListener("change", (e: any) => fillList(data.appIntents.filter(ai => ai.intent.name == e?.target?.value), e?.target?.value, callback)); @@ -42,9 +46,13 @@ const setup = (data: ResolverIntents, callback: (s: SingleAppIntent | null) => v }); }); - document.getElementById("cancel")!!.addEventListener("click", () => { - callback(null); - }) + document.getElementById("cancel")?.addEventListener("click", () => { + callback({ + action: "cancel" + }); + }); + + console.log(document.body); } function createIcon(icons: Icon[] | undefined): HTMLElement { @@ -58,22 +66,22 @@ function createIcon(icons: Icon[] | undefined): HTMLElement { return img } -const fillList = (ai: AppIntent[], intent: string, callback: (s: SingleAppIntent) => void) => { +const fillList = (ai: AppIntent[], intent: string, callback: (s: IframeResolveActionPayload) => void) => { const allApps = ai.flatMap(a => a.apps) const openApps = allApps.filter(a => a.instanceId) const newApps = allApps.filter(a => !a.instanceId) // first, populate the "New Apps" tab - const newList = document.getElementById('new-list')!! + const newList = document.getElementById('new-list') as HTMLDivElement; newList.innerHTML = ''; - newApps.forEach(({ appId, title, icons }) => { + newApps.forEach(({ appId, title, name, icons }) => { const node = document.createElement('div'); node.setAttribute('tabIndex', '0'); node.setAttribute("data-appId", appId); const span = document.createElement("span"); - span.textContent = title ?? appId; + span.textContent = title ?? name ?? appId; const img = createIcon(icons) @@ -82,11 +90,19 @@ const fillList = (ai: AppIntent[], intent: string, callback: (s: SingleAppIntent node.addEventListener('click', () => { callback({ - intent: { - name: intent, - displayName: "Whatever" - }, - chosenApp: { + action: "click", + intent, + appIdentifier: { + appId + } + }) + }); + + node.addEventListener('hover', () => { + callback({ + action: "hover", + intent, + appIdentifier: { appId } }) @@ -115,13 +131,20 @@ const fillList = (ai: AppIntent[], intent: string, callback: (s: SingleAppIntent node.addEventListener('click', () => { callback({ - intent: { - name: intent, - displayName: "Whatever" - }, - chosenApp: { - appId, - instanceId + action: "click", + intent, + appIdentifier: { + appId + } + }) + }); + + node.addEventListener('hover', () => { + callback({ + action: "hover", + intent, + appIdentifier: { + appId } }) }); @@ -132,34 +155,61 @@ const fillList = (ai: AppIntent[], intent: string, callback: (s: SingleAppIntent }; window.addEventListener("load", () => { - const parent = window.parent; const mc = new MessageChannel(); - const myPort = mc.port1 - myPort.start() - - parent.postMessage({ type: "SelectorMessageInitialize" }, "*", [mc.port2]); - - function callback(si: SingleAppIntent | null) { - myPort.postMessage({ - type: "ResolverMessageChoice", - payload: si - } as ResolverMessageChoice) - } - - myPort.addEventListener("message", (e) => { - if (e.data.type == 'ResolverIntents') { - const details = e.data as ResolverIntents - // console.log(JSON.stringify("INTENT DETAILS: " + JSON.stringify(details))) - - setup(details, callback) + const myPort = mc.port1; + myPort.start(); + myPort.onmessage = ({data}) => { + console.debug("Received message: ", data); + switch(data.type){ + case Fdc3UserInterfaceHandshake": { + break; + } + case "iframeResolve": { + myPort.postMessage({ + type: "iframeRestyle", + payload: { + updatedCSS: { + width: "100%", + height: "100%", + top: "0", + left: "0", + position: "fixed" + } + } + }); + + setup(data.payload, (s) => { + document.querySelector("dialog")?.close(); + myPort.postMessage({ + type: "iframeResolveAction", + payload: s + }); + + myPort.postMessage({ + type: "fdc3UserInterfaceRestyle", + payload: { + updatedCSS: { + width: "0", + height: "0" + } + } + }); + + }) + } } - }) - - document.getElementById("cancel")!!.addEventListener("click", () => { - callback(null); - }) - + }; + + parent.postMessage({ + type: "fdc3UserInterfaceHello", + payload: { + initialCSS: { + width: "0", + height: "0" + } + } + }, "*", [mc.port2]); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/addon/src/main.ts b/packages/addon/src/main.ts index 3313c5a9e..e2524b5db 100644 --- a/packages/addon/src/main.ts +++ b/packages/addon/src/main.ts @@ -135,7 +135,7 @@ const openChannelIframe = (e: MouseEvent) => { // User clicked on one of the channels in the channel selector // @ts-ignore: Explicit fall-through to iframeHandshake - case "iframeChannelSelected": { + case "fdc3UserInterfaceSelected": { // STEP 4B: Receive user selection information from iframe selected = data.channel; } @@ -144,7 +144,7 @@ const openChannelIframe = (e: MouseEvent) => { case "iframeHandshake": { // STEP 3A: Send channel data to iframe channel.port1.postMessage({ - type: "iframeChannels", + type: "fdc3UserInterfaceChannels", channels: recommendedChannels, selected }); @@ -171,7 +171,7 @@ const openChannelIframe = (e: MouseEvent) => { }); // STEP 1A: Send port to iframe - iframe.contentWindow?.postMessage({ type: 'iframeHello' }, '*', [channel.port2]); + iframe.contentWindow?.postMessage({ type: 'fdc3UserInterfaceHello' }, '*', [channel.port2]); }; const openResolverIframe = (e: MouseEvent )=> { @@ -203,7 +203,7 @@ const openResolverIframe = (e: MouseEvent )=> { iframe!.parentElement?.setAttribute("data-visible", "true"); // STEP 1A: Send port to iframe - iframe!.contentWindow?.postMessage({ type: 'iframeHello' }, '*', [channel.port2]); + iframe!.contentWindow?.postMessage({ type: 'fdc3UserInterfaceHello' }, '*', [channel.port2]); }; window.addEventListener('load', () => { diff --git a/packages/fdc3-get-agent/src/ui/AbstractUIComponent.ts b/packages/fdc3-get-agent/src/ui/AbstractUIComponent.ts index 8ed94dcad..d29740a9c 100644 --- a/packages/fdc3-get-agent/src/ui/AbstractUIComponent.ts +++ b/packages/fdc3-get-agent/src/ui/AbstractUIComponent.ts @@ -74,7 +74,7 @@ export abstract class AbstractUIComponent implements Connectable { return new Promise((resolve, _reject) => { const ml = (e: MessageEvent) => { // console.log("Received UI Message: " + JSON.stringify(e.data)) - if ((e.source == this.iframe?.contentWindow) && (e.data.type == 'iframeHello')) { + if ((e.source == this.iframe?.contentWindow) && (e.data.type == 'fdc3UserInterfaceHello')) { const helloData = e.data as IframeHello if (helloData.payload.initialCSS) { this.themeContainer(helloData.payload.initialCSS) diff --git a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts index 618d63764..fd21f1566 100644 --- a/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts +++ b/packages/fdc3-get-agent/src/ui/DefaultDesktopAgentChannelSelector.ts @@ -25,7 +25,7 @@ export class DefaultDesktopAgentChannelSelector extends AbstractUIComponent impl this.port = port port.addEventListener("message", (e) => { - if (e.data.type == 'iframeChannelSelected') { + if (e.data.type == 'fdc3UserInterfaceSelected') { const choice = e.data as IframeChannelSelected if ((choice.payload.selected) && (this.callback)) { this.callback(choice.payload.selected) @@ -37,7 +37,7 @@ export class DefaultDesktopAgentChannelSelector extends AbstractUIComponent impl updateChannel(channelId: string | null, availableChannels: Channel[]): void { // also send to the iframe this.port!!.postMessage({ - type: 'iframeChannels', + type: 'fdc3UserInterfaceChannels', payload: { selected: channelId, userChannels: availableChannels.map(ch => { diff --git a/packages/fdc3-get-agent/test/features/default-channel-selector.feature b/packages/fdc3-get-agent/test/features/default-channel-selector.feature index 824820abe..95532e612 100644 --- a/packages/fdc3-get-agent/test/features/default-channel-selector.feature +++ b/packages/fdc3-get-agent/test/features/default-channel-selector.feature @@ -16,5 +16,5 @@ Feature: Default Channel Selector And we wait for a period of "200" ms Then "{lastChannelSelectorMessage}" is an object with the following contents | type | payload.selected | payload.userChannels[0].id | payload.userChannels[1].id | payload.userChannels[2].id | - | iframeChannels | one | one | two | three | + | fdc3UserInterfaceChannels | one | one | two | three | And I call "{document}" with "shutdown" diff --git a/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature b/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature index 810d0aca2..42c234c95 100644 --- a/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature +++ b/packages/fdc3-get-agent/test/features/desktop-agent-strategy.feature @@ -58,8 +58,8 @@ Feature: Different Strategies for Accessing the Desktop Agent | message | WCP1Hello | | message | WCP2LoadUrl | | message | WCP3Handshake | - | message | iframeHello | - | message | iframeHello | + | message | fdc3UserInterfaceHello | + | message | fdc3UserInterfaceHello | | fdc3Ready | {null} | Then I call "{document}" with "shutdown" And I call "{desktopAgent}" with "disconnect" diff --git a/packages/fdc3-get-agent/test/step-definitions/channel-selector.steps.ts b/packages/fdc3-get-agent/test/step-definitions/channel-selector.steps.ts index 97dd30a02..12b17c510 100644 --- a/packages/fdc3-get-agent/test/step-definitions/channel-selector.steps.ts +++ b/packages/fdc3-get-agent/test/step-definitions/channel-selector.steps.ts @@ -24,7 +24,7 @@ Given('The channel selector sends a channel change message for channel {string}' const port = handleResolve("{document.iframes[0].messageChannels[0].port2}", this) port.postMessage({ - type: 'iframeChannelSelected', + type: 'fdc3UserInterfaceSelected', payload: { selected: channel } diff --git a/packages/fdc3-get-agent/test/support/FrameTypes.ts b/packages/fdc3-get-agent/test/support/FrameTypes.ts index 0de9f9f0d..1c359ab6b 100644 --- a/packages/fdc3-get-agent/test/support/FrameTypes.ts +++ b/packages/fdc3-get-agent/test/support/FrameTypes.ts @@ -38,7 +38,7 @@ export function handleChannelSelectorComms(_value: string, parent: MockWindow, s parent.dispatchEvent({ type: "message", data: { - type: "iframeHello", + type: "fdc3UserInterfaceHello", payload: { initialCSS: { "width": "100px" @@ -79,7 +79,7 @@ export function handleIntentResolverComms(_value: string, parent: MockWindow, so parent.dispatchEvent({ type: "message", data: { - type: "iframeHello", + type: "fdc3UserInterfaceHello", payload: { initialCSS: { "width": "100px" diff --git a/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts b/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts index 7c6ef30a2..ce31703ea 100644 --- a/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts +++ b/toolbox/fdc3-for-web/demo/src/client/apps/chartiq.ts @@ -26,7 +26,9 @@ const init = async () => { // Listen for changes to fdc3.instrument, and update the symbol fdc3.addContextListener("fdc3.instrument", (context) => { - stx.newChart(context.id?.ticker); + if(context.id?.ticker !== stx.chart.symbol){ + stx.newChart(context.id?.ticker); + } }); // Listen for ViewChart events diff --git a/toolbox/fdc3-for-web/demo/src/client/ui/channel-selector.ts b/toolbox/fdc3-for-web/demo/src/client/ui/channel-selector.ts index e46212f72..4eddaedc1 100644 --- a/toolbox/fdc3-for-web/demo/src/client/ui/channel-selector.ts +++ b/toolbox/fdc3-for-web/demo/src/client/ui/channel-selector.ts @@ -40,7 +40,7 @@ window.addEventListener("load", () => { // ISSUE: 1302 parent.postMessage({ - type: "iframeHello", + type: "fdc3UserInterfaceHello", payload: { initialCSS: DEFAULT_COLLAPSED_CSS, implementationDetails: "Demo Channel Selector v1.0" @@ -57,7 +57,7 @@ window.addEventListener("load", () => { if (e.data.type == 'iframeHandshake') { // ok, port is ready, send the iframe position detials myPort.postMessage({ type: "iframeRestyle", payload: { updatedCSS: DEFAULT_COLLAPSED_CSS } } as IframeRestyle) - } else if (e.data.type == 'iframeChannels') { + } else if (e.data.type == 'fdc3UserInterfaceChannels') { const details = e.data as IframeChannels console.log(JSON.stringify("CHANNEL DETAILS: " + JSON.stringify(details))) channels = details.payload.userChannels @@ -86,7 +86,7 @@ window.addEventListener("load", () => { a.onclick = () => { changeSize(false) channelId = channel.id - myPort.postMessage({ type: "iframeChannelSelected", payload: { selected: channel.id } }) + myPort.postMessage({ type: "fdc3UserInterfaceSelected", payload: { selected: channel.id } }) } }) diff --git a/toolbox/fdc3-for-web/demo/src/client/ui/intent-resolver.ts b/toolbox/fdc3-for-web/demo/src/client/ui/intent-resolver.ts index 89a23070a..617f3e2a2 100644 --- a/toolbox/fdc3-for-web/demo/src/client/ui/intent-resolver.ts +++ b/toolbox/fdc3-for-web/demo/src/client/ui/intent-resolver.ts @@ -34,7 +34,7 @@ window.addEventListener("load", () => { // ISSUE: 1302 parent.postMessage({ - type: "iframeHello", + type: "fdc3UserInterfaceHello", payload: { initialCSS: DEFAULT_COLLAPSED_CSS, implementationDetails: "Demo Intent Resolver v1.0"