// ==UserScript==
// @name WF ToolBox
// @description 将各种Workflowy插件集成
// @namespace github.com/openstyles/stylus
// @version 2024-11-12-Ver0.1
// @author YYYYang
// @license MIT
// @match *://*/*
// @grant GM_setClipboard
// @grant GM_notification
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @noframes
// ==/UserScript==
// 整体工具箱菜单代码带来 @稻米鼠(https://gf.qytechs.cn/zh-CN/users/36055)的【洗白白插件】
/* 弹出通知 */
const dmsCLNotification = function (text) {
GM_notification(text, 'Success! ', 'data:image/png;base64,iVxxxxxxxxxxxxxxxCYII=');
};
/** 获取是否显示页面工具栏 **/
let isShowPageBar = GM_getValue('SHow_page_bar', true);
GM_registerMenuCommand('显示/隐藏页面工具条', () => {
GM_setValue('SHow_page_bar', !isShowPageBar);
isShowPageBar = GM_getValue('SHow_page_bar', true);
alert(
'页面工具条已被设置为【' +
(isShowPageBar ? '显示' : '隐藏') +
'】,仅在此后新打开页面中生效。'
);
});
// ————————————————————————————
const Find_Replace = () => {
// alert("查找与替换");
(function FR_2_4() {
function toastMsg(str, sec, err) {
WF.showMessage(str, err);
setTimeout(WF.hideMessage, (sec || 3) * 1000);
}
function applyToEachItem(functionToApply, parent) {
functionToApply(parent);
for (let child of parent.getChildren()) {
applyToEachItem(functionToApply, child);
}
}
function findMatchingItems(itemPredicate, parent) {
const matches = [];
function addIfMatch(item) {
if (itemPredicate(item)) {
matches.push(item);
}
}
applyToEachItem(addIfMatch, parent);
return matches;
}
function editableItemWithVisibleMatch(item) {
const isVisible = WF.completedVisible() || !item.isWithinCompleted();
return item.data.search_result && item.data.search_result.matches && isVisible && !item.isReadOnly()
}
const escapeForRegExp = str => str.replace(/[-\[\]{}()*+?.,\\^$|#]/g, "\\$&");
function countMatches(items, rgx) {
let matchCount = 0;
items.forEach(item => {
let result = item.data.search_result;
if (result.nameMatches) {
let nameMatch = item.getName().match(rgx);
if (nameMatch) matchCount += nameMatch.length;
}
if (result.noteMatches) {
let noteMatch = item.getNote().match(rgx);
if (noteMatch) matchCount += noteMatch.length;
}
});
return matchCount;
}
const htmlEscTextForContent = str => str.replace(/&/g, "&").replace(/>/g, ">").replace(/</g, "<").replace(/\u00A0/g, " ");
function replaceMatches(items, rgx, r) {
WF.editGroup(function () {
items.forEach(item => {
let result = item.data.search_result;
if (result.nameMatches) WF.setItemName(item, item.getName().replace(rgx, htmlEscTextForContent(r)));
if (result.noteMatches) WF.setItemNote(item, item.getNote().replace(rgx, htmlEscTextForContent(r)));
});
});
r === "" ? WF.clearSearch() : WF.search(tQuery.replace(find, r));
}
const htmlEscText = str => str.replace(/&/g, "&").replace(/>/g, ">").replace(/</g, "<").replace(/"/g, """);
function getColors() {
const p = document.querySelector(".page.active");
return p ? `color:${getComputedStyle(p).color};background:${getComputedStyle(p).backgroundColor};` : "";
}
function showFindReplaceDialog(BODY, TITLE, aCount, cCount, searchValue) {
const addButton = (num, name) => `<button type="button" class="btnX" id="btn${num.toString()}">${name}</button>`;
const boxStyle = `#inputBx{${getColors()}width:95%;height:20px;display:block;margin-top:5px;border:1px solid #ccc;border-radius:4px;padding:4px}`;
const btnStyle = `.btnX{font-size:18px;background-color:gray;border:2px solid;border-radius:20px;color:#fff;padding:5px 15px;margin-top:16px;margin-right:16px}.btnX:focus,.btnX:hover{border-color:#c4c4c4;background-color:steelblue}`;
const box = `<div><b>Replace:</b><input value="${htmlEscText(searchValue)}" id="inputBx" type="text" spellcheck="false"></div>`;
const buttons = addButton(1, `Replace: All (${aCount})`) + addButton(2, `Replace: Match Case (${cCount})`);
WF.showAlertDialog(`<style>${boxStyle + btnStyle}</style><div>${BODY}</div>${box}<div>${buttons}</div>`, TITLE);
const intervalId = setInterval(function () {
let inputBx = document.getElementById("inputBx");
if (inputBx) {
clearInterval(intervalId);
let userInput;
const btn1 = document.getElementById("btn1");
const btn2 = document.getElementById("btn2");
inputBx.select();
inputBx.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
btn1.click();
}
});
btn1.onclick = function () {
userInput = inputBx.value;
WF.hideDialog();
setTimeout(function () {
replaceMatches(Matches, rgx_gi, userInput)
}, 50);
};
btn2.onclick = function () {
userInput = inputBx.value;
WF.hideDialog();
setTimeout(function () {
replaceMatches(Matches, rgx_g, userInput)
}, 50);
};
}
}, 50);
}
if (!WF.currentSearchQuery()) {
return void toastMsg('请先使用搜索框键入关键词 <a href="https://workflowy.com/s/findreplace-bookmark/ynKNSb5dA77p2siT" target="_blank">⇒ 更多信息请查看</a>', 4, true);
}
const tQuery = WF.currentSearchQuery().trim();
const Matches = findMatchingItems(editableItemWithVisibleMatch, WF.currentItem());
const isQuoted = tQuery.match(/(")(.+)(")/);
const find = isQuoted ? isQuoted[2] : tQuery.includes(" ") ? false : tQuery;
if (find === false) {
if (confirm('The search contains at least one space.\n\n1. Press OK to convert your search to "exact match".\n\n2. Activate Find/Replace again.')) {
WF.search('"' + tQuery + '"');
}
return;
}
const title = "Find/Replace";
const modeTxt = isQuoted ? "Exact Match, " : "Single Word/Tag, ";
const compTxt = `Completed: ${WF.completedVisible() ? "Included" : "Excluded"}`;
const findTxt = isQuoted ? isQuoted[0] : tQuery;
const body = `<p><b>Mode:</b><br>${modeTxt + compTxt}</p><p><b>Find:</b><br>${htmlEscText(findTxt)}</p>`;
const findRgx = escapeForRegExp(htmlEscTextForContent(find));
const rgx_gi = new RegExp(findRgx, "gi");
const rgx_g = new RegExp(findRgx, "g");
const allCount = countMatches(Matches, rgx_gi);
const caseCount = countMatches(Matches, rgx_g);
if (allCount > 0) {
showFindReplaceDialog(body, title, allCount, caseCount, find);
} else {
WF.showAlertDialog(`${body}<br><br><i>No matches found.</i>`, title);
}
})();
};
const WF_Sort = () => {
// alert("WF节点排序");
(function sortWF_4_0(maxChildren = 1000) {
function toastMsg(str, sec, err) {
WF.showMessage(str, err);
setTimeout(WF.hideMessage, (sec || 2) * 1000);
}
function sortAndMove(items, reverse) {
WF.hideDialog();
setTimeout(() => {
items.sort((a, b) => reverse ? b.getNameInPlainText().localeCompare(a.getNameInPlainText()) : a.getNameInPlainText().localeCompare(b.getNameInPlainText()));
WF.editGroup(() => {
items.forEach((item, i) => {
if (item.getPriority() !== i) WF.moveItems([item], parent, i);
});
});
// set focus to parent after sort
WF.editItemName(parent);
toastMsg(`Sorted ${reverse ? "Z-A." : "A-Z."}`, 1)
}, 50);
}
const htmlEscText = str => str.replace(/&/g, "&").replace(/>/g, ">").replace(/</g, "<").replace(/"/g, """);
function showSortDialog(bodyHtml, title) {
const addButton = (num, name) => `<button type="button" class="btnX" id="btn${num.toString()}">${name}</button>`;
const style = '.btnX{font-size:18px;background-color:gray;border:2px solid;border-radius:20px;color:#fff;padding:5px 15px;margin-top:16px;margin-right:16px}.btnX:focus,.btnX:hover{border-color:#c4c4c4;background-color:steelblue}';
const buttons = addButton(1, "A-Z") + addButton(2, "Z-A");
WF.showAlertDialog(`<style>${htmlEscText(style)}</style><div>${bodyHtml}</div><div>${buttons}</div>`, title);
const intervalId = setInterval(function () {
let btn1 = document.getElementById("btn1");
if (btn1) {
clearInterval(intervalId);
const btn2 = document.getElementById("btn2");
btn1.focus();
btn1.onclick = function () { sortAndMove(children) };
btn2.onclick = function () { sortAndMove(children, true) };
}
}, 50);
}
if (WF.currentSearchQuery()) return void toastMsg("Sorting is disabled when search is active.", 4, true);
const parent = WF.currentItem();
if (parent.isReadOnly()) return void toastMsg("Parent is read only and cannot be sorted.", 4, true);
const children = parent.getChildren();
if (children.length < 2) return void toastMsg("Nothing to sort.", 4, true);
if (children.length > maxChildren) return void toastMsg(`Sorting more than ${maxChildren} children upsets the WorkFlowy gods, and has been disabled.`, 5, true);
const sortInfo = `Sort <b>${children.length}</b> children?`;
showSortDialog(sortInfo, parent.getNameInPlainText());
})();
};
const NodeWord_Count = () => {
// alert("节点与字数统计");
function e(a) {
let b = a.getName().trim().length, c = a.getNote().trim().length;
// console.log(b, c, a.getName());
for (
let f of a.getChildren()
)
a = e(f), b += a[0], c += a[1];
return [b, c]
}
let d = WF.currentItem(), [g, h] = e(d);
WF.showMessage(
`🔘【笔记总节点 <b>${d.getNumDescendants()}</b>
,一级根节点 <b>${d.getChildren().length}</b>】
🅰️【文本字数 <b>${g}</b>₩
,注释字数 <b>${h}</b>₩】`
)
};
if (isShowPageBar) {
/** 添加样式 **/
GM_addStyle(`
#dms-link-cleaner {
width: 100%;
position: fixed;
left: 0;
bottom: 0;
z-index: 99999999;
pointer-events: none;
}
#dms-link-cleaner * {
pointer-events: auto;
}
#dms-lc-button {
position: relative;
margin: 0 auto;
width: 44px;
height: 20px;
color: rgba(0, 0, 0, .4);
font-size: 20px;
line-height: 13px;
cursor: pointer;
text-align: center;
border: 1px solid #AAA;
border-radius: 16px 16px 0 0;
background-color: rgba(255, 255, 255, .3);
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
}
#dms-lc-button:hover {
color: rgba(0, 0, 0, .8);
background-color: rgba(255, 255, 255, 0.8);
}
#dms-lc-panel {
display: none;
border-top: 5px solid #65adff;
background-color: #FFF;
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
}
#dms-lc-panel > #dms-lc-panel-content {
display: flex;
justify-content: center;
align-items: center;
flex: 1 1 none;
flex-wrap: wrap;
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 16px 0;
text-align: center;
position: relative;
}
#dms-lc-panel > #dms-lc-panel-content > .dms-lc-button {
position: relative;
padding: 8px 16px;
margin: 0 8px 0 0;
font-size: 16px;
line-height: 1.2em;
font-weight: lighter;
border: 1px solid #65adff;
border-radius: 8px;
cursor: pointer;
}
#dms-lc-panel > #dms-lc-panel-content > .dms-lc-button:hover {
border: 1px solid #0062d1;
background-color: #0062d1;
color: #FFF;
font-weight: normal;
}
#dms-lc-panel > #dms-lc-panel-content > .dms-lc-button:hover::before {
content: attr(data-tip);
background-color: rgba(0, 0, 0, .9);
border-radius:3px;
color: #fff;
padding: 10px;
position: absolute;
width: calc(100% + 20px);
left: 50%;
bottom: calc(100% + 10px);
margin-left: calc(-50% - 20px);
white-space: pre;
}
#dms-lc-panel > #dms-lc-panel-content > .dms-lc-button:hover::after {
content: "";
position: absolute;
width: 0;
height: 0;
left: calc(50% - 8px);
top: -10px;
border-top: 8px solid rgba(0, 0, 0, .8);
border-right: 8px solid transparent;
border-left: 8px solid transparent;
}
#dms-lc-panel > #dms-lc-panel-content > .dms-lc-hr {
width: 100%;
margin: 5px 0;
}
#dms-lc-panel > #dms-lc-panel-content > #dmsCLButtonCoffee {
padding: 0;
margin: 0;
}
#dms-lc-panel > #dms-lc-panel-content > #dmsCLButtonCoffee > svg {
width: 35px;
height: 35px;
}
#dms-lc-panel > #dms-lc-panel-content > #dms-lc-qrcode {
display: none;
width: 100%;
position: absolute;
left: 0;
bottom: calc(100% + 24px);
padding: 16px;
color: #333;
font-size: 18px;
line-height: 1.2em;
border: 1px solid #CCC;
border-radius: 12px 12px 0 0;
background-color: #FFF;
box-shadow: 0 6px 36px 5px rgba(0, 0, 0, .16);
}
#dms-lc-panel > #dms-lc-panel-content > #dms-lc-qrcode > img {
width: 30%;
max-width: 180px;
}
`);
/** 添加界面 **/
const dmsLCPopPanel = document.createElement('div');
dmsLCPopPanel.id = 'dms-link-cleaner';
dmsLCPopPanel.innerHTML = `<div id="dms-lc-button">
︽
</div>
<div id="dms-lc-panel">
<div id="dms-lc-panel-content">
<div class="dms-lc-button" id="dmsCLFR" data-tip="查找与替换">
查找与替换
</div>
<div class="dms-lc-button" id="dmsCLST" data-tip="WF节点排序">
WF节点排序
</div>
<div class="dms-lc-button" id="dmsCLNWC" data-tip="节点与字数统计">
节点与字数统计
</div>
<div class="dms-lc-hr"></div>
<div class="dms-lc-button" id="dmsCLButtonTitle" data-tip="预留功能1">
预留功能1
</div>
<div class="dms-lc-button" id="dmsCLButtonPure" data-tip="预留功能2">
预留功能2
</div>
<div class="dms-lc-button" id="dmsCLButtonCopyTitle" data-tip="预留功能3">
预留功能3
</div>
<div class="dms-lc-button" id="dmsCLButtonCopyLink" data-tip="预留功能4">
预留功能4
</div>
<div class="dms-lc-hr"></div>
<div class="dms-lc-button" id="dmsCLButtonCleanAll" data-tip="预留功能5">
预留功能5
</div>
<div class="dms-lc-button" id="dmsCLButtonLink" data-tip="联系作者">Issues</div>
<div class="dms-lc-button" id="dmsCLButtonCoffee" data-tip="Coffee">
<svg t="1539507279741" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1618" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M237.717333 320L277.333333 597.333333h469.333334l39.616-277.333333z" fill="#FFC107" p-id="1619"></path><path d="M832 320v-53.333333a32 32 0 0 0-32-32h-576A32 32 0 0 0 192 266.666667V320h640z" fill="#795548" p-id="1620"></path><path d="M280.384 618.666667L320 896h384l39.616-277.333333z" fill="#77574F" p-id="1621"></path><path d="M512 597.333333m-106.666667 0a106.666667 106.666667 0 1 0 213.333334 0 106.666667 106.666667 0 1 0-213.333334 0Z" fill="#77574F" p-id="1622"></path><path d="M426.666667 597.333333c0-15.616 4.501333-30.058667 11.84-42.666666h-167.253334l15.232 106.666666h169.621334A84.778667 84.778667 0 0 1 426.666667 597.333333zM585.493333 554.666667c7.338667 12.608 11.84 27.050667 11.84 42.666666a84.778667 84.778667 0 0 1-29.44 64h169.621334l15.232-106.666666h-167.253334zM768 128H256l-21.333333 106.666667h554.666666z" fill="#4E342E" p-id="1623"></path><path d="M512 448a149.333333 149.333333 0 1 1 0 298.666667 149.333333 149.333333 0 0 1 0-298.666667m0-21.333333c-94.101333 0-170.666667 76.565333-170.666667 170.666666s76.565333 170.666667 170.666667 170.666667 170.666667-76.565333 170.666667-170.666667-76.565333-170.666667-170.666667-170.666666z" fill="#5D4037" p-id="1624"></path><path d="M512 448a149.333333 149.333333 0 1 0 0 298.666667 149.333333 149.333333 0 0 0 0-298.666667z m0 234.666667a85.333333 85.333333 0 1 1 0-170.666667 85.333333 85.333333 0 0 1 0 170.666667z" fill="#FFF3E0" p-id="1625"></path></svg>
</div>
<div id="dms-lc-qrcode">
Workflowy ToolBox<br />
(Demo)
<hr />
敬请期待……
</div>
</div>
</div>`;
document.body.insertBefore(
dmsLCPopPanel,
document.body.lastChild.nextSibling
);
/** 事件响应函数 **/
/* 定义元素 */
const FR = document.getElementById('dmsCLFR');
const ST = document.getElementById('dmsCLST');
const NWC = document.getElementById('dmsCLNWC');
const button = document.getElementById('dms-lc-button');
const panel = document.getElementById('dms-lc-panel');
const qrcode = document.getElementById('dms-lc-qrcode');
const buttonTitle = document.getElementById('dmsCLButtonTitle');
const buttonPure = document.getElementById('dmsCLButtonPure');
const buttonCopyT = document.getElementById('dmsCLButtonCopyTitle');
const buttonCopyL = document.getElementById('dmsCLButtonCopyLink');
const buttonCleanLink = document.getElementById('dmsCLButtonCleanAll');
const buttonLink = document.getElementById('dmsCLButtonLink');
const buttonCoffee = document.getElementById('dmsCLButtonCoffee');
/**
* 面板切换
*/
const dmsLCToggleEl = function (el) {
const elStyle = getComputedStyle(el, '');
if (elStyle.display === 'none') {
el.style.display = 'block';
} else {
el.style.display = '';
}
};
/** 添加监听器 **/
/* 面板切换按钮 */
button.addEventListener(
'click',
() => {
dmsLCToggleEl(panel);
},
false
);
/* 买咖啡 */
buttonCoffee.addEventListener(
'click',
() => {
dmsLCToggleEl(qrcode);
},
false
);
// 查找与替换 https://rawbytz.github.io/find-replace/
FR.addEventListener('click', Find_Replace, false);
// WF节点排序 https://rawbytz.github.io/sort/
ST.addEventListener('click', WF_Sort, false);
// 节点与字数统计 @rawbytz @seyee
NWC.addEventListener('click', NodeWord_Count, false);
/* 支持链接 */
buttonLink.addEventListener('click', goToSupport, false);
/* 净化并复制标题和链接 */
buttonTitle.addEventListener('click', getCleanUrlAndTitle, false);
/* 净化并复制链接 */
buttonPure.addEventListener('click', getCleanUrl, false);
/* 复制当前链接和标题 */
buttonCopyT.addEventListener('click', getUrlAndTitle, false);
/* 复制当前链接 */
buttonCopyL.addEventListener('click', getUrlOnly, false);
/* 清理整个页面 */
buttonCleanLink.addEventListener('click', cleanAllPage, false);
/* 全屏隐藏按钮 */
document.addEventListener('fullscreenchange', function (event) {
if (document.fullscreenElement) {
button.style.display = 'none';
} else {
button.style.display = '';
}
});
}