markjs.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. class MarkJs {
  2. constructor(p_adapter, p_container) {
  3. this.className = 'vx-search-match';
  4. this.currentMatchClassName = 'vx-current-search-match';
  5. this.adapter = p_adapter;
  6. this.container = p_container;
  7. this.markjs = null;
  8. this.cache = null;
  9. this.matchedNodes = null;
  10. this.currentMatchedNodes = null;
  11. this.adapter.on('basicMarkdownRendered', () => {
  12. this.clearCache();
  13. });
  14. this.adapter.on('rendered', () => {
  15. this.clearCache();
  16. });
  17. }
  18. // @p_options: {
  19. // findBackward,
  20. // caseSensitive,
  21. // wholeWordOnly,
  22. // regularExpression
  23. // }
  24. findText(p_texts, p_options, p_currentMatchLine) {
  25. if (!this.markjs) {
  26. this.markjs = new Mark(this.container);
  27. }
  28. if (!p_texts || p_texts.length == 0) {
  29. // Clear the cache and highlight.
  30. this.clearCache();
  31. return;
  32. }
  33. if (this.findInCache(p_texts, p_options, p_currentMatchLine)) {
  34. return;
  35. }
  36. // A new find.
  37. this.clearCache();
  38. let callbackFunc = function(markjs, texts, options, currentMatchLine) {
  39. let _markjs = markjs;
  40. let _texts = texts;
  41. let _options = options;
  42. let _currentMatchLine = currentMatchLine;
  43. return function() {
  44. if (_markjs.matchedNodes === null) {
  45. _markjs.matchedNodes = _markjs.container.getElementsByClassName(_markjs.className);
  46. _markjs.currentMatchedNodes = _markjs.container.getElementsByClassName(_markjs.currentMatchClassName);
  47. }
  48. // Update cache.
  49. _markjs.cache = {
  50. texts: _texts,
  51. options: _options,
  52. currentIdx: -1
  53. }
  54. _markjs.updateCurrentMatch(_texts, !_options.findBackward, _currentMatchLine);
  55. };
  56. }
  57. if (p_options.regularExpression) {
  58. this.findByOneRegExp({
  59. 'texts': p_texts,
  60. 'options': p_options,
  61. 'textIdx': 0,
  62. 'lastCallback': callbackFunc(this, p_texts, p_options, p_currentMatchLine)
  63. });
  64. } else {
  65. let opt = this.createMarkjsOptions(p_options);
  66. opt.done = callbackFunc(this, p_texts, p_options, p_currentMatchLine);
  67. this.markjs.mark(p_texts, opt);
  68. }
  69. }
  70. createMarkjsOptions(p_options) {
  71. let opt = {
  72. 'element': 'span',
  73. 'className': this.className,
  74. 'caseSensitive': p_options.caseSensitive,
  75. 'accuracy': p_options.wholeWordOnly ? 'exactly' : 'partially',
  76. // Ignore SVG, or SVG will be corrupted.
  77. 'exclude': ['svg *'],
  78. 'separateWordSearch': false,
  79. 'acrossElements': true
  80. }
  81. return opt;
  82. }
  83. // @p_paras: {
  84. // texts,
  85. // options,
  86. // textIdx,
  87. // lastCallback
  88. // }
  89. findByOneRegExp(p_paras) {
  90. console.log('findByOneRegExp', p_paras.texts.length, p_paras.textIdx);
  91. if (p_paras.textIdx >= p_paras.texts.length) {
  92. return;
  93. }
  94. let opt = this.createMarkjsOptions(p_paras.options);
  95. if (p_paras.textIdx == p_paras.texts.length - 1) {
  96. opt.done = p_paras.lastCallback;
  97. } else {
  98. let callbackFunc = function(markjs, paras) {
  99. let _markjs = markjs;
  100. let _paras = paras;
  101. return function() {
  102. _paras.textIdx += 1;
  103. _markjs.findByOneRegExp(_paras);
  104. };
  105. };
  106. opt.done = callbackFunc(this, p_paras);
  107. }
  108. // TODO: may need transformation from QRegularExpression to RegExp.
  109. this.markjs.markRegExp(new RegExp(p_paras.texts[p_paras.textIdx]), opt);
  110. }
  111. clearCache() {
  112. if (!this.markjs) {
  113. return;
  114. }
  115. this.cache = null;
  116. this.markjs.unmark();
  117. }
  118. findInCache(p_texts, p_options, p_currentMatchLine) {
  119. if (!this.cache) {
  120. return false;
  121. }
  122. if (p_texts.length != this.cache.texts.length) {
  123. return false;
  124. }
  125. for (let i = 0; i < p_texts.length; ++i) {
  126. if (!(p_texts[i] === this.cache.texts[i])) {
  127. return false;
  128. }
  129. }
  130. if (this.cache.options.caseSensitive == p_options.caseSensitive
  131. && this.cache.options.wholeWordOnly == p_options.wholeWordOnly
  132. && this.cache.options.regularExpression == p_options.regularExpression) {
  133. // Matched. Move current match forward or backward.
  134. this.updateCurrentMatch(p_texts, !p_options.findBackward, p_currentMatchLine);
  135. return true;
  136. }
  137. return false;
  138. }
  139. updateCurrentMatch(p_texts, p_forward, p_currentMatchLine) {
  140. let matches = this.matchedNodes.length;
  141. if (matches == 0) {
  142. this.adapter.showFindResult(p_texts, 0, 0);
  143. return;
  144. }
  145. if (this.currentMatchedNodes.length > 0) {
  146. console.assert(this.currentMatchedNodes.length == 1);
  147. if (this.cache.currentIdx >= matches
  148. || this.cache.currentIdx < 0
  149. || this.matchedNodes[this.cache.currentIdx] != this.currentMatchedNodes[0]) {
  150. // Need to update current index.
  151. // The mismatch may comes from the rendering of graphs which may change the matches.
  152. for (let i = 0; i < matches; ++i) {
  153. if (this.matchedNodes[i] === this.currentMatchedNodes[0]) {
  154. this.cache.currentIdx = i;
  155. break;
  156. }
  157. }
  158. }
  159. this.matchedNodes[this.cache.currentIdx].classList.remove(this.currentMatchClassName);
  160. } else {
  161. this.cache.currentIdx = -1;
  162. }
  163. if (p_currentMatchLine > -1) {
  164. this.cache.currentIdx = this.binarySearchCurrentIndexForLineNumber(p_currentMatchLine);
  165. } else if (p_forward) {
  166. this.cache.currentIdx += 1;
  167. if (this.cache.currentIdx >= matches) {
  168. this.cache.currentIdx = 0;
  169. }
  170. } else {
  171. this.cache.currentIdx -= 1;
  172. if (this.cache.currentIdx < 0) {
  173. this.cache.currentIdx = matches - 1;
  174. }
  175. }
  176. let node = this.matchedNodes[this.cache.currentIdx];
  177. node.classList.add(this.currentMatchClassName);
  178. if (!Utils.isVisible(node)) {
  179. node.scrollIntoView();
  180. }
  181. this.adapter.showFindResult(p_texts, matches, this.cache.currentIdx);
  182. }
  183. binarySearchCurrentIndexForLineNumber(p_lineNumber) {
  184. let viewY = this.adapter.nodeLineMapper.getViewYOfLine(p_lineNumber);
  185. if (viewY === null) {
  186. return 0;
  187. }
  188. let left = 0;
  189. let right = this.matchedNodes.length - 1;
  190. let lastIdx = -1;
  191. while (left <= right) {
  192. let mid = Math.floor((left + right) / 2);
  193. let y = this.matchedNodes[mid].getBoundingClientRect().top;
  194. if (y >= viewY) {
  195. lastIdx = mid;
  196. right = mid - 1;
  197. } else {
  198. left = mid + 1;
  199. }
  200. }
  201. if (lastIdx != -1) {
  202. return lastIdx;
  203. } else {
  204. return 0;
  205. }
  206. }
  207. }