function waitFor(conditionFn, timeoutMs=5000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = () => {
if(typeof conditionFn === 'string')
conditionFn = () => document.querySelector(conditionFn);
const result = conditionFn();
if (result) resolve(result);
else if (Date.now() - start > timeoutMs) reject();
else requestAnimationFrame(check); // 利用浏览器刷新周期
};
check();
});
}
// 带超时改进版
function whenElementExistOrNull(selector, node, timeout = 5000, sleep = 0) {
timeout = isNumber(timeout) ? parseInt(timeout) : 5000;
sleep = isNumber(sleep) ? parseInt(sleep) : 0;
return new Promise(resolve => {
const startTime = Date.now();
const check = async () => {
const el = typeof selector === 'function'
? await selector()
: (node || document).querySelector(selector);
if (el || Date.now() - startTime >= timeout) {
resolve(el || null);
return;
}
sleep ? setTimeout(check, sleep) : requestAnimationFrame(check);
};
check();
});
}
// 等待元素存在,带超时
function whenElementExist(selector, node, timeout = 5000, sleep = 0) {
timeout = isNumber(timeout) ? parseInt(timeout) : 5000;
sleep = isNumber(sleep) ? parseInt(sleep) : 0;
return new Promise((resolve, reject) => {
let isResolved = false;
const check = () => {
if(typeof node === 'string') node = document.querySelector(node);
const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
if (el) {isResolved = true; resolve(el);} else if(!isResolved) sleep ? setTimeout(check, sleep) : requestAnimationFrame(check);
};
check();
if(timeout > 0) {
setTimeout(() => {
if (!isResolved) {
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
}
}, timeout);
}
});
}
function whenElementExist(selector, node, timeout = 5000) {
return new Promise((resolve, reject) => {
let isResolved = false;
const check = () => {
const el = (typeof selector === 'function' ? selector() : (node || document).querySelector(selector));
if (el) {
isResolved = true;
resolve(el);
} else if (!isResolved) {
setTimeout(check, 100); // 每 100ms 检查一次
}
};
check();
setTimeout(() => {
if (!isResolved) {
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
}
}, timeout);
});
}
function whenElementExist(selector, node, timeout = 5000) {
return new Promise((resolve, reject) => {
const observer = new MutationObserver(() => {
const el = (node || document).querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document, { childList: true, subtree: true });
// 设置超时
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timeout: Element not found for selector "${selector}" within ${timeout}ms`));
}, timeout);
});
}
// 等待元素或变量存在
function whenExist(selector, parentNode = null, timeout = 5000, errorMsg = '') {
return new Promise((resolve, reject) => {
let timeoutId;
let isResolved = false;
const check = () => {
let element = null;
if (typeof selector === 'function') {
element = selector();
} else {
element = (parentNode||document).querySelector(selector);
}
if (element) {
isResolved = true;
if(timeoutId) clearTimeout(timeoutId);
resolve(element);
} else {
requestAnimationFrame(check);
}
};
check();
// 设置超时兜底
if(timeout > 0) timeoutId = setTimeout(() => {
if(isResolved) return;
console.error(errorMsg||`Element not found within ${timeout}ms`);
reject(new Error(errorMsg||`Element not found within ${timeout}ms`));
}, timeout);
});
}
// 请求api 简版
// 调用 await requestApi('/api/block/getChildBlocks', {id: '20241208033813-onlvfmp'});
async function requestApi(url, data, method = 'POST') {
return await (await fetch(url, {method: method, body: JSON.stringify(data||{})})).json();
}
// 请求api
async function fetchSyncPost(url, data, returnType = 'json') {
const init = {
method: "POST",
};
if (data) {
if (data instanceof FormData) {
init.body = data;
} else {
init.body = JSON.stringify(data);
}
}
try {
const res = await fetch(url, init);
const res2 = returnType === 'json' ? await res.json() : await res.text();
return res2;
} catch(e) {
console.log(e);
return returnType === 'json' ? {code:e.code||1, msg: e.message||"", data: null} : "";
}
}
// 等待元素出现(简版)
function whenElementExist(selector, node) {
return new Promise(resolve => {
const check = () => {
const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
if (el) resolve(el); else requestAnimationFrame(check);
};
check();
});
}
// 等待元素渲染完成后执行
function whenElementExist(selector) {
return new Promise(resolve => {
const checkForElement = () => {
let element = null;
if (typeof selector === 'function') {
element = selector();
} else {
element = document.querySelector(selector);
}
if (element) {
resolve(element);
} else {
requestAnimationFrame(checkForElement);
}
};
checkForElement();
});
}
// 等待多个元素渲染完成
function whenElementsExist(selector) {
return new Promise(resolve => {
const checkForElement = () => {
let elements = null;
if (typeof selector === 'function') {
elements = selector();
} else {
elements = document.querySelectorAll(selector);
}
if (elements && elements.length > 0) {
resolve(elements);
} else {
requestAnimationFrame(checkForElement);
}
};
checkForElement();
});
}
// 等待元素渲染完成后执行(delay版)
function whenElementExist(selector, bySetTimeout = false, delay = 40, maxTime = 5000) {
return new Promise(resolve => {
let usedTime = 0;
const checkForElement = () => {
console.log('checkForElement', usedTime);
let element = null;
if (typeof selector === 'function') {
element = selector();
} else {
element = document.querySelector(selector);
}
if (element) {
resolve(element);
} else {
if (bySetTimeout) {
setTimeout(()=>{
usedTime += delay;
if(maxTime > usedTime) checkForElement();
}, delay);
} else {
requestAnimationFrame(checkForElement);
}
}
};
checkForElement();
});
}
// 查询SQL函数
async function query(sql) {
const result = await fetchSyncPost('/api/query/sql', { "stmt": sql });
if (result.code !== 0) {
console.error("查询数据库出错", result.msg);
return [];
}
return result.data;
}
// 添加style标签
function addStyle(css) {
// 创建一个新的style元素
const style = document.createElement('style');
// 设置CSS规则
style.innerHTML = css;
// 将style元素添加到<head>中
document.head.appendChild(style);
}
// 支持创建文件夹,当isDir true时创建文件夹,忽略文件
async function putFile(path, content = '', isDir = false) {
const formData = new FormData();
formData.append("path", path);
formData.append("isDir", isDir)
formData.append("file", new Blob([content]));
const result = await fetch("/api/file/putFile", { // 写入js到本地
method: "POST",
body: formData,
});
const json = await result.json();
return json;
}
// 存储文件
function putFile(storagePath, data) {
const formData = new FormData();
formData.append("path", storagePath);
formData.append("file", new Blob([data]));
return fetch("/api/file/putFile", {
method: "POST",
body: formData,
}).then((response) => {
if (response.ok) {
//console.log("File saved successfully");
}
else {
throw new Error("Failed to save file");
}
}).catch((error) => {
console.error(error);
});
}
// 获取文件
async function getFile(path, type = 'text') {
return fetch("/api/file/getFile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
}).then((response) => {
if (response.ok) {
if(type==='json') return response.json();
else if(type==='blob') return response.blob();
else return response.text();
} else {
throw new Error("Failed to get file content");
}
}).catch((error) => {
console.error(error);
});
}
// unicode转emoji
// 使用示例:unicode2Emoji('1f4c4');
// see https://ld246.com/article/1726920727424
function unicode2Emoji(unicode, className = "", needSpan = false, lazy = false) {
if (!unicode) {
return "";
}
let emoji = "";
if (unicode.indexOf(".") > -1) {
emoji = `<img class="${className}" ${lazy ? "data-" : ""}src="/emojis/${unicode}"/>`;
} else {
try {
unicode.split("-").forEach(item => {
if (item.length < 5) {
emoji += String.fromCodePoint(parseInt("0" + item, 16));
} else {
emoji += String.fromCodePoint(parseInt(item, 16));
}
});
if (needSpan) {
emoji = `<span class="${className}">${emoji}</span>`;
}
} catch (e) {
// 自定义表情搜索报错 https://github.com/siyuan-note/siyuan/issues/5883
// 这里忽略错误不做处理
}
}
return emoji;
}
// 延迟执行
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 发送消息
function showMessage(message, isError = false, delay = 7000) {
return fetch('/api/notification/' + (isError ? 'pushErrMsg' : 'pushMsg'), {
"method": "POST",
"body": JSON.stringify({"msg": message, "timeout": delay})
});
}
async function showMessageWithButton(message, isError = false, delay = 7000, text='',callback='', type='a') {
const result = fetch('/api/notification/' + (isError ? 'pushErrMsg' : 'pushMsg'), {
"method": "POST", "body": JSON.stringify({"msg": message, "timeout": delay})
});
if(text && callback) {
if(typeof callback === 'string') {const link=callback; callback = async () => window.open(link);}
const whenElementExist = (selector, node, timeout=5000) => { return new Promise(resolve => {
const startTime = Date.now(); const check = () => {
const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
if (el || Date.now() - startTime >= timeout) {resolve(el||null);return} requestAnimationFrame(check);
}; check();
});};
const msgContent = await whenElementExist("#message .b3-snackbar__content");
if(!msgContent) return; const br = document.createElement("br");
const button = document.createElement(type);button.style.cursor='pointer';
if(type === 'button') button.classList.add('b3-button', 'b3-button--white');
button.textContent = text; if(typeof callback === 'function') button.onclick = callback;
msgContent.appendChild(br); msgContent.appendChild(button);
}
return result;
}
// 状态栏输出
let statusMsg;
function showStatusMsg(params, append = false) {
if(!statusMsg) statusMsg = document.querySelector('#status .status__msg');
params = typeof params === 'string' ? params : JSON.stringify(params);
let html = statusMsg.innerHTML;
if(append) {
html += params;
} else {
html = params;
}
html = html.trim();
statusMsg.innerHTML = html;
}
// 自定义状态栏输出
function showMyStatusMsg(html, delay = 0, append = false) {
let myStatus = document.querySelector('#status .my_status__msg');
if(!myStatus) {
const status = document.querySelector('#status');
const statusMsg = status.querySelector('.status__msg');
if(!statusMsg) return;
const style = `
color: var(--b3-theme-on-surface); white-space: nowrap;
text-overflow: ellipsis; overflow: hidden;
padding-left: 5px; padding-right: 5px; font-size: 12px;
`;
const myStatusHtml = `<div class="my_status__msg" style="${style}"></div>`;
statusMsg.insertAdjacentHTML('afterend', myStatusHtml);
myStatus = status.querySelector('.my_status__msg');
}
if(myStatus) append ? myStatus.innerHTML += html : myStatus.innerHTML = html;
if(delay > 0) setTimeout(()=>myStatus.innerHTML = '', delay);
}
const setStyle = (() => {
let styleElement = null; // 保存当前样式元素的引用
return (css) => {
// 如果已存在样式元素,先移除它
if (styleElement) {
styleElement.parentNode.removeChild(styleElement);
}
// 创建新的样式元素
styleElement = document.createElement('style');
styleElement.textContent = css;
document.head.appendChild(styleElement);
};
})();
function setStyle() {
let styleElement = null; // 保存当前样式元素的引用
return (css) => {
// 如果已存在样式元素,先移除它
if (styleElement) {
styleElement.parentNode.removeChild(styleElement);
}
// 创建新的样式元素
styleElement = document.createElement('style');
styleElement.textContent = css;
document.head.appendChild(styleElement);
};
}
function getProtyleEl() {
return document.querySelector('[data-type="wnd"].layout__wnd--active .protyle:not(.fn__none)')||document.querySelector('[data-type="wnd"] .protyle:not(.fn__none)');
}
function getEditor() {
return getProtyleEl()?.querySelector('.protyle-wysiwyg.protyle-wysiwyg--attr');
}
// 判断空文件
async function isEmptyFile(id) {
const ret = await requestApi('/api/block/getTreeStat', {id});
return ret && ret.code === 0 && ret.data && (ret.data?.runeCount === 0 || ret.data?.stat?.runeCount === 0);
}
// 判断是文件夹
async function isFolderFile(id) {
const ret = await requestApi('/api/block/getDocInfo', {id});
return ret && ret.code === 0 && ret.data && ret.data?.subFileCount > 0;
}
本地存储
// see https://github.com/siyuan-note/siyuan/blob/1317020c1791edf440da7f836d366567e03dd843/app/src/protyle/util/compatibility.ts#L409
async function setStorageVal(key, val, cb) {
if (window.siyuan.config.readonly) return;
const result = await fetchSyncPost("/api/storage/setLocalStorageVal", {
app: window.siyuan.ws.app.appId, key, val,
});
if(result && result.code === 0) {
if (cb) cb();
return result;
}
}
// see https://github.com/siyuan-note/siyuan/blob/e47b8efc2f2611163beca9fad4ee5424001515ff/app/src/protyle/util/compatibility.ts#L258
async function getStorageVal(key) {
const result = await fetchSyncPost("/api/storage/getLocalStorage");
if(result && result.code === 0 && result.data) {
return result.data[key];
}
}
等待元素是否存在MutationObserver版
function waitForElement(selector, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
// 1. 先立即检查一次,避免无谓监听
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
// 2. 设置超时
const timeout = setTimeout(() => {
observer.disconnect();
reject(new Error(`等待元素 "${selector}" 超时 (${timeoutMs}ms)`));
}, timeoutMs);
// 3. 创建监听器
const observer = new MutationObserver((mutations) => {
// 每次 DOM 变化时检查元素是否存在
const targetElement = document.querySelector(selector);
if (targetElement) {
clearTimeout(timeout); // 清除超时
observer.disconnect(); // 停止监听
resolve(targetElement); // 返回元素
}
});
// 4. 开始监听整个文档的子树变化
observer.observe(document.body, {
childList: true, // 监听子节点增删
subtree: true // 监听所有后代节点
});
});
}
// 使用示例:等待 id="myButton" 的元素
try {
const button = await waitForElement('#myButton', 3000);
button.click(); // 元素存在后执行操作
} catch (error) {
console.error(error.message);
}
