textAreaInput.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as browser from '../../../base/browser/browser.js';
  6. import * as dom from '../../../base/browser/dom.js';
  7. import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js';
  8. import { RunOnceScheduler } from '../../../base/common/async.js';
  9. import { Emitter } from '../../../base/common/event.js';
  10. import { Disposable } from '../../../base/common/lifecycle.js';
  11. import { Mimes } from '../../../base/common/mime.js';
  12. import * as strings from '../../../base/common/strings.js';
  13. import { TextAreaState, _debugComposition } from './textAreaState.js';
  14. import { Selection } from '../../common/core/selection.js';
  15. export var TextAreaSyntethicEvents;
  16. (function (TextAreaSyntethicEvents) {
  17. TextAreaSyntethicEvents.Tap = '-monaco-textarea-synthetic-tap';
  18. })(TextAreaSyntethicEvents || (TextAreaSyntethicEvents = {}));
  19. export const CopyOptions = {
  20. forceCopyWithSyntaxHighlighting: false
  21. };
  22. /**
  23. * Every time we write to the clipboard, we record a bit of extra metadata here.
  24. * Every time we read from the cipboard, if the text matches our last written text,
  25. * we can fetch the previous metadata.
  26. */
  27. export class InMemoryClipboardMetadataManager {
  28. constructor() {
  29. this._lastState = null;
  30. }
  31. set(lastCopiedValue, data) {
  32. this._lastState = { lastCopiedValue, data };
  33. }
  34. get(pastedText) {
  35. if (this._lastState && this._lastState.lastCopiedValue === pastedText) {
  36. // match!
  37. return this._lastState.data;
  38. }
  39. this._lastState = null;
  40. return null;
  41. }
  42. }
  43. InMemoryClipboardMetadataManager.INSTANCE = new InMemoryClipboardMetadataManager();
  44. class CompositionContext {
  45. constructor() {
  46. this._lastTypeTextLength = 0;
  47. }
  48. handleCompositionUpdate(text) {
  49. text = text || '';
  50. const typeInput = {
  51. text: text,
  52. replacePrevCharCnt: this._lastTypeTextLength,
  53. replaceNextCharCnt: 0,
  54. positionDelta: 0
  55. };
  56. this._lastTypeTextLength = text.length;
  57. return typeInput;
  58. }
  59. }
  60. /**
  61. * Writes screen reader content to the textarea and is able to analyze its input events to generate:
  62. * - onCut
  63. * - onPaste
  64. * - onType
  65. *
  66. * Composition events are generated for presentation purposes (composition input is reflected in onType).
  67. */
  68. export class TextAreaInput extends Disposable {
  69. constructor(_host, _textArea, _OS, _browser) {
  70. super();
  71. this._host = _host;
  72. this._textArea = _textArea;
  73. this._OS = _OS;
  74. this._browser = _browser;
  75. this._onFocus = this._register(new Emitter());
  76. this.onFocus = this._onFocus.event;
  77. this._onBlur = this._register(new Emitter());
  78. this.onBlur = this._onBlur.event;
  79. this._onKeyDown = this._register(new Emitter());
  80. this.onKeyDown = this._onKeyDown.event;
  81. this._onKeyUp = this._register(new Emitter());
  82. this.onKeyUp = this._onKeyUp.event;
  83. this._onCut = this._register(new Emitter());
  84. this.onCut = this._onCut.event;
  85. this._onPaste = this._register(new Emitter());
  86. this.onPaste = this._onPaste.event;
  87. this._onType = this._register(new Emitter());
  88. this.onType = this._onType.event;
  89. this._onCompositionStart = this._register(new Emitter());
  90. this.onCompositionStart = this._onCompositionStart.event;
  91. this._onCompositionUpdate = this._register(new Emitter());
  92. this.onCompositionUpdate = this._onCompositionUpdate.event;
  93. this._onCompositionEnd = this._register(new Emitter());
  94. this.onCompositionEnd = this._onCompositionEnd.event;
  95. this._onSelectionChangeRequest = this._register(new Emitter());
  96. this.onSelectionChangeRequest = this._onSelectionChangeRequest.event;
  97. this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
  98. this._asyncFocusGainWriteScreenReaderContent = this._register(new RunOnceScheduler(() => this.writeScreenReaderContent('asyncFocusGain'), 0));
  99. this._textAreaState = TextAreaState.EMPTY;
  100. this._selectionChangeListener = null;
  101. this.writeScreenReaderContent('ctor');
  102. this._hasFocus = false;
  103. this._currentComposition = null;
  104. this._nextCommand = 0 /* Type */;
  105. let lastKeyDown = null;
  106. this._register(this._textArea.onKeyDown((_e) => {
  107. const e = new StandardKeyboardEvent(_e);
  108. if (e.keyCode === 109 /* KEY_IN_COMPOSITION */
  109. || (this._currentComposition && e.keyCode === 1 /* Backspace */)) {
  110. // Stop propagation for keyDown events if the IME is processing key input
  111. e.stopPropagation();
  112. }
  113. if (e.equals(9 /* Escape */)) {
  114. // Prevent default always for `Esc`, otherwise it will generate a keypress
  115. // See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
  116. e.preventDefault();
  117. }
  118. lastKeyDown = e;
  119. this._onKeyDown.fire(e);
  120. }));
  121. this._register(this._textArea.onKeyUp((_e) => {
  122. const e = new StandardKeyboardEvent(_e);
  123. this._onKeyUp.fire(e);
  124. }));
  125. this._register(this._textArea.onCompositionStart((e) => {
  126. if (_debugComposition) {
  127. console.log(`[compositionstart]`, e);
  128. }
  129. const currentComposition = new CompositionContext();
  130. if (this._currentComposition) {
  131. // simply reset the composition context
  132. this._currentComposition = currentComposition;
  133. return;
  134. }
  135. this._currentComposition = currentComposition;
  136. if (this._OS === 2 /* Macintosh */
  137. && this._textAreaState.selectionStart === this._textAreaState.selectionEnd
  138. && this._textAreaState.selectionStart > 0
  139. && this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data) {
  140. const isArrowKey = (lastKeyDown && lastKeyDown.equals(109 /* KEY_IN_COMPOSITION */)
  141. && (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft'));
  142. if (isArrowKey || this._browser.isFirefox) {
  143. // Handling long press case on Chromium/Safari macOS + arrow key => pretend the character was selected
  144. // or long press case on Firefox on macOS
  145. if (_debugComposition) {
  146. console.log(`[compositionstart] Handling long press case on macOS + arrow key or Firefox`, e);
  147. }
  148. // Pretend the previous character was composed (in order to get it removed by subsequent compositionupdate events)
  149. currentComposition.handleCompositionUpdate('x');
  150. this._onCompositionStart.fire({ revealDeltaColumns: -1 });
  151. return;
  152. }
  153. }
  154. if (this._browser.isAndroid) {
  155. // when tapping on the editor, Android enters composition mode to edit the current word
  156. // so we cannot clear the textarea on Android and we must pretend the current word was selected
  157. this._onCompositionStart.fire({ revealDeltaColumns: -this._textAreaState.selectionStart });
  158. return;
  159. }
  160. this._onCompositionStart.fire({ revealDeltaColumns: 0 });
  161. }));
  162. this._register(this._textArea.onCompositionUpdate((e) => {
  163. if (_debugComposition) {
  164. console.log(`[compositionupdate]`, e);
  165. }
  166. const currentComposition = this._currentComposition;
  167. if (!currentComposition) {
  168. // should not be possible to receive a 'compositionupdate' without a 'compositionstart'
  169. return;
  170. }
  171. if (this._browser.isAndroid) {
  172. // On Android, the data sent with the composition update event is unusable.
  173. // For example, if the cursor is in the middle of a word like Mic|osoft
  174. // and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
  175. // This is not really usable because it doesn't tell us where the edit began and where it ended.
  176. const newState = TextAreaState.readFromTextArea(this._textArea);
  177. const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
  178. this._textAreaState = newState;
  179. this._onType.fire(typeInput);
  180. this._onCompositionUpdate.fire(e);
  181. return;
  182. }
  183. const typeInput = currentComposition.handleCompositionUpdate(e.data);
  184. this._textAreaState = TextAreaState.readFromTextArea(this._textArea);
  185. this._onType.fire(typeInput);
  186. this._onCompositionUpdate.fire(e);
  187. }));
  188. this._register(this._textArea.onCompositionEnd((e) => {
  189. if (_debugComposition) {
  190. console.log(`[compositionend]`, e);
  191. }
  192. const currentComposition = this._currentComposition;
  193. if (!currentComposition) {
  194. // https://github.com/microsoft/monaco-editor/issues/1663
  195. // On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data
  196. return;
  197. }
  198. this._currentComposition = null;
  199. if (this._browser.isAndroid) {
  200. // On Android, the data sent with the composition update event is unusable.
  201. // For example, if the cursor is in the middle of a word like Mic|osoft
  202. // and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
  203. // This is not really usable because it doesn't tell us where the edit began and where it ended.
  204. const newState = TextAreaState.readFromTextArea(this._textArea);
  205. const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
  206. this._textAreaState = newState;
  207. this._onType.fire(typeInput);
  208. this._onCompositionEnd.fire();
  209. return;
  210. }
  211. const typeInput = currentComposition.handleCompositionUpdate(e.data);
  212. this._textAreaState = TextAreaState.readFromTextArea(this._textArea);
  213. this._onType.fire(typeInput);
  214. this._onCompositionEnd.fire();
  215. }));
  216. this._register(this._textArea.onInput((e) => {
  217. if (_debugComposition) {
  218. console.log(`[input]`, e);
  219. }
  220. // Pretend here we touched the text area, as the `input` event will most likely
  221. // result in a `selectionchange` event which we want to ignore
  222. this._textArea.setIgnoreSelectionChangeTime('received input event');
  223. if (this._currentComposition) {
  224. return;
  225. }
  226. const newState = TextAreaState.readFromTextArea(this._textArea);
  227. const typeInput = TextAreaState.deduceInput(this._textAreaState, newState, /*couldBeEmojiInput*/ this._OS === 2 /* Macintosh */);
  228. if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
  229. // Ignore invalid input but keep it around for next time
  230. return;
  231. }
  232. this._textAreaState = newState;
  233. const typeInputIsNoOp = (typeInput.text === ''
  234. && typeInput.replacePrevCharCnt === 0
  235. && typeInput.replaceNextCharCnt === 0
  236. && typeInput.positionDelta === 0);
  237. if (this._nextCommand === 0 /* Type */) {
  238. if (!typeInputIsNoOp) {
  239. this._onType.fire(typeInput);
  240. }
  241. }
  242. else {
  243. if (!typeInputIsNoOp) {
  244. this._firePaste(typeInput.text, null);
  245. }
  246. this._nextCommand = 0 /* Type */;
  247. }
  248. }));
  249. // --- Clipboard operations
  250. this._register(this._textArea.onCut((e) => {
  251. // Pretend here we touched the text area, as the `cut` event will most likely
  252. // result in a `selectionchange` event which we want to ignore
  253. this._textArea.setIgnoreSelectionChangeTime('received cut event');
  254. this._ensureClipboardGetsEditorSelection(e);
  255. this._asyncTriggerCut.schedule();
  256. }));
  257. this._register(this._textArea.onCopy((e) => {
  258. this._ensureClipboardGetsEditorSelection(e);
  259. }));
  260. this._register(this._textArea.onPaste((e) => {
  261. // Pretend here we touched the text area, as the `paste` event will most likely
  262. // result in a `selectionchange` event which we want to ignore
  263. this._textArea.setIgnoreSelectionChangeTime('received paste event');
  264. if (ClipboardEventUtils.canUseTextData(e)) {
  265. const [pastePlainText, metadata] = ClipboardEventUtils.getTextData(e);
  266. if (pastePlainText !== '') {
  267. this._firePaste(pastePlainText, metadata);
  268. }
  269. }
  270. else {
  271. if (this._textArea.getSelectionStart() !== this._textArea.getSelectionEnd()) {
  272. // Clean up the textarea, to get a clean paste
  273. this._setAndWriteTextAreaState('paste', TextAreaState.EMPTY);
  274. }
  275. this._nextCommand = 1 /* Paste */;
  276. }
  277. }));
  278. this._register(this._textArea.onFocus(() => {
  279. const hadFocus = this._hasFocus;
  280. this._setHasFocus(true);
  281. if (this._browser.isSafari && !hadFocus && this._hasFocus) {
  282. // When "tabbing into" the textarea, immediately after dispatching the 'focus' event,
  283. // Safari will always move the selection at offset 0 in the textarea
  284. this._asyncFocusGainWriteScreenReaderContent.schedule();
  285. }
  286. }));
  287. this._register(this._textArea.onBlur(() => {
  288. if (this._currentComposition) {
  289. // See https://github.com/microsoft/vscode/issues/112621
  290. // where compositionend is not triggered when the editor
  291. // is taken off-dom during a composition
  292. // Clear the flag to be able to write to the textarea
  293. this._currentComposition = null;
  294. // Clear the textarea to avoid an unwanted cursor type
  295. this.writeScreenReaderContent('blurWithoutCompositionEnd');
  296. // Fire artificial composition end
  297. this._onCompositionEnd.fire();
  298. }
  299. this._setHasFocus(false);
  300. }));
  301. this._register(this._textArea.onSyntheticTap(() => {
  302. if (this._browser.isAndroid && this._currentComposition) {
  303. // on Android, tapping does not cancel the current composition, so the
  304. // textarea is stuck showing the old composition
  305. // Clear the flag to be able to write to the textarea
  306. this._currentComposition = null;
  307. // Clear the textarea to avoid an unwanted cursor type
  308. this.writeScreenReaderContent('tapWithoutCompositionEnd');
  309. // Fire artificial composition end
  310. this._onCompositionEnd.fire();
  311. }
  312. }));
  313. }
  314. _installSelectionChangeListener() {
  315. // See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
  316. // When using a Braille display, it is possible for users to reposition the
  317. // system caret. This is reflected in Chrome as a `selectionchange` event.
  318. //
  319. // The `selectionchange` event appears to be emitted under numerous other circumstances,
  320. // so it is quite a challenge to distinguish a `selectionchange` coming in from a user
  321. // using a Braille display from all the other cases.
  322. //
  323. // The problems with the `selectionchange` event are:
  324. // * the event is emitted when the textarea is focused programmatically -- textarea.focus()
  325. // * the event is emitted when the selection is changed in the textarea programmatically -- textarea.setSelectionRange(...)
  326. // * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'
  327. // * the event is emitted when tabbing into the textarea
  328. // * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)
  329. // * the event sometimes comes in bursts for a single logical textarea operation
  330. // `selectionchange` events often come multiple times for a single logical change
  331. // so throttle multiple `selectionchange` events that burst in a short period of time.
  332. let previousSelectionChangeEventTime = 0;
  333. return dom.addDisposableListener(document, 'selectionchange', (e) => {
  334. if (!this._hasFocus) {
  335. return;
  336. }
  337. if (this._currentComposition) {
  338. return;
  339. }
  340. if (!this._browser.isChrome) {
  341. // Support only for Chrome until testing happens on other browsers
  342. return;
  343. }
  344. const now = Date.now();
  345. const delta1 = now - previousSelectionChangeEventTime;
  346. previousSelectionChangeEventTime = now;
  347. if (delta1 < 5) {
  348. // received another `selectionchange` event within 5ms of the previous `selectionchange` event
  349. // => ignore it
  350. return;
  351. }
  352. const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();
  353. this._textArea.resetSelectionChangeTime();
  354. if (delta2 < 100) {
  355. // received a `selectionchange` event within 100ms since we touched the textarea
  356. // => ignore it, since we caused it
  357. return;
  358. }
  359. if (!this._textAreaState.selectionStartPosition || !this._textAreaState.selectionEndPosition) {
  360. // Cannot correlate a position in the textarea with a position in the editor...
  361. return;
  362. }
  363. const newValue = this._textArea.getValue();
  364. if (this._textAreaState.value !== newValue) {
  365. // Cannot correlate a position in the textarea with a position in the editor...
  366. return;
  367. }
  368. const newSelectionStart = this._textArea.getSelectionStart();
  369. const newSelectionEnd = this._textArea.getSelectionEnd();
  370. if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {
  371. // Nothing to do...
  372. return;
  373. }
  374. const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);
  375. const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0], _newSelectionStartPosition[1], _newSelectionStartPosition[2]);
  376. const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);
  377. const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0], _newSelectionEndPosition[1], _newSelectionEndPosition[2]);
  378. const newSelection = new Selection(newSelectionStartPosition.lineNumber, newSelectionStartPosition.column, newSelectionEndPosition.lineNumber, newSelectionEndPosition.column);
  379. this._onSelectionChangeRequest.fire(newSelection);
  380. });
  381. }
  382. dispose() {
  383. super.dispose();
  384. if (this._selectionChangeListener) {
  385. this._selectionChangeListener.dispose();
  386. this._selectionChangeListener = null;
  387. }
  388. }
  389. focusTextArea() {
  390. // Setting this._hasFocus and writing the screen reader content
  391. // will result in a focus() and setSelectionRange() in the textarea
  392. this._setHasFocus(true);
  393. // If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus
  394. this.refreshFocusState();
  395. }
  396. isFocused() {
  397. return this._hasFocus;
  398. }
  399. refreshFocusState() {
  400. this._setHasFocus(this._textArea.hasFocus());
  401. }
  402. _setHasFocus(newHasFocus) {
  403. if (this._hasFocus === newHasFocus) {
  404. // no change
  405. return;
  406. }
  407. this._hasFocus = newHasFocus;
  408. if (this._selectionChangeListener) {
  409. this._selectionChangeListener.dispose();
  410. this._selectionChangeListener = null;
  411. }
  412. if (this._hasFocus) {
  413. this._selectionChangeListener = this._installSelectionChangeListener();
  414. }
  415. if (this._hasFocus) {
  416. this.writeScreenReaderContent('focusgain');
  417. }
  418. if (this._hasFocus) {
  419. this._onFocus.fire();
  420. }
  421. else {
  422. this._onBlur.fire();
  423. }
  424. }
  425. _setAndWriteTextAreaState(reason, textAreaState) {
  426. if (!this._hasFocus) {
  427. textAreaState = textAreaState.collapseSelection();
  428. }
  429. textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);
  430. this._textAreaState = textAreaState;
  431. }
  432. writeScreenReaderContent(reason) {
  433. if (this._currentComposition) {
  434. // Do not write to the text area when doing composition
  435. return;
  436. }
  437. this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent(this._textAreaState));
  438. }
  439. _ensureClipboardGetsEditorSelection(e) {
  440. const dataToCopy = this._host.getDataToCopy(ClipboardEventUtils.canUseTextData(e));
  441. const storedMetadata = {
  442. version: 1,
  443. isFromEmptySelection: dataToCopy.isFromEmptySelection,
  444. multicursorText: dataToCopy.multicursorText,
  445. mode: dataToCopy.mode
  446. };
  447. InMemoryClipboardMetadataManager.INSTANCE.set(
  448. // When writing "LINE\r\n" to the clipboard and then pasting,
  449. // Firefox pastes "LINE\n", so let's work around this quirk
  450. (this._browser.isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), storedMetadata);
  451. if (!ClipboardEventUtils.canUseTextData(e)) {
  452. // Looks like an old browser. The strategy is to place the text
  453. // we'd like to be copied to the clipboard in the textarea and select it.
  454. this._setAndWriteTextAreaState('copy or cut', TextAreaState.selectedText(dataToCopy.text));
  455. return;
  456. }
  457. ClipboardEventUtils.setTextData(e, dataToCopy.text, dataToCopy.html, storedMetadata);
  458. }
  459. _firePaste(text, metadata) {
  460. if (!metadata) {
  461. // try the in-memory store
  462. metadata = InMemoryClipboardMetadataManager.INSTANCE.get(text);
  463. }
  464. this._onPaste.fire({
  465. text: text,
  466. metadata: metadata
  467. });
  468. }
  469. }
  470. class ClipboardEventUtils {
  471. static canUseTextData(e) {
  472. if (e.clipboardData) {
  473. return true;
  474. }
  475. return false;
  476. }
  477. static getTextData(e) {
  478. if (e.clipboardData) {
  479. e.preventDefault();
  480. const text = e.clipboardData.getData(Mimes.text);
  481. let metadata = null;
  482. const rawmetadata = e.clipboardData.getData('vscode-editor-data');
  483. if (typeof rawmetadata === 'string') {
  484. try {
  485. metadata = JSON.parse(rawmetadata);
  486. if (metadata.version !== 1) {
  487. metadata = null;
  488. }
  489. }
  490. catch (err) {
  491. // no problem!
  492. }
  493. }
  494. return [text, metadata];
  495. }
  496. throw new Error('ClipboardEventUtils.getTextData: Cannot use text data!');
  497. }
  498. static setTextData(e, text, html, metadata) {
  499. if (e.clipboardData) {
  500. e.clipboardData.setData(Mimes.text, text);
  501. if (typeof html === 'string') {
  502. e.clipboardData.setData('text/html', html);
  503. }
  504. e.clipboardData.setData('vscode-editor-data', JSON.stringify(metadata));
  505. e.preventDefault();
  506. return;
  507. }
  508. throw new Error('ClipboardEventUtils.setTextData: Cannot use text data!');
  509. }
  510. }
  511. export class TextAreaWrapper extends Disposable {
  512. constructor(_actual) {
  513. super();
  514. this._actual = _actual;
  515. this.onKeyDown = this._register(dom.createEventEmitter(this._actual, 'keydown')).event;
  516. this.onKeyUp = this._register(dom.createEventEmitter(this._actual, 'keyup')).event;
  517. this.onCompositionStart = this._register(dom.createEventEmitter(this._actual, 'compositionstart')).event;
  518. this.onCompositionUpdate = this._register(dom.createEventEmitter(this._actual, 'compositionupdate')).event;
  519. this.onCompositionEnd = this._register(dom.createEventEmitter(this._actual, 'compositionend')).event;
  520. this.onInput = this._register(dom.createEventEmitter(this._actual, 'input')).event;
  521. this.onCut = this._register(dom.createEventEmitter(this._actual, 'cut')).event;
  522. this.onCopy = this._register(dom.createEventEmitter(this._actual, 'copy')).event;
  523. this.onPaste = this._register(dom.createEventEmitter(this._actual, 'paste')).event;
  524. this.onFocus = this._register(dom.createEventEmitter(this._actual, 'focus')).event;
  525. this.onBlur = this._register(dom.createEventEmitter(this._actual, 'blur')).event;
  526. this._onSyntheticTap = this._register(new Emitter());
  527. this.onSyntheticTap = this._onSyntheticTap.event;
  528. this._ignoreSelectionChangeTime = 0;
  529. this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
  530. }
  531. hasFocus() {
  532. const shadowRoot = dom.getShadowRoot(this._actual);
  533. if (shadowRoot) {
  534. return shadowRoot.activeElement === this._actual;
  535. }
  536. else if (dom.isInDOM(this._actual)) {
  537. return document.activeElement === this._actual;
  538. }
  539. else {
  540. return false;
  541. }
  542. }
  543. setIgnoreSelectionChangeTime(reason) {
  544. this._ignoreSelectionChangeTime = Date.now();
  545. }
  546. getIgnoreSelectionChangeTime() {
  547. return this._ignoreSelectionChangeTime;
  548. }
  549. resetSelectionChangeTime() {
  550. this._ignoreSelectionChangeTime = 0;
  551. }
  552. getValue() {
  553. // console.log('current value: ' + this._textArea.value);
  554. return this._actual.value;
  555. }
  556. setValue(reason, value) {
  557. const textArea = this._actual;
  558. if (textArea.value === value) {
  559. // No change
  560. return;
  561. }
  562. // console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);
  563. this.setIgnoreSelectionChangeTime('setValue');
  564. textArea.value = value;
  565. }
  566. getSelectionStart() {
  567. return this._actual.selectionDirection === 'backward' ? this._actual.selectionEnd : this._actual.selectionStart;
  568. }
  569. getSelectionEnd() {
  570. return this._actual.selectionDirection === 'backward' ? this._actual.selectionStart : this._actual.selectionEnd;
  571. }
  572. setSelectionRange(reason, selectionStart, selectionEnd) {
  573. const textArea = this._actual;
  574. let activeElement = null;
  575. const shadowRoot = dom.getShadowRoot(textArea);
  576. if (shadowRoot) {
  577. activeElement = shadowRoot.activeElement;
  578. }
  579. else {
  580. activeElement = document.activeElement;
  581. }
  582. const currentIsFocused = (activeElement === textArea);
  583. const currentSelectionStart = textArea.selectionStart;
  584. const currentSelectionEnd = textArea.selectionEnd;
  585. if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {
  586. // No change
  587. // Firefox iframe bug https://github.com/microsoft/monaco-editor/issues/643#issuecomment-367871377
  588. if (browser.isFirefox && window.parent !== window) {
  589. textArea.focus();
  590. }
  591. return;
  592. }
  593. // console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);
  594. if (currentIsFocused) {
  595. // No need to focus, only need to change the selection range
  596. this.setIgnoreSelectionChangeTime('setSelectionRange');
  597. textArea.setSelectionRange(selectionStart, selectionEnd);
  598. if (browser.isFirefox && window.parent !== window) {
  599. textArea.focus();
  600. }
  601. return;
  602. }
  603. // If the focus is outside the textarea, browsers will try really hard to reveal the textarea.
  604. // Here, we try to undo the browser's desperate reveal.
  605. try {
  606. const scrollState = dom.saveParentsScrollTop(textArea);
  607. this.setIgnoreSelectionChangeTime('setSelectionRange');
  608. textArea.focus();
  609. textArea.setSelectionRange(selectionStart, selectionEnd);
  610. dom.restoreParentsScrollTop(textArea, scrollState);
  611. }
  612. catch (e) {
  613. // Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
  614. }
  615. }
  616. }