feat: sidebar runtime extraction + history-safe commands + checklist updates
This commit is contained in:
@@ -199,10 +199,153 @@ function runSessionConflictAction(action, deps) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受磁盘版本:用磁盘快照替换当前 session 内容。
|
||||
* 依赖通过 deps 注入,确保 runtime 不依赖 inline 作用域闭包。
|
||||
* @param {Object} session
|
||||
* @param {Object} deps
|
||||
* @param {Function} deps.setSessionStatus
|
||||
* @param {Function} deps.fetchLatestResourceSnapshot
|
||||
* @param {Function} deps.applyResourceSnapshotToSession
|
||||
* @param {Function} deps.fetchLatestSessionAggregate
|
||||
* @param {Function} deps.applyAggregateSnapshotToSession
|
||||
*/
|
||||
function acceptDiskVersion(session, deps) {
|
||||
deps = deps || {};
|
||||
var setSessionStatus = deps.setSessionStatus || function() {};
|
||||
var fetchLatestResourceSnapshot = deps.fetchLatestResourceSnapshot || function() { return Promise.resolve(null); };
|
||||
var applyResourceSnapshotToSession = deps.applyResourceSnapshotToSession || function() {};
|
||||
var fetchLatestSessionAggregate = deps.fetchLatestSessionAggregate || function() { return Promise.resolve(null); };
|
||||
var applyAggregateSnapshotToSession = deps.applyAggregateSnapshotToSession || function() {};
|
||||
|
||||
setSessionStatus(session, 'conflict-resolving', '正在接受磁盘版本...');
|
||||
if (session.sessionKind === 'resource') {
|
||||
return fetchLatestResourceSnapshot(session).then(function(latestResource) {
|
||||
applyResourceSnapshotToSession(session, latestResource, 'mnote-web-resource-conflict-accept-disk');
|
||||
});
|
||||
}
|
||||
return fetchLatestSessionAggregate(session).then(function(latest) {
|
||||
applyAggregateSnapshotToSession(session, latest, 'mnote-web-conflict-accept-disk');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保留当前编辑器版本:读取 editor 实时文本,更新 conflictDetectionKey,
|
||||
* 清空冲突标记,持久化后关闭冲突面板。
|
||||
* 依赖通过 deps 注入。
|
||||
* @param {Object} session
|
||||
* @param {Object} deps
|
||||
* @param {Function} deps.setSessionStatus
|
||||
* @param {Function} deps.fetchLatestResourceSnapshot - 返回 Promise<Object>
|
||||
* @param {Function} deps.fetchLatestSessionAggregate - 返回 Promise<Object>
|
||||
* @param {Function} deps.sessionViews - session → views[]
|
||||
* @param {Function} deps.currentEditorText - view → string
|
||||
* @param {Function} deps.normalizePlainText - string → string
|
||||
* @param {Function} deps.hydrateMindmapAttrsFromDom - (doc, root) → doc
|
||||
* @param {Function} deps.textToTiptapDocument - string → doc
|
||||
* @param {Function} deps.conflictDetectionKeyFromBody - body → string
|
||||
* @param {Function} deps.clearSessionConflictSurface - session → void
|
||||
* @param {Function} deps.persistSession - session → Promise
|
||||
*/
|
||||
function keepCurrentEditorVersion(session, deps) {
|
||||
deps = deps || {};
|
||||
var setSessionStatus = deps.setSessionStatus || function() {};
|
||||
var fetchLatestResourceSnapshot = deps.fetchLatestResourceSnapshot || function() { return Promise.resolve(null); };
|
||||
var fetchLatestSessionAggregate = deps.fetchLatestSessionAggregate || function() { return Promise.resolve(null); };
|
||||
var sessionViews = deps.sessionViews || function() { return []; };
|
||||
var currentEditorText = deps.currentEditorText || function() { return ''; };
|
||||
var normalizePlainText = deps.normalizePlainText || function(s) { return s; };
|
||||
var hydrateMindmapAttrsFromDom = deps.hydrateMindmapAttrsFromDom || function(doc) { return doc; };
|
||||
var textToTiptapDocument = deps.textToTiptapDocument || function(s) { return {type: 'doc', content: [{type: 'paragraph', content: [{type: 'text', text: s}]}]}; };
|
||||
var conflictDetectionKeyFromBody = deps.conflictDetectionKeyFromBody || function() { return ''; };
|
||||
var clearSessionConflictSurface = deps.clearSessionConflictSurface || function() {};
|
||||
var persistSession = deps.persistSession || function() { return Promise.resolve(); };
|
||||
|
||||
setSessionStatus(session, 'conflict-resolving', '正在保留当前编辑器版本...');
|
||||
var latestPromise = session.sessionKind === 'resource'
|
||||
? fetchLatestResourceSnapshot(session)
|
||||
: fetchLatestSessionAggregate(session);
|
||||
return latestPromise.then(function(latest) {
|
||||
var hydrateView = sessionViews(session).find(function(item) { return item.mountId != null; }) || sessionViews(session)[0];
|
||||
if (hydrateView) {
|
||||
var liveText = normalizePlainText(currentEditorText(hydrateView));
|
||||
if (liveText) {
|
||||
session.currentTiptapDocument = hydrateMindmapAttrsFromDom(
|
||||
textToTiptapDocument(liveText),
|
||||
hydrateView.runtimeDescriptor && hydrateView.runtimeDescriptor.root,
|
||||
);
|
||||
}
|
||||
session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
|
||||
}
|
||||
var nextKey = session.sessionKind === 'resource'
|
||||
? String((latest && (latest.fileVersion || latest.conflictDetectionKey) || '')).trim()
|
||||
: conflictDetectionKeyFromBody((latest && latest.body) || {});
|
||||
if (nextKey) {
|
||||
session.conflictDetectionKey = nextKey;
|
||||
session.lastExternalConflictDetectionKey = nextKey;
|
||||
}
|
||||
session.hasExternalConflict = false;
|
||||
session.externalChangePending = false;
|
||||
session.saving = false;
|
||||
session.dirty = true;
|
||||
clearSessionConflictSurface(session);
|
||||
return persistSession(session);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写回合并结果:将 merge textarea 中的文本写入 session,
|
||||
* 更新 conflictDetectionKey,清除冲突标记,持久化后移除面板。
|
||||
* @param {Object} session
|
||||
* @param {HTMLElement|null} panel - 待移除的面板 DOM
|
||||
* @param {string} mergedText
|
||||
* @param {Object} deps
|
||||
* @param {Function} deps.setSessionStatus
|
||||
* @param {Function} deps.fetchLatestResourceSnapshot - Promise<Object>
|
||||
* @param {Function} deps.fetchLatestSessionAggregate - Promise<Object>
|
||||
* @param {Function} deps.conflictDetectionKeyFromBody - body → string
|
||||
* @param {Function} deps.textToTiptapDocument - string → doc
|
||||
* @param {Function} deps.persistSession - session → Promise
|
||||
*/
|
||||
function writeMergedConflictResult(session, panel, mergedText, deps) {
|
||||
deps = deps || {};
|
||||
var setSessionStatus = deps.setSessionStatus || function() {};
|
||||
var fetchLatestResourceSnapshot = deps.fetchLatestResourceSnapshot || function() { return Promise.resolve(null); };
|
||||
var fetchLatestSessionAggregate = deps.fetchLatestSessionAggregate || function() { return Promise.resolve(null); };
|
||||
var conflictDetectionKeyFromBody = deps.conflictDetectionKeyFromBody || function() { return ''; };
|
||||
var textToTiptapDocument = deps.textToTiptapDocument || function(s) { return {type: 'doc', content: [{type: 'paragraph', content: [{type: 'text', text: s}]}]}; };
|
||||
var persistSession = deps.persistSession || function() { return Promise.resolve(); };
|
||||
|
||||
setSessionStatus(session, 'conflict-resolving', '正在写回合并结果...');
|
||||
var latestPromise = session.sessionKind === 'resource'
|
||||
? fetchLatestResourceSnapshot(session)
|
||||
: fetchLatestSessionAggregate(session);
|
||||
return latestPromise.then(function(latest) {
|
||||
var nextKey = session.sessionKind === 'resource'
|
||||
? String((latest && (latest.fileVersion || latest.conflictDetectionKey) || '')).trim()
|
||||
: conflictDetectionKeyFromBody((latest && latest.body) || {});
|
||||
if (nextKey) {
|
||||
session.conflictDetectionKey = nextKey;
|
||||
session.lastExternalConflictDetectionKey = nextKey;
|
||||
}
|
||||
session.currentTiptapDocument = textToTiptapDocument(String(mergedText || ''));
|
||||
session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
|
||||
session.hasExternalConflict = false;
|
||||
session.externalChangePending = false;
|
||||
session.saving = false;
|
||||
session.dirty = true;
|
||||
if (panel && typeof panel.remove === 'function') panel.remove();
|
||||
return persistSession(session);
|
||||
});
|
||||
}
|
||||
|
||||
window.__mnoteDocumentConflictPanelRuntime = {
|
||||
clearSessionConflictSurface: clearSessionConflictSurface,
|
||||
createSessionConflictPanel: createSessionConflictPanel,
|
||||
mountSessionConflictPanel: mountSessionConflictPanel,
|
||||
populateSessionConflictDiffPanel: populateSessionConflictDiffPanel,
|
||||
runSessionConflictAction: runSessionConflictAction
|
||||
runSessionConflictAction: runSessionConflictAction,
|
||||
acceptDiskVersion: acceptDiskVersion,
|
||||
keepCurrentEditorVersion: keepCurrentEditorVersion,
|
||||
writeMergedConflictResult: writeMergedConflictResult
|
||||
};
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// MNote filetree DND 运行时外置模块。
|
||||
// 承接 filetree dragstart/dragover/drop/dragend 的业务逻辑,
|
||||
// 不处理 page tree DND(留在 layout.rs)。
|
||||
// inline script 保留完整 fallback。
|
||||
|
||||
// ─── 常量 ───────────────────────────────────────────────
|
||||
|
||||
var FILETREE_DRAG_MIME = 'application/x-mnote-file-tree';
|
||||
|
||||
// ─── 状态 ───────────────────────────────────────────────
|
||||
|
||||
var draggingFileTreeRowIds = [];
|
||||
var activeFileTreeDropRow = null;
|
||||
|
||||
// ─── 纯 helper ─────────────────────────────────────────
|
||||
|
||||
function filetreeDragPayload(rowIds) {
|
||||
return JSON.stringify({
|
||||
type: 'mnote-file-tree-dnd',
|
||||
version: 1,
|
||||
rowIds: rowIds || draggingFileTreeRowIds,
|
||||
});
|
||||
}
|
||||
|
||||
function filetreeDropDetail(targetRow, fileTree, deps) {
|
||||
deps = deps || {};
|
||||
var resolveWorkspaceId = deps.resolveWorkspaceId || function() { return ''; };
|
||||
var fileTreeRowLocalUploadTargetRelativePath = deps.fileTreeRowLocalUploadTargetRelativePath || function() { return ''; };
|
||||
return {
|
||||
workspaceId: resolveWorkspaceId(targetRow || fileTree),
|
||||
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
|
||||
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
|
||||
documentId: targetRow
|
||||
? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id')
|
||||
: null,
|
||||
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null,
|
||||
targetRelativePath: targetRow ? fileTreeRowLocalUploadTargetRelativePath(targetRow) : '',
|
||||
uploadIntent: 'filetree.folder.drop',
|
||||
};
|
||||
}
|
||||
|
||||
function filetreeHasFiles(dataTransfer) {
|
||||
return dataTransfer && Array.prototype.indexOf.call(dataTransfer.types || [], 'Files') >= 0;
|
||||
}
|
||||
|
||||
function filetreeHasInternalDrag(dataTransfer) {
|
||||
return dataTransfer && Array.prototype.indexOf.call(dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
|
||||
}
|
||||
|
||||
function parseFileTreeDragPayload(raw) {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed.rowIds)) return parsed.rowIds;
|
||||
} catch (_) {}
|
||||
return [];
|
||||
}
|
||||
|
||||
function clearFileTreeDropFeedback() {
|
||||
if (activeFileTreeDropRow instanceof HTMLElement) {
|
||||
activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
||||
}
|
||||
activeFileTreeDropRow = null;
|
||||
}
|
||||
|
||||
function resetFileTreeDragState() {
|
||||
draggingFileTreeRowIds = [];
|
||||
clearFileTreeDropFeedback();
|
||||
}
|
||||
|
||||
// ─── 文件树 DND 开始 ───────────────────────────────────
|
||||
|
||||
function startFileTreeDrag(fileRow, deps) {
|
||||
deps = deps || {};
|
||||
var selectedSidebarFileTreeRowIdsForDrag = deps.selectedSidebarFileTreeRowIdsForDrag || function(r) {
|
||||
return [r.getAttribute('data-row-id') || ''].filter(Boolean);
|
||||
};
|
||||
draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
|
||||
return {
|
||||
payload: filetreeDragPayload(draggingFileTreeRowIds),
|
||||
effectAllowed: 'copyMove',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 文件树 DND 悬浮 ───────────────────────────────────
|
||||
|
||||
function handleFileTreeDragOver(event, deps) {
|
||||
deps = deps || {};
|
||||
var closestAction = deps.closestAction || function() { return null; };
|
||||
var fileTree = document.getElementById('sidebar-file-tree-root');
|
||||
if (!fileTree || !fileTree.contains(event.target)) return false;
|
||||
|
||||
var hasFiles = filetreeHasFiles(event.dataTransfer);
|
||||
var hasInternal = filetreeHasInternalDrag(event.dataTransfer);
|
||||
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return false;
|
||||
|
||||
event.preventDefault();
|
||||
clearFileTreeDropFeedback();
|
||||
activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
|
||||
if (activeFileTreeDropRow instanceof HTMLElement) {
|
||||
activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
|
||||
}
|
||||
if (event.dataTransfer) {
|
||||
var copyModifier = event.altKey || event.ctrlKey || event.metaKey;
|
||||
event.dataTransfer.dropEffect = hasFiles || copyModifier ? 'copy' : 'move';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── 文件树 DND 释放 ───────────────────────────────────
|
||||
|
||||
function handleFileTreeDrop(event, deps) {
|
||||
deps = deps || {};
|
||||
var closestAction = deps.closestAction || function() { return null; };
|
||||
var resolveWorkspaceId = deps.resolveWorkspaceId || function() { return ''; };
|
||||
var fileTreeRowLocalUploadTargetRelativePath = deps.fileTreeRowLocalUploadTargetRelativePath
|
||||
|| function() { return ''; };
|
||||
var ensureFileTreeWritableTarget = deps.ensureFileTreeWritableTarget || function() { return Promise.resolve(true); };
|
||||
var dispatchSidebarEvent = deps.dispatchSidebarEvent || function() {};
|
||||
var fileTree = document.getElementById('sidebar-file-tree-root');
|
||||
if (!fileTree || !fileTree.contains(event.target)) return;
|
||||
|
||||
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
||||
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
|
||||
var raw = event.dataTransfer
|
||||
? event.dataTransfer.getData(FILETREE_DRAG_MIME)
|
||||
|| event.dataTransfer.getData('application/x-mnote-file-tree')
|
||||
|| ''
|
||||
: '';
|
||||
var rowIds = draggingFileTreeRowIds.slice();
|
||||
if (raw) {
|
||||
rowIds = parseFileTreeDragPayload(raw);
|
||||
}
|
||||
if (!files.length && !rowIds.length) return;
|
||||
|
||||
event.preventDefault();
|
||||
var detail = filetreeDropDetail(targetRow, fileTree, {
|
||||
resolveWorkspaceId: resolveWorkspaceId,
|
||||
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
|
||||
});
|
||||
clearFileTreeDropFeedback();
|
||||
if (files.length) {
|
||||
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
|
||||
} else {
|
||||
var copyModifier = event.altKey === true || event.ctrlKey === true || event.metaKey === true;
|
||||
void Promise.resolve(ensureFileTreeWritableTarget('drop', targetRow, rowIds, copyModifier))
|
||||
.then(function(writable) {
|
||||
if (!writable) return;
|
||||
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, {
|
||||
rowIds: rowIds,
|
||||
copy: copyModifier,
|
||||
}));
|
||||
});
|
||||
}
|
||||
draggingFileTreeRowIds = [];
|
||||
}
|
||||
|
||||
// ─── 导出 ───────────────────────────────────────────────
|
||||
|
||||
window.__mnoteFileTreeDndRuntime = {
|
||||
FILETREE_DRAG_MIME: FILETREE_DRAG_MIME,
|
||||
draggingFileTreeRowIds: draggingFileTreeRowIds,
|
||||
activeFileTreeDropRow: activeFileTreeDropRow,
|
||||
filetreeDragPayload: filetreeDragPayload,
|
||||
filetreeDropDetail: filetreeDropDetail,
|
||||
filetreeHasFiles: filetreeHasFiles,
|
||||
filetreeHasInternalDrag: filetreeHasInternalDrag,
|
||||
parseFileTreeDragPayload: parseFileTreeDragPayload,
|
||||
clearFileTreeDropFeedback: clearFileTreeDropFeedback,
|
||||
resetFileTreeDragState: resetFileTreeDragState,
|
||||
startFileTreeDrag: startFileTreeDrag,
|
||||
handleFileTreeDragOver: handleFileTreeDragOver,
|
||||
handleFileTreeDrop: handleFileTreeDrop,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
// MNote filetree keyboard 运行时外置模块。
|
||||
// 承接 filetree keydown 处理和 clipboard 状态管理。
|
||||
// inline script 保留完整 fallback。
|
||||
|
||||
// ─── Clipboard 状态 ─────────────────────────────────────
|
||||
|
||||
var sidebarFileTreeClipboard = null;
|
||||
|
||||
function getSidebarFileTreeClipboard() {
|
||||
return sidebarFileTreeClipboard;
|
||||
}
|
||||
|
||||
function setSidebarFileTreeClipboard(action, rowIds) {
|
||||
sidebarFileTreeClipboard = { action: action, rowIds: rowIds };
|
||||
document.documentElement.setAttribute('data-mnote-filetree-clipboard-action', action);
|
||||
return sidebarFileTreeClipboard;
|
||||
}
|
||||
|
||||
function clearSidebarFileTreeClipboard() {
|
||||
sidebarFileTreeClipboard = null;
|
||||
document.documentElement.removeAttribute('data-mnote-filetree-clipboard-action');
|
||||
}
|
||||
|
||||
// ─── Keyboard handler ───────────────────────────────────
|
||||
|
||||
function handleFileTreeKeyDown(event, deps) {
|
||||
deps = deps || {};
|
||||
var closestAction = deps.closestAction || function() { return null; };
|
||||
var beginFileTreeInlineRename = deps.beginFileTreeInlineRename || function() {};
|
||||
var selectedSidebarFileTreeRows = deps.selectedSidebarFileTreeRows || function() { return []; };
|
||||
var buildSidebarFileTreeContext = deps.buildSidebarFileTreeContext || function() { return {}; };
|
||||
var evaluateSidebarFileTreeWhen = deps.evaluateSidebarFileTreeWhen || function() { return false; };
|
||||
var deleteSelectedSidebarFileTreeRows = deps.deleteSelectedSidebarFileTreeRows || function() { return Promise.resolve(); };
|
||||
|
||||
var keyTarget = event.target;
|
||||
var fileTreeRootForKey = document.getElementById('sidebar-file-tree-root');
|
||||
var fileTreeRowForKey = closestAction(keyTarget, '.tree-row[data-shell-mode="filetree"]');
|
||||
if (!fileTreeRowForKey && fileTreeRootForKey && fileTreeRootForKey.contains(document.activeElement)) {
|
||||
fileTreeRowForKey = closestAction(document.activeElement, '.tree-row[data-shell-mode="filetree"]');
|
||||
}
|
||||
if (!fileTreeRowForKey) return false;
|
||||
|
||||
if (event.key === 'F2') {
|
||||
event.preventDefault();
|
||||
beginFileTreeInlineRename(fileTreeRowForKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key) {
|
||||
var shortcutKey = event.key.toLowerCase();
|
||||
if (shortcutKey === 'c' || shortcutKey === 'x') {
|
||||
var selectedRows = selectedSidebarFileTreeRows();
|
||||
var selectedRowIds = selectedRows.map(function(row) {
|
||||
return row.getAttribute('data-row-id') || '';
|
||||
}).filter(Boolean);
|
||||
if (selectedRowIds.length > 0) {
|
||||
event.preventDefault();
|
||||
setSidebarFileTreeClipboard(shortcutKey === 'x' ? 'cut' : 'copy', selectedRowIds);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (shortcutKey === 'v') {
|
||||
event.preventDefault();
|
||||
if (typeof deps.pasteSidebarFileTreeClipboard === 'function') {
|
||||
void deps.pasteSidebarFileTreeClipboard(fileTreeRowForKey);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
var delCtx = buildSidebarFileTreeContext('filetree');
|
||||
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')) {
|
||||
return true;
|
||||
}
|
||||
void deleteSelectedSidebarFileTreeRows(fileTreeRowForKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── 导出 ───────────────────────────────────────────────
|
||||
|
||||
window.__mnoteFileTreeKeyboardRuntime = {
|
||||
sidebarFileTreeClipboard: sidebarFileTreeClipboard,
|
||||
getSidebarFileTreeClipboard: getSidebarFileTreeClipboard,
|
||||
setSidebarFileTreeClipboard: setSidebarFileTreeClipboard,
|
||||
clearSidebarFileTreeClipboard: clearSidebarFileTreeClipboard,
|
||||
handleFileTreeKeyDown: handleFileTreeKeyDown,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user