// ========================================
// 模块:视图选择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 = `
`;
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 ``;
} else {
return `
`;
}
}).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;
}
};