一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
289 行
10 KiB
JavaScript
289 行
10 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
function initTree() {
|
|
const container = document.getElementById('folder-tree');
|
|
if (!container) return;
|
|
if (!window.mar10 || !window.mar10.Wunderbaum) return;
|
|
|
|
const dataEl = document.getElementById('folder-tree-data');
|
|
if (!dataEl) return;
|
|
|
|
let treeData;
|
|
try {
|
|
treeData = JSON.parse(dataEl.content.textContent || '{}');
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
|
|
const collapsedNotebooksStorageKey = `folder-tree-collapsed:${window.location.pathname}`;
|
|
const loadCollapsedNotebookKeys = function() {
|
|
try {
|
|
const keys = JSON.parse(window.sessionStorage.getItem(collapsedNotebooksStorageKey) || '[]');
|
|
return new Set(Array.isArray(keys) ? keys.filter(key => typeof key === 'string') : []);
|
|
} catch (error) {
|
|
return new Set();
|
|
}
|
|
};
|
|
const collapsedNotebookKeys = loadCollapsedNotebookKeys();
|
|
const saveCollapsedNotebookKeys = function() {
|
|
try {
|
|
if (collapsedNotebookKeys.size) {
|
|
window.sessionStorage.setItem(collapsedNotebooksStorageKey, JSON.stringify([...collapsedNotebookKeys]));
|
|
} else {
|
|
window.sessionStorage.removeItem(collapsedNotebooksStorageKey);
|
|
}
|
|
} catch (error) {
|
|
// Ignore unavailable browser storage.
|
|
}
|
|
};
|
|
const restoreCollapsedNotebooks = function(nodes) {
|
|
for (const node of nodes) {
|
|
if (node.folder && collapsedNotebookKeys.has(node.key)) node.expanded = false;
|
|
if (node.children) restoreCollapsedNotebooks(node.children);
|
|
}
|
|
};
|
|
restoreCollapsedNotebooks(treeData.source || []);
|
|
const scrollStorageKey = `folder-tree-scroll:${window.location.pathname}`;
|
|
const pointerPositionStorageKey = 'folder-tree-pointer-position';
|
|
const syntheticHoverClassName = 'is-pointer-hover';
|
|
const treeRowSelector = '.wb-row[data-tree-item-type]';
|
|
const accessSessionStorage = function(callback) {
|
|
try {
|
|
return callback(window.sessionStorage);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
let hoverPosition = accessSessionStorage(storage => JSON.parse(storage.getItem(pointerPositionStorageKey) || 'null'));
|
|
|
|
const applySyntheticHoverAt = function(x, y) {
|
|
if (typeof x !== 'number' || typeof y !== 'number') return;
|
|
const target = document.elementFromPoint(x, y);
|
|
const row = target ? target.closest(treeRowSelector) : null;
|
|
const previousRow = container.querySelector(`.${syntheticHoverClassName}`);
|
|
if (previousRow && previousRow !== row) previousRow.classList.remove(syntheticHoverClassName);
|
|
if (row && container.contains(row)) row.classList.add(syntheticHoverClassName);
|
|
};
|
|
|
|
const savePointerPosition = function(event, nodeId) {
|
|
if (!event || typeof event.clientX !== 'number' || typeof event.clientY !== 'number') return;
|
|
const row = nodeId || !event.target.closest ? null : event.target.closest(treeRowSelector);
|
|
const treeItem = row ? row.querySelector('[role="treeitem"]') : null;
|
|
hoverPosition = {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
id: nodeId || (treeItem ? treeItem.id : null),
|
|
};
|
|
accessSessionStorage(storage => storage.setItem(pointerPositionStorageKey, JSON.stringify(hoverPosition)));
|
|
};
|
|
|
|
const navigate = function(node, event) {
|
|
if (!node || !node.data || !node.data.url) return;
|
|
savePointerPosition(event, rowId(node));
|
|
accessSessionStorage(storage => storage.setItem(scrollStorageKey, String(container.scrollTop)));
|
|
window.location.href = node.data.url;
|
|
};
|
|
|
|
const rowId = function(node) {
|
|
return `folder-tree-item-${node.key}`;
|
|
};
|
|
|
|
let selectedTreeItem = null;
|
|
const setSelectedTreeItem = function(treeItem) {
|
|
if (selectedTreeItem && selectedTreeItem !== treeItem) {
|
|
selectedTreeItem.setAttribute('aria-selected', 'false');
|
|
}
|
|
|
|
selectedTreeItem = treeItem;
|
|
if (selectedTreeItem) selectedTreeItem.setAttribute('aria-selected', 'true');
|
|
};
|
|
|
|
const setActiveTreeItem = function(node, moveFocus) {
|
|
if (!node) return;
|
|
const nodeElement = document.getElementById(rowId(node));
|
|
if (!nodeElement) return;
|
|
|
|
container.setAttribute('aria-activedescendant', nodeElement.id);
|
|
setSelectedTreeItem(nodeElement);
|
|
if (moveFocus) node.tree.setFocus();
|
|
};
|
|
|
|
container.setAttribute('role', 'tree');
|
|
container.setAttribute('aria-label', 'Notebook navigation');
|
|
|
|
const liveRegion = document.createElement('div');
|
|
liveRegion.className = 'sr-only';
|
|
liveRegion.setAttribute('role', 'status');
|
|
liveRegion.setAttribute('aria-atomic', 'true');
|
|
container.after(liveRegion);
|
|
|
|
const updateTreeItemLabel = function(node, treeItem) {
|
|
const siblings = node.parent ? node.parent.children || [] : [];
|
|
const position = siblings.indexOf(node) + 1;
|
|
const stateText = node.isExpandable() ? `, ${node.isExpanded() ? 'expanded' : 'collapsed'}` : '';
|
|
treeItem.setAttribute('aria-label', `${node.title}${stateText}, ${position} of ${siblings.length}`);
|
|
};
|
|
|
|
const updateExpandedState = function(node, nodeElem) {
|
|
const treeItem = nodeElem || document.getElementById(rowId(node));
|
|
if (!treeItem) return;
|
|
|
|
treeItem.removeAttribute('aria-expanded');
|
|
updateTreeItemLabel(node, treeItem);
|
|
};
|
|
|
|
const announceExpandedState = function(node) {
|
|
liveRegion.textContent = `${node.title} ${node.isExpanded() ? 'expanded' : 'collapsed'}`;
|
|
};
|
|
|
|
const renderTreeItem = function(e) {
|
|
const row = e.nodeElem ? e.nodeElem.closest('.wb-row') : null;
|
|
if (!row) return;
|
|
|
|
const siblings = e.node.parent ? e.node.parent.children || [] : [];
|
|
const position = siblings.indexOf(e.node) + 1;
|
|
row.setAttribute('role', 'presentation');
|
|
row.dataset.treeItemType = e.node.data.url ? 'note' : 'folder';
|
|
e.nodeElem.id = rowId(e.node);
|
|
const wasRowHovered = Boolean(hoverPosition && hoverPosition.id === e.nodeElem.id);
|
|
row.classList.toggle(syntheticHoverClassName, wasRowHovered);
|
|
const title = e.nodeElem.querySelector('.wb-title');
|
|
if (title && title.scrollWidth > title.clientWidth) {
|
|
title.setAttribute('title', e.node.title);
|
|
} else if (title) {
|
|
title.removeAttribute('title');
|
|
}
|
|
e.nodeElem.setAttribute('role', 'treeitem');
|
|
if (e.node.hasFocus() || e.node.isActive() || container.getAttribute('aria-activedescendant') === e.nodeElem.id) {
|
|
setSelectedTreeItem(e.nodeElem);
|
|
} else {
|
|
e.nodeElem.setAttribute('aria-selected', 'false');
|
|
}
|
|
// Added to fix screen reader on Safari
|
|
e.nodeElem.setAttribute('aria-roledescription', 'outline row');
|
|
e.nodeElem.setAttribute('aria-level', e.node.getLevel());
|
|
e.nodeElem.setAttribute('aria-posinset', position);
|
|
e.nodeElem.setAttribute('aria-setsize', siblings.length);
|
|
|
|
updateExpandedState(e.node, e.nodeElem);
|
|
if (e.node.hasFocus() || e.node.isActive()) {
|
|
setActiveTreeItem(e.node, false);
|
|
}
|
|
};
|
|
|
|
new window.mar10.Wunderbaum({
|
|
element: container,
|
|
adjustHeight: false,
|
|
header: false,
|
|
rowHeightPx: 40,
|
|
source: treeData.source || [],
|
|
navigationModeOption: 'row',
|
|
emptyChildListExpandable: true,
|
|
icon: false,
|
|
iconMap: {
|
|
expanderExpanded: 'fas fa-chevron-down',
|
|
expanderCollapsed: 'fas fa-chevron-right',
|
|
expanderLazy: 'fas fa-chevron-right',
|
|
doc: 'fas fa-sticky-note',
|
|
folder: 'fas fa-folder',
|
|
folderOpen: 'fas fa-folder-open',
|
|
folderLazy: 'fas fa-folder',
|
|
},
|
|
init: function(e) {
|
|
if (treeData.activeKey) {
|
|
const active = e.tree.findKey(treeData.activeKey);
|
|
if (active) {
|
|
e.tree._setActiveNode(active);
|
|
active.update();
|
|
setActiveTreeItem(active, false);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
container.scrollTop = Number(accessSessionStorage(storage => storage.getItem(scrollStorageKey))) || 0;
|
|
requestAnimationFrame(() => {
|
|
if (hoverPosition) applySyntheticHoverAt(hoverPosition.x, hoverPosition.y);
|
|
container.classList.remove('is-initializing');
|
|
});
|
|
});
|
|
},
|
|
activate: function(e) {
|
|
setActiveTreeItem(e.node, true);
|
|
},
|
|
render: function(e) {
|
|
renderTreeItem(e);
|
|
},
|
|
expand: function(e) {
|
|
if (e.flag) {
|
|
collapsedNotebookKeys.delete(e.node.key);
|
|
} else {
|
|
collapsedNotebookKeys.add(e.node.key);
|
|
}
|
|
saveCollapsedNotebookKeys();
|
|
updateExpandedState(e.node);
|
|
setActiveTreeItem(e.node, container.contains(document.activeElement));
|
|
announceExpandedState(e.node);
|
|
},
|
|
click: function(e) {
|
|
if (e.node && e.node.data.folder) {
|
|
setActiveTreeItem(e.node, true);
|
|
e.node.setExpanded(!e.node.isExpanded());
|
|
return false;
|
|
} else if (e.node) {
|
|
if (e.event && e.event.preventDefault) e.event.preventDefault();
|
|
navigate(e.node, e.event);
|
|
return false;
|
|
}
|
|
},
|
|
dblclick: function() {
|
|
return false;
|
|
},
|
|
keydown: function(e) {
|
|
if (e.event && e.event.key === 'Enter' && e.node && !e.node.folder) {
|
|
e.event.preventDefault();
|
|
navigate(e.node);
|
|
return false;
|
|
}
|
|
},
|
|
});
|
|
|
|
const updateSyntheticHover = function(event) {
|
|
savePointerPosition(event);
|
|
const pointerLeftTree = event.type === 'pointerleave';
|
|
const pointerX = pointerLeftTree ? -1 : event.clientX;
|
|
const pointerY = pointerLeftTree ? -1 : event.clientY;
|
|
applySyntheticHoverAt(pointerX, pointerY);
|
|
};
|
|
container.addEventListener('pointermove', updateSyntheticHover);
|
|
container.addEventListener('pointerleave', updateSyntheticHover);
|
|
|
|
const listContainer = container.querySelector('.wb-list-container');
|
|
const nodeList = container.querySelector('.wb-node-list');
|
|
if (listContainer) listContainer.setAttribute('role', 'presentation');
|
|
if (nodeList) nodeList.setAttribute('role', 'presentation');
|
|
}
|
|
|
|
function initSidebarToggle() {
|
|
const toggle = document.querySelector('.folder-tree-toggle');
|
|
const sidebar = document.getElementById('folder-tree-sidebar');
|
|
if (!toggle || !sidebar) return;
|
|
|
|
toggle.addEventListener('click', () => {
|
|
const isOpen = sidebar.classList.toggle('is-open');
|
|
toggle.classList.toggle('is-active', isOpen);
|
|
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
});
|
|
|
|
const overlay = document.querySelector('.folder-tree-overlay');
|
|
if (overlay) overlay.addEventListener('click', () => toggle.click());
|
|
}
|
|
|
|
const initFolderTree = function() {
|
|
initTree();
|
|
initSidebarToggle();
|
|
};
|
|
|
|
onDocumentReady(initFolderTree);
|
|
})();
|