inputText.ts 38 KB

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