批量选择

This commit is contained in:
2026-04-16 15:44:35 +08:00
parent 23b8a1d031
commit 5745a36393

View File

@@ -1345,6 +1345,11 @@ export default {
hasChange: false,
hasInit: false,
selectedIds: [],
isMouseSelecting: false,
_selectionSyncToCheckboxesTimer: null,
_onDocumentSelectionChange: null,
_onDocumentMouseUp: null,
_onManuscriptMouseDown: null,
displayList: [],
currentTypeText: '',
@@ -1505,51 +1510,14 @@ export default {
});
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
document.addEventListener('selectionchange', () => {
if(this.isPreview)return;
const selection = window.getSelection();
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
// 依然保留 trim() 后的文本判断,用来决定是否显示气泡
const plainText = selection.toString().trim();
if (plainText !== '' && selection.rangeCount > 0) {
// --- 1. 获取包含标签的 HTML 内容 ---
const fragment = range.cloneContents();
const tempDiv = document.createElement('div');
tempDiv.appendChild(fragment);
// 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 <myfigure>
const htmlLabel = tempDiv.innerHTML;
const allPMainElements = this.getInvolvedPMain(range);
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
if (allIds.length > 0) {
this.updateBubblePosition(range);
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
this.currentSelection = {
// 将 label 设置为包含标签的 HTML 字符串
label: htmlLabel,
mainId: allIds[0],
index: this.wordList.indexOf(rootItem),
content: rootItem ? rootItem.content : ''
};
this.currentId = allIds[0];
this.currentData = rootItem;
}
} else {
this.currentTag = '';
this.currentTagData = {};
this.currentSelection = {
label: '',
mainId: '',
index: 0,
content: {}
};
}
});
this._onDocumentSelectionChange = this.handleDocumentSelectionChange.bind(this);
this._onDocumentMouseUp = this.handleDocumentMouseUp.bind(this);
this._onManuscriptMouseDown = this.handleManuscriptMouseDown.bind(this);
document.addEventListener('selectionchange', this._onDocumentSelectionChange);
document.addEventListener('mouseup', this._onDocumentMouseUp);
if (this.$refs.scroll) {
this.$refs.scroll.addEventListener('mousedown', this._onManuscriptMouseDown);
}
},
activated() {
// 主动触发 MathJax 渲染
@@ -1580,6 +1548,16 @@ export default {
if (this.resizeObs) this.resizeObs.disconnect();
if (this.mutObs) this.mutObs.disconnect();
if (this._syncRefOrderTimer) clearTimeout(this._syncRefOrderTimer);
if (this._selectionSyncToCheckboxesTimer) clearTimeout(this._selectionSyncToCheckboxesTimer);
if (this._onDocumentSelectionChange) {
document.removeEventListener('selectionchange', this._onDocumentSelectionChange);
}
if (this._onDocumentMouseUp) {
document.removeEventListener('mouseup', this._onDocumentMouseUp);
}
if (this._onManuscriptMouseDown && this.$refs.scroll) {
this.$refs.scroll.removeEventListener('mousedown', this._onManuscriptMouseDown);
}
},
destroy() {
this.destroyTinymce();
@@ -1702,6 +1680,115 @@ renderCiteLabels(html) {
return rangePs;
},
handleManuscriptMouseDown(event) {
if (this.isPreview) return;
const root = this.$refs && this.$refs.scroll;
if (!root) return;
if (root.contains(event.target)) {
this.isMouseSelecting = true;
}
},
handleDocumentMouseUp() {
this.isMouseSelecting = false;
},
handleDocumentSelectionChange() {
if (this.isPreview) return;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const plainText = selection.toString().trim();
if (plainText !== '' && selection.rangeCount > 0) {
const fragment = range.cloneContents();
const tempDiv = document.createElement('div');
tempDiv.appendChild(fragment);
const htmlLabel = tempDiv.innerHTML;
const allPMainElements = this.getInvolvedPMain(range);
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
if (allIds.length > 0) {
this.updateBubblePosition(range);
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
this.currentSelection = {
label: htmlLabel,
mainId: allIds[0],
index: this.wordList.indexOf(rootItem),
content: rootItem ? rootItem.content : ''
};
this.currentId = allIds[0];
this.currentData = rootItem;
}
} else {
this.currentTag = '';
this.currentTagData = {};
this.currentSelection = {
label: '',
mainId: '',
index: 0,
content: {}
};
}
this.scheduleSyncSelectedIdsFromRange();
},
scheduleSyncSelectedIdsFromRange() {
if (this._selectionSyncToCheckboxesTimer) {
clearTimeout(this._selectionSyncToCheckboxesTimer);
}
this._selectionSyncToCheckboxesTimer = setTimeout(() => {
this.syncSelectedIdsFromRangeInternal();
}, 80);
},
syncSelectedIdsFromRangeInternal() {
if (this.isPreview || this.isInternalAction) return;
const scrollRoot = this.$refs && this.$refs.scroll;
if (!scrollRoot) return;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
const range = selection.getRangeAt(0);
const common = range.commonAncestorContainer;
const commonElement = common && common.nodeType === 1 ? common : common && common.parentElement;
if (!commonElement || !scrollRoot.contains(commonElement)) return;
const nodes = Array.from(scrollRoot.querySelectorAll('.drop-target[main-id]'));
const touchedMainIds = [];
nodes.forEach((node) => {
const id = node.getAttribute('main-id');
if (!id || id === 'References') return;
try {
if (range.intersectsNode(node)) {
touchedMainIds.push(id);
}
} catch (e) {}
});
if (!touchedMainIds.length) return;
const indexMap = {};
this.wordList.forEach((item, idx) => {
if (item && item.am_id != null) {
indexMap[String(item.am_id)] = idx;
}
});
const indices = [...new Set(touchedMainIds.map((id) => indexMap[String(id)]).filter((v) => Number.isInteger(v)))];
if (!indices.length) return;
const lo = Math.min(...indices);
const hi = Math.max(...indices);
const rangeIds = this.wordList
.slice(lo, hi + 1)
.map((item) => (item && item.am_id != null ? item.am_id : null))
.filter((id) => id != null);
if (!rangeIds.length) return;
const existingSet = new Set(this.selectedIds || []);
rangeIds.forEach((id) => existingSet.add(id));
const ordered = this.wordList
.map((item) => (item && item.am_id != null ? item.am_id : null))
.filter((id) => id != null && existingSet.has(id));
this.selectedIds = ordered;
this.$forceUpdate();
},
handleUnbindLink(type) {
const rootItem = this.wordList.find((item) => item.am_id == this.currentTagData.main_id);
@@ -3837,10 +3924,18 @@ renderCiteLabels(html) {
this.currentData = {};
this.manuscriptAutociteContext = null;
// 4. 清除浏览器原生的文字选中蓝色区域
// 这样滚动时就不会有一大片蓝色的选区跟着走,视觉上更干净
// 4. 仅当“没有按住鼠标连续拖选”时,才清理浏览器原生选区
// 按住拖选并滚动时保留选区,让用户可以持续扩展选中范围
const selection = window.getSelection();
if (selection) {
const hasActiveRange = !!(selection && selection.rangeCount > 0 && !selection.isCollapsed);
const range = hasActiveRange ? selection.getRangeAt(0) : null;
const common = range ? range.commonAncestorContainer : null;
const commonElement = common && common.nodeType === 1 ? common : common && common.parentElement;
const inManuscript = !!(commonElement && this.$refs.scroll && this.$refs.scroll.contains(commonElement));
const shouldKeepSelection = this.isMouseSelecting && hasActiveRange && inManuscript;
if (shouldKeepSelection) {
this.scheduleSyncSelectedIdsFromRange();
} else if (selection) {
selection.removeAllRanges();
}
// const scrollTop = scrollDiv.scrollTop; // 获取垂直滚动距离