Files
MarkLite/renderer/search.js
T

159 lines
6.5 KiB
JavaScript

// MarkLite — Search & Replace Module
(function () {
'use strict';
var ML = window.MarkLite;
var searchMatches = [];
var searchIndex = -1;
var searchCaseSensitive = false;
var searchUseRegex = false;
ML.openSearch = function (showReplace) {
ML.searchBar.classList.remove('hidden');
ML.replaceRow.classList.toggle('hidden', !showReplace);
ML.searchInput.focus();
var selected = ML.editor.value.substring(ML.editor.selectionStart, ML.editor.selectionEnd);
if (selected && !selected.includes('\n')) ML.searchInput.value = selected;
doSearch();
};
ML.closeSearch = function () {
ML.searchBar.classList.add('hidden');
clearHighlights();
searchMatches = []; searchIndex = -1;
ML.searchCount.textContent = '';
ML.editor.focus();
};
ML.toggleSearchOption = function (type) {
if (type === 'case') { searchCaseSensitive = !searchCaseSensitive; ML.btnCase.classList.toggle('active', searchCaseSensitive); }
else { searchUseRegex = !searchUseRegex; ML.btnRegex.classList.toggle('active', searchUseRegex); }
doSearch();
};
function doSearch() {
clearHighlights();
var text = ML.searchInput.value;
if (!text) { searchMatches = []; searchIndex = -1; ML.searchCount.textContent = ''; return; }
searchMatches = findMatches(text);
if (searchMatches.length > 0) {
var cursor = ML.editor.selectionStart;
searchIndex = searchMatches.findIndex(function (m) { return m.start >= cursor; });
if (searchIndex === -1) searchIndex = 0;
scrollToMatch(searchIndex);
} else { searchIndex = -1; }
ML.searchCount.textContent = searchMatches.length > 0 ? (searchIndex + 1) + '/' + searchMatches.length : '无结果';
highlightMatches();
}
function findMatches(text) {
var content = ML.editor.value;
var matches = [];
if (searchUseRegex) {
try {
var flags = searchCaseSensitive ? 'g' : 'gi';
var regex = new RegExp(text, flags);
var m;
while ((m = regex.exec(content)) !== null) {
matches.push({ start: m.index, end: m.index + m[0].length });
if (m[0].length === 0) regex.lastIndex++;
}
} catch (e) { /* invalid regex */ }
} else {
var haystack = searchCaseSensitive ? content : content.toLowerCase();
var needle = searchCaseSensitive ? text : text.toLowerCase();
var idx = 0;
while ((idx = haystack.indexOf(needle, idx)) !== -1) {
matches.push({ start: idx, end: idx + needle.length });
idx += needle.length;
}
}
return matches;
}
function highlightMatches() {
clearHighlights();
if (searchMatches.length === 0) return;
var content = ML.editor.value;
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
var editorStyle = getComputedStyle(ML.editor);
var paddingTop = parseFloat(editorStyle.paddingTop);
var paddingLeft = parseFloat(editorStyle.paddingLeft);
var lineStarts = [0];
for (var i = 0; i < content.length; i++) { if (content.charCodeAt(i) === 10) lineStarts.push(i + 1); }
function posToXY(pos) {
var line = 0;
for (var _i = 0; _i < lineStarts.length; _i++) { if (lineStarts[_i] > pos) break; line = _i; }
return { top: line * lineHeight + paddingTop, left: (pos - lineStarts[line]) * ML.getCharWidth() + paddingLeft };
}
var overlay = document.createElement('div');
overlay.id = 'search-highlights';
overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;z-index:1;';
var wrapper = document.getElementById('editor-wrapper');
wrapper.style.position = 'relative';
wrapper.appendChild(overlay);
for (var _i2 = 0; _i2 < searchMatches.length; _i2++) {
var m = searchMatches[_i2];
var startPos = posToXY(m.start);
var endPos = posToXY(m.end);
var isCurrent = _i2 === searchIndex;
var div = document.createElement('div');
div.className = 'search-hl' + (isCurrent ? ' current' : '');
div.style.cssText = 'position:absolute;top:' + startPos.top + 'px;left:' + startPos.left + 'px;height:' + lineHeight + 'px;width:' + Math.max(endPos.left - startPos.left, 8) + 'px;';
overlay.appendChild(div);
}
}
function clearHighlights() { var old = document.getElementById('search-highlights'); if (old) old.remove(); }
function scrollToMatch(index) {
if (index < 0 || index >= searchMatches.length) return;
searchIndex = index;
var m = searchMatches[index];
ML.editor.setSelectionRange(m.start, m.end);
var lineHeight = parseFloat(getComputedStyle(ML.editor).lineHeight) || 20.8;
var content = ML.editor.value;
var line = 0;
for (var i = 0; i < m.start; i++) { if (content.charCodeAt(i) === 10) line++; }
var matchTop = line * lineHeight;
var viewTop = ML.editor.scrollTop;
var viewBottom = viewTop + ML.editor.clientHeight;
if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) ML.editor.scrollTop = matchTop - ML.editor.clientHeight / 2;
ML.searchCount.textContent = (index + 1) + '/' + searchMatches.length;
highlightMatches();
}
ML.findNext = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex + 1) % searchMatches.length); };
ML.findPrev = function () { if (searchMatches.length === 0) return; scrollToMatch((searchIndex - 1 + searchMatches.length) % searchMatches.length); };
ML.replaceCurrent = function () {
if (searchMatches.length === 0 || searchIndex < 0) return;
var m = searchMatches[searchIndex];
ML.editor.setSelectionRange(m.start, m.end);
document.execCommand('insertText', false, ML.replaceInput.value);
doSearch();
};
ML.replaceAll = function () {
if (searchMatches.length === 0) return;
var replacement = ML.replaceInput.value;
for (var i = searchMatches.length - 1; i >= 0; i--) {
var m = searchMatches[i];
ML.editor.setSelectionRange(m.start, m.end);
document.execCommand('insertText', false, replacement);
}
doSearch();
};
ML.initSearchBar = function () {
ML.searchInput.addEventListener('input', doSearch);
ML.btnCase.addEventListener('click', function () { ML.toggleSearchOption('case'); });
ML.btnRegex.addEventListener('click', function () { ML.toggleSearchOption('regex'); });
ML.btnNext.addEventListener('click', ML.findNext);
ML.btnPrev.addEventListener('click', ML.findPrev);
ML.btnSearchClose.addEventListener('click', ML.closeSearch);
ML.btnReplace.addEventListener('click', ML.replaceCurrent);
ML.btnReplaceAll.addEventListener('click', ML.replaceAll);
};
})();