e303045262fd2503857d59f5c0e69947e8bb8914af0178bf8502648e148fd3b8f4fa0ef7be6630a470fe727c259fca96fad3d9702d381f3941eefd0b63a0b0 28 KB

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