refactor: externalize filetree selection runtime
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// MNote 主壳文件树选择运行时外置模块。
|
||||
// 当前只承接 selection 状态同步和选择算法;事件监听、DND 执行和命令分发仍留在主壳。
|
||||
|
||||
function cssEscapeValue(value) {
|
||||
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
|
||||
return String(value).replace(/["\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function visibleFileTreeRows(deps) {
|
||||
if (deps && typeof deps.visibleFileTreeRows === 'function') {
|
||||
return deps.visibleFileTreeRows();
|
||||
}
|
||||
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
|
||||
.filter(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.closest('.tree-children--collapsed')) return false;
|
||||
return row.offsetParent !== null || row.getClientRects().length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function selectedRowIdSet(selection) {
|
||||
var selected = selection && selection.selectedRowIds;
|
||||
if (selected instanceof Set) return new Set(selected);
|
||||
if (Array.isArray(selected)) return new Set(selected);
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function syncSidebarFileTreeSelection(selection, deps) {
|
||||
var state = selection || {};
|
||||
var selected = selectedRowIdSet(state);
|
||||
var focusedRowId = String(state.focusedRowId || '');
|
||||
visibleFileTreeRows(deps).forEach(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
row.setAttribute('data-selected', String(Boolean(rowId && selected.has(rowId))));
|
||||
row.setAttribute('data-focused', String(rowId === focusedRowId));
|
||||
});
|
||||
var detail = {
|
||||
selectedRowIds: Array.from(selected),
|
||||
anchorRowId: state.anchorRowId || null,
|
||||
focusedRowId: state.focusedRowId || null
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', { detail: detail }));
|
||||
return detail;
|
||||
}
|
||||
|
||||
function selectSidebarFileTreeRow(row, selection, modifiers, deps) {
|
||||
if (!(row instanceof HTMLElement)) return [];
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
if (!rowId) return [];
|
||||
var state = selection || {};
|
||||
var rows = visibleFileTreeRows(deps);
|
||||
var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
|
||||
var selected = selectedRowIdSet(state);
|
||||
var shiftKey = Boolean(modifiers && modifiers.shiftKey);
|
||||
var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
|
||||
if (shiftKey && state.anchorRowId) {
|
||||
var anchorIndex = visibleRowIds.indexOf(state.anchorRowId);
|
||||
var targetIndex = visibleRowIds.indexOf(rowId);
|
||||
if (anchorIndex >= 0 && targetIndex >= 0) {
|
||||
selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
|
||||
} else {
|
||||
selected = new Set([rowId]);
|
||||
state.anchorRowId = rowId;
|
||||
}
|
||||
} else if (ctrlKey) {
|
||||
if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
|
||||
else selected.add(rowId);
|
||||
state.anchorRowId = rowId;
|
||||
} else {
|
||||
selected = new Set([rowId]);
|
||||
state.anchorRowId = rowId;
|
||||
}
|
||||
state.selectedRowIds = selected;
|
||||
state.focusedRowId = rowId;
|
||||
syncSidebarFileTreeSelection(state, deps);
|
||||
return Array.from(selected);
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRowIdsForDrag(row, selection) {
|
||||
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
|
||||
var selected = selectedRowIdSet(selection || {});
|
||||
if (rowId && selected.has(rowId)) return Array.from(selected);
|
||||
return rowId ? [rowId] : [];
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRows(selection, deps) {
|
||||
var state = selection || {};
|
||||
var selectedIds = selectedRowIdSet(state);
|
||||
var rows = visibleFileTreeRows(deps).filter(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
return rowId && selectedIds.has(rowId);
|
||||
});
|
||||
if (rows.length > 0) return rows;
|
||||
if (state.focusedRowId) {
|
||||
var escape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : cssEscapeValue;
|
||||
var focused = document.querySelector(
|
||||
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + escape(state.focusedRowId) + '"]'
|
||||
);
|
||||
if (focused instanceof HTMLElement) return [focused];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
window.__mnoteFileTreeSelectionRuntime = {
|
||||
visibleFileTreeRows: visibleFileTreeRows,
|
||||
syncSidebarFileTreeSelection: syncSidebarFileTreeSelection,
|
||||
selectSidebarFileTreeRow: selectSidebarFileTreeRow,
|
||||
selectedSidebarFileTreeRowIdsForDrag: selectedSidebarFileTreeRowIdsForDrag,
|
||||
selectedSidebarFileTreeRows: selectedSidebarFileTreeRows
|
||||
};
|
||||
@@ -106,6 +106,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||
get(web_shell::filetree_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
|
||||
get(web_shell::filetree_selection_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||
get(web_shell::filetree_context_menu_runtime_asset),
|
||||
@@ -522,6 +526,7 @@ mod tests {
|
||||
"/api/mnote-browser-runtime/resource-open-runtime.js",
|
||||
"/api/mnote-browser-runtime/local-upload-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||
|
||||
@@ -4423,6 +4423,20 @@ pub async fn filetree_runtime_asset() -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn filetree_selection_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/filetree-selection-runtime.js");
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn tree_live_controller_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/tree-live-controller.js");
|
||||
Response::builder()
|
||||
|
||||
@@ -3162,6 +3162,18 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
};
|
||||
}
|
||||
|
||||
function fileTreeSelectionRuntimeFunction(name) {
|
||||
var runtime = window.__mnoteFileTreeSelectionRuntime;
|
||||
var fn = runtime && runtime[name];
|
||||
return typeof fn === 'function' ? fn : null;
|
||||
}
|
||||
|
||||
function fileTreeSelectionRuntimeDeps() {
|
||||
return {
|
||||
cssEscape: cssEscape
|
||||
};
|
||||
}
|
||||
|
||||
function uploadedAssetTitle(asset) {
|
||||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
|
||||
if (runtimeFn) return runtimeFn(asset);
|
||||
@@ -4946,6 +4958,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function visibleFileTreeRows() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows');
|
||||
if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps());
|
||||
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
|
||||
.filter(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
@@ -4955,6 +4969,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function syncSidebarFileTreeSelection() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection');
|
||||
if (runtimeFn) {
|
||||
runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||||
return;
|
||||
}
|
||||
var rows = visibleFileTreeRows();
|
||||
rows.forEach(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
@@ -4971,6 +4990,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectSidebarFileTreeRow(row, modifiers) {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow');
|
||||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps());
|
||||
if (!(row instanceof HTMLElement)) return [];
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
if (!rowId) return [];
|
||||
@@ -5110,6 +5131,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRowIdsForDrag(row) {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag');
|
||||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection);
|
||||
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
|
||||
if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
|
||||
return Array.from(sidebarFileTreeSelection.selectedRowIds);
|
||||
@@ -5118,6 +5141,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRows() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows');
|
||||
if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||||
var selectedIds = sidebarFileTreeSelection.selectedRowIds;
|
||||
var rows = visibleFileTreeRows().filter(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
@@ -10538,6 +10563,7 @@ pub fn PageLayout(
|
||||
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-selection-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-context-menu-runtime.js"></script>
|
||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
|
||||
@@ -10582,6 +10608,8 @@ mod tests {
|
||||
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
||||
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
|
||||
const FILETREE_SELECTION_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/filetree-selection-runtime.js");
|
||||
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
||||
|
||||
fn js_function_body(source: &str, name: &str) -> String {
|
||||
@@ -10937,6 +10965,40 @@ mod tests {
|
||||
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function fileTreeSelectionRuntimeFunction"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')"));
|
||||
let select_body = js_function_body(SIDEBAR_TREE_JS, "selectSidebarFileTreeRow");
|
||||
assert!(
|
||||
select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId")
|
||||
&& select_body.contains("ctrlKey"),
|
||||
"inline fallback 必须保留 shift range 与 ctrl/meta toggle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filetree_selection_runtime_contains_selection_helpers() {
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function visibleFileTreeRows"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function syncSidebarFileTreeSelection"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectSidebarFileTreeRow"));
|
||||
assert!(
|
||||
FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRowIdsForDrag")
|
||||
);
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRows"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("tree.filetree.selection.changed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
||||
|
||||
Reference in New Issue
Block a user