inputText.ts 37 KB

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