// 思源把用户自定义属性显示到块的右上侧
// see https://ld246.com/article/1732940163490
// version: 0.0.2
// 更新记录
// 0.0.2 可自定义是否显示提示,可自定义属性白名单和黑名单
// 注意,打开属性面板通过模拟按键实现,如果修改了打开属性快捷键请修改openCustomAttrsPage函数的按键映射,否则可能无法模拟按键
(()=>{
// 鼠标移上去是否显示提示
const isShowTips = false;
// 属性白名单
// 下面的自定义属性们显示到右侧,不设置保持空即可。
// 不需要带custom-前缀,白名单和黑名单有任意一个生效即生效
let onlyTheseAttrs = [
//'',
];
// 属性黑名单
// 下面的自定义属性们不显示到右侧,不设置保持空即可。
// 不需要带custom-前缀,白名单和黑名单有任意一个生效即生效
let notTheseAttrs = [
'avs', // 默认过滤数据库的自定义属性
];
// 自定义属性的svg
const attrSvg = `
`;
// 格式化白名单和黑名单
if(onlyTheseAttrs.length > 0) onlyTheseAttrs = onlyTheseAttrs.map(item => item.replace(/^custom\-/i, ''));
if(notTheseAttrs.length > 0) notTheseAttrs = notTheseAttrs.map(item => item.replace(/^custom\-/i, ''));
// 监听自定义属性被添加
let lastAttrWrap = null;
observeCustomAttributes('custom-', (element, attrName, attrValue, type) => {
// 过滤白名单和黑名单,有任意一个生效即生效
if(onlyTheseAttrs.length > 0 && !onlyTheseAttrs.includes(attrName.replace(/^custom\-/i, ''))) return;
if(notTheseAttrs.length > 0 && notTheseAttrs.includes(attrName.replace(/^custom\-/i, ''))) return;
// 把自定义属性显示到右侧
const protyleAttr = element.querySelector('.protyle-attr');
if(!protyleAttr) return;
// 创建wrap
let customAttrsWrap = element.querySelector('.protyle-attr--custom');
if(!customAttrsWrap){
customAttrsWrap = document.createElement('div');
customAttrsWrap.className = 'protyle-attr--custom';
customAttrsWrap.innerHTML = attrSvg;
customAttrsWrap.addEventListener('click', (event) => {
openCustomAttrsPage(element);
});
protyleAttr.appendChild(customAttrsWrap);
}
// 创建属性
let attrEl = customAttrsWrap.querySelector('.' + attrName);
if(!attrEl){
attrEl = document.createElement('span');
attrEl.className = 'custom-attr-item ' + attrName;
customAttrsWrap.appendChild(attrEl);
}
attrEl.textContent = attrValue;
// 添加标题
if (isShowTips) {
if(lastAttrWrap !== customAttrsWrap) {
lastAttrWrap = customAttrsWrap;
customAttrsWrap.title = '';
setTimeout(() => {
lastAttrWrap = null;
customAttrsWrap.title = customAttrsWrap.title.replace(/\n$/, '');
}, 1000);
}
customAttrsWrap.title += attrName + ' : ' + attrValue + "\n";
}
});
// 添加样式
addStyle(`.protyle-attr--custom { .custom-attr-item:not(:last-child) {margin-right: 4px;} svg {fill: currentColor;} }`);
// 添加style标签
function addStyle(css) {
// 创建一个新的style元素
const style = document.createElement('style');
// 设置CSS规则
style.innerHTML = css;
// 将style元素添加到中
document.head.appendChild(style);
}
// 打开属性面板,并切换到自定义属性选项卡
function openCustomAttrsPage(node) {
// 模拟按键
let keyInit = {
ctrlKey: !isMac(),
altKey: true,
metaKey: isMac(),
shiftKey: false,
key: 'A',
keyCode: 65
}
keyInit["bubbles"] = true;
let keydownEvent = new KeyboardEvent('keydown', keyInit);
(node || document.getElementsByTagName("body")[0]).dispatchEvent(keydownEvent);
let keyUpEvent = new KeyboardEvent('keyup', keyInit);
(node || document.getElementsByTagName("body")[0]).dispatchEvent(keyUpEvent);
// 模拟点击
whenElementExist('div.b3-dialog--open[data-key="dialog-attr"]').then((dialog) => {
dialog.querySelector('.layout-tab-bar .item--full[data-type="custom"]').click();
});
}
// 判断是否Mac
function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}
// 等待元素渲染完成后执行
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 observeCustomAttributes(prefix = 'custom-', callback, targetNode = document.body) {
let lastAttrNames = [], lastAttrNamesTimer = null;
// 观察器的配置
const config = { childList: true, attributes: true, subtree: true };
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
// 监听元素加载
if (mutation.type === 'childList') {
// 检查新增的节点
for (const node of mutation.addedNodes) {
if(node.nodeType === Node.ELEMENT_NODE) {
// 获取所有以'custom-'开头的属性
const customAttrs = Array.from(node.attributes).filter(attr => attr.name.startsWith(prefix));
if (customAttrs.length > 0) {
customAttrs.forEach(attr => {
callback(node, attr.name, attr.value, 'load');
});
}
}
}
}
// 监听属性变化
if (mutation.type === 'attributes') {
const attributeName = mutation.attributeName;
// 检查属性名是否以指定前缀开头
if (attributeName.startsWith(prefix)) {
// 防止多次重复触发
if(lastAttrNames.includes(attributeName)) continue;
lastAttrNames.push(attributeName);
if(!lastAttrNamesTimer) lastAttrNamesTimer = setTimeout(() => {
lastAttrNames = [];
lastAttrNamesTimer = null;
}, 1000);
// 如果提供了回调函数,则调用它
if (typeof callback === 'function') {
callback(mutation.target, attributeName, mutation.target.getAttribute(attributeName), 'add');
}
}
}
}
});
// 开始观察目标节点
observer.observe(targetNode, config);
// 返回一个断开连接的方法
return () => observer.disconnect();
}
})();