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";