背景
vue项目使用CodeMirror组件,要求增加一个类似浏览器的Ctrl+F的查询功能。为什么不直接使用浏览器的Ctrl+F功能?因为当CodeMirror里面的文本内容超出显示高度有滚动条时,超出滚动的内容是没有渲染在页面的,所以Ctrl+F是搜索不到的。
CodeMirror有自带的搜索功能,但是太简陋了,无法满足需求。
网上某位大神编写了一个替换搜索插件,我在此基础上又进行了修改。最终效果如下图:
代码
安装搜索插件 npm install --save codemirror-revisedsearch
<template>
...
<codemirror :ref="myCm"
v-model.trim="_value">
</codemirror>
...
</template>
import { codemirror } from 'vue-codemirror'
...
// 搜索插件相关
import 'codemirror/addon/display/panel.js'
import 'codemirror/addon/search/matchesonscrollbar.js'
import 'codemirror/addon/search/matchesonscrollbar.css'
import '../../../static/js/codemirror-revisedsearch.js'
export default {
computed: {
myCm() {
return `myCm${+new Date()}`
},
codemirror() {
return this.$refs[this.myCm].codemirror
}
}
}
...
...
codemirror-revisedsearch.js
"use strict";
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Revised search plugin written by Jamie Morris
// Define search commands. Depends on advanceddialog.js
(function (mod) {
if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) == "object" && (typeof module === "undefined" ? "undefined" : _typeof(module)) == "object") // CommonJS
mod(require("codemirror"), require("codemirror-advanceddialog"));else if (typeof define == "function" && define.amd) // AMD
define(["codemirror", "codemirror-advanceddialog"], mod);else // Plain browser env
mod(CodeMirror);
})(function (CodeMirror) {
"use strict";
var replaceDialog = "\n <div class="row find">\n <label for="CodeMirror-find-field">替换:</label>\n <input id="CodeMirror-find-field" type="text" class="CodeMirror-search-field" placeholder="Find" />\n <span class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>\n <span class="CodeMirror-search-count"></span>\n </div>\n <div class="row replace">\n <label for="CodeMirror-replace-field">With:</label>\n <input id="CodeMirror-replace-field" type="text" class="CodeMirror-search-field" placeholder="Replace" />\n </div>\n <div class="buttons">\n <button type="button">Find Previous</button>\n <button type="button">Find Next</button>\n <button type="button">Replace</button>\n <button type="button">Replace All</button>\n <button type="button">Close</button>\n </div>\n ";
var findDialog = "\n <div class="row find">\n <label for="CodeMirror-find-field"></label>\n <input id="CodeMirror-find-field" type="text" class="CodeMirror-search-field" placeholder="请输入关键字查询" />\n <span class="CodeMirror-search-hint"></span>\n <span class="CodeMirror-search-count">0/0</span>\n </div>\n <div class="buttons">\n <button type="button"><i class="iconfont el-icon-arrow-up"></i></button>\n <button type="button"><i class="iconfont el-icon-arrow-down"></i></button>\n <button type="button"><i class="iconfont el-icon-close"></i></button>\n </div>\n ";
var numMatches = 0;
var searchOverlay = function searchOverlay(query, caseInsensitive) {
if (typeof query == "string") query = new RegExp(query.replace(/[-[]/{}()*+?.\^$|]/g, "\$&"), caseInsensitive ? "gi" : "g");else if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
return {
token: function token(stream) {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return "searching";
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
};
};
function SearchState() {
this.posFrom = this.posTo = this.lastQuery = this.query = null;
this.overlay = null;
}
var getSearchState = function getSearchState(cm) {
return cm.state.search || (cm.state.search = new SearchState());
};
var queryCaseInsensitive = function queryCaseInsensitive(query) {
return typeof query == "string" && query == query.toLowerCase();
};
var getSearchCursor = function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(parseQuery(query), pos, queryCaseInsensitive(query));
};
var parseString = function parseString(string) {
return string.replace(/\(.)/g, function (_, ch) {
if (ch == "n") return "\n";
if (ch == "r") return "\r";
return ch;
});
};
var parseQuery = function parseQuery(query) {
if (query.exec) {
return query;
}
var isRE = query.indexOf('/') === 0 && query.lastIndexOf('/') > 0;
if (!!isRE) {
try {
var matches = query.match(/^/(.*)/([a-z]*)$/);
query = new RegExp(matches[1], matches[2].indexOf("i") == -1 ? "" : "i");
} catch (e) {} // Not a regular expression after all, do a string search
} else {
query = parseString(query);
}
if (typeof query == "string" ? query == "" : query.test("")) query = /x^/;
return query;
};
var startSearch = function startSearch(cm, state, query) {
if (!query || query === '') return;
state.queryText = query;
state.query = parseQuery(query);
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
cm.addOverlay(state.overlay);
if (cm.showMatchesOnScrollbar) {
if (state.annotate) {
state.annotate.clear();
state.annotate = null;
}
state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
}
};
var doSearch = function doSearch(cm, query, reverse, moveToNext) {
var hiding = null;
var state = getSearchState(cm);
if (query != state.queryText) {
startSearch(cm, state, query);
state.posFrom = state.posTo = cm.getCursor();
}
if (moveToNext || moveToNext === undefined) {
findNext(cm, reverse || false);
}
updateCount(cm);
};
var clearSearch = function clearSearch(cm) {
cm.operation(function () {
var state = getSearchState(cm);
state.lastQuery = state.query;
if (!state.query) return;
state.query = state.queryText = null;
cm.removeOverlay(state.overlay);
if (state.annotate) {
state.annotate.clear();
state.annotate = null;
}
});
};
var findNext = function findNext(cm, reverse, callback) {
cm.operation(function () {
var state = getSearchState(cm);
var cursor = getSearchCursor(cm, state.query, reverse ? state.posFrom : state.posTo);
if (!cursor.find(reverse)) {
cursor = getSearchCursor(cm, state.query, reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
if (!cursor.find(reverse)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({
from: cursor.from(),
to: cursor.to()
}, 20);
state.posFrom = cursor.from();
state.posTo = cursor.to();
if (callback) callback(cursor.from(), cursor.to());
});
};
var replaceNext = function replaceNext(cm, query, text) {
var cursor = getSearchCursor(cm, query, cm.getCursor('from'));
var start = cursor.from();
var match = cursor.findNext();
if (!match) {
cursor = getSearchCursor(cm, query);
match = cursor.findNext();
if (!match || start && cursor.from().line === start.line && cursor.from().ch === start.ch) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({
from: cursor.from(),
to: cursor.to()
});
cursor.replace(typeof query === 'string' ? text : text.replace(/$(\d)/g, function (_, i) {
return match[i];
}));
};
var replaceAll = function replaceAll(cm, query, text) {
cm.operation(function () {
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
if (typeof query != "string") {
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
cursor.replace(text.replace(/$(\d)/g, function (_, i) {
return match[i];
}));
} else cursor.replace(text);
}
});
};
var closeSearchCallback = function closeSearchCallback(cm, state) {
if (state.annotate) {
state.annotate.clear();
state.annotate = null;
}
clearSearch(cm);
};
var getOnReadOnlyCallback = function getOnReadOnlyCallback(callback) {
var closeFindDialogOnReadOnly = function closeFindDialogOnReadOnly(cm, opt) {
if (opt === 'readOnly' && !!cm.getOption('readOnly')) {
callback();
cm.off('optionChange', closeFindDialogOnReadOnly);
}
};
return closeFindDialogOnReadOnly;
};
var updateCount = function updateCount(cm) {
var state = getSearchState(cm);
var value = cm.getDoc().getValue();
var globalQuery = void 0;
var queryText = state.queryText;
if (!queryText || queryText === '') {
resetCount(cm);
return;
}
while (queryText.charAt(queryText.length - 1) === '\') {
queryText = queryText.substring(0, queryText.lastIndexOf('\'));
}
if (typeof state.query === 'string') {
globalQuery = new RegExp(queryText, 'ig');
} else {
globalQuery = new RegExp(state.query.source, state.query.flags + 'g');
}
var matches = value.match(globalQuery);
var count = matches ? matches.length : 0;
var countText = '0/' + count;
var state = getSearchState(cm);
var searchMatches = state.annotate.matches
if(searchMatches) {
var fa = searchMatches.findIndex(ha => ha.from.ch === state.posFrom.ch && ha.from.line === state.posFrom.line && ha.to.ch === state.posTo.ch && ha.to.line === state.posTo.line
)
var curSearchIndex = searchMatches.length > 0 ? fa + 1 : 0
countText = curSearchIndex + '/' + count;
}
cm.getWrapperElement().parentNode.querySelector('.CodeMirror-search-count').innerHTML = countText;
};
var resetCount = function resetCount(cm) {
cm.getWrapperElement().parentNode.querySelector('.CodeMirror-search-count').innerHTML = '0/0';
};
var getFindBehaviour = function getFindBehaviour(cm, defaultText, callback) {
if (!defaultText) {
defaultText = '';
}
var behaviour = {
value: defaultText,
focus: true,
selectValueOnOpen: true,
closeOnEnter: false,
closeOnBlur: false,
callback: function callback(inputs, e) {
var query = inputs[0].value;
if (!query) return;
doSearch(cm, query, !!e.shiftKey);
},
onInput: function onInput(inputs, e) {
var query = inputs[0].value;
if (!query) {
resetCount(cm);
clearSearch(cm);
return;
};
doSearch(cm, query, !!e.shiftKey, false);
}
};
if (!!callback) {
behaviour.callback = callback;
}
return behaviour;
};
var getFindPrevBtnBehaviour = function getFindPrevBtnBehaviour(cm) {
return {
callback: function callback(inputs) {
var query = inputs[0].value;
if (!query) return;
doSearch(cm, query, true);
}
};
};
var getFindNextBtnBehaviour = function getFindNextBtnBehaviour(cm) {
return {
callback: function callback(inputs) {
var query = inputs[0].value;
if (!query) return;
doSearch(cm, query, false);
}
};
};
var closeBtnBehaviour = {
callback: null
};
CodeMirror.commands.find = function (cm) {
if (cm.getOption("readOnly")) return;
clearSearch(cm);
var state = getSearchState(cm);
var query = cm.getSelection() || getSearchState(cm).lastQuery;
var closeDialog = cm.openAdvancedDialog(findDialog, {
shrinkEditor: true,
inputBehaviours: [getFindBehaviour(cm, query)],
buttonBehaviours: [getFindPrevBtnBehaviour(cm), getFindNextBtnBehaviour(cm), closeBtnBehaviour],
onClose: function onClose() {
closeSearchCallback(cm, state);
}
});
cm.on("optionChange", getOnReadOnlyCallback(closeDialog));
startSearch(cm, state, query);
updateCount(cm);
};
CodeMirror.commands.replace = function (cm, all) {
if (cm.getOption("readOnly")) return;
clearSearch(cm);
var replaceNextCallback = function replaceNextCallback(inputs) {
var query = parseQuery(inputs[0].value);
var text = parseString(inputs[1].value);
if (!query) return;
replaceNext(cm, query, text);
doSearch(cm, query);
};
var state = getSearchState(cm);
var query = cm.getSelection() || state.lastQuery;
var closeDialog = cm.openAdvancedDialog(replaceDialog, {
shrinkEditor: true,
inputBehaviours: [getFindBehaviour(cm, query, function (inputs) {
inputs[1].focus();
inputs[1].select();
}), {
closeOnEnter: false,
closeOnBlur: false,
callback: replaceNextCallback
}],
buttonBehaviours: [getFindPrevBtnBehaviour(cm), getFindNextBtnBehaviour(cm), {
callback: replaceNextCallback
}, {
callback: function callback(inputs) {
// Replace all
var query = parseQuery(inputs[0].value);
var text = parseString(inputs[1].value);
if (!query) return;
replaceAll(cm, query, text);
}
}, closeBtnBehaviour],
onClose: function onClose() {
closeSearchCallback(cm, state);
}
});
cm.on("optionChange", getOnReadOnlyCallback(closeDialog));
startSearch(cm, state, query);
updateCount(cm);
};
console.warn('CodeMirror:', CodeMirror)
});
相关样式:
// codemirror 查找功能
.CodeMirror-advanced-dialog {
position: absolute;
z-index: 100;
right: 22px;
top: 5px;
padding: 5px;
border: 1px solid rgba(0,0,0,.1);
background-color: rgba(250,250,250,1);
box-shadow: 1px 1px 12px 0 rgba(0,0,0,.15);
display: flex;
align-items: center;
.row {
width: auto;
display: flex;
align-items: center;
.CodeMirror-search-hint{
display: none;
}
}
.buttons {
margin-left: 5px;
button {
display: inline-block;
padding: 1px 3px 0px;
cursor: pointer;
border: none;
i{
font-size: 16px;
color: #3c72e7;
}
}
}
.CodeMirror-search-field {
display: block;
resize: vertical;
padding: 5px 15px;
line-height: 1.5;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
font-size: inherit;
color: #1c202d;
background-color: #fff;
background-image: none;
border: 1px solid #dfdfdf;
height: 32px;
line-height: 32px;
-webkit-border-radius: 4px;
border-radius: 4px;
-webkit-transition: border-color .2s cubic-bezier(.645,.045,.355,1);
-o-transition: border-color .2s cubic-bezier(.645,.045,.355,1);
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
&:focus {
outline: 0;
border-color: #3c72e7;
}
&::placeholder {
color: #c0c4cc;
}
}
.CodeMirror-search-count {
display: inline-block;
padding: 0 5px;
color: #1c202d;
}
}
调用:
在组件聚焦的时候按下CTRL+F 或者 this.codemirror.execCommand('find')