Workflowy選択中テキストで検索するChrome拡張
ついでにいらんメニューを消す
code: manifest.json
{
"manifest_version": 3,
"name": "Workflowy Menu Tweaker",
"version": "1.0",
"description": "装飾メニューをカスタマイズして虫眼鏡ボタンを追加",
"permissions": [
"activeTab",
"scripting"
],
"content_scripts": [
{
}
]
}
code:content.js
console.log("Workflowy拡張ロード中");
// ----------------------------
// 残すアイコン名
// ----------------------------
const allowedIcons = [
"fa-bold",
"fa-link",
"fa-chevron-down"
];
// ----------------------------
// 虫眼鏡ボタン追加関数
// ----------------------------
const addSearchButton = (menu) => {
if (menu.querySelector(".ctx-menu-item.my-search-btn")) return;
const btn = document.createElement("div");
btn.className = "ctx-menu-item my-search-btn iconButton md shape-square";
btn.innerHTML = `
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="magnifying-glass"
viewBox="0 0 512 512" style="width:16px;height:16px;">
<path fill="currentColor"
d="M384 208A176 176 0 1 0 32 208a176 176 0 1 0 352 0zM343.3 366C307 397.2 259.7 416 208 416C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208c0 51.7-18.8 99-50 135.3L507.3 484.7c6.2 6.2 6.2 16.4 0 22.6s-16.4 6.2-22.6 0L343.3 366z">
</path>
</svg>
`;
btn.addEventListener("click", () => {
const selection = window.getSelection().toString().trim();
if (!selection) return;
const input = document.querySelector("input.searchBoxInput");
if (!input) return;
input.focus();
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
setter.call(input, selection);
input.dispatchEvent(new Event('input', { bubbles: true }));
// 1フレーム後に Enter 発火
requestAnimationFrame(() => {
input.dispatchEvent(new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter"
}));
});
});
menu.firstChild.before(btn);
};
// ----------------------------
// MutationObserver
// ----------------------------
const observer = new MutationObserver(() => {
const menus = document.querySelectorAll(".flyout.ctx-menu");
menus.forEach(menu => {
// 処理済みならスキップ
if (menu.dataset.processed === "true") return;
const items = menu.querySelectorAll(".ctx-menu-item");
items.forEach(item => {
const svg = item.querySelector("svg");
if (!svg) return;
const iconClass = Array.from(svg.classList).find(cls => cls.startsWith("fa-"));
if (!allowedIcons.includes(iconClass)) {
item.remove();
}
});
// 横並び化
menu.style.display = "flex";
menu.style.flexDirection = "row";
addSearchButton(menu);
menu.dataset.processed = "true";
});
});
// ----------------------------
// body 監視
// ----------------------------
observer.observe(document.body, { childList: true, subtree: true });
「btn.innerHTML」に関して、DOM生成用便利ツールのsvg版を使うとinnerHTMLを避けられて良いですよbsahd.icon 一旦ホームに戻ってから検索するバージョン
code: content.js
console.log("Workflowy拡張ロード中");
// ----------------------------
// 残すアイコン名
// ----------------------------
const allowedIcons = [
"fa-bold",
"fa-link",
"fa-chevron-down"
];
// ----------------------------
// 虫眼鏡ボタン追加関数
// ----------------------------
const addSearchButton = (menu) => {
if (menu.querySelector(".ctx-menu-item.my-search-btn")) return;
const btn = document.createElement("div");
btn.className = "ctx-menu-item my-search-btn iconButton md shape-square";
btn.innerHTML = `
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="magnifying-glass"
viewBox="0 0 512 512" style="width:16px;height:16px;">
<path fill="currentColor"
d="M384 208A176 176 0 1 0 32 208a176 176 0 1 0 352 0zM343.3 366C307 397.2 259.7 416 208 416C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208c0 51.7-18.8 99-50 135.3L507.3 484.7c6.2 6.2 6.2 16.4 0 22.6s-16.4 6.2-22.6 0L343.3 366z">
</path>
</svg>
`;
btn.addEventListener("click", () => {
const selection = window.getSelection().toString().trim();
if (!selection) return;
const homeBtn = document.querySelector("a.breadcrumb-item .homeButton");
if (homeBtn) {
homeBtn.click();
}
setTimeout(() => {
const input = document.querySelector("input.searchBoxInput");
if (!input) return;
input.focus();
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
setter.call(input, selection);
input.dispatchEvent(new Event('input', { bubbles: true }));
requestAnimationFrame(() => {
input.dispatchEvent(new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Enter",
code: "Enter"
}));
});
}, 100); // 100msくらいトップに戻る時間を確保
});
menu.firstChild.before(btn);
};
// ----------------------------
// MutationObserver
// ----------------------------
const observer = new MutationObserver(() => {
const menus = document.querySelectorAll(".flyout.ctx-menu");
menus.forEach(menu => {
if (menu.dataset.processed === "true") return;
const items = menu.querySelectorAll(".ctx-menu-item");
items.forEach(item => {
const svg = item.querySelector("svg");
if (!svg) return;
const iconClass = Array.from(svg.classList).find(cls => cls.startsWith("fa-"));
if (!allowedIcons.includes(iconClass)) {
item.remove();
}
});
menu.style.display = "flex";
menu.style.flexDirection = "row";
addSearchButton(menu);
menu.dataset.processed = "true";
});
});
observer.observe(document.body, { childList: true, subtree: true });