Skip to content

Commit 39ced9b

Browse files
wip
1 parent 5bda4f2 commit 39ced9b

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

packages/teams-js/src/internal/globalVars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export class GlobalVars {
1010
public static hostClientType: string | undefined = undefined;
1111
public static clientSupportedSDKVersion: string;
1212
public static printCapabilityEnabled = false;
13+
public static teamsShortcutCapabilityEnabled = false;
1314
public static readonly teamsJsInstanceId: string = new UUID().toString();
1415
}

packages/teams-js/src/internal/telemetry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export const enum ApiName {
7272
App_NotifySuccess = 'app.notifySuccess',
7373
App_OpenLink = 'app.openLink',
7474
App_RegisterOnThemeChangeHandler = 'app.registerOnThemeChangeHandler',
75+
App_RequestTeamsShortcut = 'app.requestTeamsShortcut',
76+
App_ProcessShortcutKeydown = 'app.processShortcutKeydown',
7577
AppInitialization_NotifyAppLoaded = 'appInitialization.notifyAppLoaded',
7678
AppInitialization_NotifyExpectedFailure = 'appInitialization.notifyExpectedFailure',
7779
AppInitialization_NotifyFailure = 'appInitialization.notifyFailure',
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { callFunctionInHost, callFunctionInHostAndHandleResponse } from '../internal/communication';
2+
import { GlobalVars } from '../internal/globalVars';
3+
import { ensureInitialized } from '../internal/internalAPIs';
4+
import { ResponseHandler } from '../internal/responseHandler';
5+
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
6+
import { errorNotSupportedOnPlatform } from './constants';
7+
import { runtime } from './runtime';
8+
9+
/* ------------------------------------------------------------------ */
10+
/* Types */
11+
/* ------------------------------------------------------------------ */
12+
13+
type TeamsShortcuts = Map<string, string[]>;
14+
15+
type TeamsShortcutResponse = {
16+
shortcuts: TeamsShortcuts;
17+
overridableShortcuts: string[];
18+
};
19+
20+
/* ------------------------------------------------------------------ */
21+
/* Utils */
22+
/* ------------------------------------------------------------------ */
23+
24+
function isSupported(): boolean {
25+
return ensureInitialized(runtime) && runtime.supports.teamsCore ? true : false;
26+
}
27+
28+
function getMatchingShortcut(
29+
shortcuts: TeamsShortcuts,
30+
e: KeyboardEvent,
31+
): { id: string; segments: string[] } | undefined {
32+
for (const [id, keyCombinations] of shortcuts.entries()) {
33+
for (const keyCombination of keyCombinations) {
34+
const segments = keyCombination.toLowerCase().split('+');
35+
if (
36+
// TODO update the matching logic to also verify segments length against event
37+
segments.every((key) => {
38+
switch (key) {
39+
case 'ctrl':
40+
return e.ctrlKey;
41+
case 'shift':
42+
return e.shiftKey;
43+
case 'alt':
44+
case 'option':
45+
return e.altKey;
46+
case 'meta':
47+
case 'cmd':
48+
return e.metaKey;
49+
default:
50+
return e.key.toLowerCase() === key;
51+
}
52+
})
53+
) {
54+
return { id, segments };
55+
}
56+
}
57+
}
58+
return undefined;
59+
}
60+
61+
const overridableShortcuts: Set<string> = new Set();
62+
/**
63+
* A map of shortcut command id and the list of key combinations that trigger it.
64+
* The key combinations are stored in lower-case, sorted order.
65+
* For example, "SlashCommands" shortcut will be stored as ["ctrl+/", "ctrl+shift+/"].
66+
*/
67+
const teamsShortcuts: TeamsShortcuts = new Map();
68+
69+
class GetTeamsShortcutResponseHandler extends ResponseHandler<TeamsShortcutResponse, TeamsShortcutResponse> {
70+
public validate(response: TeamsShortcutResponse): boolean {
71+
return response && response.shortcuts instanceof Map && Array.isArray(response.overridableShortcuts);
72+
}
73+
74+
public deserialize(response: TeamsShortcutResponse): TeamsShortcutResponse {
75+
this.onSuccess(response);
76+
return response;
77+
}
78+
79+
/** Persist the received shortcuts in memory */
80+
private onSuccess(response: TeamsShortcutResponse): void {
81+
teamsShortcuts.clear();
82+
response.shortcuts.forEach((value: string[], key: string) => {
83+
teamsShortcuts.set(key, value);
84+
});
85+
overridableShortcuts.clear();
86+
response.overridableShortcuts.forEach((shortcut: string) => {
87+
overridableShortcuts.add(shortcut);
88+
});
89+
}
90+
}
91+
92+
/* ------------------------------------------------------------------ */
93+
/* API */
94+
/* ------------------------------------------------------------------ */
95+
96+
type OverrideTeamsShortcutFunctionType = (shortcut: { id: string; segments: string[] }) => boolean;
97+
98+
/**
99+
* Enable capability to support Teams shortcuts.
100+
*/
101+
export function enableTeamsShortcutCapability(onOverridableShortcut?: OverrideTeamsShortcutFunctionType): void {
102+
if (!GlobalVars.teamsShortcutCapabilityEnabled) {
103+
ensureInitialized(runtime);
104+
if (!isSupported()) {
105+
throw errorNotSupportedOnPlatform;
106+
}
107+
GlobalVars.teamsShortcutCapabilityEnabled = true;
108+
109+
/* 1. Ask host for the list of enabled shortcuts */
110+
callFunctionInHostAndHandleResponse(
111+
ApiName.App_RequestTeamsShortcut,
112+
[],
113+
new GetTeamsShortcutResponseHandler(),
114+
getApiVersionTag(ApiVersionNumber.V_2, ApiName.App_RequestTeamsShortcut),
115+
);
116+
117+
/* 2. Global key-down handler */
118+
document.addEventListener(
119+
'keydown',
120+
(event: KeyboardEvent) => {
121+
const matchingShortcut = getMatchingShortcut(teamsShortcuts, event);
122+
123+
if (!matchingShortcut) {
124+
return; // ignore unrelated events
125+
}
126+
127+
if (onOverridableShortcut && overridableShortcuts.has(matchingShortcut?.id)) {
128+
const shouldOverride = onOverridableShortcut({
129+
id: matchingShortcut.id,
130+
segments: matchingShortcut.segments,
131+
});
132+
if (shouldOverride) {
133+
return; // Do not forward shortcut to host
134+
}
135+
}
136+
137+
/* Forward shortcut to host */
138+
const payload = JSON.stringify({
139+
type: event.type,
140+
matchingShortcut: matchingShortcut,
141+
key: event.key,
142+
code: event.code,
143+
altKey: event.altKey,
144+
ctrlKey: event.ctrlKey,
145+
metaKey: event.metaKey,
146+
shiftKey: event.shiftKey,
147+
repeat: event.repeat,
148+
timeStamp: event.timeStamp,
149+
});
150+
151+
callFunctionInHost(
152+
ApiName.App_ProcessShortcutKeydown,
153+
[payload],
154+
getApiVersionTag(ApiVersionNumber.V_2, ApiName.App_ProcessShortcutKeydown),
155+
);
156+
157+
event.cancelBubble = true;
158+
event.preventDefault();
159+
event.stopImmediatePropagation();
160+
},
161+
{ capture: true },
162+
);
163+
}
164+
}

packages/teams-js/src/public/teamsAPIs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { errorNotSupportedOnPlatform } from './constants';
1313
import { LoadContext } from './interfaces';
1414
import { runtime } from './runtime';
1515

16+
export { enableTeamsShortcutCapability } from './shortcut';
17+
1618
/**
1719
* v2 APIs telemetry file: All of APIs in this capability file should send out API version v2 ONLY
1820
*/

0 commit comments

Comments
 (0)