Skip to content

Add browser module #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions browser/caret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom" />

import { textInput } from "./dom.ts";

/** editor上の位置情報 */
export interface Position {
/** 行数 */ line: number;
/** 何文字目の後ろにいるか */ char: number;
}

/** 選択範囲を表すデータ
*
* 選択範囲がないときは、開始と終了が同じ位置になる
*/
export interface Range {
/** 選択範囲の開始位置 */ start: Position;
/** 選択範囲の終了位置 */ end: Position;
}

/** #text-inputを構築しているReact Componentに含まれるカーソルの情報 */
export interface CaretInfo {
/** カーソルの位置 */ position: Position;
/** 選択範囲中の文字列 */ selectedText: string;
/** 選択範囲の位置 */ selectionRange: Range;
}

interface ReactInternalInstance {
return: {
return: {
stateNode: {
props: CaretInfo;
};
};
};
}

/** 現在のカーソルと選択範囲の位置情報を取得する
*
* @return カーソルと選択範囲の情報
* @throws {Error} #text-inputとReact Componentの隠しpropertyが見つからなかった
*/
export function caret(): CaretInfo {
const textarea = textInput();
if (!textarea) {
throw Error(`#text-input is not found.`);
}

const reactKey = Object.keys(textarea)
.find((key) => key.startsWith("__reactInternalInstance"));
if (!reactKey) {
throw Error(
"div.cursor must has the property whose name starts with `__reactInternalInstance`",
);
}

// @ts-ignore DOMを無理矢理objectとして扱っている
return (textarea[
reactKey
] as ReactInternalInstance).return.return.stateNode.props;
}
82 changes: 82 additions & 0 deletions browser/click.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom" />

import { sleep } from "../sleep.ts";

/** the options for `click()` */
export interface ClickOptions {
button?: number;
X: number;
Y: number;
shiftKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
}

/** Emulate click event sequences */
export async function click(
element: HTMLElement,
options: ClickOptions,
): Promise<void> {
const mouseOptions: MouseEventInit = {
button: options.button ?? 0,
clientX: options.X,
clientY: options.Y,
bubbles: true,
cancelable: true,
shiftKey: options.shiftKey,
ctrlKey: options.ctrlKey,
altKey: options.altKey,
view: window,
};
element.dispatchEvent(new MouseEvent("mousedown", mouseOptions));
element.dispatchEvent(new MouseEvent("mouseup", mouseOptions));
element.dispatchEvent(new MouseEvent("click", mouseOptions));

// ScrapboxのReactの処理が終わるまで少し待つ
// 待ち時間は感覚で決めた
await sleep(10);
}

export interface HoldDownOptions extends ClickOptions {
holding?: number;
}

/** Emulate long tap event sequence */
export async function holdDown(
element: HTMLElement,
options: HoldDownOptions,
): Promise<void> {
const touch = new Touch({
identifier: 0,
target: element,
clientX: options.X,
clientY: options.Y,
pageX: options.X + window.scrollX,
pageY: options.Y + window.scrollY,
});
const mouseOptions = {
button: options.button ?? 0,
clientX: options.X,
clientY: options.Y,
changedTouches: [touch],
touches: [touch],
bubbles: true,
cancelable: true,
shiftKey: options.shiftKey,
ctrlKey: options.ctrlKey,
altKey: options.altKey,
view: window,
};
element.dispatchEvent(new TouchEvent("touchstart", mouseOptions));
element.dispatchEvent(new MouseEvent("mousedown", mouseOptions));
await sleep(options.holding ?? 1000);
element.dispatchEvent(new MouseEvent("mouseup", mouseOptions));
element.dispatchEvent(new TouchEvent("touchend", mouseOptions));
element.dispatchEvent(new MouseEvent("click", mouseOptions));

// ScrapboxのReactの処理が終わるまで少し待つ
// 待ち時間は感覚で決めた
await sleep(10);
}
98 changes: 98 additions & 0 deletions browser/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom" />
import {
ensureHTMLAnchorElement,
ensureHTMLDivElement,
ensureHTMLTextAreaElement,
} from "./ensure.ts";

export const editor = (): HTMLDivElement | undefined =>
checkDiv(document.getElementById("editor"), "div#editor");
export const lines = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("lines").item(0),
"div.lines",
);
export const computeLine = (): HTMLDivElement | undefined =>
checkDiv(document.getElementById("compute-line"), "div#compute-line");
export const cursorLine = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("cursor-line").item(0),
"div.cursor-line",
);
export const textInput = (): HTMLTextAreaElement | undefined => {
const textarea = document.getElementById("text-input");
if (!textarea) return;
ensureHTMLTextAreaElement(textarea, "textarea#text-input");
return textarea;
};
export const cursor = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("cursor").item(0),
"div.cursor",
);
export const selections = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("selections")?.[0],
"div.selections",
);
export const grid = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("related-page-list clearfix")[0]
?.getElementsByClassName?.("grid")?.item(0),
".related-page-list.clearfix div.grid",
);
export const popupMenu = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("popup-menu")?.[0],
"div.popup-menu",
);
export const pageMenu = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("page-menu")?.[0],
"div.page-menu",
);
export const pageInfoMenu = (): HTMLAnchorElement | undefined =>
checkAnchor(
document.getElementById("page-info-menu"),
"a#page-info-menu",
);
export const pageEditMenu = (): HTMLAnchorElement | undefined =>
checkAnchor(
document.getElementById("page-edit-menu"),
"a#page-edit-menu",
);
export const pageEditButtons = (): HTMLAnchorElement[] =>
Array.from(
pageEditMenu()?.nextElementSibling?.getElementsByTagName?.("a") ?? [],
);
export const randomJumpButton = (): HTMLAnchorElement | undefined =>
checkAnchor(
document.getElementsByClassName("random-jump-button").item(0),
"a#random-jump-button",
);
export const pageCustomButtons = (): HTMLAnchorElement[] =>
Array.from(document.getElementsByClassName("page-menu-extension")).flatMap(
(div) => {
const a = div.getElementsByTagName("a").item(0);
return a ? [a] : [];
},
);
export const statusBar = (): HTMLDivElement | undefined =>
checkDiv(
document.getElementsByClassName("status-bar")?.[0],
"div.status-bar",
);

const checkDiv = (div: Element | null, name: string) => {
if (!div) return;
ensureHTMLDivElement(div, name);
return div;
};

const checkAnchor = (a: Element | null, name: string) => {
if (!a) return;
ensureHTMLAnchorElement(a, name);
return a;
};
153 changes: 153 additions & 0 deletions browser/edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { goHead, goLine } from "./motion.ts";
import { press } from "./press.ts";
import { getLineCount } from "./node.ts";
import { range } from "../range.ts";
import { textInput } from "./dom.ts";
import { isArray, isNumber, isString } from "../is.ts";
import { sleep } from "../sleep.ts";

export function undo(count = 1) {
for (const _ of range(0, count)) {
press("z", { ctrlKey: true });
}
}
export function redo(count = 1) {
for (const _ of range(0, count)) {
press("z", { shiftKey: true, ctrlKey: true });
}
}

export function insertTimestamp(index = 1) {
for (const _ of range(0, index)) {
press("t", { altKey: true });
}
}

export async function insertLine(lineNo: number, text: string) {
await goLine(lineNo);
goHead();
press("Enter");
press("ArrowUp");
await insertText(text);
}

export async function replaceLines(start: number, end: number, text: string) {
await goLine(start);
goHead();
for (const _ of range(start, end)) {
press("ArrowDown", { shiftKey: true });
}
press("End", { shiftKey: true });
await insertText(text);
}

export async function deleteLines(from: number | string | string[], count = 1) {
if (isNumber(from)) {
if (getLineCount() === from + count) {
await goLine(from - 1);
press("ArrowRight", { shiftKey: true });
} else {
await goLine(from);
goHead();
}
for (let i = 0; i < count; i++) {
press("ArrowRight", { shiftKey: true });
press("End", { shiftKey: true });
}
press("ArrowRight", { shiftKey: true });
press("Delete");
return;
}
if (isString(from) || isArray(from)) {
const ids = Array.isArray(from) ? from : [from];
for (const id of ids) {
await goLine(id);
press("Home", { shiftKey: true });
press("Home", { shiftKey: true });
press("Backspace");
press("Backspace");
}
return;
}
throw new TypeError(
`The type of value must be number | string | string[] but actual is "${typeof from}"`,
);
}

export function indentLines(count = 1) {
for (const _ of range(0, count)) {
press("ArrowRight", { ctrlKey: true });
}
}
export function deindentLines(count = 1) {
for (const _ of range(0, count)) {
press("ArrowLeft", { ctrlKey: true });
}
}
export function moveLines(count: number) {
if (count > 0) {
downLines(count);
} else {
upLines(-count);
}
}
// to行目の後ろに移動させる
export function moveLinesBefore(from: number, to: number) {
const count = to - from;
if (count >= 0) {
downLines(count);
} else {
upLines(-count - 1);
}
}
export function upLines(count = 1) {
for (const _ of range(0, count)) {
press("ArrowUp", { ctrlKey: true });
}
}
export function downLines(count = 1) {
for (const _ of range(0, count)) {
press("ArrowDown", { ctrlKey: true });
}
}

export function indentBlocks(count = 1) {
for (const _ of range(0, count)) {
press("ArrowRight", { altKey: true });
}
}
export function deindentBlocks(count = 1) {
for (const _ of range(0, count)) {
press("ArrowLeft", { altKey: true });
}
}
export function moveBlocks(count: number) {
if (count > 0) {
downBlocks(count);
} else {
upBlocks(-count);
}
}
export function upBlocks(count = 1) {
for (const _ of range(0, count)) {
press("ArrowUp", { altKey: true });
}
}
export function downBlocks(count = 1) {
for (const _ of range(0, count)) {
press("ArrowDown", { altKey: true });
}
}

export async function insertText(text: string) {
const cursor = textInput();
if (!cursor) {
throw Error("#text-input is not ditected.");
}
cursor.focus();
cursor.value = text;

const event = new InputEvent("input", { bubbles: true });
cursor.dispatchEvent(event);
await sleep(1); // 待ち時間は感覚で決めた
}
Loading