Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/MutationEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,14 @@ export function observeMutations(
}
}
} else {
for (let i = 0; i < removed.length; i++) {
const removedNode = removed[i];
for (const removedNode of removed) {
removedNodes.add(removedNode);
updateTabsterElements(removedNode, true);
tabster._dummyObserver.domChanged?.(target as HTMLElement);
}

for (let i = 0; i < added.length; i++) {
updateTabsterElements(added[i]);
for (const addedNode of added) {
updateTabsterElements(addedNode);
tabster._dummyObserver.domChanged?.(target as HTMLElement);
}
}
Expand Down
8 changes: 2 additions & 6 deletions src/Outline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,10 @@ export class OutlineAPI implements Types.OutlineAPI {
this._fullScreenEventName = "fullscreenchange";
this._fullScreenElementName = "fullscreenElement";
} else if ("onwebkitfullscreenchange" in document) {
// Safari 15-16.3 only ships the webkit-prefixed API;
// unprefixed landed in Safari 16.4.
this._fullScreenEventName = "webkitfullscreenchange";
this._fullScreenElementName = "webkitFullscreenElement";
} else if ("onmozfullscreenchange" in document) {
this._fullScreenEventName = "mozfullscreenchange";
this._fullScreenElementName = "mozFullScreenElement";
} else if ("onmsfullscreenchange" in document) {
this._fullScreenEventName = "msfullscreenchange";
this._fullScreenElementName = "msFullscreenElement";
}
}
}
Expand Down
120 changes: 39 additions & 81 deletions src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,23 @@ interface WindowWithUtilsConext extends Window {

export function getInstanceContext(getWindow: GetWindow): InstanceContext {
const win = getWindow() as WindowWithUtilsConext;

let ctx = win.__tabsterInstanceContext;

if (!ctx) {
ctx = {
elementByUId: {},
containerBoundingRectCache: {},
lastContainerBoundingRectCacheId: 0,
};

win.__tabsterInstanceContext = ctx;
}

return ctx;
return (win.__tabsterInstanceContext ??= {
elementByUId: {},
containerBoundingRectCache: {},
lastContainerBoundingRectCacheId: 0,
});
}

export function disposeInstanceContext(win: Window): void {
const ctx = (win as WindowWithUtilsConext).__tabsterInstanceContext;

const w = win as WindowWithUtilsConext;
const ctx = w.__tabsterInstanceContext;
if (ctx) {
ctx.elementByUId = {};

ctx.containerBoundingRectCache = {};

if (ctx.containerBoundingRectCacheTimer) {
win.clearTimeout(ctx.containerBoundingRectCacheTimer);
}

delete (win as WindowWithUtilsConext).__tabsterInstanceContext;
delete w.__tabsterInstanceContext;
}
}

Expand Down Expand Up @@ -333,41 +321,28 @@ export function shouldIgnoreFocus(element: HTMLElement): boolean {

export function getUId(wnd: Window): string {
const rnd = new Uint32Array(4);

wnd.crypto.getRandomValues(rnd);

const srnd: string[] = [];

for (let i = 0; i < rnd.length; i++) {
srnd.push(rnd[i].toString(36));
}

srnd.push("|");
srnd.push((++_uidCounter).toString(36));
srnd.push("|");
srnd.push(Date.now().toString(36));

return srnd.join("");
return (
Array.from(rnd, (n) => n.toString(36)).join("") +
"|" +
(++_uidCounter).toString(36) +
"|" +
Date.now().toString(36)
);
}

export function getElementUId(
getWindow: GetWindow,
element: HTMLElementWithUID
): string {
const context = getInstanceContext(getWindow);
let uid = element.__tabsterElementUID;

if (!uid) {
uid = element.__tabsterElementUID = getUId(getWindow());
}

const uid = (element.__tabsterElementUID ??= getUId(getWindow()));
if (
!context.elementByUId[uid] &&
documentContains(element.ownerDocument, element)
) {
context.elementByUId[uid] = new WeakHTMLElement(element);
}

return uid;
}

Expand All @@ -379,32 +354,22 @@ export function getElementByUId(
}

export function getWindowUId(win: WindowWithUID): string {
let uid = win.__tabsterCrossOriginWindowUID;

if (!uid) {
uid = win.__tabsterCrossOriginWindowUID = getUId(win);
}

return uid;
return (win.__tabsterCrossOriginWindowUID ??= getUId(win));
}

export function clearElementCache(
getWindow: GetWindow,
parent?: HTMLElement
): void {
const context = getInstanceContext(getWindow);

for (const key of Object.keys(context.elementByUId)) {
const wel = context.elementByUId[key];
const el = wel && wel.get();

if (el && parent) {
if (!dom.nodeContains(parent, el)) {
continue;
}
const cache = getInstanceContext(getWindow).elementByUId;
for (const key of Object.keys(cache)) {
const el = cache[key]?.get();
// When `parent` is given, only entries inside it are pruned; otherwise
// the entire cache is cleared.
if (parent && el && !dom.nodeContains(parent, el)) {
continue;
}

delete context.elementByUId[key];
delete cache[key];
}
}

Expand Down Expand Up @@ -551,22 +516,18 @@ export function augmentAttribute(
export function getTabsterAttributeOnElement(
element: HTMLElement
): TabsterAttributeProps | null {
if (!element.hasAttribute(TABSTER_ATTRIBUTE_NAME)) {
// `getAttribute` already returns null when the attribute is absent —
// no need for a separate `hasAttribute` probe.
const rawAttribute = element.getAttribute(TABSTER_ATTRIBUTE_NAME);
if (rawAttribute === null) {
return null;
}

// We already checked the presence with `hasAttribute`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const rawAttribute = element.getAttribute(TABSTER_ATTRIBUTE_NAME)!;
let tabsterAttribute: TabsterAttributeProps;
try {
tabsterAttribute = JSON.parse(rawAttribute);
return JSON.parse(rawAttribute);
} catch {
console.error("Tabster: failed to parse attribute", rawAttribute);
tabsterAttribute = {};
return {};
}

return tabsterAttribute;
}

export function isDisplayNone(element: HTMLElement): boolean {
Expand Down Expand Up @@ -621,24 +582,21 @@ export function getRadioButtonGroup(
if (!isRadio(element)) {
return;
}

const name = (element as HTMLInputElement).name;
let radioButtons = Array.from(dom.getElementsByName(element, name));
const radioButtons: HTMLInputElement[] = [];
let checked: HTMLInputElement | undefined;

radioButtons = radioButtons.filter((el) => {
for (const el of dom.getElementsByName(element, name)) {
if (isRadio(el)) {
if ((el as HTMLInputElement).checked) {
checked = el as HTMLInputElement;
const input = el as HTMLInputElement;
radioButtons.push(input);
if (input.checked) {
checked = input;
}
return true;
}
return false;
});

}
return {
name,
buttons: new Set(radioButtons as HTMLInputElement[]),
buttons: new Set(radioButtons),
checked,
};
}
Expand Down
Loading