inputText.ts 39 KB

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