Skip to content

Commit 1e0e308

Browse files
authored
Merge pull request #3 from takker99/add-browser-module
Add browser module
2 parents bd8b8a8 + f38735b commit 1e0e308

23 files changed

+1329
-0
lines changed

browser/caret.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/// <reference no-default-lib="true"/>
2+
/// <reference lib="esnext"/>
3+
/// <reference lib="dom" />
4+
5+
import { textInput } from "./dom.ts";
6+
7+
/** editor上の位置情報 */
8+
export interface Position {
9+
/** 行数 */ line: number;
10+
/** 何文字目の後ろにいるか */ char: number;
11+
}
12+
13+
/** 選択範囲を表すデータ
14+
*
15+
* 選択範囲がないときは、開始と終了が同じ位置になる
16+
*/
17+
export interface Range {
18+
/** 選択範囲の開始位置 */ start: Position;
19+
/** 選択範囲の終了位置 */ end: Position;
20+
}
21+
22+
/** #text-inputを構築しているReact Componentに含まれるカーソルの情報 */
23+
export interface CaretInfo {
24+
/** カーソルの位置 */ position: Position;
25+
/** 選択範囲中の文字列 */ selectedText: string;
26+
/** 選択範囲の位置 */ selectionRange: Range;
27+
}
28+
29+
interface ReactInternalInstance {
30+
return: {
31+
return: {
32+
stateNode: {
33+
props: CaretInfo;
34+
};
35+
};
36+
};
37+
}
38+
39+
/** 現在のカーソルと選択範囲の位置情報を取得する
40+
*
41+
* @return カーソルと選択範囲の情報
42+
* @throws {Error} #text-inputとReact Componentの隠しpropertyが見つからなかった
43+
*/
44+
export function caret(): CaretInfo {
45+
const textarea = textInput();
46+
if (!textarea) {
47+
throw Error(`#text-input is not found.`);
48+
}
49+
50+
const reactKey = Object.keys(textarea)
51+
.find((key) => key.startsWith("__reactInternalInstance"));
52+
if (!reactKey) {
53+
throw Error(
54+
"div.cursor must has the property whose name starts with `__reactInternalInstance`",
55+
);
56+
}
57+
58+
// @ts-ignore DOMを無理矢理objectとして扱っている
59+
return (textarea[
60+
reactKey
61+
] as ReactInternalInstance).return.return.stateNode.props;
62+
}

browser/click.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/// <reference no-default-lib="true"/>
2+
/// <reference lib="esnext"/>
3+
/// <reference lib="dom" />
4+
5+
import { sleep } from "../sleep.ts";
6+
7+
/** the options for `click()` */
8+
export interface ClickOptions {
9+
button?: number;
10+
X: number;
11+
Y: number;
12+
shiftKey?: boolean;
13+
ctrlKey?: boolean;
14+
altKey?: boolean;
15+
}
16+
17+
/** Emulate click event sequences */
18+
export async function click(
19+
element: HTMLElement,
20+
options: ClickOptions,
21+
): Promise<void> {
22+
const mouseOptions: MouseEventInit = {
23+
button: options.button ?? 0,
24+
clientX: options.X,
25+
clientY: options.Y,
26+
bubbles: true,
27+
cancelable: true,
28+
shiftKey: options.shiftKey,
29+
ctrlKey: options.ctrlKey,
30+
altKey: options.altKey,
31+
view: window,
32+
};
33+
element.dispatchEvent(new MouseEvent("mousedown", mouseOptions));
34+
element.dispatchEvent(new MouseEvent("mouseup", mouseOptions));
35+
element.dispatchEvent(new MouseEvent("click", mouseOptions));
36+
37+
// ScrapboxのReactの処理が終わるまで少し待つ
38+
// 待ち時間は感覚で決めた
39+
await sleep(10);
40+
}
41+
42+
export interface HoldDownOptions extends ClickOptions {
43+
holding?: number;
44+
}
45+
46+
/** Emulate long tap event sequence */
47+
export async function holdDown(
48+
element: HTMLElement,
49+
options: HoldDownOptions,
50+
): Promise<void> {
51+
const touch = new Touch({
52+
identifier: 0,
53+
target: element,
54+
clientX: options.X,
55+
clientY: options.Y,
56+
pageX: options.X + window.scrollX,
57+
pageY: options.Y + window.scrollY,
58+
});
59+
const mouseOptions = {
60+
button: options.button ?? 0,
61+
clientX: options.X,
62+
clientY: options.Y,
63+
changedTouches: [touch],
64+
touches: [touch],
65+
bubbles: true,
66+
cancelable: true,
67+
shiftKey: options.shiftKey,
68+
ctrlKey: options.ctrlKey,
69+
altKey: options.altKey,
70+
view: window,
71+
};
72+
element.dispatchEvent(new TouchEvent("touchstart", mouseOptions));
73+
element.dispatchEvent(new MouseEvent("mousedown", mouseOptions));
74+
await sleep(options.holding ?? 1000);
75+
element.dispatchEvent(new MouseEvent("mouseup", mouseOptions));
76+
element.dispatchEvent(new TouchEvent("touchend", mouseOptions));
77+
element.dispatchEvent(new MouseEvent("click", mouseOptions));
78+
79+
// ScrapboxのReactの処理が終わるまで少し待つ
80+
// 待ち時間は感覚で決めた
81+
await sleep(10);
82+
}

browser/dom.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/// <reference no-default-lib="true"/>
2+
/// <reference lib="esnext"/>
3+
/// <reference lib="dom" />
4+
import {
5+
ensureHTMLAnchorElement,
6+
ensureHTMLDivElement,
7+
ensureHTMLTextAreaElement,
8+
} from "./ensure.ts";
9+
10+
export const editor = (): HTMLDivElement | undefined =>
11+
checkDiv(document.getElementById("editor"), "div#editor");
12+
export const lines = (): HTMLDivElement | undefined =>
13+
checkDiv(
14+
document.getElementsByClassName("lines").item(0),
15+
"div.lines",
16+
);
17+
export const computeLine = (): HTMLDivElement | undefined =>
18+
checkDiv(document.getElementById("compute-line"), "div#compute-line");
19+
export const cursorLine = (): HTMLDivElement | undefined =>
20+
checkDiv(
21+
document.getElementsByClassName("cursor-line").item(0),
22+
"div.cursor-line",
23+
);
24+
export const textInput = (): HTMLTextAreaElement | undefined => {
25+
const textarea = document.getElementById("text-input");
26+
if (!textarea) return;
27+
ensureHTMLTextAreaElement(textarea, "textarea#text-input");
28+
return textarea;
29+
};
30+
export const cursor = (): HTMLDivElement | undefined =>
31+
checkDiv(
32+
document.getElementsByClassName("cursor").item(0),
33+
"div.cursor",
34+
);
35+
export const selections = (): HTMLDivElement | undefined =>
36+
checkDiv(
37+
document.getElementsByClassName("selections")?.[0],
38+
"div.selections",
39+
);
40+
export const grid = (): HTMLDivElement | undefined =>
41+
checkDiv(
42+
document.getElementsByClassName("related-page-list clearfix")[0]
43+
?.getElementsByClassName?.("grid")?.item(0),
44+
".related-page-list.clearfix div.grid",
45+
);
46+
export const popupMenu = (): HTMLDivElement | undefined =>
47+
checkDiv(
48+
document.getElementsByClassName("popup-menu")?.[0],
49+
"div.popup-menu",
50+
);
51+
export const pageMenu = (): HTMLDivElement | undefined =>
52+
checkDiv(
53+
document.getElementsByClassName("page-menu")?.[0],
54+
"div.page-menu",
55+
);
56+
export const pageInfoMenu = (): HTMLAnchorElement | undefined =>
57+
checkAnchor(
58+
document.getElementById("page-info-menu"),
59+
"a#page-info-menu",
60+
);
61+
export const pageEditMenu = (): HTMLAnchorElement | undefined =>
62+
checkAnchor(
63+
document.getElementById("page-edit-menu"),
64+
"a#page-edit-menu",
65+
);
66+
export const pageEditButtons = (): HTMLAnchorElement[] =>
67+
Array.from(
68+
pageEditMenu()?.nextElementSibling?.getElementsByTagName?.("a") ?? [],
69+
);
70+
export const randomJumpButton = (): HTMLAnchorElement | undefined =>
71+
checkAnchor(
72+
document.getElementsByClassName("random-jump-button").item(0),
73+
"a#random-jump-button",
74+
);
75+
export const pageCustomButtons = (): HTMLAnchorElement[] =>
76+
Array.from(document.getElementsByClassName("page-menu-extension")).flatMap(
77+
(div) => {
78+
const a = div.getElementsByTagName("a").item(0);
79+
return a ? [a] : [];
80+
},
81+
);
82+
export const statusBar = (): HTMLDivElement | undefined =>
83+
checkDiv(
84+
document.getElementsByClassName("status-bar")?.[0],
85+
"div.status-bar",
86+
);
87+
88+
const checkDiv = (div: Element | null, name: string) => {
89+
if (!div) return;
90+
ensureHTMLDivElement(div, name);
91+
return div;
92+
};
93+
94+
const checkAnchor = (a: Element | null, name: string) => {
95+
if (!a) return;
96+
ensureHTMLAnchorElement(a, name);
97+
return a;
98+
};

browser/edit.ts

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { goHead, goLine } from "./motion.ts";
2+
import { press } from "./press.ts";
3+
import { getLineCount } from "./node.ts";
4+
import { range } from "../range.ts";
5+
import { textInput } from "./dom.ts";
6+
import { isArray, isNumber, isString } from "../is.ts";
7+
import { sleep } from "../sleep.ts";
8+
9+
export function undo(count = 1) {
10+
for (const _ of range(0, count)) {
11+
press("z", { ctrlKey: true });
12+
}
13+
}
14+
export function redo(count = 1) {
15+
for (const _ of range(0, count)) {
16+
press("z", { shiftKey: true, ctrlKey: true });
17+
}
18+
}
19+
20+
export function insertTimestamp(index = 1) {
21+
for (const _ of range(0, index)) {
22+
press("t", { altKey: true });
23+
}
24+
}
25+
26+
export async function insertLine(lineNo: number, text: string) {
27+
await goLine(lineNo);
28+
goHead();
29+
press("Enter");
30+
press("ArrowUp");
31+
await insertText(text);
32+
}
33+
34+
export async function replaceLines(start: number, end: number, text: string) {
35+
await goLine(start);
36+
goHead();
37+
for (const _ of range(start, end)) {
38+
press("ArrowDown", { shiftKey: true });
39+
}
40+
press("End", { shiftKey: true });
41+
await insertText(text);
42+
}
43+
44+
export async function deleteLines(from: number | string | string[], count = 1) {
45+
if (isNumber(from)) {
46+
if (getLineCount() === from + count) {
47+
await goLine(from - 1);
48+
press("ArrowRight", { shiftKey: true });
49+
} else {
50+
await goLine(from);
51+
goHead();
52+
}
53+
for (let i = 0; i < count; i++) {
54+
press("ArrowRight", { shiftKey: true });
55+
press("End", { shiftKey: true });
56+
}
57+
press("ArrowRight", { shiftKey: true });
58+
press("Delete");
59+
return;
60+
}
61+
if (isString(from) || isArray(from)) {
62+
const ids = Array.isArray(from) ? from : [from];
63+
for (const id of ids) {
64+
await goLine(id);
65+
press("Home", { shiftKey: true });
66+
press("Home", { shiftKey: true });
67+
press("Backspace");
68+
press("Backspace");
69+
}
70+
return;
71+
}
72+
throw new TypeError(
73+
`The type of value must be number | string | string[] but actual is "${typeof from}"`,
74+
);
75+
}
76+
77+
export function indentLines(count = 1) {
78+
for (const _ of range(0, count)) {
79+
press("ArrowRight", { ctrlKey: true });
80+
}
81+
}
82+
export function deindentLines(count = 1) {
83+
for (const _ of range(0, count)) {
84+
press("ArrowLeft", { ctrlKey: true });
85+
}
86+
}
87+
export function moveLines(count: number) {
88+
if (count > 0) {
89+
downLines(count);
90+
} else {
91+
upLines(-count);
92+
}
93+
}
94+
// to行目の後ろに移動させる
95+
export function moveLinesBefore(from: number, to: number) {
96+
const count = to - from;
97+
if (count >= 0) {
98+
downLines(count);
99+
} else {
100+
upLines(-count - 1);
101+
}
102+
}
103+
export function upLines(count = 1) {
104+
for (const _ of range(0, count)) {
105+
press("ArrowUp", { ctrlKey: true });
106+
}
107+
}
108+
export function downLines(count = 1) {
109+
for (const _ of range(0, count)) {
110+
press("ArrowDown", { ctrlKey: true });
111+
}
112+
}
113+
114+
export function indentBlocks(count = 1) {
115+
for (const _ of range(0, count)) {
116+
press("ArrowRight", { altKey: true });
117+
}
118+
}
119+
export function deindentBlocks(count = 1) {
120+
for (const _ of range(0, count)) {
121+
press("ArrowLeft", { altKey: true });
122+
}
123+
}
124+
export function moveBlocks(count: number) {
125+
if (count > 0) {
126+
downBlocks(count);
127+
} else {
128+
upBlocks(-count);
129+
}
130+
}
131+
export function upBlocks(count = 1) {
132+
for (const _ of range(0, count)) {
133+
press("ArrowUp", { altKey: true });
134+
}
135+
}
136+
export function downBlocks(count = 1) {
137+
for (const _ of range(0, count)) {
138+
press("ArrowDown", { altKey: true });
139+
}
140+
}
141+
142+
export async function insertText(text: string) {
143+
const cursor = textInput();
144+
if (!cursor) {
145+
throw Error("#text-input is not ditected.");
146+
}
147+
cursor.focus();
148+
cursor.value = text;
149+
150+
const event = new InputEvent("input", { bubbles: true });
151+
cursor.dispatchEvent(event);
152+
await sleep(1); // 待ち時間は感覚で決めた
153+
}

0 commit comments

Comments
 (0)