diff --git a/src/background/clients/HintClient.ts b/src/background/clients/HintClient.ts index cba4398d..01015377 100644 --- a/src/background/clients/HintClient.ts +++ b/src/background/clients/HintClient.ts @@ -12,28 +12,27 @@ export type Size = { }; export default interface HintClient { - countHints( + lookupTargets( tabId: number, frameId: number, viewSize: Size, framePosition: Point, - ): Promise; + ): Promise; - createHints( + assignTags( tabId: number, frameId: number, - hints: string[], - viewSize: Size, - framePosition: Point, + elementTags: Record, ): Promise; - filterHints(tabId: number, prefix: string): Promise; + showHints(tabId: number, elements: string[]): Promise; clearHints(tabId: number): Promise; - activateIfExists( + activate( tabId: number, - tag: string, + frameId: number, + element: string, newTab: boolean, background: boolean, ): Promise; @@ -41,51 +40,47 @@ export default interface HintClient { @injectable() export class HintClientImpl implements HintClient { - countHints( + async lookupTargets( tabId: number, frameId: number, viewSize: Size, framePosition: Point, - ): Promise { + ): Promise { const sender = newSender(tabId, frameId); - return sender.send("follow.count.hints", { + const { elements } = await sender.send("follow.lookup", { viewSize, framePosition, }); + return elements; } - createHints( + assignTags( tabId: number, frameId: number, - hints: string[], - viewSize: Size, - framePosition: Point, + elementTags: Record, ): Promise { const sender = newSender(tabId, frameId); - return sender.send("follow.create.hints", { - viewSize, - framePosition, - hints, - }); + return sender.send("follow.assign", { elementTags }); } - filterHints(tabId: number, prefix: string): Promise { + showHints(tabId: number, elements: string[]): Promise { const sender = newSender(tabId); - return sender.send("follow.filter.hints", { prefix }); + return sender.send("follow.show", { elements }); } clearHints(tabId: number): Promise { const sender = newSender(tabId); - return sender.send("follow.remove.hints"); + return sender.send("follow.clear"); } - activateIfExists( + activate( tabId: number, - hint: string, + frameId: number, + element: string, newTab: boolean, background: boolean, ): Promise { - const sender = newSender(tabId); - return sender.send("follow.activate", { hint, newTab, background }); + const sender = newSender(tabId, frameId); + return sender.send("follow.activate", { element, newTab, background }); } } diff --git a/src/background/repositories/HintRepository.ts b/src/background/repositories/HintRepository.ts index e24fdd8e..fb1b4913 100644 --- a/src/background/repositories/HintRepository.ts +++ b/src/background/repositories/HintRepository.ts @@ -1,10 +1,16 @@ import { injectable } from "inversify"; import LocalCache, { LocalCacheImpl } from "../db/LocalStorage"; +export type HintTarget = { + frameId: number; + element: string; + tag: string; +}; + export default interface HintRepository { startHintMode( opts: { newTab: boolean; background: boolean }, - hints: string[], + targets: HintTarget[], ): Promise; getOption(): Promise<{ newTab: boolean; background: boolean }>; @@ -13,9 +19,7 @@ export default interface HintRepository { popKey(): Promise; - getMatchedHints(): Promise; - - getKeys(): Promise; + getMatchedHints(): Promise; } type Option = { @@ -25,7 +29,7 @@ type Option = { type State = { option: Option; - hints: string[]; + hintMap: Record; // tag -> target keys: string[]; }; @@ -36,7 +40,7 @@ export class HintRepositoryImpl implements HintRepository { HintRepositoryImpl.name, { option: { newTab: false, background: false }, - hints: [], + hintMap: {}, keys: [], }, ), @@ -44,11 +48,19 @@ export class HintRepositoryImpl implements HintRepository { startHintMode( option: { newTab: boolean; background: boolean }, - hints: string[], + hints: HintTarget[], ): Promise { + const hintMap = hints.reduce( + (acc, hint) => { + acc[hint.tag] = hint; + return acc; + }, + {} as Record, + ); + const state: State = { option, - hints, + hintMap, keys: [], }; return this.cache.setValue(state); @@ -71,14 +83,10 @@ export class HintRepositoryImpl implements HintRepository { await this.cache.setValue(state); } - async getMatchedHints(): Promise { + async getMatchedHints(): Promise { const state = await this.cache.getValue(); const prefix = state.keys.join(""); - return state.hints.filter((t) => t.startsWith(prefix)); - } - - async getKeys(): Promise { - const { keys } = await this.cache.getValue(); - return keys.join(""); + const tags = Object.keys(state.hintMap).filter((t) => t.startsWith(prefix)); + return tags.map((t) => state.hintMap[t]); } } diff --git a/src/background/usecases/HintKeyUseCase.ts b/src/background/usecases/HintKeyUseCase.ts index e88a0db5..932e0be3 100644 --- a/src/background/usecases/HintKeyUseCase.ts +++ b/src/background/usecases/HintKeyUseCase.ts @@ -1,6 +1,8 @@ import { injectable, inject } from "inversify"; import HintClient from "../clients/HintClient"; -import HintRepository from "../repositories/HintRepository"; +import HintRepository, { + type HintTarget, +} from "../repositories/HintRepository"; @injectable() export default class HintKeyUseCase { @@ -13,39 +15,49 @@ export default class HintKeyUseCase { async pressKey(tabId: number, key: string): Promise { switch (key) { - case "Enter": - await this.activate(tabId, await this.hintRepository.getKeys()); + case "Enter": { + const matched = await this.hintRepository.getMatchedHints(); + if (matched.length === 1) { + await this.activate(tabId, matched[0]); + } return false; + } case "Esc": return false; case "Backspace": case "Delete": await this.hintRepository.popKey(); - await this.filter(tabId, await this.hintRepository.getKeys()); + await this.showOnlyMatched(tabId); return true; } await this.hintRepository.pushKey(key); - const prefix = await this.hintRepository.getKeys(); const matched = await this.hintRepository.getMatchedHints(); if (matched.length === 0) { return false; } else if (matched.length === 1) { - await this.activate(tabId, prefix); + await this.activate(tabId, matched[0]); return false; } else { - await this.filter(tabId, prefix); + await this.showOnlyMatched(tabId); return true; } } - private async activate(tabId: number, tag: string): Promise { + private async activate(tabId: number, target: HintTarget): Promise { const { newTab, background } = await this.hintRepository.getOption(); - await this.hintClient.activateIfExists(tabId, tag, newTab, background); + await this.hintClient.activate( + tabId, + target.frameId, + target.element, + newTab, + background, + ); } - private async filter(tabId: number, prefix: string): Promise { - await this.hintClient.filterHints(tabId, prefix); + private async showOnlyMatched(tabId: number): Promise { + const hints = await this.hintRepository.getMatchedHints(); + await this.hintClient.showHints(tabId, hints.map((hint) => hint.element)); } } diff --git a/src/background/usecases/HintModeUseCase.ts b/src/background/usecases/HintModeUseCase.ts index 00943552..68b8c67f 100644 --- a/src/background/usecases/HintModeUseCase.ts +++ b/src/background/usecases/HintModeUseCase.ts @@ -4,7 +4,9 @@ import PropertySettings from "../settings/PropertySettings"; import TopFrameClient from "../clients/TopFrameClient"; import HintTagProducer from "./HintTagProducer"; import HintClient from "../clients/HintClient"; -import HintRepository from "../repositories/HintRepository"; +import HintRepository, { + type HintTarget, +} from "../repositories/HintRepository"; @injectable() export default class HintModeUseCaes { @@ -36,7 +38,7 @@ export default class HintModeUseCaes { const viewport = await this.topFrameClient.getWindowViewport(tabId); const hintKeys = new HintTagProducer(hintchars); - const hints = []; + const targets: HintTarget[] = []; for (const frameId of frameIds) { const framePos = await this.topFrameClient.getFramePosition( tabId, @@ -45,24 +47,26 @@ export default class HintModeUseCaes { if (!framePos) { continue; } - const count = await this.hintClient.countHints( + const ids = await this.hintClient.lookupTargets( tabId, frameId, viewport, framePos, ); - const tags = hintKeys.produceN(count); - hints.push(...tags); - await this.hintClient.createHints( - tabId, - frameId, - tags, - viewport, - framePos, - ); + const idTags = hintKeys.produceN(ids.length); + const idTagMap = Object.fromEntries(ids.map((id, i) => [id, idTags[i]])); + await this.hintClient.assignTags(tabId, frameId, idTagMap); + + for (const [element, tag] of Object.entries(idTagMap)) { + targets.push({ + frameId, + element, + tag, + }); + } } - await this.hintRepository.startHintMode({ newTab, background }, hints); + await this.hintRepository.startHintMode({ newTab, background }, targets); } async stop(tabId: number): Promise { diff --git a/src/content/controllers/HintController.ts b/src/content/controllers/HintController.ts index 04532af0..d3f3e572 100644 --- a/src/content/controllers/HintController.ts +++ b/src/content/controllers/HintController.ts @@ -8,45 +8,50 @@ export default class HintController { private readonly hintUseCase: HintUseCase, ) {} - async countHints({ + async lookupTargets({ viewSize, framePosition, }: { viewSize: { width: number; height: number }; framePosition: { x: number; y: number }; - }): Promise { - return this.hintUseCase.countHints(viewSize, framePosition); + }): Promise<{ elements: string[] }> { + throw new Error("not implemented"); + // return this.hintUseCase.lookupTargets(viewSize, framePosition); } - async createHints({ - viewSize, - framePosition, - hints, + async assignTags({ + elementTags, }: { - viewSize: { width: number; height: number }; - framePosition: { x: number; y: number }; - hints: string[]; + elementTags: Record; }): Promise { - return this.hintUseCase.createHints(viewSize, framePosition, hints); + throw new Error("not implemented"); + // return this.hintUseCase.assignTags(elementTags); } - async filterHints({ prefix }: { prefix: string }): Promise { - return this.hintUseCase.filterHints(prefix); + async showHints({ + elements, + }: { + elements: string[]; + }): Promise { + throw new Error("not implemented"); + // return this.hintUseCase.showHints(elements); } - async remove(): Promise { - return this.hintUseCase.remove(); + async clearHints(): Promise { + throw new Error("not implemented"); + // return this.hintUseCase.clearHints(); } - async activateIfExists({ - hint, + async activate({ + element, newTab, background, }: { - hint: string; + element: string; newTab: boolean; background: boolean; }): Promise { - return this.hintUseCase.activateIfExists(hint, newTab, background); + throw new Error("not implemented"); + // return this.hintUseCase.activate(element, newTab, background); } } diff --git a/src/content/messaging/ContentMessageListener.ts b/src/content/messaging/ContentMessageListener.ts index d9e8c282..1ab95c9c 100644 --- a/src/content/messaging/ContentMessageListener.ts +++ b/src/content/messaging/ContentMessageListener.ts @@ -120,20 +120,20 @@ export default class ContentMessageListener { .route("get.frame.position") .to(topFrameController.getFramePosition.bind(topFrameController)); this.receiver - .route("follow.count.hints") - .to(followController.countHints.bind(followController)); + .route("follow.lookup") + .to(followController.lookupTargets.bind(followController)); this.receiver - .route("follow.create.hints") - .to(followController.createHints.bind(followController)); + .route("follow.assign") + .to(followController.assignTags.bind(followController)); this.receiver - .route("follow.filter.hints") - .to(followController.filterHints.bind(followController)); + .route("follow.show") + .to(followController.showHints.bind(followController)); this.receiver - .route("follow.remove.hints") - .to(followController.remove.bind(followController)); + .route("follow.clear") + .to(followController.clearHints.bind(followController)); this.receiver .route("follow.activate") - .to(followController.activateIfExists.bind(followController)); + .to(followController.activate.bind(followController)); } listen() { diff --git a/src/messaging/schema/content.ts b/src/messaging/schema/content.ts index 85368cd6..124492c7 100644 --- a/src/messaging/schema/content.ts +++ b/src/messaging/schema/content.ts @@ -33,22 +33,26 @@ export type Schema = { { frameId: number }, { x: number; y: number } | undefined >; - "follow.count.hints": Duplex< + "follow.lookup": Duplex< { viewSize: { width: number; height: number }; framePosition: { x: number; y: number }; }, - number + { elements: string[] } >; - "follow.create.hints": Duplex<{ - viewSize: { width: number; height: number }; - framePosition: { x: number; y: number }; - hints: string[]; - }>; - "follow.filter.hints": Duplex<{ prefix: string }>; - "follow.remove.hints": Duplex; + "follow.assign": Duplex< + { + elementTags: Record + } + >; + "follow.show": Duplex< + { + elements: string[]; + } + >; + "follow.clear": Duplex; "follow.activate": Duplex<{ - hint: string; + element: string, newTab: boolean; background: boolean; }>;