inputText.ts 42 KB

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