textinput.js 17 KB

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