Files
mnote/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js
T

352 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MNote document conflict panel 运行时外置模块。
// 第一刀:纯 DOM 清理 helper clearSessionConflictSurface。
// 本文件以 type="module" 加载,执行时机可能晚于 inline script。
// web_shell.rs 的 inline clearSessionConflictSurface 会优先委托到
// window.__mnoteDocumentConflictPanelRuntime
// 模块尚未加载或加载失败时,inline script 保留原地实现兜底。
/**
* 清除指定 session 所有视图中冲突面板 DOM。
* 只做 DOM 清理,不处理冲突 merge/accept/keep 业务逻辑。
* @param {Object} session - document session 对象
* @param {Object} deps - 依赖注入,从 inline 上下文传入
* @param {Function} deps.sessionViews - session.views 迭代器
*/
function clearSessionConflictSurface(session, deps) {
var views = deps && typeof deps.sessionViews === 'function'
? deps.sessionViews(session)
: [];
views.forEach(function(view) {
var host = conflictPanelHost(view && view.runtimeDescriptor && view.runtimeDescriptor.root);
if (!(host instanceof HTMLElement)) return;
host.querySelectorAll('[data-testid="mnote-editor-conflict-panel"]').forEach(function(node) { node.remove(); });
});
}
function conflictPanelHost(root) {
if (!(root instanceof Element)) return null;
return root.closest('.document-pane') || root;
}
function mountSessionConflictPanel(root, panel) {
var host = conflictPanelHost(root);
if (!(host instanceof HTMLElement) || !(panel instanceof HTMLElement)) return false;
var header = host.querySelector('.document-shell-header');
if (header && header.parentNode) {
header.parentNode.insertBefore(panel, header.nextSibling);
} else {
host.prepend(panel);
}
return true;
}
function createSessionConflictPanel(session, message, deps) {
deps = deps || {};
var externalConflictMessage = String(deps.externalConflictMessage || '').trim() || '文件已在外部修改';
var panel = document.createElement('section');
panel.className = 'mnote-editor-conflict-panel';
panel.setAttribute('data-testid', 'mnote-editor-conflict-panel');
panel.setAttribute('role', 'status');
panel.setAttribute('aria-live', 'polite');
var heading = document.createElement('h2');
heading.textContent = '文件冲突';
var text = document.createElement('p');
text.textContent = message || externalConflictMessage;
var meta = document.createElement('div');
meta.className = 'mnote-conflict-meta';
var fileLabel = session && session.sessionKind === 'resource'
? String(session.rootUri || '') + '/' + String(session.resourcePath || session.documentId || '')
: String(session && (session.rootUri || session.documentId) || '');
var sourceLabel = typeof deps.conflictSourceLabel === 'function'
? deps.conflictSourceLabel(session)
: '';
meta.textContent = '文件:' + fileLabel + ' · 来源:' + (sourceLabel || '本地文件变更');
var actions = document.createElement('div');
actions.className = 'mnote-conflict-actions';
var acceptDisk = document.createElement('button');
acceptDisk.type = 'button';
acceptDisk.textContent = '接受磁盘版本';
acceptDisk.setAttribute('data-testid', 'mnote-conflict-accept-disk');
var keepCurrent = document.createElement('button');
keepCurrent.type = 'button';
keepCurrent.textContent = '保留当前编辑器版本';
keepCurrent.setAttribute('data-testid', 'mnote-conflict-keep-current');
var openDiff = document.createElement('button');
openDiff.type = 'button';
openDiff.textContent = '打开 diff';
openDiff.setAttribute('data-testid', 'mnote-conflict-open-diff');
actions.append(acceptDisk, keepCurrent, openDiff);
var diffPanel = document.createElement('div');
diffPanel.className = 'mnote-conflict-diff-panel';
diffPanel.setAttribute('data-testid', 'mnote-conflict-diff-panel');
diffPanel.hidden = true;
panel.append(heading, text, meta, actions, diffPanel);
acceptDisk.addEventListener('click', function() {
if (typeof deps.onAcceptDisk === 'function') deps.onAcceptDisk(session, panel);
});
keepCurrent.addEventListener('click', function() {
if (typeof deps.onKeepCurrent === 'function') deps.onKeepCurrent(session, panel);
});
openDiff.addEventListener('click', function() {
if (typeof deps.onOpenDiff === 'function') deps.onOpenDiff(session, panel);
});
return panel;
}
/**
* 在已有 diffPanel 中填充 current/disk/merge DOM 并绑定回调。
* 不处理 fetch、persist、status 等业务逻辑。
* @param {HTMLElement} panel - 已清空的 diffPanel 元素
* @param {Object} context
* @param {string} context.currentText - 当前编辑器的文本(含占位文案)
* @param {string} context.diskText - 磁盘版本的文本(含占位文案)
* @param {string} [context.currentMergeText] - 写入合并区域的当前编辑器原始文本
* @param {string} [context.diskMergeText] - 写入合并区域的磁盘版本原始文本
* @param {string} [context.mergeDefaultText] - 合并区域默认文本
* @param {Object} deps
* @param {Function} [deps.onUseCurrent] - 可选:返回当前版本写入合并区域的文本
* @param {Function} [deps.onUseDisk] - 可选:返回磁盘版本写入合并区域的文本
* @param {Function} deps.onSaveMerge - 保存合并结果回调,接收合并文本字符串
*/
function populateSessionConflictDiffPanel(panel, context, deps) {
context = context || {};
deps = deps || {};
var currentText = context.currentText || '';
var diskText = context.diskText || '';
var currentMergeText = Object.prototype.hasOwnProperty.call(context, 'currentMergeText')
? String(context.currentMergeText || '')
: currentText;
var diskMergeText = Object.prototype.hasOwnProperty.call(context, 'diskMergeText')
? String(context.diskMergeText || '')
: diskText;
var mergeDefaultText = context.mergeDefaultText || currentText || diskText || '';
var current = document.createElement('pre');
current.setAttribute('data-testid', 'mnote-conflict-current-text');
current.textContent = currentText;
var disk = document.createElement('pre');
disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
disk.textContent = diskText;
var currentTitle = document.createElement('h3');
currentTitle.textContent = '当前编辑器版本';
var diskTitle = document.createElement('h3');
diskTitle.textContent = '磁盘版本';
var currentBox = document.createElement('section');
currentBox.append(currentTitle, current);
var diskBox = document.createElement('section');
diskBox.append(diskTitle, disk);
var mergeTitle = document.createElement('h3');
mergeTitle.textContent = '合并结果';
var mergeText = document.createElement('textarea');
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
mergeText.value = mergeDefaultText;
var mergeActions = document.createElement('div');
mergeActions.className = 'mnote-conflict-actions';
var useCurrent = document.createElement('button');
useCurrent.type = 'button';
useCurrent.textContent = '使用当前版本';
useCurrent.setAttribute('data-testid', 'mnote-conflict-merge-use-current');
var useDisk = document.createElement('button');
useDisk.type = 'button';
useDisk.textContent = '使用磁盘版本';
useDisk.setAttribute('data-testid', 'mnote-conflict-merge-use-disk');
var saveMerge = document.createElement('button');
saveMerge.type = 'button';
saveMerge.textContent = '写回合并结果';
saveMerge.setAttribute('data-testid', 'mnote-conflict-merge-save');
mergeActions.append(useCurrent, useDisk, saveMerge);
var mergeBox = document.createElement('section');
mergeBox.className = 'mnote-conflict-merge-box';
mergeBox.append(mergeTitle, mergeText, mergeActions);
panel.append(currentBox, diskBox, mergeBox);
useCurrent.addEventListener('click', function() {
var nextText = typeof deps.onUseCurrent === 'function'
? deps.onUseCurrent(context)
: currentMergeText;
mergeText.value = typeof nextText === 'string' ? nextText : currentMergeText;
});
useDisk.addEventListener('click', function() {
var nextText = typeof deps.onUseDisk === 'function'
? deps.onUseDisk(context)
: diskMergeText;
mergeText.value = typeof nextText === 'string' ? nextText : diskMergeText;
});
saveMerge.addEventListener('click', function() {
if (typeof deps.onSaveMerge === 'function') {
deps.onSaveMerge(mergeText.value);
}
});
}
function runSessionConflictAction(action, deps) {
deps = deps || {};
if (typeof action !== 'function') return;
try {
var result = action();
if (result && typeof result.then === 'function') {
result.catch(function(error) {
if (typeof deps.onError === 'function') deps.onError(error);
});
}
} catch (error) {
if (typeof deps.onError === 'function') deps.onError(error);
}
}
/**
* 接受磁盘版本:用磁盘快照替换当前 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,
acceptDiskVersion: acceptDiskVersion,
keepCurrentEditorVersion: keepCurrentEditorVersion,
writeMergedConflictResult: writeMergedConflictResult
};