159 lines
6.5 KiB
JavaScript
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);
|
|
};
|
|
})();
|