CodeMirror自定义点击搜索事件显示搜索框进行搜索

1,768 阅读4分钟

背景

vue项目使用CodeMirror组件,要求增加一个类似浏览器的Ctrl+F的查询功能。为什么不直接使用浏览器的Ctrl+F功能?因为当CodeMirror里面的文本内容超出显示高度有滚动条时,超出滚动的内容是没有渲染在页面的,所以Ctrl+F是搜索不到的。

CodeMirror有自带的搜索功能,但是太简陋了,无法满足需求。

image.png 网上某位大神编写了一个替换搜索插件,我在此基础上又进行了修改。最终效果如下图:

image.png

代码

安装搜索插件 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')

参考文章

www.thinbug.com/q/26230365

blog.csdn.net/fan_jiong/a…