From 60853a167912d5307ccf1fa08d86835c686e94ae Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Sun, 24 Apr 2022 17:17:55 +0900 Subject: [PATCH] =?UTF-8?q?:sparkles:=20caret=E3=81=A8selection=E3=82=92?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E3=81=99=E3=82=8B=E5=86=85=E9=83=A8=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=B9=E3=82=92=E7=84=A1=E7=90=86=E3=82=84=E3=82=8A?= =?UTF-8?q?=E5=BC=95=E3=81=8D=E3=81=9A=E3=82=8A=E5=87=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser/dom/cursor.d.ts | 154 +++++++++++++++++++++++++++++++++++++ browser/dom/cursor.ts | 13 ++++ browser/dom/mod.ts | 2 + browser/dom/position.ts | 5 ++ browser/dom/selection.d.ts | 70 +++++++++++++++++ browser/dom/selection.ts | 13 ++++ browser/dom/stores.ts | 37 +++++++++ deps/scrapbox.ts | 3 + 8 files changed, 297 insertions(+) create mode 100644 browser/dom/cursor.d.ts create mode 100644 browser/dom/cursor.ts create mode 100644 browser/dom/position.ts create mode 100644 browser/dom/selection.d.ts create mode 100644 browser/dom/selection.ts create mode 100644 browser/dom/stores.ts diff --git a/browser/dom/cursor.d.ts b/browser/dom/cursor.d.ts new file mode 100644 index 0000000..fd35e3d --- /dev/null +++ b/browser/dom/cursor.d.ts @@ -0,0 +1,154 @@ +/// +/// +/// + +import { BaseStore } from "../../deps/scrapbox.ts"; +import { Position } from "./position.ts"; + +export interface SetPositionOptions { + /** カーソルが画面外に移動したとき、カーソルが見える位置までページをスクロールするかどうか + * + * @default true + */ + scrollInView?: boolean; + + /** カーソル移動イベントの発生箇所? + * + * コード内だと、"mouse"が指定されていた場合があった。詳細は不明 + */ + source?: string; +} + +/** カーソル操作クラス */ +export declare class Cursor extends BaseStore { + constructor(); + + /** カーソルの位置を初期化し、editorからカーソルを外す */ + clear(): void; + + /** カーソルの位置を取得する */ + getPosition(): Position; + + /** カーソルが表示されているか調べる */ + getVisible(): boolean; + + /** カーソルを指定した位置に動かす */ + setPosition( + position: Position, + option?: SetPositionOptions, + ): void; + + /** popup menuを表示する */ + showEditPopupMenu(): void; + + /** popup menuを消す */ + hidePopupMenu(): void; + + /** #text-inputにカーソルをfocusし、同時にカーソルを表示する */ + focus(): void; + + /** #text-inputからfocusを外す。カーソルの表示状態は変えない */ + blur(): void; + + /** カーソルの位置が行や列の外に出ていた場合に、存在する行と列の中に納める */ + fixPosition(): void; + + /** カーソルが行頭にいてかつ表示されていたら`true` */ + isAtLineHead(): boolean; + + /** カーソルが行末にいてかつ表示されていたら`true` */ + isAtLineTail(): boolean; + + /** カーソルを表示する + * + * #text-inputのfocus状態は変えない + */ + show(): void; + + /** カーソルを非表示にする + * + * touch deviceの場合は、#text-inputからfocusを外す + */ + hide(): void; + + /** カーソル操作コマンド + * + * | Command | Description | + * | ------ | ----------- | + * | go-up | 1行上に動かす | + * | go-down | 1行下に動かす | + * | go-left | 1文字左に動かす | + * | go-right | 1文字右に動かす | + * | go-forward | Emacs key bindingsで使われているコマンド。go-rightとほぼ同じ | + * | go-backward | Emacs key bindingsで使われているコマンド。go-leftとほぼ同じ | + * | go-top | タイトル行の行頭に飛ぶ | + * | go-bottom | 最後の行の行末に飛ぶ | + * | go-word-head | 1単語右に動かす | + * | go-word-tail | 1単語左に動かす | + * | go-line-head | 行頭に飛ぶ | + * | go-line-tail | 行末に飛ぶ | + * | go-pagedown | 1ページ分下の行に飛ぶ | + * | go-pageup | 1ページ分上の行に飛ぶ | + */ + goByAction( + action: + | "go-up" + | "go-down" + | "go-left" + | "go-right" + | "go-forward" + | "go-backward" + | "go-top" + | "go-bottom" + | "go-word-head" + | "go-word-tail" + | "go-line-head" + | "go-line-tail" + | "go-pagedown" + | "go-pageup", + ): void; + + /* `scrapbox.Page.lines`とほぼ同じ */ + get lines(): unknown[]; + + /* `scrapbox.Project.pages`とほぼ同じ */ + get pages(): unknown; + + private goUp(): void; + private goPageUp(): void; + private goDown(): void; + private goPageDown(): void; + private getNextLineHead(): void; + private getPrevLineTail(): void; + private goBackward(init?: { scrollInView: boolean }): void; + private goForward(init?: { scrollInView: boolean }): void; + private goLeft(): void; + private goRight(): void; + /** タイトルの先頭文字に飛ぶ */ + private goTop(): void; + /** 最後の行の末尾に飛ぶ */ + private goBottom(): void; + private goWordHead(): void; + private getWordHead(): Position; + private goWordTail(): void; + private getWordTail(): Position; + /** インデントの後ろに飛ぶ + * + * インデントの後ろかインデントの中にいるときは行頭に飛ぶ + */ + private goLineHead(): void; + /** 行末に飛ぶ */ + private goLineTail(): void; + + private sync(): void; + private syncNow(): void; + private updateTemporalHorizontalPoint(): number; + private emitScroll(): void; + emitChange(event: string): void; + + private data: Position; + private temporalHorizontalPoint: number; + private visible: boolean; + private visiblePopupMenu: boolean; + private focusTextarea: boolean; +} diff --git a/browser/dom/cursor.ts b/browser/dom/cursor.ts new file mode 100644 index 0000000..5507f1f --- /dev/null +++ b/browser/dom/cursor.ts @@ -0,0 +1,13 @@ +/// +/// +/// + +import { takeStores } from "./stores.ts"; +import { Cursor } from "./cursor.d.ts"; + +export const takeCursor = (): Cursor => { + for (const store of takeStores()) { + if ("goByAction" in store) return store; + } + throw Error('#text-input must has a "Cursor" store.'); +}; diff --git a/browser/dom/mod.ts b/browser/dom/mod.ts index 03dc042..9134e01 100644 --- a/browser/dom/mod.ts +++ b/browser/dom/mod.ts @@ -8,3 +8,5 @@ export * from "./caret.ts"; export * from "./dom.ts"; export * from "./open.ts"; export * from "./cache.ts"; +export * from "./cursor.ts"; +export * from "./selection.ts"; diff --git a/browser/dom/position.ts b/browser/dom/position.ts new file mode 100644 index 0000000..40de49a --- /dev/null +++ b/browser/dom/position.ts @@ -0,0 +1,5 @@ +/** editor上の位置情報 */ +export interface Position { + /** 行数 */ line: number; + /** 何文字目の後ろにいるか */ char: number; +} diff --git a/browser/dom/selection.d.ts b/browser/dom/selection.d.ts new file mode 100644 index 0000000..946d289 --- /dev/null +++ b/browser/dom/selection.d.ts @@ -0,0 +1,70 @@ +/// +/// +/// + +import { BaseStore } from "../../deps/scrapbox.ts"; +import { Position } from "./position.ts"; + +export interface Range { + start: Position; + end: Position; +} + +export declare class Selection extends BaseStore { + constructor(); + + /** `scrapbox.Page.lines`とほぼ同じ */ + get lines(): unknown[]; + + /** 現在の選択範囲を取得する */ + getRange(init?: { normalizeOrder: boolean }): Range; + + /** 選択範囲を変更する */ + setRange(range: Range): void; + + /** 選択を解除する */ + clear(): void; + + /** algorithmがよくわからない + * + * 何らかの条件に基づいて、startとendを入れ替えているのはわかる + */ + normalizeOrder(range: Range): Range; + + /** 選択範囲の文字列を取得する */ + getSelectedText(): string; + + /** 選択範囲の描画上の高さを取得する */ + getSelectionsHeight(): number; + + /** 選択範囲の右上のy座標を取得する */ + getSelectionTop(): number; + + /** 全選択する */ + selectAll(): void; + + /** 与えられた選択範囲が空かどうか判定する + * + * defaultだと、このclassが持っている選択範囲を判定する + */ + hasSelection(range?: Range): boolean; + + /** 与えられた範囲が1行だけ選択しているかどうか判定する + * + * defaultだと、このclassが持っている選択範囲を判定する + */ + hasSingleLineSelection(range?: Range): boolean; + + /** 与えられた範囲が2行以上選択しているかどうか判定する + * + * defaultだと、このclassが持っている選択範囲を判定する + */ + hasMultiLinesSelection(range?: Range): boolean; + + /** 全選択しているかどうか */ + hasSelectionAll(): boolean; + + private fixPosition(position: Position): void; + private fixRange(): void; + private data: Range; +} diff --git a/browser/dom/selection.ts b/browser/dom/selection.ts new file mode 100644 index 0000000..691c21d --- /dev/null +++ b/browser/dom/selection.ts @@ -0,0 +1,13 @@ +/// +/// +/// + +import { takeStores } from "./stores.ts"; +import { Selection } from "./selection.d.ts"; + +export const takeSelection = (): Selection => { + for (const store of takeStores()) { + if ("hasSelection" in store) return store; + } + throw Error('#text-input must has a "Selection" store.'); +}; diff --git a/browser/dom/stores.ts b/browser/dom/stores.ts new file mode 100644 index 0000000..99d536f --- /dev/null +++ b/browser/dom/stores.ts @@ -0,0 +1,37 @@ +/// +/// +/// + +import { textInput } from "./dom.ts"; +import { Cursor } from "./cursor.d.ts"; +import { Selection } from "./selection.d.ts"; + +export const takeStores = (): (Cursor | Selection)[] => { + const textarea = textInput(); + if (!textarea) { + throw Error(`#text-input is not found.`); + } + + const reactKey = Object.keys(textarea) + .find((key) => key.startsWith("__reactFiber")); + if (!reactKey) { + throw Error( + '#text-input must has the property whose name starts with "__reactFiber"', + ); + } + + // @ts-ignore DOMを無理矢理objectとして扱っている + return (textarea[ + reactKey + ] as ReactFiber).return.return.stateNode._stores; +}; + +interface ReactFiber { + return: { + return: { + stateNode: { + _stores: (Cursor | Selection)[]; + }; + }; + }; +} diff --git a/deps/scrapbox.ts b/deps/scrapbox.ts index 6a6bb5a..041e392 100644 --- a/deps/scrapbox.ts +++ b/deps/scrapbox.ts @@ -25,4 +25,7 @@ export type { export type { Scrapbox, } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.2/userscript.ts"; +export type { + BaseStore, +} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.2/baseStore.ts"; export * from "https://esm.sh/@progfay/scrapbox-parser@7.2.0";