選択範囲に対してwindowを表示するtest
2022-08-28
23:38:52 空白一致を優先して出すようにした https://gyazo.com/2b4c59f54a3834387d82d667971cc6b8
23:12:50 あいまい検索に切り替えた
https://gyazo.com/4f1ead1fd5a4be01750b46a691508220
https://gyazo.com/144f4d0278a1f83dcd61ae6a0edc26dc
22:56:28 hookを切り出した
22:48:18 並び替えを修正した
https://gyazo.com/43c04a01b28416aa26f7b315a424797d
https://gyazo.com/4a81e129a303a01fda7c2c0cde279435
https://gyazo.com/7a1794e49a6358d4d01395315bd9f2e0
並び替えがイマイチだな
screenshotの並び替え
1. 一致した箇所が早い順
2. 更新日時updatedが新しい順
3. 文字列長が短い順
2.が3.の前にあるせいで、中身のあるページが中身のないページより優先して表示されてしまう
中身のないは常にupdated === 0
2.と3.を逆にしよう
かつ空リンクの優先度を低くする
↓の位置だと見づらくなったので、元に戻した
22:01:07 位置調節
選択範囲の真ん中当たりにwindowがくるようにした
code:js
transform: translateX(calc(${rect.width}px / 2 - 50%)),
21:54:17 1行だけ選択しているときのみ表示 & 隙間を14px開ける
https://gyazo.com/2e4383884c4a6aaf654be3b7288292a5
code:sh
code:App.tsx
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/** @jsx h */
/** @jsxFrag Fragment */
import { Fragment, h, render } from "../preact/mod.tsx";
import { useCallback, useState, useMemo, useEffect, useRef } from "../preact/hooks.ts";
import { useSelection } from "./useSelection.ts";
import { useSearch } from "./useSearch.ts";
import { selections } from "../scrapbox-userscript-std/dom.ts";
export interface Options {
/** 表示する最大候補数
*
* @default 5
*/
limit?: number;
}
const App = ({ limit }: Options) => {
const { text, range } = useSelection();
const ref = useRef<HTMLDivElement>(null); // 座標計算用
const style = useMemo<h.JSX.CSSProperties>(() => {
// 一行だけ選択している時のみ表示する
if (text === "" || text.includes("\n") || !ref.current) {
return { display: "none" };
}
// 座標を取得する
const root = ref.current.parentNode;
if (!(root instanceof ShadowRoot)) {
throw Error(The parent of "div.container" must be ShadowRoot);
}
/** 基準座標 */
const parentRect = root.host?.parentElement?.getBoundingClientRect?.();
/** 選択範囲の座標 */
const rect = selections()?.lastElementChild?.getBoundingClientRect?.();
if (!rect || !parentRect) return { display: "none" };
return {
top: ${rect.bottom - parentRect.top}px,
left: ${(rect.left - parentRect.left)}px,
};
}, text, range); // 選択範囲のサイズ変更でも再計算するために、rangeを依存配列に加えている const candidates = useSearch(text, limit ?? 5);
return (<>
<style>{`
.container {
position: absolute;
max-width: 80vw;
max-height: 80vh;
margin-top: 14px;
overflow-x: hidden;
overflow-y: auto;
z-index: 1000;
background-color: var(--dropdown-menu-bg, #fff); color: var(--dropdown-menu-text-color, #333); border: var(--dropdown-menu-border, 1px solid rgba(0,0,0,.15));
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
}
`}</style>
<div ref={ref} className="container" style={style}>
{candidates.map((title) => (
<div key={title} className="candidate" title={title}>{title}</div>
))}
</div>
</>);
};
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
document.body.append(app);
render(<App limit={10} />, shadowRoot);
検索hook
前方あいまい一致と任意位置あいまい一致とで、優先順位を変える
code:useSearch.ts
import { useMemo } from "../preact/hooks.ts";
import { toTitleLc, revertTitleLc } from "../scrapbox-userscript-std/dom.ts";
import { Asearch } from "../deno-asearch/mod.ts";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
const getMaxDistance = [
0, // 空文字のとき
0, 0,
1, 1,
2, 2, 2, 2,
3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
];
export const useSearch = (text: string, limit: number): string[] => useMemo(() => {
// 空白を_に置換して、空白一致できるようにする
const textLc = toTitleLc(text.replace(/\s+/g, " "));
const forwardMatch = Asearch(${textLc} ).match;
const match = Asearch( ${textLc} ).match;
const textIgnoreSpace = revertTitleLc(textLc).trim();
// 空白をワイルドカードとして検索する
// 検索文字列が空白を含むときのみ実行
const ignoreSpace = /\s/.test(text) ? {
forwardMatch: Asearch(${textIgnoreSpace} ).match,
match: Asearch( ${textIgnoreSpace} ).match,
} : undefined
return scrapbox.Project.pages
.flatMap((page) => {
// 空白一致検索
{
const result = forwardMatch(page.titleLc, maxDistance);
if (result.found) return [{
point: result.distance,
...page
}];
}
{
const result = match(page.titleLc, maxDistance);
if (result.found) return [{
point: result.distance + 0.25,
...page
}];
}
if (!ignoreSpace) return [];
// 空白をワイルドカードとして検索
{
const result = ignoreSpace.forwardMatch(page.title, ignoreSpace.distance);
if (result.found) return [{
point: result.distance + 0.5,
...page
}];
}
{
const result = ignoreSpace.match(page.title, ignoreSpace.distance);
if (result.found) return [{
point: result.distance + 0.75,
...page
}];
}
return [];
})
.sort((a,b) => {
// 1. 優先順位順
const diff = a.point - b.point;
if (diff !== 0) return diff;
// 2. 文字列が短い順
const ldiff = a.title.length - b.title.length
if (ldiff !== 0 ) return ldiff;
// 3. つながっているリンク順
if (a.exists !== b.exists) a.exists ? -1 : 1;
// 4. 更新日時が新しい順
return b.updated - a.updated;
})
.slice(0, limit)
.map((page) => page.title)
単純部分一致検索
code:useSimpleSearch.ts
import { useMemo } from "../preact/hooks.ts";
import { toTitleLc } from "../scrapbox-userscript-std/dom.ts";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
export const useSearch = (text: string, maxCandidates = 10): string[] => useMemo(() => {
const textLc = toTitleLc(text);
return scrapbox.Project.pages
.flatMap((page) => {
const index = page.titleLc.indexOf(textLc);
if (index < 0) return [];
return index, ...page };
})
.sort((a,b) => {
// 1. 一致した箇所が早い順
const diff = a.index - b.index;
if (diff !== 0) return diff;
// 2. 文字列が短い順
const ldiff = a.title.length - b.title.length
if (ldiff !== 0 ) return ldiff;
// 3. つながっているリンク順
if (a.exists !== b.exists) a.exists ? -1 : 1;
// 4. 更新日時が新しい順
return b.updated - a.updated;
})
.slice(0, maxCandidates)
.map((page) => page.title)
選択範囲を取得するhook
code:useSelection.ts
import { useState, useEffect } from "../preact/hooks.ts";
import { takeSelection, Range } from "../scrapbox-userscript-std/dom.ts";
export const useSelection = (): { text: string; range: Range; } => {
start: { line: 0, char: 0 },
end: { line: 0, char: 0 }
});
useEffect(() => {
const selection = takeSelection();
const update = () => {
setRange(selection.getRange());
setText(selection.getSelectedText());
};
selection.addChangeListener(update);
return () => selection.removeChangeListener(update);
}, []);
return { text, range } as const;
};