inputText.ts 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  1. import { Nullable } from "babylonjs/types";
  2. import { Observable, Observer } from "babylonjs/Misc/observable";
  3. import { Vector2 } from "babylonjs/Maths/math.vector";
  4. import { ClipboardEventTypes, ClipboardInfo } from "babylonjs/Events/clipboardEvents";
  5. import { PointerInfo, PointerEventTypes, PointerInfoBase } from 'babylonjs/Events/pointerEvents';
  6. import { Control } from "./control";
  7. import { IFocusableControl } from "../advancedDynamicTexture";
  8. import { ValueAndUnit } from "../valueAndUnit";
  9. import { VirtualKeyboard } from "./virtualKeyboard";
  10. import { _TypeStore } from 'babylonjs/Misc/typeStore';
  11. import { Measure } from '../measure';
  12. /**
  13. * Class used to create input text control
  14. */
  15. export class InputText extends Control implements IFocusableControl {
  16. private _text = "";
  17. private _placeholderText = "";
  18. private _background = "#222222";
  19. private _focusedBackground = "#000000";
  20. private _focusedColor = "white";
  21. private _placeholderColor = "gray";
  22. private _thickness = 1;
  23. private _margin = new ValueAndUnit(10, ValueAndUnit.UNITMODE_PIXEL);
  24. private _autoStretchWidth = true;
  25. private _maxWidth = new ValueAndUnit(1, ValueAndUnit.UNITMODE_PERCENTAGE, false);
  26. private _isFocused = false;
  27. private _blinkTimeout: number;
  28. private _blinkIsEven = false;
  29. private _cursorOffset = 0;
  30. private _scrollLeft: Nullable<number>;
  31. private _textWidth: number;
  32. private _clickedCoordinate: Nullable<number>;
  33. private _deadKey = false;
  34. private _addKey = true;
  35. private _currentKey = "";
  36. private _isTextHighlightOn = false;
  37. private _textHighlightColor = "#d5e0ff";
  38. private _highligherOpacity = 0.4;
  39. private _highlightedText = "";
  40. private _startHighlightIndex = 0;
  41. private _endHighlightIndex = 0;
  42. private _cursorIndex = -1;
  43. private _onFocusSelectAll = false;
  44. private _isPointerDown = false;
  45. private _onClipboardObserver: Nullable<Observer<ClipboardInfo>>;
  46. private _onPointerDblTapObserver: Nullable<Observer<PointerInfo>>;
  47. /** @hidden */
  48. public _connectedVirtualKeyboard: Nullable<VirtualKeyboard>;
  49. /** Gets or sets a string representing the message displayed on mobile when the control gets the focus */
  50. public promptMessage = "Please enter text:";
  51. /** Force disable prompt on mobile device */
  52. public disableMobilePrompt = false;
  53. /** Observable raised when the text changes */
  54. public onTextChangedObservable = new Observable<InputText>();
  55. /** Observable raised just before an entered character is to be added */
  56. public onBeforeKeyAddObservable = new Observable<InputText>();
  57. /** Observable raised when the control gets the focus */
  58. public onFocusObservable = new Observable<InputText>();
  59. /** Observable raised when the control loses the focus */
  60. public onBlurObservable = new Observable<InputText>();
  61. /**Observable raised when the text is highlighted */
  62. public onTextHighlightObservable = new Observable<InputText>();
  63. /**Observable raised when copy event is triggered */
  64. public onTextCopyObservable = new Observable<InputText>();
  65. /** Observable raised when cut event is triggered */
  66. public onTextCutObservable = new Observable<InputText>();
  67. /** Observable raised when paste event is triggered */
  68. public onTextPasteObservable = new Observable<InputText>();
  69. /** Observable raised when a key event was processed */
  70. public onKeyboardEventProcessedObservable = new Observable<KeyboardEvent>();
  71. /** Gets or sets the maximum width allowed by the control */
  72. public get maxWidth(): string | number {
  73. return this._maxWidth.toString(this._host);
  74. }
  75. /** Gets the maximum width allowed by the control in pixels */
  76. public get maxWidthInPixels(): number {
  77. return this._maxWidth.getValueInPixel(this._host, this._cachedParentMeasure.width);
  78. }
  79. public set maxWidth(value: string | number) {
  80. if (this._maxWidth.toString(this._host) === value) {
  81. return;
  82. }
  83. if (this._maxWidth.fromString(value)) {
  84. this._markAsDirty();
  85. }
  86. }
  87. /** Gets or sets the text highlighter transparency; default: 0.4 */
  88. public get highligherOpacity(): number {
  89. return this._highligherOpacity;
  90. }
  91. public set highligherOpacity(value: number) {
  92. if (this._highligherOpacity === value) {
  93. return;
  94. }
  95. this._highligherOpacity = value;
  96. this._markAsDirty();
  97. }
  98. /** Gets or sets a boolean indicating whether to select complete text by default on input focus */
  99. public get onFocusSelectAll(): boolean {
  100. return this._onFocusSelectAll;
  101. }
  102. public set onFocusSelectAll(value: boolean) {
  103. if (this._onFocusSelectAll === value) {
  104. return;
  105. }
  106. this._onFocusSelectAll = value;
  107. this._markAsDirty();
  108. }
  109. /** Gets or sets the text hightlight color */
  110. public get textHighlightColor(): string {
  111. return this._textHighlightColor;
  112. }
  113. public set textHighlightColor(value: string) {
  114. if (this._textHighlightColor === value) {
  115. return;
  116. }
  117. this._textHighlightColor = value;
  118. this._markAsDirty();
  119. }
  120. /** Gets or sets control margin */
  121. public get margin(): string {
  122. return this._margin.toString(this._host);
  123. }
  124. /** Gets control margin in pixels */
  125. public get marginInPixels(): number {
  126. return this._margin.getValueInPixel(this._host, this._cachedParentMeasure.width);
  127. }
  128. public set margin(value: string) {
  129. if (this._margin.toString(this._host) === value) {
  130. return;
  131. }
  132. if (this._margin.fromString(value)) {
  133. this._markAsDirty();
  134. }
  135. }
  136. /** Gets or sets a boolean indicating if the control can auto stretch its width to adapt to the text */
  137. public get autoStretchWidth(): boolean {
  138. return this._autoStretchWidth;
  139. }
  140. public set autoStretchWidth(value: boolean) {
  141. if (this._autoStretchWidth === value) {
  142. return;
  143. }
  144. this._autoStretchWidth = value;
  145. this._markAsDirty();
  146. }
  147. /** Gets or sets border thickness */
  148. public get thickness(): number {
  149. return this._thickness;
  150. }
  151. public set thickness(value: number) {
  152. if (this._thickness === value) {
  153. return;
  154. }
  155. this._thickness = value;
  156. this._markAsDirty();
  157. }
  158. /** Gets or sets the background color when focused */
  159. public get focusedBackground(): string {
  160. return this._focusedBackground;
  161. }
  162. public set focusedBackground(value: string) {
  163. if (this._focusedBackground === value) {
  164. return;
  165. }
  166. this._focusedBackground = value;
  167. this._markAsDirty();
  168. }
  169. /** Gets or sets the background color when focused */
  170. public get focusedColor(): string {
  171. return this._focusedColor;
  172. }
  173. public set focusedColor(value: string) {
  174. if (this._focusedColor === value) {
  175. return;
  176. }
  177. this._focusedColor = value;
  178. this._markAsDirty();
  179. }
  180. /** Gets or sets the background color */
  181. public get background(): string {
  182. return this._background;
  183. }
  184. public set background(value: string) {
  185. if (this._background === value) {
  186. return;
  187. }
  188. this._background = value;
  189. this._markAsDirty();
  190. }
  191. /** Gets or sets the placeholder color */
  192. public get placeholderColor(): string {
  193. return this._placeholderColor;
  194. }
  195. public set placeholderColor(value: string) {
  196. if (this._placeholderColor === value) {
  197. return;
  198. }
  199. this._placeholderColor = value;
  200. this._markAsDirty();
  201. }
  202. /** Gets or sets the text displayed when the control is empty */
  203. public get placeholderText(): string {
  204. return this._placeholderText;
  205. }
  206. public set placeholderText(value: string) {
  207. if (this._placeholderText === value) {
  208. return;
  209. }
  210. this._placeholderText = value;
  211. this._markAsDirty();
  212. }
  213. /** Gets or sets the dead key flag */
  214. public get deadKey(): boolean {
  215. return this._deadKey;
  216. }
  217. public set deadKey(flag: boolean) {
  218. this._deadKey = flag;
  219. }
  220. /** Gets or sets the highlight text */
  221. public get highlightedText(): string {
  222. return this._highlightedText;
  223. }
  224. public set highlightedText(text: string) {
  225. if (this._highlightedText === text) {
  226. return;
  227. }
  228. this._highlightedText = text;
  229. this._markAsDirty();
  230. }
  231. /** Gets or sets if the current key should be added */
  232. public get addKey(): boolean {
  233. return this._addKey;
  234. }
  235. public set addKey(flag: boolean) {
  236. this._addKey = flag;
  237. }
  238. /** Gets or sets the value of the current key being entered */
  239. public get currentKey(): string {
  240. return this._currentKey;
  241. }
  242. public set currentKey(key: string) {
  243. this._currentKey = key;
  244. }
  245. /** Gets or sets the text displayed in the control */
  246. public get text(): string {
  247. return this._text;
  248. }
  249. public set text(value: string) {
  250. let valueAsString = value.toString(); // Forcing convertion
  251. if (this._text === valueAsString) {
  252. return;
  253. }
  254. this._text = valueAsString;
  255. this._markAsDirty();
  256. this.onTextChangedObservable.notifyObservers(this);
  257. }
  258. /** Gets or sets control width */
  259. public get width(): string | number {
  260. return this._width.toString(this._host);
  261. }
  262. public set width(value: string | number) {
  263. if (this._width.toString(this._host) === value) {
  264. return;
  265. }
  266. if (this._width.fromString(value)) {
  267. this._markAsDirty();
  268. }
  269. this.autoStretchWidth = false;
  270. }
  271. /**
  272. * Creates a new InputText
  273. * @param name defines the control name
  274. * @param text defines the text of the control
  275. */
  276. constructor(public name?: string, text: string = "") {
  277. super(name);
  278. this.text = text;
  279. this.isPointerBlocker = true;
  280. }
  281. /** @hidden */
  282. public onBlur(): void {
  283. this._isFocused = false;
  284. this._scrollLeft = null;
  285. this._cursorOffset = 0;
  286. clearTimeout(this._blinkTimeout);
  287. this._markAsDirty();
  288. this.onBlurObservable.notifyObservers(this);
  289. this._host.unRegisterClipboardEvents();
  290. if (this._onClipboardObserver) {
  291. this._host.onClipboardObservable.remove(this._onClipboardObserver);
  292. }
  293. let scene = this._host.getScene();
  294. if (this._onPointerDblTapObserver && scene) {
  295. scene.onPointerObservable.remove(this._onPointerDblTapObserver);
  296. }
  297. }
  298. /** @hidden */
  299. public onFocus(): void {
  300. if (!this._isEnabled) {
  301. return;
  302. }
  303. this._scrollLeft = null;
  304. this._isFocused = true;
  305. this._blinkIsEven = false;
  306. this._cursorOffset = 0;
  307. this._markAsDirty();
  308. this.onFocusObservable.notifyObservers(this);
  309. if (navigator.userAgent.indexOf("Mobile") !== -1 && !this.disableMobilePrompt) {
  310. let value = prompt(this.promptMessage);
  311. if (value !== null) {
  312. this.text = value;
  313. }
  314. this._host.focusedControl = null;
  315. return;
  316. }
  317. this._host.registerClipboardEvents();
  318. this._onClipboardObserver = this._host.onClipboardObservable.add((clipboardInfo) => {
  319. // process clipboard event, can be configured.
  320. switch (clipboardInfo.type) {
  321. case ClipboardEventTypes.COPY:
  322. this._onCopyText(clipboardInfo.event);
  323. this.onTextCopyObservable.notifyObservers(this);
  324. break;
  325. case ClipboardEventTypes.CUT:
  326. this._onCutText(clipboardInfo.event);
  327. this.onTextCutObservable.notifyObservers(this);
  328. break;
  329. case ClipboardEventTypes.PASTE:
  330. this._onPasteText(clipboardInfo.event);
  331. this.onTextPasteObservable.notifyObservers(this);
  332. break;
  333. default: return;
  334. }
  335. });
  336. let scene = this._host.getScene();
  337. if (scene) {
  338. //register the pointer double tap event
  339. this._onPointerDblTapObserver = scene.onPointerObservable.add((pointerInfo) => {
  340. if (!this._isFocused) {
  341. return;
  342. }
  343. if (pointerInfo.type === PointerEventTypes.POINTERDOUBLETAP) {
  344. this._processDblClick(pointerInfo);
  345. }
  346. });
  347. }
  348. if (this._onFocusSelectAll) {
  349. this._selectAllText();
  350. }
  351. }
  352. protected _getTypeName(): string {
  353. return "InputText";
  354. }
  355. /**
  356. * Function called to get the list of controls that should not steal the focus from this control
  357. * @returns an array of controls
  358. */
  359. public keepsFocusWith(): Nullable<Control[]> {
  360. if (!this._connectedVirtualKeyboard) {
  361. return null;
  362. }
  363. return [this._connectedVirtualKeyboard];
  364. }
  365. /** @hidden */
  366. public processKey(keyCode: number, key?: string, evt?: KeyboardEvent) {
  367. //return if clipboard event keys (i.e -ctr/cmd + c,v,x)
  368. if (evt && (evt.ctrlKey || evt.metaKey) && (keyCode === 67 || keyCode === 86 || keyCode === 88)) {
  369. return;
  370. }
  371. //select all
  372. if (evt && (evt.ctrlKey || evt.metaKey) && keyCode === 65) {
  373. this._selectAllText();
  374. evt.preventDefault();
  375. return;
  376. }
  377. // Specific cases
  378. switch (keyCode) {
  379. case 32: //SPACE
  380. key = " "; //ie11 key for space is "Spacebar"
  381. break;
  382. case 191: //SLASH
  383. if (evt) {
  384. evt.preventDefault();
  385. }
  386. break;
  387. case 8: // BACKSPACE
  388. if (this._text && this._text.length > 0) {
  389. //delete the highlighted text
  390. if (this._isTextHighlightOn) {
  391. this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
  392. this._isTextHighlightOn = false;
  393. this._cursorOffset = this.text.length - this._startHighlightIndex;
  394. this._blinkIsEven = false;
  395. if (evt) {
  396. evt.preventDefault();
  397. }
  398. return;
  399. }
  400. //delete single character
  401. if (this._cursorOffset === 0) {
  402. this.text = this._text.substr(0, this._text.length - 1);
  403. } else {
  404. let deletePosition = this._text.length - this._cursorOffset;
  405. if (deletePosition > 0) {
  406. this.text = this._text.slice(0, deletePosition - 1) + this._text.slice(deletePosition);
  407. }
  408. }
  409. }
  410. if (evt) {
  411. evt.preventDefault();
  412. }
  413. return;
  414. case 46: // DELETE
  415. if (this._isTextHighlightOn) {
  416. this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
  417. let decrementor = (this._endHighlightIndex - this._startHighlightIndex);
  418. while (decrementor > 0 && this._cursorOffset > 0) {
  419. this._cursorOffset--;
  420. }
  421. this._isTextHighlightOn = false;
  422. this._cursorOffset = this.text.length - this._startHighlightIndex;
  423. if (evt) {
  424. evt.preventDefault();
  425. }
  426. return;
  427. }
  428. if (this._text && this._text.length > 0 && this._cursorOffset > 0) {
  429. let deletePosition = this._text.length - this._cursorOffset;
  430. this.text = this._text.slice(0, deletePosition) + this._text.slice(deletePosition + 1);
  431. this._cursorOffset--;
  432. }
  433. if (evt) {
  434. evt.preventDefault();
  435. }
  436. return;
  437. case 13: // RETURN
  438. this._host.focusedControl = null;
  439. this._isTextHighlightOn = false;
  440. return;
  441. case 35: // END
  442. this._cursorOffset = 0;
  443. this._blinkIsEven = false;
  444. this._isTextHighlightOn = false;
  445. this._markAsDirty();
  446. return;
  447. case 36: // HOME
  448. this._cursorOffset = this._text.length;
  449. this._blinkIsEven = false;
  450. this._isTextHighlightOn = false;
  451. this._markAsDirty();
  452. return;
  453. case 37: // LEFT
  454. this._cursorOffset++;
  455. if (this._cursorOffset > this._text.length) {
  456. this._cursorOffset = this._text.length;
  457. }
  458. if (evt && evt.shiftKey) {
  459. // update the cursor
  460. this._blinkIsEven = false;
  461. // shift + ctrl/cmd + <-
  462. if (evt.ctrlKey || evt.metaKey) {
  463. if (!this._isTextHighlightOn) {
  464. if (this._text.length === this._cursorOffset) {
  465. return;
  466. }
  467. else {
  468. this._endHighlightIndex = this._text.length - this._cursorOffset + 1;
  469. }
  470. }
  471. this._startHighlightIndex = 0;
  472. this._cursorIndex = this._text.length - this._endHighlightIndex;
  473. this._cursorOffset = this._text.length;
  474. this._isTextHighlightOn = true;
  475. this._markAsDirty();
  476. return;
  477. }
  478. //store the starting point
  479. if (!this._isTextHighlightOn) {
  480. this._isTextHighlightOn = true;
  481. this._cursorIndex = (this._cursorOffset >= this._text.length) ? this._text.length : this._cursorOffset - 1;
  482. }
  483. //if text is already highlighted
  484. else if (this._cursorIndex === -1) {
  485. this._cursorIndex = this._text.length - this._endHighlightIndex;
  486. this._cursorOffset = (this._startHighlightIndex === 0) ? this._text.length : this._text.length - this._startHighlightIndex + 1;
  487. }
  488. //set the highlight indexes
  489. if (this._cursorIndex < this._cursorOffset) {
  490. this._endHighlightIndex = this._text.length - this._cursorIndex;
  491. this._startHighlightIndex = this._text.length - this._cursorOffset;
  492. }
  493. else if (this._cursorIndex > this._cursorOffset) {
  494. this._endHighlightIndex = this._text.length - this._cursorOffset;
  495. this._startHighlightIndex = this._text.length - this._cursorIndex;
  496. }
  497. else {
  498. this._isTextHighlightOn = false;
  499. }
  500. this._markAsDirty();
  501. return;
  502. }
  503. if (this._isTextHighlightOn) {
  504. this._cursorOffset = this._text.length - this._startHighlightIndex;
  505. this._isTextHighlightOn = false;
  506. }
  507. if (evt && (evt.ctrlKey || evt.metaKey)) {
  508. this._cursorOffset = this.text.length;
  509. evt.preventDefault();
  510. }
  511. this._blinkIsEven = false;
  512. this._isTextHighlightOn = false;
  513. this._cursorIndex = -1;
  514. this._markAsDirty();
  515. return;
  516. case 39: // RIGHT
  517. this._cursorOffset--;
  518. if (this._cursorOffset < 0) {
  519. this._cursorOffset = 0;
  520. }
  521. if (evt && evt.shiftKey) {
  522. //update the cursor
  523. this._blinkIsEven = false;
  524. //shift + ctrl/cmd + ->
  525. if (evt.ctrlKey || evt.metaKey) {
  526. if (!this._isTextHighlightOn) {
  527. if (this._cursorOffset === 0) {
  528. return;
  529. }
  530. else {
  531. this._startHighlightIndex = this._text.length - this._cursorOffset - 1;
  532. }
  533. }
  534. this._endHighlightIndex = this._text.length;
  535. this._isTextHighlightOn = true;
  536. this._cursorIndex = this._text.length - this._startHighlightIndex;
  537. this._cursorOffset = 0;
  538. this._markAsDirty();
  539. return;
  540. }
  541. if (!this._isTextHighlightOn) {
  542. this._isTextHighlightOn = true;
  543. this._cursorIndex = (this._cursorOffset <= 0) ? 0 : this._cursorOffset + 1;
  544. }
  545. //if text is already highlighted
  546. else if (this._cursorIndex === -1) {
  547. this._cursorIndex = this._text.length - this._startHighlightIndex;
  548. this._cursorOffset = (this._text.length === this._endHighlightIndex) ? 0 : this._text.length - this._endHighlightIndex - 1;
  549. }
  550. //set the highlight indexes
  551. if (this._cursorIndex < this._cursorOffset) {
  552. this._endHighlightIndex = this._text.length - this._cursorIndex;
  553. this._startHighlightIndex = this._text.length - this._cursorOffset;
  554. }
  555. else if (this._cursorIndex > this._cursorOffset) {
  556. this._endHighlightIndex = this._text.length - this._cursorOffset;
  557. this._startHighlightIndex = this._text.length - this._cursorIndex;
  558. }
  559. else {
  560. this._isTextHighlightOn = false;
  561. }
  562. this._markAsDirty();
  563. return;
  564. }
  565. if (this._isTextHighlightOn) {
  566. this._cursorOffset = this._text.length - this._endHighlightIndex;
  567. this._isTextHighlightOn = false;
  568. }
  569. //ctr + ->
  570. if (evt && (evt.ctrlKey || evt.metaKey)) {
  571. this._cursorOffset = 0;
  572. evt.preventDefault();
  573. }
  574. this._blinkIsEven = false;
  575. this._isTextHighlightOn = false;
  576. this._cursorIndex = -1;
  577. this._markAsDirty();
  578. return;
  579. case 222: // Dead
  580. if (evt) {
  581. evt.preventDefault();
  582. }
  583. this._cursorIndex = -1;
  584. this.deadKey = true;
  585. break;
  586. }
  587. // Printable characters
  588. if (key &&
  589. ((keyCode === -1) || // Direct access
  590. (keyCode === 32) || // Space
  591. (keyCode > 47 && keyCode < 64) || // Numbers
  592. (keyCode > 64 && keyCode < 91) || // Letters
  593. (keyCode > 159 && keyCode < 193) || // Special characters
  594. (keyCode > 218 && keyCode < 223) || // Special characters
  595. (keyCode > 95 && keyCode < 112))) { // Numpad
  596. this._currentKey = key;
  597. this.onBeforeKeyAddObservable.notifyObservers(this);
  598. key = this._currentKey;
  599. if (this._addKey) {
  600. if (this._isTextHighlightOn) {
  601. this.text = this._text.slice(0, this._startHighlightIndex) + key + this._text.slice(this._endHighlightIndex);
  602. this._cursorOffset = this.text.length - (this._startHighlightIndex + 1);
  603. this._isTextHighlightOn = false;
  604. this._blinkIsEven = false;
  605. this._markAsDirty();
  606. }
  607. else if (this._cursorOffset === 0) {
  608. this.text += key;
  609. } else {
  610. let insertPosition = this._text.length - this._cursorOffset;
  611. this.text = this._text.slice(0, insertPosition) + key + this._text.slice(insertPosition);
  612. }
  613. }
  614. }
  615. }
  616. /** @hidden */
  617. private _updateValueFromCursorIndex(offset: number) {
  618. //update the cursor
  619. this._blinkIsEven = false;
  620. if (this._cursorIndex === -1) {
  621. this._cursorIndex = offset;
  622. } else {
  623. if (this._cursorIndex < this._cursorOffset) {
  624. this._endHighlightIndex = this._text.length - this._cursorIndex;
  625. this._startHighlightIndex = this._text.length - this._cursorOffset;
  626. }
  627. else if (this._cursorIndex > this._cursorOffset) {
  628. this._endHighlightIndex = this._text.length - this._cursorOffset;
  629. this._startHighlightIndex = this._text.length - this._cursorIndex;
  630. }
  631. else {
  632. this._isTextHighlightOn = false;
  633. this._markAsDirty();
  634. return;
  635. }
  636. }
  637. this._isTextHighlightOn = true;
  638. this._markAsDirty();
  639. }
  640. /** @hidden */
  641. private _processDblClick(evt: PointerInfo) {
  642. //pre-find the start and end index of the word under cursor, speeds up the rendering
  643. this._startHighlightIndex = this._text.length - this._cursorOffset;
  644. this._endHighlightIndex = this._startHighlightIndex;
  645. let rWord = /\w+/g, moveLeft, moveRight;
  646. do {
  647. moveRight = this._endHighlightIndex < this._text.length && (this._text[this._endHighlightIndex].search(rWord) !== -1) ? ++this._endHighlightIndex : 0;
  648. moveLeft = this._startHighlightIndex > 0 && (this._text[this._startHighlightIndex - 1].search(rWord) !== -1) ? --this._startHighlightIndex : 0;
  649. } while (moveLeft || moveRight);
  650. this._cursorOffset = this.text.length - this._startHighlightIndex;
  651. this.onTextHighlightObservable.notifyObservers(this);
  652. this._isTextHighlightOn = true;
  653. this._clickedCoordinate = null;
  654. this._blinkIsEven = true;
  655. this._cursorIndex = -1;
  656. this._markAsDirty();
  657. }
  658. /** @hidden */
  659. private _selectAllText() {
  660. this._blinkIsEven = true;
  661. this._isTextHighlightOn = true;
  662. this._startHighlightIndex = 0;
  663. this._endHighlightIndex = this._text.length;
  664. this._cursorOffset = this._text.length;
  665. this._cursorIndex = -1;
  666. this._markAsDirty();
  667. }
  668. /**
  669. * Handles the keyboard event
  670. * @param evt Defines the KeyboardEvent
  671. */
  672. public processKeyboard(evt: KeyboardEvent): void {
  673. // process pressed key
  674. this.processKey(evt.keyCode, evt.key, evt);
  675. this.onKeyboardEventProcessedObservable.notifyObservers(evt);
  676. }
  677. /** @hidden */
  678. private _onCopyText(ev: ClipboardEvent): void {
  679. this._isTextHighlightOn = false;
  680. //when write permission to clipbaord data is denied
  681. try {
  682. ev.clipboardData && ev.clipboardData.setData("text/plain", this._highlightedText);
  683. }
  684. catch { } //pass
  685. this._host.clipboardData = this._highlightedText;
  686. }
  687. /** @hidden */
  688. private _onCutText(ev: ClipboardEvent): void {
  689. if (!this._highlightedText) {
  690. return;
  691. }
  692. this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
  693. this._isTextHighlightOn = false;
  694. this._cursorOffset = this.text.length - this._startHighlightIndex;
  695. //when write permission to clipbaord data is denied
  696. try {
  697. ev.clipboardData && ev.clipboardData.setData("text/plain", this._highlightedText);
  698. }
  699. catch { } //pass
  700. this._host.clipboardData = this._highlightedText;
  701. this._highlightedText = "";
  702. }
  703. /** @hidden */
  704. private _onPasteText(ev: ClipboardEvent): void {
  705. let data: string = "";
  706. if (ev.clipboardData && ev.clipboardData.types.indexOf("text/plain") !== -1) {
  707. data = ev.clipboardData.getData("text/plain");
  708. }
  709. else {
  710. //get the cached data; returns blank string by default
  711. data = this._host.clipboardData;
  712. }
  713. let insertPosition = this._text.length - this._cursorOffset;
  714. this.text = this._text.slice(0, insertPosition) + data + this._text.slice(insertPosition);
  715. }
  716. public _draw(context: CanvasRenderingContext2D, invalidatedRectangle?: Nullable<Measure>): void {
  717. context.save();
  718. this._applyStates(context);
  719. if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
  720. context.shadowColor = this.shadowColor;
  721. context.shadowBlur = this.shadowBlur;
  722. context.shadowOffsetX = this.shadowOffsetX;
  723. context.shadowOffsetY = this.shadowOffsetY;
  724. }
  725. // Background
  726. if (this._isFocused) {
  727. if (this._focusedBackground) {
  728. context.fillStyle = this._isEnabled ? this._focusedBackground : this._disabledColor;
  729. context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  730. }
  731. } else if (this._background) {
  732. context.fillStyle = this._isEnabled ? this._background : this._disabledColor;
  733. context.fillRect(this._currentMeasure.left, this._currentMeasure.top, this._currentMeasure.width, this._currentMeasure.height);
  734. }
  735. if (this.shadowBlur || this.shadowOffsetX || this.shadowOffsetY) {
  736. context.shadowBlur = 0;
  737. context.shadowOffsetX = 0;
  738. context.shadowOffsetY = 0;
  739. }
  740. if (!this._fontOffset) {
  741. this._fontOffset = Control._GetFontOffset(context.font);
  742. }
  743. // Text
  744. let clipTextLeft = this._currentMeasure.left + this._margin.getValueInPixel(this._host, this._tempParentMeasure.width);
  745. if (this.color) {
  746. context.fillStyle = this.color;
  747. }
  748. let text = this._beforeRenderText(this._text);
  749. if (!this._isFocused && !this._text && this._placeholderText) {
  750. text = this._placeholderText;
  751. if (this._placeholderColor) {
  752. context.fillStyle = this._placeholderColor;
  753. }
  754. }
  755. this._textWidth = context.measureText(text).width;
  756. let marginWidth = this._margin.getValueInPixel(this._host, this._tempParentMeasure.width) * 2;
  757. if (this._autoStretchWidth) {
  758. this.width = Math.min(this._maxWidth.getValueInPixel(this._host, this._tempParentMeasure.width), this._textWidth + marginWidth) + "px";
  759. }
  760. let rootY = this._fontOffset.ascent + (this._currentMeasure.height - this._fontOffset.height) / 2;
  761. let availableWidth = this._width.getValueInPixel(this._host, this._tempParentMeasure.width) - marginWidth;
  762. context.save();
  763. context.beginPath();
  764. context.rect(clipTextLeft, this._currentMeasure.top + (this._currentMeasure.height - this._fontOffset.height) / 2, availableWidth + 2, this._currentMeasure.height);
  765. context.clip();
  766. if (this._isFocused && this._textWidth > availableWidth) {
  767. let textLeft = clipTextLeft - this._textWidth + availableWidth;
  768. if (!this._scrollLeft) {
  769. this._scrollLeft = textLeft;
  770. }
  771. } else {
  772. this._scrollLeft = clipTextLeft;
  773. }
  774. context.fillText(text, this._scrollLeft, this._currentMeasure.top + rootY);
  775. // Cursor
  776. if (this._isFocused) {
  777. // Need to move cursor
  778. if (this._clickedCoordinate) {
  779. var rightPosition = this._scrollLeft + this._textWidth;
  780. var absoluteCursorPosition = rightPosition - this._clickedCoordinate;
  781. var currentSize = 0;
  782. this._cursorOffset = 0;
  783. var previousDist = 0;
  784. do {
  785. if (this._cursorOffset) {
  786. previousDist = Math.abs(absoluteCursorPosition - currentSize);
  787. }
  788. this._cursorOffset++;
  789. currentSize = context.measureText(text.substr(text.length - this._cursorOffset, this._cursorOffset)).width;
  790. } while (currentSize < absoluteCursorPosition && (text.length >= this._cursorOffset));
  791. // Find closest move
  792. if (Math.abs(absoluteCursorPosition - currentSize) > previousDist) {
  793. this._cursorOffset--;
  794. }
  795. this._blinkIsEven = false;
  796. this._clickedCoordinate = null;
  797. }
  798. // Render cursor
  799. if (!this._blinkIsEven) {
  800. let cursorOffsetText = this.text.substr(this._text.length - this._cursorOffset);
  801. let cursorOffsetWidth = context.measureText(cursorOffsetText).width;
  802. let cursorLeft = this._scrollLeft + this._textWidth - cursorOffsetWidth;
  803. if (cursorLeft < clipTextLeft) {
  804. this._scrollLeft += (clipTextLeft - cursorLeft);
  805. cursorLeft = clipTextLeft;
  806. this._markAsDirty();
  807. } else if (cursorLeft > clipTextLeft + availableWidth) {
  808. this._scrollLeft += (clipTextLeft + availableWidth - cursorLeft);
  809. cursorLeft = clipTextLeft + availableWidth;
  810. this._markAsDirty();
  811. }
  812. if (!this._isTextHighlightOn) {
  813. context.fillRect(cursorLeft, this._currentMeasure.top + (this._currentMeasure.height - this._fontOffset.height) / 2, 2, this._fontOffset.height);
  814. }
  815. }
  816. clearTimeout(this._blinkTimeout);
  817. this._blinkTimeout = <any>setTimeout(() => {
  818. this._blinkIsEven = !this._blinkIsEven;
  819. this._markAsDirty();
  820. }, 500);
  821. //show the highlighted text
  822. if (this._isTextHighlightOn) {
  823. clearTimeout(this._blinkTimeout);
  824. let highlightCursorOffsetWidth = context.measureText(this.text.substring(this._startHighlightIndex)).width;
  825. let highlightCursorLeft = this._scrollLeft + this._textWidth - highlightCursorOffsetWidth;
  826. this._highlightedText = this.text.substring(this._startHighlightIndex, this._endHighlightIndex);
  827. let width = context.measureText(this.text.substring(this._startHighlightIndex, this._endHighlightIndex)).width;
  828. if (highlightCursorLeft < clipTextLeft) {
  829. width = width - (clipTextLeft - highlightCursorLeft);
  830. if (!width) {
  831. // when using left arrow on text.length > availableWidth;
  832. // assigns the width of the first letter after clipTextLeft
  833. width = context.measureText(this.text.charAt(this.text.length - this._cursorOffset)).width;
  834. }
  835. highlightCursorLeft = clipTextLeft;
  836. }
  837. //for transparancy
  838. context.globalAlpha = this._highligherOpacity;
  839. context.fillStyle = this._textHighlightColor;
  840. context.fillRect(highlightCursorLeft, this._currentMeasure.top + (this._currentMeasure.height - this._fontOffset.height) / 2, width, this._fontOffset.height);
  841. context.globalAlpha = 1.0;
  842. }
  843. }
  844. context.restore();
  845. // Border
  846. if (this._thickness) {
  847. if (this._isFocused) {
  848. if (this.focusedColor) {
  849. context.strokeStyle = this.focusedColor;
  850. }
  851. } else {
  852. if (this.color) {
  853. context.strokeStyle = this.color;
  854. }
  855. }
  856. context.lineWidth = this._thickness;
  857. context.strokeRect(this._currentMeasure.left + this._thickness / 2, this._currentMeasure.top + this._thickness / 2,
  858. this._currentMeasure.width - this._thickness, this._currentMeasure.height - this._thickness);
  859. }
  860. context.restore();
  861. }
  862. public _onPointerDown(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number, pi: PointerInfoBase): boolean {
  863. if (!super._onPointerDown(target, coordinates, pointerId, buttonIndex, pi)) {
  864. return false;
  865. }
  866. this._clickedCoordinate = coordinates.x;
  867. this._isTextHighlightOn = false;
  868. this._highlightedText = "";
  869. this._cursorIndex = -1;
  870. this._isPointerDown = true;
  871. this._host._capturingControl[pointerId] = this;
  872. if (this._host.focusedControl === this) {
  873. // Move cursor
  874. clearTimeout(this._blinkTimeout);
  875. this._markAsDirty();
  876. return true;
  877. }
  878. if (!this._isEnabled) {
  879. return false;
  880. }
  881. this._host.focusedControl = this;
  882. return true;
  883. }
  884. public _onPointerMove(target: Control, coordinates: Vector2, pointerId: number, pi: PointerInfoBase): void {
  885. if (this._host.focusedControl === this && this._isPointerDown) {
  886. this._clickedCoordinate = coordinates.x;
  887. this._markAsDirty();
  888. this._updateValueFromCursorIndex(this._cursorOffset);
  889. }
  890. super._onPointerMove(target, coordinates, pointerId, pi);
  891. }
  892. public _onPointerUp(target: Control, coordinates: Vector2, pointerId: number, buttonIndex: number, notifyClick: boolean): void {
  893. this._isPointerDown = false;
  894. delete this._host._capturingControl[pointerId];
  895. super._onPointerUp(target, coordinates, pointerId, buttonIndex, notifyClick);
  896. }
  897. protected _beforeRenderText(text: string): string {
  898. return text;
  899. }
  900. public dispose() {
  901. super.dispose();
  902. this.onBlurObservable.clear();
  903. this.onFocusObservable.clear();
  904. this.onTextChangedObservable.clear();
  905. this.onTextCopyObservable.clear();
  906. this.onTextCutObservable.clear();
  907. this.onTextPasteObservable.clear();
  908. this.onTextHighlightObservable.clear();
  909. this.onKeyboardEventProcessedObservable.clear();
  910. }
  911. }
  912. _TypeStore.RegisteredTypes["BABYLON.GUI.InputText"] = InputText;