searchbox.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. /* global CodeMirror */
  2. /* global define */
  3. (function(mod) {
  4. 'use strict';
  5. if (typeof exports === 'object' && typeof module === 'object') // CommonJS
  6. mod(require('../../lib/codemirror'));
  7. else if (typeof define === 'function' && define.amd) // AMD
  8. define(['../../lib/codemirror'], mod);
  9. else
  10. mod(CodeMirror);
  11. })(function(CodeMirror) {
  12. 'use strict';
  13. var Search;
  14. CodeMirror.defineOption('searchbox', false, function(cm) {
  15. cm.addKeyMap({
  16. 'Ctrl-F': function() {
  17. if (!Search)
  18. Search = new SearchBox(cm);
  19. Search.show();
  20. },
  21. 'Esc': function() {
  22. if (Search && Search.isVisible()) {
  23. Search.hide();
  24. if (typeof event !== 'undefined')
  25. event.stopPropagation();
  26. }
  27. return false;
  28. },
  29. 'Cmd-F': function() {
  30. if (!Search)
  31. Search = new SearchBox(cm);
  32. Search.show();
  33. }
  34. });
  35. });
  36. function SearchBox(cm) {
  37. var self = this;
  38. init();
  39. function initElements(el) {
  40. self.searchBox = el.querySelector('.ace_search_form');
  41. self.replaceBox = el.querySelector('.ace_replace_form');
  42. self.searchOptions = el.querySelector('.ace_search_options');
  43. self.regExpOption = el.querySelector('[action=toggleRegexpMode]');
  44. self.caseSensitiveOption = el.querySelector('[action=toggleCaseSensitive]');
  45. self.wholeWordOption = el.querySelector('[action=toggleWholeWords]');
  46. self.searchInput = self.searchBox.querySelector('.ace_search_field');
  47. self.replaceInput = self.replaceBox.querySelector('.ace_search_field');
  48. }
  49. function init() {
  50. var el = self.element = addHtml();
  51. addStyle();
  52. initElements(el);
  53. bindKeys();
  54. el.addEventListener('mousedown', function(e) {
  55. setTimeout(function(){
  56. self.activeInput.focus();
  57. }, 0);
  58. e.stopPropagation();
  59. });
  60. el.addEventListener('click', function(e) {
  61. var t = e.target || e.srcElement;
  62. var action = t.getAttribute('action');
  63. if (action && self[action])
  64. self[action]();
  65. else if (self.commands[action])
  66. self.commands[action]();
  67. e.stopPropagation();
  68. });
  69. self.searchInput.addEventListener('input', function() {
  70. self.$onChange.schedule(20);
  71. });
  72. self.searchInput.addEventListener('focus', function() {
  73. self.activeInput = self.searchInput;
  74. });
  75. self.replaceInput.addEventListener('focus', function() {
  76. self.activeInput = self.replaceInput;
  77. });
  78. self.$onChange = delayedCall(function() {
  79. self.find(false, false);
  80. });
  81. }
  82. function bindKeys() {
  83. var sb = self,
  84. obj = {
  85. 'Ctrl-F|Cmd-F|Ctrl-H|Command-Alt-F': function() {
  86. var isReplace = sb.isReplace = !sb.isReplace;
  87. sb.replaceBox.style.display = isReplace ? '' : 'none';
  88. sb[isReplace ? 'replaceInput' : 'searchInput'].focus();
  89. },
  90. 'Ctrl-G|Cmd-G': function() {
  91. sb.findNext();
  92. },
  93. 'Ctrl-Shift-G|Cmd-Shift-G': function() {
  94. sb.findPrev();
  95. },
  96. 'Esc': function() {
  97. setTimeout(function() { sb.hide();});
  98. },
  99. 'Enter': function() {
  100. if (sb.activeInput === sb.replaceInput)
  101. sb.replace();
  102. sb.findNext();
  103. },
  104. 'Shift-Enter': function() {
  105. if (sb.activeInput === sb.replaceInput)
  106. sb.replace();
  107. sb.findPrev();
  108. },
  109. 'Alt-Enter': function() {
  110. if (sb.activeInput === sb.replaceInput)
  111. sb.replaceAll();
  112. sb.findAll();
  113. },
  114. 'Tab': function() {
  115. if (self.activeInput === self.replaceInput)
  116. self.searchInput.focus();
  117. else
  118. self.replaceInput.focus();
  119. }
  120. };
  121. self.element.addEventListener('keydown', function(event) {
  122. Object.keys(obj).some(function(name) {
  123. var is = key(name, event);
  124. if (is) {
  125. event.stopPropagation();
  126. event.preventDefault();
  127. obj[name](event);
  128. }
  129. return is;
  130. });
  131. });
  132. }
  133. this.commands = {
  134. toggleRegexpMode: function() {
  135. self.regExpOption.checked = !self.regExpOption.checked;
  136. self.$syncOptions();
  137. },
  138. toggleCaseSensitive: function() {
  139. self.caseSensitiveOption.checked = !self.caseSensitiveOption.checked;
  140. self.$syncOptions();
  141. },
  142. toggleWholeWords: function() {
  143. self.wholeWordOption.checked = !self.wholeWordOption.checked;
  144. self.$syncOptions();
  145. }
  146. };
  147. this.$syncOptions = function() {
  148. setCssClass(this.regExpOption, 'checked', this.regExpOption.checked);
  149. setCssClass(this.wholeWordOption, 'checked', this.wholeWordOption.checked);
  150. setCssClass(this.caseSensitiveOption, 'checked', this.caseSensitiveOption.checked);
  151. this.find(false, false);
  152. };
  153. this.find = function(skipCurrent, backwards) {
  154. var value = this.searchInput.value,
  155. options = {
  156. skipCurrent: skipCurrent,
  157. backwards: backwards,
  158. regExp: this.regExpOption.checked,
  159. caseSensitive: this.caseSensitiveOption.checked,
  160. wholeWord: this.wholeWordOption.checked
  161. };
  162. find(value, options, function(searchCursor) {
  163. var current = searchCursor.matches(false, searchCursor.from());
  164. cm.setSelection(current.from, current.to);
  165. });
  166. };
  167. function find(value, options, callback) {
  168. var done,
  169. noMatch, searchCursor, next, prev, matches, cursor,
  170. position,
  171. o = options,
  172. is = true,
  173. caseSensitive = o.caseSensitive,
  174. regExp = o.regExp,
  175. wholeWord = o.wholeWord;
  176. if (regExp || wholeWord) {
  177. if (options.wholeWord)
  178. value = '\\b' + value + '\\b';
  179. value = RegExp(value);
  180. }
  181. if (o.backwards)
  182. position = o.skipCurrent ? 'from': 'to';
  183. else
  184. position = o.skipCurrent ? 'to' : 'from';
  185. cursor = cm.getCursor(position);
  186. searchCursor = cm.getSearchCursor(value, cursor, !caseSensitive);
  187. next = searchCursor.findNext.bind(searchCursor),
  188. prev = searchCursor.findPrevious.bind(searchCursor),
  189. matches = searchCursor.matches.bind(searchCursor);
  190. if (o.backwards && !prev()) {
  191. is = next();
  192. if (is) {
  193. cm.setCursor(cm.doc.size - 1, 0);
  194. find(true, true, callback);
  195. done = true;
  196. }
  197. } else if (!o.backwards && !next()) {
  198. is = prev();
  199. if (is) {
  200. cm.setCursor(0, 0);
  201. find(true, false, callback);
  202. done = true;
  203. }
  204. }
  205. noMatch = !is && self.searchInput.value;
  206. setCssClass(self.searchBox, 'ace_nomatch', noMatch);
  207. if (!done && is)
  208. callback(searchCursor);
  209. }
  210. this.findNext = function() {
  211. this.find(true, false);
  212. };
  213. this.findPrev = function() {
  214. this.find(true, true);
  215. };
  216. this.findAll = function(){
  217. /*
  218. var range = this.editor.findAll(this.searchInput.value, {
  219. regExp: this.regExpOption.checked,
  220. caseSensitive: this.caseSensitiveOption.checked,
  221. wholeWord: this.wholeWordOption.checked
  222. });
  223. */
  224. var value = this.searchInput.value,
  225. range,
  226. noMatch = !range && this.searchInput.value;
  227. setCssClass(this.searchBox, 'ace_nomatch', noMatch);
  228. if (cm.showMatchesOnScrollbar)
  229. cm.showMatchesOnScrollbar(value);
  230. this.hide();
  231. };
  232. this.replace = function() {
  233. if (!cm.getOption('readOnly'))
  234. cm.replaceSelection(this.replaceInput.value, 'start');
  235. };
  236. this.replaceAndFindNext = function() {
  237. if (!cm.getOption('readOnly')) {
  238. this.editor.replace(this.replaceInput.value);
  239. this.findNext();
  240. }
  241. };
  242. this.replaceAll = function() {
  243. var value,
  244. cursor,
  245. from = this.searchInput.value,
  246. to = this.replaceInput.value,
  247. reg = RegExp(from, 'g');
  248. if (!cm.getOption('readOnly')) {
  249. cursor = cm.getCursor();
  250. value = cm.getValue();
  251. value = value.replace(reg, to);
  252. cm.setValue(value);
  253. cm.setCursor(cursor);
  254. }
  255. };
  256. this.hide = function() {
  257. this.element.style.display = 'none';
  258. cm.focus();
  259. };
  260. this.isVisible = function() {
  261. var is = this.element.style.display === '';
  262. return is;
  263. };
  264. this.show = function(value, isReplace) {
  265. this.element.style.display = '';
  266. this.replaceBox.style.display = isReplace ? '' : 'none';
  267. this.isReplace = isReplace;
  268. if (value)
  269. this.searchInput.value = value;
  270. this.searchInput.focus();
  271. this.searchInput.select();
  272. };
  273. this.isFocused = function() {
  274. var el = document.activeElement;
  275. return el === this.searchInput || el === this.replaceInput;
  276. };
  277. function addStyle() {
  278. var style = document.createElement('style'),
  279. css = [
  280. '.ace_search {',
  281. 'background-color: #ddd;',
  282. 'border: 1px solid #cbcbcb;',
  283. 'border-top: 0 none;',
  284. 'max-width: 325px;',
  285. 'overflow: hidden;',
  286. 'margin: 0;',
  287. 'padding: 4px;',
  288. 'padding-right: 6px;',
  289. 'padding-bottom: 0;',
  290. 'position: absolute;',
  291. 'top: 0px;',
  292. 'z-index: 99;',
  293. 'white-space: normal;',
  294. '}',
  295. '.ace_search.left {',
  296. 'border-left: 0 none;',
  297. 'border-radius: 0px 0px 5px 0px;',
  298. 'left: 0;',
  299. '}',
  300. '.ace_search.right {',
  301. 'border-radius: 0px 0px 0px 5px;',
  302. 'border-right: 0 none;',
  303. 'right: 0;',
  304. '}',
  305. '.ace_search_form, .ace_replace_form {',
  306. 'border-radius: 3px;',
  307. 'border: 1px solid #cbcbcb;',
  308. 'float: left;',
  309. 'margin-bottom: 4px;',
  310. 'overflow: hidden;',
  311. '}',
  312. '.ace_search_form.ace_nomatch {',
  313. 'outline: 1px solid red;',
  314. '}',
  315. '.ace_search_field {',
  316. 'background-color: white;',
  317. 'border-right: 1px solid #cbcbcb;',
  318. 'border: 0 none;',
  319. '-webkit-box-sizing: border-box;',
  320. '-moz-box-sizing: border-box;',
  321. 'box-sizing: border-box;',
  322. 'float: left;',
  323. 'height: 22px;',
  324. 'outline: 0;',
  325. 'padding: 0 7px;',
  326. 'width: 214px;',
  327. 'margin: 0;',
  328. '}',
  329. '.ace_searchbtn,',
  330. '.ace_replacebtn {',
  331. 'background: #fff;',
  332. 'border: 0 none;',
  333. 'border-left: 1px solid #dcdcdc;',
  334. 'cursor: pointer;',
  335. 'float: left;',
  336. 'height: 22px;',
  337. 'margin: 0;',
  338. 'padding: 0;',
  339. 'position: relative;',
  340. '}',
  341. '.ace_searchbtn:last-child,',
  342. '.ace_replacebtn:last-child {',
  343. 'border-top-right-radius: 3px;',
  344. 'border-bottom-right-radius: 3px;',
  345. '}',
  346. '.ace_searchbtn:disabled {',
  347. 'background: none;',
  348. 'cursor: default;',
  349. '}',
  350. '.ace_searchbtn {',
  351. 'background-position: 50% 50%;',
  352. 'background-repeat: no-repeat;',
  353. 'width: 27px;',
  354. '}',
  355. '.ace_searchbtn.prev {',
  356. 'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADFJREFUeNpiSU1NZUAC/6E0I0yACYskCpsJiySKIiY0SUZk40FyTEgCjGgKwTRAgAEAQJUIPCE+qfkAAAAASUVORK5CYII=); ',
  357. '}',
  358. '.ace_searchbtn.next {',
  359. 'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNpiTE1NZQCC/0DMyIAKwGJMUAYDEo3M/s+EpvM/mkKwCQxYjIeLMaELoLMBAgwAU7UJObTKsvAAAAAASUVORK5CYII=); ',
  360. '}',
  361. '.ace_searchbtn_close {',
  362. 'background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;',
  363. 'border-radius: 50%;',
  364. 'border: 0 none;',
  365. 'color: #656565;',
  366. 'cursor: pointer;',
  367. 'float: right;',
  368. 'font: 16px/16px Arial;',
  369. 'height: 14px;',
  370. 'margin: 5px 1px 9px 5px;',
  371. 'padding: 0;',
  372. 'text-align: center;',
  373. 'width: 14px;',
  374. '}',
  375. '.ace_searchbtn_close:hover {',
  376. 'background-color: #656565;',
  377. 'background-position: 50% 100%;',
  378. 'color: white;',
  379. '}',
  380. '.ace_replacebtn.prev {',
  381. 'width: 54px',
  382. '}',
  383. '.ace_replacebtn.next {',
  384. 'width: 27px',
  385. '}',
  386. '.ace_button {',
  387. 'margin-left: 2px;',
  388. 'cursor: pointer;',
  389. '-webkit-user-select: none;',
  390. '-moz-user-select: none;',
  391. '-o-user-select: none;',
  392. '-ms-user-select: none;',
  393. 'user-select: none;',
  394. 'overflow: hidden;',
  395. 'opacity: 0.7;',
  396. 'border: 1px solid rgba(100,100,100,0.23);',
  397. 'padding: 1px;',
  398. '-moz-box-sizing: border-box;',
  399. 'box-sizing: border-box;',
  400. 'color: black;',
  401. '}',
  402. '.ace_button:hover {',
  403. 'background-color: #eee;',
  404. 'opacity:1;',
  405. '}',
  406. '.ace_button:active {',
  407. 'background-color: #ddd;',
  408. '}',
  409. '.ace_button.checked {',
  410. 'border-color: #3399ff;',
  411. 'opacity:1;',
  412. '}',
  413. '.ace_search_options{',
  414. 'margin-bottom: 3px;',
  415. 'text-align: right;',
  416. '-webkit-user-select: none;',
  417. '-moz-user-select: none;',
  418. '-o-user-select: none;',
  419. '-ms-user-select: none;',
  420. 'user-select: none;',
  421. '}'
  422. ].join('');
  423. style.setAttribute('data-name', 'js-searchbox');
  424. style.textContent = css;
  425. document.head.appendChild(style);
  426. }
  427. function addHtml() {
  428. var elSearch,
  429. el = document.querySelector('.CodeMirror'),
  430. div = document.createElement('div'),
  431. html = [
  432. '<div class="ace_search right">',
  433. '<button type="button" action="hide" class="ace_searchbtn_close"></button>',
  434. '<div class="ace_search_form">',
  435. '<input class="ace_search_field" placeholder="Search for" spellcheck="false"></input>',
  436. '<button type="button" action="findNext" class="ace_searchbtn next"></button>',
  437. '<button type="button" action="findPrev" class="ace_searchbtn prev"></button>',
  438. '<button type="button" action="findAll" class="ace_searchbtn" title="Alt-Enter">All</button>',
  439. '</div>',
  440. '<div class="ace_replace_form">',
  441. '<input class="ace_search_field" placeholder="Replace with" spellcheck="false"></input>',
  442. '<button type="button" action="replaceAndFindNext" class="ace_replacebtn">Replace</button>',
  443. '<button type="button" action="replaceAll" class="ace_replacebtn">All</button>',
  444. '</div>',
  445. '<div class="ace_search_options">',
  446. '<span action="toggleRegexpMode" class="ace_button" title="RegExp Search">.*</span>',
  447. '<span action="toggleCaseSensitive" class="ace_button" title="CaseSensitive Search">Aa</span>',
  448. '<span action="toggleWholeWords" class="ace_button" title="Whole Word Search">\\b</span>',
  449. '</div>',
  450. '</div>'
  451. ].join('');
  452. div.innerHTML = html;
  453. elSearch = div.firstChild;
  454. el.parentElement.appendChild(elSearch);
  455. return elSearch;
  456. }
  457. }
  458. function setCssClass(el, className, condition) {
  459. var list = el.classList;
  460. list[condition ? 'add' : 'remove'](className);
  461. }
  462. function delayedCall(fcn, defaultTimeout) {
  463. var timer,
  464. callback = function() {
  465. timer = null;
  466. fcn();
  467. },
  468. _self = function(timeout) {
  469. if (!timer)
  470. timer = setTimeout(callback, timeout || defaultTimeout);
  471. };
  472. _self.delay = function(timeout) {
  473. timer && clearTimeout(timer);
  474. timer = setTimeout(callback, timeout || defaultTimeout);
  475. };
  476. _self.schedule = _self;
  477. _self.call = function() {
  478. this.cancel();
  479. fcn();
  480. };
  481. _self.cancel = function() {
  482. timer && clearTimeout(timer);
  483. timer = null;
  484. };
  485. _self.isPending = function() {
  486. return timer;
  487. };
  488. return _self;
  489. }
  490. /* https://github.com/coderaiser/key */
  491. function key(str, event) {
  492. var right,
  493. KEY = {
  494. BACKSPACE : 8,
  495. TAB : 9,
  496. ENTER : 13,
  497. ESC : 27,
  498. SPACE : 32,
  499. PAGE_UP : 33,
  500. PAGE_DOWN : 34,
  501. END : 35,
  502. HOME : 36,
  503. UP : 38,
  504. DOWN : 40,
  505. INSERT : 45,
  506. DELETE : 46,
  507. INSERT_MAC : 96,
  508. ASTERISK : 106,
  509. PLUS : 107,
  510. MINUS : 109,
  511. F1 : 112,
  512. F2 : 113,
  513. F3 : 114,
  514. F4 : 115,
  515. F5 : 116,
  516. F6 : 117,
  517. F7 : 118,
  518. F8 : 119,
  519. F9 : 120,
  520. F10 : 121,
  521. SLASH : 191,
  522. TRA : 192, /* Typewritten Reverse Apostrophe (`) */
  523. BACKSLASH : 220
  524. };
  525. keyCheck(str, event);
  526. right = str.split('|').some(function(combination) {
  527. var wrong;
  528. wrong = combination.split('-').some(function(key) {
  529. var right;
  530. switch(key) {
  531. case 'Ctrl':
  532. right = event.ctrlKey;
  533. break;
  534. case 'Shift':
  535. right = event.shiftKey;
  536. break;
  537. case 'Alt':
  538. right = event.altKey;
  539. break;
  540. case 'Cmd':
  541. right = event.metaKey;
  542. break;
  543. default:
  544. if (key.length === 1)
  545. right = event.keyCode === key.charCodeAt(0);
  546. else
  547. Object.keys(KEY).some(function(name) {
  548. var up = key.toUpperCase();
  549. if (up === name)
  550. right = event.keyCode === KEY[name];
  551. });
  552. break;
  553. }
  554. return !right;
  555. });
  556. return !wrong;
  557. });
  558. return right;
  559. }
  560. function keyCheck(str, event) {
  561. if (typeof str !== 'string')
  562. throw(Error('str should be string!'));
  563. if (typeof event !== 'object')
  564. throw(Error('event should be object!'));
  565. }
  566. });