mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-06 06:19:52 +08:00
238 lines
7.5 KiB
JavaScript
238 lines
7.5 KiB
JavaScript
class MarkJs {
|
|
constructor(p_adapter, p_container) {
|
|
this.className = 'vx-search-match';
|
|
this.currentMatchClassName = 'vx-current-search-match';
|
|
this.adapter = p_adapter;
|
|
this.container = p_container;
|
|
this.markjs = null;
|
|
this.cache = null;
|
|
this.matchedNodes = null;
|
|
this.currentMatchedNodes = null;
|
|
|
|
this.adapter.on('basicMarkdownRendered', () => {
|
|
this.clearCache();
|
|
});
|
|
|
|
this.adapter.on('rendered', () => {
|
|
this.clearCache();
|
|
});
|
|
}
|
|
|
|
// @p_options: {
|
|
// findBackward,
|
|
// caseSensitive,
|
|
// wholeWordOnly,
|
|
// regularExpression
|
|
// }
|
|
findText(p_texts, p_options, p_currentMatchLine) {
|
|
if (!this.markjs) {
|
|
this.markjs = new Mark(this.container);
|
|
}
|
|
|
|
if (!p_texts || p_texts.length == 0) {
|
|
// Clear the cache and highlight.
|
|
this.clearCache();
|
|
return;
|
|
}
|
|
|
|
if (this.findInCache(p_texts, p_options, p_currentMatchLine)) {
|
|
return;
|
|
}
|
|
|
|
// A new find.
|
|
this.clearCache();
|
|
|
|
let callbackFunc = function(markjs, texts, options, currentMatchLine) {
|
|
let _markjs = markjs;
|
|
let _texts = texts;
|
|
let _options = options;
|
|
let _currentMatchLine = currentMatchLine;
|
|
return function() {
|
|
if (_markjs.matchedNodes === null) {
|
|
_markjs.matchedNodes = _markjs.container.getElementsByClassName(_markjs.className);
|
|
_markjs.currentMatchedNodes = _markjs.container.getElementsByClassName(_markjs.currentMatchClassName);
|
|
}
|
|
|
|
// Update cache.
|
|
_markjs.cache = {
|
|
texts: _texts,
|
|
options: _options,
|
|
currentIdx: -1
|
|
}
|
|
|
|
_markjs.updateCurrentMatch(_texts, !_options.findBackward, _currentMatchLine);
|
|
};
|
|
}
|
|
|
|
if (p_options.regularExpression) {
|
|
this.findByOneRegExp({
|
|
'texts': p_texts,
|
|
'options': p_options,
|
|
'textIdx': 0,
|
|
'lastCallback': callbackFunc(this, p_texts, p_options, p_currentMatchLine)
|
|
});
|
|
} else {
|
|
let opt = this.createMarkjsOptions(p_options);
|
|
opt.done = callbackFunc(this, p_texts, p_options, p_currentMatchLine);
|
|
this.markjs.mark(p_texts, opt);
|
|
}
|
|
}
|
|
|
|
createMarkjsOptions(p_options) {
|
|
let opt = {
|
|
'element': 'span',
|
|
'className': this.className,
|
|
'caseSensitive': p_options.caseSensitive,
|
|
'accuracy': p_options.wholeWordOnly ? 'exactly' : 'partially',
|
|
// Ignore SVG, or SVG will be corrupted.
|
|
'exclude': ['svg *'],
|
|
'separateWordSearch': false,
|
|
'acrossElements': true
|
|
}
|
|
return opt;
|
|
}
|
|
|
|
// @p_paras: {
|
|
// texts,
|
|
// options,
|
|
// textIdx,
|
|
// lastCallback
|
|
// }
|
|
findByOneRegExp(p_paras) {
|
|
console.log('findByOneRegExp', p_paras.texts.length, p_paras.textIdx);
|
|
|
|
if (p_paras.textIdx >= p_paras.texts.length) {
|
|
return;
|
|
}
|
|
|
|
let opt = this.createMarkjsOptions(p_paras.options);
|
|
if (p_paras.textIdx == p_paras.texts.length - 1) {
|
|
opt.done = p_paras.lastCallback;
|
|
} else {
|
|
let callbackFunc = function(markjs, paras) {
|
|
let _markjs = markjs;
|
|
let _paras = paras;
|
|
return function() {
|
|
_paras.textIdx += 1;
|
|
_markjs.findByOneRegExp(_paras);
|
|
};
|
|
};
|
|
opt.done = callbackFunc(this, p_paras);
|
|
}
|
|
|
|
// TODO: may need transformation from QRegularExpression to RegExp.
|
|
this.markjs.markRegExp(new RegExp(p_paras.texts[p_paras.textIdx]), opt);
|
|
}
|
|
|
|
clearCache() {
|
|
if (!this.markjs) {
|
|
return;
|
|
}
|
|
|
|
this.cache = null;
|
|
this.markjs.unmark();
|
|
}
|
|
|
|
findInCache(p_texts, p_options, p_currentMatchLine) {
|
|
if (!this.cache) {
|
|
return false;
|
|
}
|
|
|
|
if (p_texts.length != this.cache.texts.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < p_texts.length; ++i) {
|
|
if (!(p_texts[i] === this.cache.texts[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (this.cache.options.caseSensitive == p_options.caseSensitive
|
|
&& this.cache.options.wholeWordOnly == p_options.wholeWordOnly
|
|
&& this.cache.options.regularExpression == p_options.regularExpression) {
|
|
// Matched. Move current match forward or backward.
|
|
this.updateCurrentMatch(p_texts, !p_options.findBackward, p_currentMatchLine);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
updateCurrentMatch(p_texts, p_forward, p_currentMatchLine) {
|
|
let matches = this.matchedNodes.length;
|
|
if (matches == 0) {
|
|
this.adapter.showFindResult(p_texts, 0, 0);
|
|
return;
|
|
}
|
|
|
|
if (this.currentMatchedNodes.length > 0) {
|
|
console.assert(this.currentMatchedNodes.length == 1);
|
|
if (this.cache.currentIdx >= matches
|
|
|| this.cache.currentIdx < 0
|
|
|| this.matchedNodes[this.cache.currentIdx] != this.currentMatchedNodes[0]) {
|
|
// Need to update current index.
|
|
// The mismatch may comes from the rendering of graphs which may change the matches.
|
|
for (let i = 0; i < matches; ++i) {
|
|
if (this.matchedNodes[i] === this.currentMatchedNodes[0]) {
|
|
this.cache.currentIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.matchedNodes[this.cache.currentIdx].classList.remove(this.currentMatchClassName);
|
|
} else {
|
|
this.cache.currentIdx = -1;
|
|
}
|
|
|
|
if (p_currentMatchLine > -1) {
|
|
this.cache.currentIdx = this.binarySearchCurrentIndexForLineNumber(p_currentMatchLine);
|
|
} else if (p_forward) {
|
|
this.cache.currentIdx += 1;
|
|
if (this.cache.currentIdx >= matches) {
|
|
this.cache.currentIdx = 0;
|
|
}
|
|
} else {
|
|
this.cache.currentIdx -= 1;
|
|
if (this.cache.currentIdx < 0) {
|
|
this.cache.currentIdx = matches - 1;
|
|
}
|
|
}
|
|
|
|
let node = this.matchedNodes[this.cache.currentIdx];
|
|
node.classList.add(this.currentMatchClassName);
|
|
if (!Utils.isVisible(node)) {
|
|
node.scrollIntoView();
|
|
}
|
|
this.adapter.showFindResult(p_texts, matches, this.cache.currentIdx);
|
|
}
|
|
|
|
binarySearchCurrentIndexForLineNumber(p_lineNumber) {
|
|
let viewY = this.adapter.nodeLineMapper.getViewYOfLine(p_lineNumber);
|
|
if (viewY === null) {
|
|
return 0;
|
|
}
|
|
|
|
let left = 0;
|
|
let right = this.matchedNodes.length - 1;
|
|
let lastIdx = -1;
|
|
while (left <= right) {
|
|
let mid = Math.floor((left + right) / 2);
|
|
let y = this.matchedNodes[mid].getBoundingClientRect().top;
|
|
if (y >= viewY) {
|
|
lastIdx = mid;
|
|
right = mid - 1;
|
|
} else {
|
|
left = mid + 1;
|
|
}
|
|
}
|
|
|
|
if (lastIdx != -1) {
|
|
return lastIdx;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|