295 lines
11 KiB
JavaScript
295 lines
11 KiB
JavaScript
|
|
// ========================================
|
|||
|
|
// 模块:视图选择UI
|
|||
|
|
// ========================================
|
|||
|
|
|
|||
|
|
import { i18n } from './i18n.js';
|
|||
|
|
|
|||
|
|
// 视图按钮配置
|
|||
|
|
const viewButtons = {
|
|||
|
|
NodeList: [
|
|||
|
|
{ id: "GraphView", attrName: "f", attrValue: "dt", icon: "iconFiles", labelKey: "转换为导图" },
|
|||
|
|
{ id: "TableView", attrName: "f", attrValue: "bg", icon: "iconTable", labelKey: "转换为表格" },
|
|||
|
|
{ id: "kanbanView", attrName: "f", attrValue: "kb", icon: "iconMenu", labelKey: "转换为看板" },
|
|||
|
|
{ id: "timelineView", attrName: "f", attrValue: "tl", icon: "iconList", labelKey: "转换为时间线" },
|
|||
|
|
{ id: "tabView", attrName: "f", attrValue: "tab", icon: "iconDock", labelKey: "转换为标签页" },
|
|||
|
|
{ id: "DefaultView", attrName: "f", attrValue: "", icon: "iconList", labelKey: "恢复为列表" }
|
|||
|
|
],
|
|||
|
|
NodeTable: [
|
|||
|
|
{ id: "FixWidth", attrName: "f", attrValue: "", icon: "iconTable", labelKey: "默认宽度" },
|
|||
|
|
{ id: "FullWidth", attrName: "f", attrValue: "full", icon: "iconTable", labelKey: "页面宽度" },
|
|||
|
|
{ separator: true },
|
|||
|
|
{ id: "vHeader", attrName: "t", attrValue: "vbiaotou", icon: "iconSuper", labelKey: "竖向表头样式" },
|
|||
|
|
{ id: "Removeth", attrName: "t", attrValue: "biaotou", icon: "iconSuper", labelKey: "空白表头样式" },
|
|||
|
|
{ id: "Defaultth", attrName: "t", attrValue: "", icon: "iconSuper", labelKey: "恢复表头样式" }
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 创建视图选择菜单
|
|||
|
|
const ViewSelect = (selectid, selecttype) => {
|
|||
|
|
const button = document.createElement("button");
|
|||
|
|
button.id = "viewselect";
|
|||
|
|
button.className = "b3-menu__item";
|
|||
|
|
button.innerHTML = `
|
|||
|
|
<svg class="b3-menu__icon"><use xlink:href="#iconGlobalGraph"></use></svg>
|
|||
|
|
<span class="b3-menu__label">${i18n.t("视图选择")}</span>
|
|||
|
|
<svg class="b3-menu__icon b3-menu__icon--arrow"><use xlink:href="#iconRight"></use></svg>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const submenu = document.createElement("button");
|
|||
|
|
submenu.id = "viewselectSub";
|
|||
|
|
submenu.className = "b3-menu__submenu";
|
|||
|
|
|
|||
|
|
const menuItems = document.createElement("div");
|
|||
|
|
menuItems.className = "b3-menu__items";
|
|||
|
|
|
|||
|
|
const buttons = viewButtons[selecttype] || [];
|
|||
|
|
|
|||
|
|
menuItems.innerHTML = buttons.map(btn => {
|
|||
|
|
if (btn.separator) {
|
|||
|
|
return `<button class="b3-menu__separator"></button>`;
|
|||
|
|
} else {
|
|||
|
|
return `
|
|||
|
|
<button class="b3-menu__item" data-view-item="1" data-node-id="${selectid}"
|
|||
|
|
data-attr-name="${btn.attrName}" data-attr-value="${btn.attrValue}">
|
|||
|
|
<svg class="b3-menu__icon"><use xlink:href="#${btn.icon}"></use></svg>
|
|||
|
|
<span class="b3-menu__label">${i18n.t(btn.labelKey)}</span>
|
|||
|
|
</button>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
}).join("");
|
|||
|
|
|
|||
|
|
submenu.appendChild(menuItems);
|
|||
|
|
button.appendChild(submenu);
|
|||
|
|
return button;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 获取选中的块信息
|
|||
|
|
const getBlockSelected = () => {
|
|||
|
|
const node = document.querySelector(".protyle-wysiwyg--select");
|
|||
|
|
return node?.dataset?.nodeId ? {
|
|||
|
|
id: node.dataset.nodeId,
|
|||
|
|
type: node.dataset.type,
|
|||
|
|
subtype: node.dataset.subtype
|
|||
|
|
} : null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 清除转换数据
|
|||
|
|
const clearTransformData = (id, blocks) => {
|
|||
|
|
try {
|
|||
|
|
const positions = JSON.parse(localStorage.getItem("dt-positions") || "{}");
|
|||
|
|
if (positions[id]) {
|
|||
|
|
delete positions[id];
|
|||
|
|
localStorage.setItem("dt-positions", JSON.stringify(positions));
|
|||
|
|
// 注意:cleanupDraggable函数需要在其他模块中定义
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
// 清除transform数据出错: error
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 统一API请求函数
|
|||
|
|
const apiRequest = async (url, data) => {
|
|||
|
|
try {
|
|||
|
|
return await fetch(url, {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
"Authorization": `Token ${window.siyuan?.config?.api?.token ?? ""}`
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify(data)
|
|||
|
|
});
|
|||
|
|
} catch (e) { /* API请求失败: e */ }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// API请求队列
|
|||
|
|
const apiRequestQueue = new Map();
|
|||
|
|
|
|||
|
|
// 设置思源块属性
|
|||
|
|
const 设置思源块属性 = async (id, attrs) => {
|
|||
|
|
const key = `${id}-${JSON.stringify(attrs)}`;
|
|||
|
|
if (apiRequestQueue.has(key)) return;
|
|||
|
|
apiRequestQueue.set(key, true);
|
|||
|
|
try {
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|||
|
|
await apiRequest("/api/attr/setBlockAttrs", { id, attrs });
|
|||
|
|
apiRequestQueue.delete(key);
|
|||
|
|
} catch (e) {
|
|||
|
|
setTimeout(() => apiRequestQueue.delete(key), 1000);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 插入菜单项
|
|||
|
|
const InsertMenuItem = (selectid, selecttype) => {
|
|||
|
|
const commonMenu = document.querySelector("#commonMenu .b3-menu__items");
|
|||
|
|
if (!commonMenu) return;
|
|||
|
|
|
|||
|
|
const readonly = commonMenu.querySelector(`[data-id="updateAndCreatedAt"]`);
|
|||
|
|
const selectview = commonMenu.querySelector(`[id="viewselect"]`);
|
|||
|
|
|
|||
|
|
if (readonly && !selectview) {
|
|||
|
|
const separator = document.createElement("button");
|
|||
|
|
separator.className = "b3-menu__separator";
|
|||
|
|
|
|||
|
|
commonMenu.insertBefore(ViewSelect(selectid, selecttype), readonly);
|
|||
|
|
commonMenu.insertBefore(separator, readonly);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 菜单监控处理器
|
|||
|
|
let menuHandler = null;
|
|||
|
|
let viewSelectClickHandler = null;
|
|||
|
|
|
|||
|
|
// 将列表转换为标签页结构
|
|||
|
|
const convertToTabView = (listElement) => {
|
|||
|
|
if (listElement._convertedToTab) return;
|
|||
|
|
|
|||
|
|
const children = listElement.querySelectorAll(':scope > .li');
|
|||
|
|
if (children.length < 1) return;
|
|||
|
|
|
|||
|
|
listElement._originalHTML = listElement.innerHTML;
|
|||
|
|
listElement._convertedToTab = true;
|
|||
|
|
|
|||
|
|
const headerContainer = document.createElement('div');
|
|||
|
|
headerContainer.className = 'tab-header-container';
|
|||
|
|
|
|||
|
|
const headers = document.createElement('div');
|
|||
|
|
headers.className = 'tab-headers';
|
|||
|
|
|
|||
|
|
const contentsContainer = document.createElement('div');
|
|||
|
|
contentsContainer.className = 'tab-contents';
|
|||
|
|
|
|||
|
|
children.forEach((li, index) => {
|
|||
|
|
const firstChild = li.querySelector(':scope > [data-node-id]:not(.list)');
|
|||
|
|
const subList = li.querySelector(':scope > .list');
|
|||
|
|
|
|||
|
|
const tabHeader = document.createElement('div');
|
|||
|
|
tabHeader.className = 'tab-header' + (index === 0 ? ' active' : '');
|
|||
|
|
|
|||
|
|
if (firstChild) tabHeader.appendChild(firstChild.cloneNode(true));
|
|||
|
|
|
|||
|
|
const tabContent = document.createElement('div');
|
|||
|
|
tabContent.className = 'tab-content' + (index === 0 ? ' active' : '');
|
|||
|
|
if (subList) tabContent.appendChild(subList.cloneNode(true));
|
|||
|
|
|
|||
|
|
headers.appendChild(tabHeader);
|
|||
|
|
contentsContainer.appendChild(tabContent);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
headerContainer.appendChild(headers);
|
|||
|
|
listElement.innerHTML = '';
|
|||
|
|
listElement.appendChild(headerContainer);
|
|||
|
|
listElement.appendChild(contentsContainer);
|
|||
|
|
|
|||
|
|
// 为每个标签页单独绑定事件,避免全局监听
|
|||
|
|
headers.addEventListener('click', (event) => {
|
|||
|
|
const clickedTab = event.target.closest('.tab-header');
|
|||
|
|
if (!clickedTab) return;
|
|||
|
|
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
|
|||
|
|
const allTabs = headers.querySelectorAll('.tab-header');
|
|||
|
|
const allContents = contentsContainer.querySelectorAll('.tab-content');
|
|||
|
|
|
|||
|
|
allTabs.forEach(t => t.classList.remove('active'));
|
|||
|
|
allContents.forEach(c => c.classList.remove('active'));
|
|||
|
|
|
|||
|
|
clickedTab.classList.add('active');
|
|||
|
|
const index = [...allTabs].indexOf(clickedTab);
|
|||
|
|
allContents[index]?.classList.add('active');
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const restoreFromTabView = (listElement) => {
|
|||
|
|
if (!listElement._convertedToTab) return;
|
|||
|
|
listElement.innerHTML = listElement._originalHTML || '';
|
|||
|
|
delete listElement._originalHTML;
|
|||
|
|
delete listElement._convertedToTab;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化菜单监控
|
|||
|
|
export const initViewSelect = () => {
|
|||
|
|
if (menuHandler) return;
|
|||
|
|
|
|||
|
|
const initTabViews = () => {
|
|||
|
|
document.querySelectorAll('.protyle-wysiwyg [data-type="NodeList"][custom-f~="tab"]')
|
|||
|
|
.forEach(el => !el._convertedToTab && convertToTabView(el));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
setTimeout(initTabViews, 100);
|
|||
|
|
|
|||
|
|
// 保存事件监听器引用以便清理
|
|||
|
|
viewSelectClickHandler = viewSelectClickHandler || {};
|
|||
|
|
viewSelectClickHandler._loadedProtyleHandler = () => setTimeout(initTabViews, 200);
|
|||
|
|
window.siyuan?.eventBus?.on('loaded-protyle', viewSelectClickHandler._loadedProtyleHandler);
|
|||
|
|
|
|||
|
|
// 防抖函数
|
|||
|
|
let debounceTimer;
|
|||
|
|
const debouncedInit = () => {
|
|||
|
|
clearTimeout(debounceTimer);
|
|||
|
|
debounceTimer = setTimeout(initTabViews, 150);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 只监听 .protyle-wysiwyg 容器,减少监听范围
|
|||
|
|
const observer = new MutationObserver(debouncedInit);
|
|||
|
|
const protyleContainers = document.querySelectorAll('.protyle-wysiwyg');
|
|||
|
|
protyleContainers.forEach(container => {
|
|||
|
|
observer.observe(container, { childList: true, subtree: true, attributes: true, attributeFilter: ['custom-f'] });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
viewSelectClickHandler._tabObserver = observer;
|
|||
|
|
|
|||
|
|
// 存储事件监听器引用,以便后续清理
|
|||
|
|
menuHandler = () => {
|
|||
|
|
requestAnimationFrame(() => {
|
|||
|
|
const selectinfo = getBlockSelected();
|
|||
|
|
if (selectinfo && (selectinfo.type === "NodeList" || selectinfo.type === "NodeTable")) {
|
|||
|
|
InsertMenuItem(selectinfo.id, selectinfo.type);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.addEventListener("mouseup", menuHandler);
|
|||
|
|
|
|||
|
|
// 统一的事件委托,处理视图选择子项点击
|
|||
|
|
viewSelectClickHandler = (event) => {
|
|||
|
|
const item = event.target.closest('.b3-menu__item[data-view-item="1"]');
|
|||
|
|
if (!item) return;
|
|||
|
|
|
|||
|
|
const id = item.dataset.nodeId;
|
|||
|
|
const attrName = "custom-" + item.dataset.attrName;
|
|||
|
|
const attrValue = item.dataset.attrValue;
|
|||
|
|
const blocks = document.querySelectorAll(`.protyle-wysiwyg [data-node-id="${id}"]`);
|
|||
|
|
|
|||
|
|
clearTransformData(id, blocks);
|
|||
|
|
|
|||
|
|
if (blocks?.length > 0) {
|
|||
|
|
blocks.forEach(block => {
|
|||
|
|
block.setAttribute(attrName, attrValue);
|
|||
|
|
attrValue === "tab" ? convertToTabView(block) : restoreFromTabView(block);
|
|||
|
|
});
|
|||
|
|
设置思源块属性(id, { [attrName]: attrValue });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
document.addEventListener("click", viewSelectClickHandler, true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 清理视图选择功能
|
|||
|
|
export const cleanupViewSelect = () => {
|
|||
|
|
if (menuHandler) {
|
|||
|
|
window.removeEventListener("mouseup", menuHandler);
|
|||
|
|
menuHandler = null;
|
|||
|
|
}
|
|||
|
|
if (viewSelectClickHandler) {
|
|||
|
|
document.removeEventListener("click", viewSelectClickHandler, true);
|
|||
|
|
if (viewSelectClickHandler._tabObserver) {
|
|||
|
|
viewSelectClickHandler._tabObserver.disconnect();
|
|||
|
|
}
|
|||
|
|
if (viewSelectClickHandler._loadedProtyleHandler && window.siyuan?.eventBus) {
|
|||
|
|
window.siyuan.eventBus.off('loaded-protyle', viewSelectClickHandler._loadedProtyleHandler);
|
|||
|
|
}
|
|||
|
|
viewSelectClickHandler = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|