2022-12-22 12:37:33 +08:00

161 lines
3.3 KiB
TypeScript

export class FocusTracker {
mount(win: Window) {
const checkDown = (e: MouseEvent) => {
if (this.checkModalDown(e)) {
return;
}
const { first } = this;
if (first && !first.internalCheckInRange(e)) {
this.internalSuspenseItem(first);
first.internalTriggerBlur();
}
};
win.document.addEventListener('click', checkDown, true);
return () => {
win.document.removeEventListener('click', checkDown, true);
};
}
private actives: Focusable[] = [];
get first() {
return this.actives[0];
}
private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = [];
addModal(checkDown: (e: MouseEvent) => boolean, checkOpen: () => boolean) {
this.modals.push({
checkDown,
checkOpen,
});
}
private checkModalOpen(): boolean {
return this.modals.some((item) => item.checkOpen());
}
private checkModalDown(e: MouseEvent): boolean {
return this.modals.some((item) => item.checkDown(e));
}
execSave() {
// has Modal return;
if (this.checkModalOpen()) {
return;
}
// catch
if (this.first) {
this.first.internalTriggerSave();
}
}
execEsc() {
const { first } = this;
if (first) {
this.internalSuspenseItem(first);
first.internalTriggerEsc();
}
}
create(config: FocusableConfig) {
return new Focusable(this, config);
}
internalActiveItem(item: Focusable) {
const first = this.actives[0];
if (first === item) {
return;
}
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
}
this.actives.unshift(item);
if (!item.isModal && first) {
// trigger Blur
first.internalTriggerBlur();
}
// trigger onActive
item.internalTriggerActive();
}
internalSuspenseItem(item: Focusable) {
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
this.first?.internalTriggerActive();
}
}
}
export interface FocusableConfig {
range: HTMLElement | ((e: MouseEvent) => boolean);
modal?: boolean; // 模态窗口级别
onEsc?: () => void;
onBlur?: () => void;
onSave?: () => void;
onActive?: () => void;
}
export class Focusable {
readonly isModal: boolean;
constructor(private tracker: FocusTracker, private config: FocusableConfig) {
this.isModal = config.modal == null ? false : config.modal;
}
active() {
this.tracker.internalActiveItem(this);
}
suspense() {
this.tracker.internalSuspenseItem(this);
}
purge() {
this.tracker.internalSuspenseItem(this);
}
internalCheckInRange(e: MouseEvent) {
const { range } = this.config;
if (!range) {
return false;
}
if (typeof range === 'function') {
return range(e);
}
return range.contains(e.target as HTMLElement);
}
internalTriggerBlur() {
if (this.config.onBlur) {
this.config.onBlur();
}
}
internalTriggerSave() {
if (this.config.onSave) {
this.config.onSave();
return true;
}
return false;
}
internalTriggerEsc() {
if (this.config.onEsc) {
this.config.onEsc();
}
}
internalTriggerActive() {
if (this.config.onActive) {
this.config.onActive();
}
}
}
export const focusTracker = new FocusTracker();
focusTracker.mount(window);