feat: Initial clean release of CHM
一些检查失败了
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
一些检查失败了
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
这个提交包含在:
@@ -0,0 +1,288 @@
|
||||
(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);
|
||||
})();
|
||||
在新工单中引用
屏蔽一个用户