searchbox.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var dom = require("../lib/dom");
  33. var lang = require("../lib/lang");
  34. var event = require("../lib/event");
  35. var searchboxCss = require("../requirejs/text!./searchbox.css");
  36. var HashHandler = require("../keyboard/hash_handler").HashHandler;
  37. var keyUtil = require("../lib/keys");
  38. dom.importCssString(searchboxCss, "ace_searchbox");
  39. var html = '<div class="ace_search right">\
  40. <button type="button" action="hide" class="ace_searchbtn_close"></button>\
  41. <div class="ace_search_form">\
  42. <input class="ace_search_field" placeholder="Search for" spellcheck="false"></input>\
  43. <button type="button" action="findNext" class="ace_searchbtn next"></button>\
  44. <button type="button" action="findPrev" class="ace_searchbtn prev"></button>\
  45. <button type="button" action="findAll" class="ace_searchbtn" title="Alt-Enter">All</button>\
  46. </div>\
  47. <div class="ace_replace_form">\
  48. <input class="ace_search_field" placeholder="Replace with" spellcheck="false"></input>\
  49. <button type="button" action="replaceAndFindNext" class="ace_replacebtn">Replace</button>\
  50. <button type="button" action="replaceAll" class="ace_replacebtn">All</button>\
  51. </div>\
  52. <div class="ace_search_options">\
  53. <span action="toggleRegexpMode" class="ace_button" title="RegExp Search">.*</span>\
  54. <span action="toggleCaseSensitive" class="ace_button" title="CaseSensitive Search">Aa</span>\
  55. <span action="toggleWholeWords" class="ace_button" title="Whole Word Search">\\b</span>\
  56. </div>\
  57. </div>'.replace(/>\s+/g, ">");
  58. var SearchBox = function(editor, range, showReplaceForm) {
  59. var div = dom.createElement("div");
  60. div.innerHTML = html;
  61. this.element = div.firstChild;
  62. this.$init();
  63. this.setEditor(editor);
  64. };
  65. (function() {
  66. this.setEditor = function(editor) {
  67. editor.searchBox = this;
  68. editor.container.appendChild(this.element);
  69. this.editor = editor;
  70. };
  71. this.$initElements = function(sb) {
  72. this.searchBox = sb.querySelector(".ace_search_form");
  73. this.replaceBox = sb.querySelector(".ace_replace_form");
  74. this.searchOptions = sb.querySelector(".ace_search_options");
  75. this.regExpOption = sb.querySelector("[action=toggleRegexpMode]");
  76. this.caseSensitiveOption = sb.querySelector("[action=toggleCaseSensitive]");
  77. this.wholeWordOption = sb.querySelector("[action=toggleWholeWords]");
  78. this.searchInput = this.searchBox.querySelector(".ace_search_field");
  79. this.replaceInput = this.replaceBox.querySelector(".ace_search_field");
  80. };
  81. this.$init = function() {
  82. var sb = this.element;
  83. this.$initElements(sb);
  84. var _this = this;
  85. event.addListener(sb, "mousedown", function(e) {
  86. setTimeout(function(){
  87. _this.activeInput.focus();
  88. }, 0);
  89. event.stopPropagation(e);
  90. });
  91. event.addListener(sb, "click", function(e) {
  92. var t = e.target || e.srcElement;
  93. var action = t.getAttribute("action");
  94. if (action && _this[action])
  95. _this[action]();
  96. else if (_this.$searchBarKb.commands[action])
  97. _this.$searchBarKb.commands[action].exec(_this);
  98. event.stopPropagation(e);
  99. });
  100. event.addCommandKeyListener(sb, function(e, hashId, keyCode) {
  101. var keyString = keyUtil.keyCodeToString(keyCode);
  102. var command = _this.$searchBarKb.findKeyCommand(hashId, keyString);
  103. if (command && command.exec) {
  104. command.exec(_this);
  105. event.stopEvent(e);
  106. }
  107. });
  108. this.$onChange = lang.delayedCall(function() {
  109. _this.find(false, false);
  110. });
  111. event.addListener(this.searchInput, "input", function() {
  112. _this.$onChange.schedule(20);
  113. });
  114. event.addListener(this.searchInput, "focus", function() {
  115. _this.activeInput = _this.searchInput;
  116. _this.searchInput.value && _this.highlight();
  117. });
  118. event.addListener(this.replaceInput, "focus", function() {
  119. _this.activeInput = _this.replaceInput;
  120. _this.searchInput.value && _this.highlight();
  121. });
  122. };
  123. //keybinging outsite of the searchbox
  124. this.$closeSearchBarKb = new HashHandler([{
  125. bindKey: "Esc",
  126. name: "closeSearchBar",
  127. exec: function(editor) {
  128. editor.searchBox.hide();
  129. }
  130. }]);
  131. //keybinging outsite of the searchbox
  132. this.$searchBarKb = new HashHandler();
  133. this.$searchBarKb.bindKeys({
  134. "Ctrl-f|Command-f": function(sb) {
  135. var isReplace = sb.isReplace = !sb.isReplace;
  136. sb.replaceBox.style.display = isReplace ? "" : "none";
  137. sb.searchInput.focus();
  138. },
  139. "Ctrl-H|Command-Option-F": function(sb) {
  140. sb.replaceBox.style.display = "";
  141. sb.replaceInput.focus();
  142. },
  143. "Ctrl-G|Command-G": function(sb) {
  144. sb.findNext();
  145. },
  146. "Ctrl-Shift-G|Command-Shift-G": function(sb) {
  147. sb.findPrev();
  148. },
  149. "esc": function(sb) {
  150. setTimeout(function() { sb.hide();});
  151. },
  152. "Return": function(sb) {
  153. if (sb.activeInput == sb.replaceInput)
  154. sb.replace();
  155. sb.findNext();
  156. },
  157. "Shift-Return": function(sb) {
  158. if (sb.activeInput == sb.replaceInput)
  159. sb.replace();
  160. sb.findPrev();
  161. },
  162. "Alt-Return": function(sb) {
  163. if (sb.activeInput == sb.replaceInput)
  164. sb.replaceAll();
  165. sb.findAll();
  166. },
  167. "Tab": function(sb) {
  168. (sb.activeInput == sb.replaceInput ? sb.searchInput : sb.replaceInput).focus();
  169. }
  170. });
  171. this.$searchBarKb.addCommands([{
  172. name: "toggleRegexpMode",
  173. bindKey: {win: "Alt-R|Alt-/", mac: "Ctrl-Alt-R|Ctrl-Alt-/"},
  174. exec: function(sb) {
  175. sb.regExpOption.checked = !sb.regExpOption.checked;
  176. sb.$syncOptions();
  177. }
  178. }, {
  179. name: "toggleCaseSensitive",
  180. bindKey: {win: "Alt-C|Alt-I", mac: "Ctrl-Alt-R|Ctrl-Alt-I"},
  181. exec: function(sb) {
  182. sb.caseSensitiveOption.checked = !sb.caseSensitiveOption.checked;
  183. sb.$syncOptions();
  184. }
  185. }, {
  186. name: "toggleWholeWords",
  187. bindKey: {win: "Alt-B|Alt-W", mac: "Ctrl-Alt-B|Ctrl-Alt-W"},
  188. exec: function(sb) {
  189. sb.wholeWordOption.checked = !sb.wholeWordOption.checked;
  190. sb.$syncOptions();
  191. }
  192. }]);
  193. this.$syncOptions = function() {
  194. dom.setCssClass(this.regExpOption, "checked", this.regExpOption.checked);
  195. dom.setCssClass(this.wholeWordOption, "checked", this.wholeWordOption.checked);
  196. dom.setCssClass(this.caseSensitiveOption, "checked", this.caseSensitiveOption.checked);
  197. this.find(false, false);
  198. };
  199. this.highlight = function(re) {
  200. this.editor.session.highlight(re || this.editor.$search.$options.re);
  201. this.editor.renderer.updateBackMarkers()
  202. };
  203. this.find = function(skipCurrent, backwards, preventScroll) {
  204. var range = this.editor.find(this.searchInput.value, {
  205. skipCurrent: skipCurrent,
  206. backwards: backwards,
  207. wrap: true,
  208. regExp: this.regExpOption.checked,
  209. caseSensitive: this.caseSensitiveOption.checked,
  210. wholeWord: this.wholeWordOption.checked,
  211. preventScroll: preventScroll
  212. });
  213. var noMatch = !range && this.searchInput.value;
  214. dom.setCssClass(this.searchBox, "ace_nomatch", noMatch);
  215. this.editor._emit("findSearchBox", { match: !noMatch });
  216. this.highlight();
  217. };
  218. this.findNext = function() {
  219. this.find(true, false);
  220. };
  221. this.findPrev = function() {
  222. this.find(true, true);
  223. };
  224. this.findAll = function(){
  225. var range = this.editor.findAll(this.searchInput.value, {
  226. regExp: this.regExpOption.checked,
  227. caseSensitive: this.caseSensitiveOption.checked,
  228. wholeWord: this.wholeWordOption.checked
  229. });
  230. var noMatch = !range && this.searchInput.value;
  231. dom.setCssClass(this.searchBox, "ace_nomatch", noMatch);
  232. this.editor._emit("findSearchBox", { match: !noMatch });
  233. this.highlight();
  234. this.hide();
  235. };
  236. this.replace = function() {
  237. if (!this.editor.getReadOnly())
  238. this.editor.replace(this.replaceInput.value);
  239. };
  240. this.replaceAndFindNext = function() {
  241. if (!this.editor.getReadOnly()) {
  242. this.editor.replace(this.replaceInput.value);
  243. this.findNext()
  244. }
  245. };
  246. this.replaceAll = function() {
  247. if (!this.editor.getReadOnly())
  248. this.editor.replaceAll(this.replaceInput.value);
  249. };
  250. this.hide = function() {
  251. this.element.style.display = "none";
  252. this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb);
  253. this.editor.focus();
  254. };
  255. this.show = function(value, isReplace) {
  256. this.element.style.display = "";
  257. this.replaceBox.style.display = isReplace ? "" : "none";
  258. this.isReplace = isReplace;
  259. if (value)
  260. this.searchInput.value = value;
  261. this.find(false, false, true);
  262. this.searchInput.focus();
  263. this.searchInput.select();
  264. this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb);
  265. };
  266. this.isFocused = function() {
  267. var el = document.activeElement;
  268. return el == this.searchInput || el == this.replaceInput;
  269. }
  270. }).call(SearchBox.prototype);
  271. exports.SearchBox = SearchBox;
  272. exports.Search = function(editor, isReplace) {
  273. var sb = editor.searchBox || new SearchBox(editor);
  274. sb.show(editor.session.getTextRange(), isReplace);
  275. };
  276. });
  277. /* ------------------------------------------------------------------------------------------
  278. * TODO
  279. * --------------------------------------------------------------------------------------- */
  280. /*
  281. - move search form to the left if it masks current word
  282. - includ all options that search has. ex: regex
  283. - searchbox.searchbox is not that pretty. we should have just searchbox
  284. - disable prev button if it makes sence
  285. */