textinput.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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 event = require("../lib/event");
  33. var useragent = require("../lib/useragent");
  34. var dom = require("../lib/dom");
  35. var lang = require("../lib/lang");
  36. var BROKEN_SETDATA = useragent.isChrome < 18;
  37. var TextInput = function(parentNode, host) {
  38. var text = dom.createElement("textarea");
  39. text.className = "ace_text-input";
  40. if (useragent.isTouchPad)
  41. text.setAttribute("x-palm-disable-auto-cap", true);
  42. text.wrap = "off";
  43. text.autocorrect = "off";
  44. text.autocapitalize = "off";
  45. text.spellcheck = false;
  46. text.style.opacity = "0";
  47. parentNode.insertBefore(text, parentNode.firstChild);
  48. var PLACEHOLDER = "\x01\x01";
  49. var cut = false;
  50. var copied = false;
  51. var pasted = false;
  52. var inComposition = false;
  53. var tempStyle = '';
  54. var isSelectionEmpty = true;
  55. // FOCUS
  56. // ie9 throws error if document.activeElement is accessed too soon
  57. try { var isFocused = document.activeElement === text; } catch(e) {}
  58. event.addListener(text, "blur", function() {
  59. host.onBlur();
  60. isFocused = false;
  61. });
  62. event.addListener(text, "focus", function() {
  63. isFocused = true;
  64. host.onFocus();
  65. resetSelection();
  66. });
  67. this.focus = function() { text.focus(); };
  68. this.blur = function() { text.blur(); };
  69. this.isFocused = function() {
  70. return isFocused;
  71. };
  72. // modifying selection of blured textarea can focus it (chrome mac/linux)
  73. var syncSelection = lang.delayedCall(function() {
  74. isFocused && resetSelection(isSelectionEmpty);
  75. });
  76. var syncValue = lang.delayedCall(function() {
  77. if (!inComposition) {
  78. text.value = PLACEHOLDER;
  79. isFocused && resetSelection();
  80. }
  81. });
  82. function resetSelection(isEmpty) {
  83. if (inComposition)
  84. return;
  85. if (inputHandler) {
  86. selectionStart = 0;
  87. selectionEnd = isEmpty ? 0 : text.value.length - 1;
  88. } else {
  89. var selectionStart = isEmpty ? 2 : 1;
  90. var selectionEnd = 2;
  91. }
  92. // on firefox this throws if textarea is hidden
  93. try {
  94. text.setSelectionRange(selectionStart, selectionEnd);
  95. } catch(e){}
  96. }
  97. function resetValue() {
  98. if (inComposition)
  99. return;
  100. text.value = PLACEHOLDER;
  101. //http://code.google.com/p/chromium/issues/detail?id=76516
  102. if (useragent.isWebKit)
  103. syncValue.schedule();
  104. }
  105. useragent.isWebKit || host.addEventListener('changeSelection', function() {
  106. if (host.selection.isEmpty() != isSelectionEmpty) {
  107. isSelectionEmpty = !isSelectionEmpty;
  108. syncSelection.schedule();
  109. }
  110. });
  111. resetValue();
  112. if (isFocused)
  113. host.onFocus();
  114. var isAllSelected = function(text) {
  115. return text.selectionStart === 0 && text.selectionEnd === text.value.length;
  116. };
  117. // IE8 does not support setSelectionRange
  118. if (!text.setSelectionRange && text.createTextRange) {
  119. text.setSelectionRange = function(selectionStart, selectionEnd) {
  120. var range = this.createTextRange();
  121. range.collapse(true);
  122. range.moveStart('character', selectionStart);
  123. range.moveEnd('character', selectionEnd);
  124. range.select();
  125. };
  126. isAllSelected = function(text) {
  127. try {
  128. var range = text.ownerDocument.selection.createRange();
  129. }catch(e) {}
  130. if (!range || range.parentElement() != text) return false;
  131. return range.text == text.value;
  132. }
  133. }
  134. if (useragent.isOldIE) {
  135. var inPropertyChange = false;
  136. var onPropertyChange = function(e){
  137. if (inPropertyChange)
  138. return;
  139. var data = text.value;
  140. if (inComposition || !data || data == PLACEHOLDER)
  141. return;
  142. // can happen either after delete or during insert operation
  143. if (e && data == PLACEHOLDER[0])
  144. return syncProperty.schedule();
  145. sendText(data);
  146. // ie8 calls propertychange handlers synchronously!
  147. inPropertyChange = true;
  148. resetValue();
  149. inPropertyChange = false;
  150. };
  151. var syncProperty = lang.delayedCall(onPropertyChange);
  152. event.addListener(text, "propertychange", onPropertyChange);
  153. var keytable = { 13:1, 27:1 };
  154. event.addListener(text, "keyup", function (e) {
  155. if (inComposition && (!text.value || keytable[e.keyCode]))
  156. setTimeout(onCompositionEnd, 0);
  157. if ((text.value.charCodeAt(0)||0) < 129) {
  158. return syncProperty.call();
  159. }
  160. inComposition ? onCompositionUpdate() : onCompositionStart();
  161. });
  162. // when user presses backspace after focusing the editor
  163. // propertychange isn't called for the next character
  164. event.addListener(text, "keydown", function (e) {
  165. syncProperty.schedule(50);
  166. });
  167. }
  168. var onSelect = function(e) {
  169. if (cut) {
  170. cut = false;
  171. } else if (copied) {
  172. copied = false;
  173. } else if (isAllSelected(text)) {
  174. host.selectAll();
  175. resetSelection();
  176. } else if (inputHandler) {
  177. resetSelection(host.selection.isEmpty());
  178. }
  179. };
  180. var inputHandler = null;
  181. this.setInputHandler = function(cb) {inputHandler = cb};
  182. this.getInputHandler = function() {return inputHandler};
  183. var afterContextMenu = false;
  184. var sendText = function(data) {
  185. if (inputHandler) {
  186. data = inputHandler(data);
  187. inputHandler = null;
  188. }
  189. if (pasted) {
  190. resetSelection();
  191. if (data)
  192. host.onPaste(data);
  193. pasted = false;
  194. } else if (data == PLACEHOLDER.charAt(0)) {
  195. if (afterContextMenu)
  196. host.execCommand("del", {source: "ace"});
  197. else // some versions of android do not fire keydown when pressing backspace
  198. host.execCommand("backspace", {source: "ace"});
  199. } else {
  200. if (data.substring(0, 2) == PLACEHOLDER)
  201. data = data.substr(2);
  202. else if (data.charAt(0) == PLACEHOLDER.charAt(0))
  203. data = data.substr(1);
  204. else if (data.charAt(data.length - 1) == PLACEHOLDER.charAt(0))
  205. data = data.slice(0, -1);
  206. // can happen if undo in textarea isn't stopped
  207. if (data.charAt(data.length - 1) == PLACEHOLDER.charAt(0))
  208. data = data.slice(0, -1);
  209. if (data)
  210. host.onTextInput(data);
  211. }
  212. if (afterContextMenu)
  213. afterContextMenu = false;
  214. };
  215. var onInput = function(e) {
  216. // console.log("onInput", inComposition)
  217. if (inComposition)
  218. return;
  219. var data = text.value;
  220. sendText(data);
  221. resetValue();
  222. };
  223. var onCut = function(e) {
  224. var data = host.getCopyText();
  225. if (!data) {
  226. event.preventDefault(e);
  227. return;
  228. }
  229. var clipboardData = e.clipboardData || window.clipboardData;
  230. if (clipboardData && !BROKEN_SETDATA) {
  231. // Safari 5 has clipboardData object, but does not handle setData()
  232. var supported = clipboardData.setData("Text", data);
  233. if (supported) {
  234. host.onCut();
  235. event.preventDefault(e);
  236. }
  237. }
  238. if (!supported) {
  239. cut = true;
  240. text.value = data;
  241. text.select();
  242. setTimeout(function(){
  243. cut = false;
  244. resetValue();
  245. resetSelection();
  246. host.onCut();
  247. });
  248. }
  249. };
  250. var onCopy = function(e) {
  251. var data = host.getCopyText();
  252. if (!data) {
  253. event.preventDefault(e);
  254. return;
  255. }
  256. var clipboardData = e.clipboardData || window.clipboardData;
  257. if (clipboardData && !BROKEN_SETDATA) {
  258. // Safari 5 has clipboardData object, but does not handle setData()
  259. var supported = clipboardData.setData("Text", data);
  260. if (supported) {
  261. host.onCopy();
  262. event.preventDefault(e);
  263. }
  264. }
  265. if (!supported) {
  266. copied = true;
  267. text.value = data;
  268. text.select();
  269. setTimeout(function(){
  270. copied = false;
  271. resetValue();
  272. resetSelection();
  273. host.onCopy();
  274. });
  275. }
  276. };
  277. var onPaste = function(e) {
  278. var clipboardData = e.clipboardData || window.clipboardData;
  279. if (clipboardData) {
  280. var data = clipboardData.getData("Text");
  281. if (data)
  282. host.onPaste(data);
  283. if (useragent.isIE)
  284. setTimeout(resetSelection);
  285. event.preventDefault(e);
  286. }
  287. else {
  288. text.value = "";
  289. pasted = true;
  290. }
  291. };
  292. event.addCommandKeyListener(text, host.onCommandKey.bind(host));
  293. event.addListener(text, "select", onSelect);
  294. event.addListener(text, "input", onInput);
  295. event.addListener(text, "cut", onCut);
  296. event.addListener(text, "copy", onCopy);
  297. event.addListener(text, "paste", onPaste);
  298. // Opera has no clipboard events
  299. if (!('oncut' in text) || !('oncopy' in text) || !('onpaste' in text)){
  300. event.addListener(parentNode, "keydown", function(e) {
  301. if ((useragent.isMac && !e.metaKey) || !e.ctrlKey)
  302. return;
  303. switch (e.keyCode) {
  304. case 67:
  305. onCopy(e);
  306. break;
  307. case 86:
  308. onPaste(e);
  309. break;
  310. case 88:
  311. onCut(e);
  312. break;
  313. }
  314. });
  315. }
  316. // COMPOSITION
  317. var onCompositionStart = function(e) {
  318. if (inComposition) return;
  319. // console.log("onCompositionStart", inComposition)
  320. inComposition = {};
  321. host.onCompositionStart();
  322. setTimeout(onCompositionUpdate, 0);
  323. host.on("mousedown", onCompositionEnd);
  324. if (!host.selection.isEmpty()) {
  325. host.insert("");
  326. host.session.markUndoGroup();
  327. host.selection.clearSelection();
  328. }
  329. host.session.markUndoGroup();
  330. };
  331. var onCompositionUpdate = function() {
  332. // console.log("onCompositionUpdate", inComposition && JSON.stringify(text.value))
  333. if (!inComposition) return;
  334. var val = text.value.replace(/\x01/g, "");
  335. if (inComposition.lastValue === val) return;
  336. host.onCompositionUpdate(val);
  337. if (inComposition.lastValue)
  338. host.undo();
  339. inComposition.lastValue = val;
  340. if (inComposition.lastValue) {
  341. var r = host.selection.getRange();
  342. host.insert(inComposition.lastValue);
  343. host.session.markUndoGroup();
  344. inComposition.range = host.selection.getRange();
  345. host.selection.setRange(r);
  346. host.selection.clearSelection();
  347. }
  348. };
  349. var onCompositionEnd = function(e) {
  350. // console.log("onCompositionEnd", inComposition &&inComposition.lastValue)
  351. var c = inComposition;
  352. inComposition = false;
  353. var timer = setTimeout(function() {
  354. timer = null;
  355. var str = text.value.replace(/\x01/g, "");
  356. // console.log(str, c.lastValue)
  357. if (inComposition)
  358. return
  359. else if (str == c.lastValue)
  360. resetValue();
  361. else if (!c.lastValue && str) {
  362. resetValue();
  363. sendText(str);
  364. }
  365. });
  366. inputHandler = function compositionInputHandler(str) {
  367. // console.log("onCompositionEnd", str, c.lastValue)
  368. if (timer)
  369. clearTimeout(timer);
  370. str = str.replace(/\x01/g, "");
  371. if (str == c.lastValue)
  372. return "";
  373. if (c.lastValue && timer)
  374. host.undo();
  375. return str;
  376. };
  377. host.onCompositionEnd();
  378. host.removeListener("mousedown", onCompositionEnd);
  379. if (e.type == "compositionend" && c.range) {
  380. host.selection.setRange(c.range);
  381. }
  382. };
  383. var syncComposition = lang.delayedCall(onCompositionUpdate, 50);
  384. event.addListener(text, "compositionstart", onCompositionStart);
  385. if (useragent.isGecko) {
  386. event.addListener(text, "text", function(){syncComposition.schedule()});
  387. } else {
  388. event.addListener(text, "keyup", function(){syncComposition.schedule()});
  389. event.addListener(text, "keydown", function(){syncComposition.schedule()});
  390. }
  391. event.addListener(text, "compositionend", onCompositionEnd);
  392. this.getElement = function() {
  393. return text;
  394. };
  395. this.setReadOnly = function(readOnly) {
  396. text.readOnly = readOnly;
  397. };
  398. this.onContextMenu = function(e) {
  399. afterContextMenu = true;
  400. if (!tempStyle)
  401. tempStyle = text.style.cssText;
  402. text.style.cssText = "z-index:100000;" + (useragent.isIE ? "opacity:0.1;" : "");
  403. resetSelection(host.selection.isEmpty());
  404. host._emit("nativecontextmenu", {target: host, domEvent: e});
  405. var rect = host.container.getBoundingClientRect();
  406. var style = dom.computedStyle(host.container);
  407. var top = rect.top + (parseInt(style.borderTopWidth) || 0);
  408. var left = rect.left + (parseInt(rect.borderLeftWidth) || 0);
  409. var maxTop = rect.bottom - top - text.clientHeight;
  410. var move = function(e) {
  411. text.style.left = e.clientX - left - 2 + "px";
  412. text.style.top = Math.min(e.clientY - top - 2, maxTop) + "px";
  413. };
  414. move(e);
  415. if (e.type != "mousedown")
  416. return;
  417. if (host.renderer.$keepTextAreaAtCursor)
  418. host.renderer.$keepTextAreaAtCursor = null;
  419. // on windows context menu is opened after mouseup
  420. if (useragent.isWin)
  421. event.capture(host.container, move, onContextMenuClose);
  422. };
  423. this.onContextMenuClose = onContextMenuClose;
  424. function onContextMenuClose() {
  425. setTimeout(function () {
  426. if (tempStyle) {
  427. text.style.cssText = tempStyle;
  428. tempStyle = '';
  429. }
  430. if (host.renderer.$keepTextAreaAtCursor == null) {
  431. host.renderer.$keepTextAreaAtCursor = true;
  432. host.renderer.$moveTextAreaToCursor();
  433. }
  434. }, 0);
  435. }
  436. // firefox fires contextmenu event after opening it
  437. if (!useragent.isGecko || useragent.isMac) {
  438. var onContextMenu = function(e) {
  439. host.textInput.onContextMenu(e);
  440. onContextMenuClose();
  441. };
  442. event.addListener(host.renderer.scroller, "contextmenu", onContextMenu);
  443. event.addListener(text, "contextmenu", onContextMenu);
  444. }
  445. };
  446. exports.TextInput = TextInput;
  447. });