textinput.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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 = "\u2028\u2028";
  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. if (typeof host.middleClick != "undefined" && host.middleClick){
  241. return host.getSelectedText() || " ";
  242. }
  243. var clipboardData = e.clipboardData || window.clipboardData;
  244. if (!clipboardData || BROKEN_SETDATA)
  245. return;
  246. // using "Text" doesn't work on old webkit but ie needs it
  247. // TODO are there other browsers that require "Text"?
  248. var mime = USE_IE_MIME_TYPE ? "Text" : "text/plain";
  249. if (data) {
  250. // Safari 5 has clipboardData object, but does not handle setData()
  251. return clipboardData.setData(mime, data) !== false;
  252. } else {
  253. return clipboardData.getData(mime);
  254. }
  255. };
  256. var doCopy = function(e, isCut) {
  257. var data = host.getCopyText();
  258. if (!data)
  259. return event.preventDefault(e);
  260. if (handleClipboardData(e, data)) {
  261. isCut ? host.onCut() : host.onCopy();
  262. event.preventDefault(e);
  263. } else {
  264. copied = true;
  265. text.value = data;
  266. text.select();
  267. setTimeout(function(){
  268. copied = false;
  269. resetValue();
  270. resetSelection();
  271. isCut ? host.onCut() : host.onCopy();
  272. });
  273. }
  274. };
  275. var onCut = function(e) {
  276. doCopy(e, true);
  277. };
  278. var onCopy = function(e) {
  279. doCopy(e, false);
  280. };
  281. var onPaste = function(e) {
  282. var data = handleClipboardData(e);
  283. if (typeof data == "string") {
  284. if (data)
  285. host.onPaste(data, e);
  286. if (useragent.isIE)
  287. setTimeout(resetSelection);
  288. event.preventDefault(e);
  289. }
  290. else {
  291. text.value = "";
  292. pasted = true;
  293. }
  294. };
  295. event.addCommandKeyListener(text, host.onCommandKey.bind(host));
  296. event.addListener(text, "select", onSelect);
  297. event.addListener(text, "input", onInput);
  298. event.addListener(text, "cut", onCut);
  299. event.addListener(text, "copy", onCopy);
  300. event.addListener(text, "paste", onPaste);
  301. // Opera has no clipboard events
  302. if (!('oncut' in text) || !('oncopy' in text) || !('onpaste' in text)){
  303. event.addListener(parentNode, "keydown", function(e) {
  304. if ((useragent.isMac && !e.metaKey) || !e.ctrlKey)
  305. return;
  306. switch (e.keyCode) {
  307. case 67:
  308. onCopy(e);
  309. break;
  310. case 86:
  311. onPaste(e);
  312. break;
  313. case 88:
  314. onCut(e);
  315. break;
  316. }
  317. });
  318. }
  319. // COMPOSITION
  320. var onCompositionStart = function(e) {
  321. if (inComposition || !host.onCompositionStart || host.$readOnly)
  322. return;
  323. // console.log("onCompositionStart", inComposition)
  324. inComposition = {};
  325. host.onCompositionStart();
  326. setTimeout(onCompositionUpdate, 0);
  327. host.on("mousedown", onCompositionEnd);
  328. if (!host.selection.isEmpty()) {
  329. host.insert("");
  330. host.session.markUndoGroup();
  331. host.selection.clearSelection();
  332. }
  333. host.session.markUndoGroup();
  334. };
  335. var onCompositionUpdate = function() {
  336. // console.log("onCompositionUpdate", inComposition && JSON.stringify(text.value))
  337. if (!inComposition || !host.onCompositionUpdate || host.$readOnly)
  338. return;
  339. var val = text.value.replace(/\u2028/g, "");
  340. if (inComposition.lastValue === val) return;
  341. host.onCompositionUpdate(val);
  342. if (inComposition.lastValue)
  343. host.undo();
  344. inComposition.lastValue = val;
  345. if (inComposition.lastValue) {
  346. var r = host.selection.getRange();
  347. host.insert(inComposition.lastValue);
  348. host.session.markUndoGroup();
  349. inComposition.range = host.selection.getRange();
  350. host.selection.setRange(r);
  351. host.selection.clearSelection();
  352. }
  353. };
  354. var onCompositionEnd = function(e) {
  355. if (!host.onCompositionEnd || host.$readOnly) return;
  356. // console.log("onCompositionEnd", inComposition &&inComposition.lastValue)
  357. var c = inComposition;
  358. inComposition = false;
  359. var timer = setTimeout(function() {
  360. timer = null;
  361. var str = text.value.replace(/\u2028/g, "");
  362. // console.log(str, c.lastValue)
  363. if (inComposition)
  364. return;
  365. else if (str == c.lastValue)
  366. resetValue();
  367. else if (!c.lastValue && str) {
  368. resetValue();
  369. sendText(str);
  370. }
  371. });
  372. inputHandler = function compositionInputHandler(str) {
  373. // console.log("onCompositionEnd", str, c.lastValue)
  374. if (timer)
  375. clearTimeout(timer);
  376. str = str.replace(/\u2028/g, "");
  377. if (str == c.lastValue)
  378. return "";
  379. if (c.lastValue && timer)
  380. host.undo();
  381. return str;
  382. };
  383. host.onCompositionEnd();
  384. host.removeListener("mousedown", onCompositionEnd);
  385. if (e.type == "compositionend" && c.range) {
  386. host.selection.setRange(c.range);
  387. }
  388. // Workaround for #3027, #3045, #3097, #3100, #3249
  389. var needsOnInput =
  390. (!!useragent.isChrome && useragent.isChrome >= 53) ||
  391. (!!useragent.isWebKit && useragent.isWebKit >= 603);
  392. if (needsOnInput) {
  393. onInput();
  394. }
  395. };
  396. var syncComposition = lang.delayedCall(onCompositionUpdate, 50);
  397. event.addListener(text, "compositionstart", onCompositionStart);
  398. if (useragent.isGecko) {
  399. event.addListener(text, "text", function(){syncComposition.schedule()});
  400. } else {
  401. event.addListener(text, "keyup", function(){syncComposition.schedule()});
  402. event.addListener(text, "keydown", function(){syncComposition.schedule()});
  403. }
  404. event.addListener(text, "compositionend", onCompositionEnd);
  405. this.getElement = function() {
  406. return text;
  407. };
  408. this.setReadOnly = function(readOnly) {
  409. text.readOnly = readOnly;
  410. };
  411. this.onContextMenu = function(e) {
  412. afterContextMenu = true;
  413. resetSelection(host.selection.isEmpty());
  414. host._emit("nativecontextmenu", {target: host, domEvent: e});
  415. this.moveToMouse(e, true);
  416. };
  417. this.moveToMouse = function(e, bringToFront) {
  418. if (!bringToFront && useragent.isOldIE)
  419. return;
  420. if (!tempStyle)
  421. tempStyle = text.style.cssText;
  422. text.style.cssText = (bringToFront ? "z-index:100000;" : "")
  423. + "height:" + text.style.height + ";"
  424. + (useragent.isIE ? "opacity:0.1;" : "");
  425. var rect = host.container.getBoundingClientRect();
  426. var style = dom.computedStyle(host.container);
  427. var top = rect.top + (parseInt(style.borderTopWidth) || 0);
  428. var left = rect.left + (parseInt(rect.borderLeftWidth) || 0);
  429. var maxTop = rect.bottom - top - text.clientHeight -2;
  430. var move = function(e) {
  431. text.style.left = e.clientX - left - 2 + "px";
  432. text.style.top = Math.min(e.clientY - top - 2, maxTop) + "px";
  433. };
  434. move(e);
  435. if (e.type != "mousedown")
  436. return;
  437. if (host.renderer.$keepTextAreaAtCursor)
  438. host.renderer.$keepTextAreaAtCursor = null;
  439. // on windows context menu is opened after mouseup
  440. if (useragent.isWin && !useragent.isOldIE)
  441. event.capture(host.container, move, onContextMenuClose);
  442. };
  443. this.onContextMenuClose = onContextMenuClose;
  444. var closeTimeout;
  445. function onContextMenuClose() {
  446. clearTimeout(closeTimeout);
  447. closeTimeout = setTimeout(function () {
  448. if (tempStyle) {
  449. text.style.cssText = tempStyle;
  450. tempStyle = '';
  451. }
  452. if (host.renderer.$keepTextAreaAtCursor == null) {
  453. host.renderer.$keepTextAreaAtCursor = true;
  454. host.renderer.$moveTextAreaToCursor();
  455. }
  456. }, useragent.isOldIE ? 200 : 0);
  457. }
  458. var onContextMenu = function(e) {
  459. host.textInput.onContextMenu(e);
  460. onContextMenuClose();
  461. };
  462. event.addListener(host.renderer.scroller, "contextmenu", onContextMenu);
  463. event.addListener(text, "contextmenu", onContextMenu);
  464. };
  465. exports.TextInput = TextInput;
  466. });