4935649c10d64a4b2726d1ede3d38dd014f4d641675e5a96bff3c4d87f8903f16d8a4f77f4be12cae72467d96371f9840ef4449bc390d5aae61b196938f9b8 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as dom from '../../dom.js';
  6. import { DomEmitter } from '../../event.js';
  7. import { renderFormattedText, renderText } from '../../formattedTextRenderer.js';
  8. import { ActionBar } from '../actionbar/actionbar.js';
  9. import * as aria from '../aria/aria.js';
  10. import { ScrollableElement } from '../scrollbar/scrollableElement.js';
  11. import { Widget } from '../widget.js';
  12. import { Color } from '../../../common/color.js';
  13. import { Emitter, Event } from '../../../common/event.js';
  14. import { HistoryNavigator } from '../../../common/history.js';
  15. import { mixin } from '../../../common/objects.js';
  16. import './inputBox.css';
  17. import * as nls from '../../../../nls.js';
  18. const $ = dom.$;
  19. const defaultOpts = {
  20. inputBackground: Color.fromHex('#3C3C3C'),
  21. inputForeground: Color.fromHex('#CCCCCC'),
  22. inputValidationInfoBorder: Color.fromHex('#55AAFF'),
  23. inputValidationInfoBackground: Color.fromHex('#063B49'),
  24. inputValidationWarningBorder: Color.fromHex('#B89500'),
  25. inputValidationWarningBackground: Color.fromHex('#352A05'),
  26. inputValidationErrorBorder: Color.fromHex('#BE1100'),
  27. inputValidationErrorBackground: Color.fromHex('#5A1D1D')
  28. };
  29. export class InputBox extends Widget {
  30. constructor(container, contextViewProvider, options) {
  31. var _a;
  32. super();
  33. this.state = 'idle';
  34. this.maxHeight = Number.POSITIVE_INFINITY;
  35. this._onDidChange = this._register(new Emitter());
  36. this.onDidChange = this._onDidChange.event;
  37. this._onDidHeightChange = this._register(new Emitter());
  38. this.onDidHeightChange = this._onDidHeightChange.event;
  39. this.contextViewProvider = contextViewProvider;
  40. this.options = options || Object.create(null);
  41. mixin(this.options, defaultOpts, false);
  42. this.message = null;
  43. this.placeholder = this.options.placeholder || '';
  44. this.tooltip = (_a = this.options.tooltip) !== null && _a !== void 0 ? _a : (this.placeholder || '');
  45. this.ariaLabel = this.options.ariaLabel || '';
  46. this.inputBackground = this.options.inputBackground;
  47. this.inputForeground = this.options.inputForeground;
  48. this.inputBorder = this.options.inputBorder;
  49. this.inputValidationInfoBorder = this.options.inputValidationInfoBorder;
  50. this.inputValidationInfoBackground = this.options.inputValidationInfoBackground;
  51. this.inputValidationInfoForeground = this.options.inputValidationInfoForeground;
  52. this.inputValidationWarningBorder = this.options.inputValidationWarningBorder;
  53. this.inputValidationWarningBackground = this.options.inputValidationWarningBackground;
  54. this.inputValidationWarningForeground = this.options.inputValidationWarningForeground;
  55. this.inputValidationErrorBorder = this.options.inputValidationErrorBorder;
  56. this.inputValidationErrorBackground = this.options.inputValidationErrorBackground;
  57. this.inputValidationErrorForeground = this.options.inputValidationErrorForeground;
  58. if (this.options.validationOptions) {
  59. this.validation = this.options.validationOptions.validation;
  60. }
  61. this.element = dom.append(container, $('.monaco-inputbox.idle'));
  62. const tagName = this.options.flexibleHeight ? 'textarea' : 'input';
  63. const wrapper = dom.append(this.element, $('.ibwrapper'));
  64. this.input = dom.append(wrapper, $(tagName + '.input.empty'));
  65. this.input.setAttribute('autocorrect', 'off');
  66. this.input.setAttribute('autocapitalize', 'off');
  67. this.input.setAttribute('spellcheck', 'false');
  68. this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
  69. this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
  70. if (this.options.flexibleHeight) {
  71. this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
  72. this.mirror = dom.append(wrapper, $('div.mirror'));
  73. this.mirror.innerText = '\u00a0';
  74. this.scrollableElement = new ScrollableElement(this.element, { vertical: 1 /* ScrollbarVisibility.Auto */ });
  75. if (this.options.flexibleWidth) {
  76. this.input.setAttribute('wrap', 'off');
  77. this.mirror.style.whiteSpace = 'pre';
  78. this.mirror.style.wordWrap = 'initial';
  79. }
  80. dom.append(container, this.scrollableElement.getDomNode());
  81. this._register(this.scrollableElement);
  82. // from ScrollableElement to DOM
  83. this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
  84. const onSelectionChange = this._register(new DomEmitter(document, 'selectionchange'));
  85. const onAnchoredSelectionChange = Event.filter(onSelectionChange.event, () => {
  86. const selection = document.getSelection();
  87. return (selection === null || selection === void 0 ? void 0 : selection.anchorNode) === wrapper;
  88. });
  89. // from DOM to ScrollableElement
  90. this._register(onAnchoredSelectionChange(this.updateScrollDimensions, this));
  91. this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
  92. }
  93. else {
  94. this.input.type = this.options.type || 'text';
  95. this.input.setAttribute('wrap', 'off');
  96. }
  97. if (this.ariaLabel) {
  98. this.input.setAttribute('aria-label', this.ariaLabel);
  99. }
  100. if (this.placeholder && !this.options.showPlaceholderOnFocus) {
  101. this.setPlaceHolder(this.placeholder);
  102. }
  103. if (this.tooltip) {
  104. this.setTooltip(this.tooltip);
  105. }
  106. this.oninput(this.input, () => this.onValueChange());
  107. this.onblur(this.input, () => this.onBlur());
  108. this.onfocus(this.input, () => this.onFocus());
  109. this.ignoreGesture(this.input);
  110. setTimeout(() => this.updateMirror(), 0);
  111. // Support actions
  112. if (this.options.actions) {
  113. this.actionbar = this._register(new ActionBar(this.element));
  114. this.actionbar.push(this.options.actions, { icon: true, label: false });
  115. }
  116. this.applyStyles();
  117. }
  118. onBlur() {
  119. this._hideMessage();
  120. if (this.options.showPlaceholderOnFocus) {
  121. this.input.setAttribute('placeholder', '');
  122. }
  123. }
  124. onFocus() {
  125. this._showMessage();
  126. if (this.options.showPlaceholderOnFocus) {
  127. this.input.setAttribute('placeholder', this.placeholder || '');
  128. }
  129. }
  130. setPlaceHolder(placeHolder) {
  131. this.placeholder = placeHolder;
  132. this.input.setAttribute('placeholder', placeHolder);
  133. }
  134. setTooltip(tooltip) {
  135. this.tooltip = tooltip;
  136. this.input.title = tooltip;
  137. }
  138. setAriaLabel(label) {
  139. this.ariaLabel = label;
  140. if (label) {
  141. this.input.setAttribute('aria-label', this.ariaLabel);
  142. }
  143. else {
  144. this.input.removeAttribute('aria-label');
  145. }
  146. }
  147. getAriaLabel() {
  148. return this.ariaLabel;
  149. }
  150. get inputElement() {
  151. return this.input;
  152. }
  153. get value() {
  154. return this.input.value;
  155. }
  156. set value(newValue) {
  157. if (this.input.value !== newValue) {
  158. this.input.value = newValue;
  159. this.onValueChange();
  160. }
  161. }
  162. get height() {
  163. return typeof this.cachedHeight === 'number' ? this.cachedHeight : dom.getTotalHeight(this.element);
  164. }
  165. focus() {
  166. this.input.focus();
  167. }
  168. blur() {
  169. this.input.blur();
  170. }
  171. hasFocus() {
  172. return document.activeElement === this.input;
  173. }
  174. select(range = null) {
  175. this.input.select();
  176. if (range) {
  177. this.input.setSelectionRange(range.start, range.end);
  178. if (range.end === this.input.value.length) {
  179. this.input.scrollLeft = this.input.scrollWidth;
  180. }
  181. }
  182. }
  183. isSelectionAtEnd() {
  184. return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
  185. }
  186. enable() {
  187. this.input.removeAttribute('disabled');
  188. }
  189. disable() {
  190. this.blur();
  191. this.input.disabled = true;
  192. this._hideMessage();
  193. }
  194. get width() {
  195. return dom.getTotalWidth(this.input);
  196. }
  197. set width(width) {
  198. if (this.options.flexibleHeight && this.options.flexibleWidth) {
  199. // textarea with horizontal scrolling
  200. let horizontalPadding = 0;
  201. if (this.mirror) {
  202. const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0;
  203. const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0;
  204. horizontalPadding = paddingLeft + paddingRight;
  205. }
  206. this.input.style.width = (width - horizontalPadding) + 'px';
  207. }
  208. else {
  209. this.input.style.width = width + 'px';
  210. }
  211. if (this.mirror) {
  212. this.mirror.style.width = width + 'px';
  213. }
  214. }
  215. set paddingRight(paddingRight) {
  216. // Set width to avoid hint text overlapping buttons
  217. this.input.style.width = `calc(100% - ${paddingRight}px)`;
  218. if (this.mirror) {
  219. this.mirror.style.paddingRight = paddingRight + 'px';
  220. }
  221. }
  222. updateScrollDimensions() {
  223. if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
  224. return;
  225. }
  226. const scrollHeight = this.cachedContentHeight;
  227. const height = this.cachedHeight;
  228. const scrollTop = this.input.scrollTop;
  229. this.scrollableElement.setScrollDimensions({ scrollHeight, height });
  230. this.scrollableElement.setScrollPosition({ scrollTop });
  231. }
  232. showMessage(message, force) {
  233. this.message = message;
  234. this.element.classList.remove('idle');
  235. this.element.classList.remove('info');
  236. this.element.classList.remove('warning');
  237. this.element.classList.remove('error');
  238. this.element.classList.add(this.classForType(message.type));
  239. const styles = this.stylesForType(this.message.type);
  240. this.element.style.border = styles.border ? `1px solid ${styles.border}` : '';
  241. if (this.hasFocus() || force) {
  242. this._showMessage();
  243. }
  244. }
  245. hideMessage() {
  246. this.message = null;
  247. this.element.classList.remove('info');
  248. this.element.classList.remove('warning');
  249. this.element.classList.remove('error');
  250. this.element.classList.add('idle');
  251. this._hideMessage();
  252. this.applyStyles();
  253. }
  254. validate() {
  255. let errorMsg = null;
  256. if (this.validation) {
  257. errorMsg = this.validation(this.value);
  258. if (errorMsg) {
  259. this.inputElement.setAttribute('aria-invalid', 'true');
  260. this.showMessage(errorMsg);
  261. }
  262. else if (this.inputElement.hasAttribute('aria-invalid')) {
  263. this.inputElement.removeAttribute('aria-invalid');
  264. this.hideMessage();
  265. }
  266. }
  267. return errorMsg === null || errorMsg === void 0 ? void 0 : errorMsg.type;
  268. }
  269. stylesForType(type) {
  270. switch (type) {
  271. case 1 /* MessageType.INFO */: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground, foreground: this.inputValidationInfoForeground };
  272. case 2 /* MessageType.WARNING */: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground, foreground: this.inputValidationWarningForeground };
  273. default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground, foreground: this.inputValidationErrorForeground };
  274. }
  275. }
  276. classForType(type) {
  277. switch (type) {
  278. case 1 /* MessageType.INFO */: return 'info';
  279. case 2 /* MessageType.WARNING */: return 'warning';
  280. default: return 'error';
  281. }
  282. }
  283. _showMessage() {
  284. if (!this.contextViewProvider || !this.message) {
  285. return;
  286. }
  287. let div;
  288. const layout = () => div.style.width = dom.getTotalWidth(this.element) + 'px';
  289. this.contextViewProvider.showContextView({
  290. getAnchor: () => this.element,
  291. anchorAlignment: 1 /* AnchorAlignment.RIGHT */,
  292. render: (container) => {
  293. if (!this.message) {
  294. return null;
  295. }
  296. div = dom.append(container, $('.monaco-inputbox-container'));
  297. layout();
  298. const renderOptions = {
  299. inline: true,
  300. className: 'monaco-inputbox-message'
  301. };
  302. const spanElement = (this.message.formatContent
  303. ? renderFormattedText(this.message.content, renderOptions)
  304. : renderText(this.message.content, renderOptions));
  305. spanElement.classList.add(this.classForType(this.message.type));
  306. const styles = this.stylesForType(this.message.type);
  307. spanElement.style.backgroundColor = styles.background ? styles.background.toString() : '';
  308. spanElement.style.color = styles.foreground ? styles.foreground.toString() : '';
  309. spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
  310. dom.append(div, spanElement);
  311. return null;
  312. },
  313. onHide: () => {
  314. this.state = 'closed';
  315. },
  316. layout: layout
  317. });
  318. // ARIA Support
  319. let alertText;
  320. if (this.message.type === 3 /* MessageType.ERROR */) {
  321. alertText = nls.localize('alertErrorMessage', "Error: {0}", this.message.content);
  322. }
  323. else if (this.message.type === 2 /* MessageType.WARNING */) {
  324. alertText = nls.localize('alertWarningMessage', "Warning: {0}", this.message.content);
  325. }
  326. else {
  327. alertText = nls.localize('alertInfoMessage', "Info: {0}", this.message.content);
  328. }
  329. aria.alert(alertText);
  330. this.state = 'open';
  331. }
  332. _hideMessage() {
  333. if (!this.contextViewProvider) {
  334. return;
  335. }
  336. if (this.state === 'open') {
  337. this.contextViewProvider.hideContextView();
  338. }
  339. this.state = 'idle';
  340. }
  341. onValueChange() {
  342. this._onDidChange.fire(this.value);
  343. this.validate();
  344. this.updateMirror();
  345. this.input.classList.toggle('empty', !this.value);
  346. if (this.state === 'open' && this.contextViewProvider) {
  347. this.contextViewProvider.layout();
  348. }
  349. }
  350. updateMirror() {
  351. if (!this.mirror) {
  352. return;
  353. }
  354. const value = this.value;
  355. const lastCharCode = value.charCodeAt(value.length - 1);
  356. const suffix = lastCharCode === 10 ? ' ' : '';
  357. const mirrorTextContent = (value + suffix)
  358. .replace(/\u000c/g, ''); // Don't measure with the form feed character, which messes up sizing
  359. if (mirrorTextContent) {
  360. this.mirror.textContent = value + suffix;
  361. }
  362. else {
  363. this.mirror.innerText = '\u00a0';
  364. }
  365. this.layout();
  366. }
  367. style(styles) {
  368. this.inputBackground = styles.inputBackground;
  369. this.inputForeground = styles.inputForeground;
  370. this.inputBorder = styles.inputBorder;
  371. this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
  372. this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
  373. this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
  374. this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
  375. this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
  376. this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
  377. this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
  378. this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
  379. this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
  380. this.applyStyles();
  381. }
  382. applyStyles() {
  383. const background = this.inputBackground ? this.inputBackground.toString() : '';
  384. const foreground = this.inputForeground ? this.inputForeground.toString() : '';
  385. const border = this.inputBorder ? this.inputBorder.toString() : '';
  386. this.element.style.backgroundColor = background;
  387. this.element.style.color = foreground;
  388. this.input.style.backgroundColor = 'inherit';
  389. this.input.style.color = foreground;
  390. this.element.style.borderWidth = border ? '1px' : '';
  391. this.element.style.borderStyle = border ? 'solid' : '';
  392. this.element.style.borderColor = border;
  393. }
  394. layout() {
  395. if (!this.mirror) {
  396. return;
  397. }
  398. const previousHeight = this.cachedContentHeight;
  399. this.cachedContentHeight = dom.getTotalHeight(this.mirror);
  400. if (previousHeight !== this.cachedContentHeight) {
  401. this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
  402. this.input.style.height = this.cachedHeight + 'px';
  403. this._onDidHeightChange.fire(this.cachedContentHeight);
  404. }
  405. }
  406. insertAtCursor(text) {
  407. const inputElement = this.inputElement;
  408. const start = inputElement.selectionStart;
  409. const end = inputElement.selectionEnd;
  410. const content = inputElement.value;
  411. if (start !== null && end !== null) {
  412. this.value = content.substr(0, start) + text + content.substr(end);
  413. inputElement.setSelectionRange(start + 1, start + 1);
  414. this.layout();
  415. }
  416. }
  417. dispose() {
  418. this._hideMessage();
  419. this.message = null;
  420. if (this.actionbar) {
  421. this.actionbar.dispose();
  422. }
  423. super.dispose();
  424. }
  425. }
  426. export class HistoryInputBox extends InputBox {
  427. constructor(container, contextViewProvider, options) {
  428. const NLS_PLACEHOLDER_HISTORY_HINT = nls.localize({ key: 'history.inputbox.hint', comment: ['Text will be prefixed with \u21C5 plus a single space, then used as a hint where input field keeps history'] }, "for history");
  429. const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX = ` or \u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT}`;
  430. const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = ` (\u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT})`;
  431. super(container, contextViewProvider, options);
  432. this._onDidFocus = this._register(new Emitter());
  433. this.onDidFocus = this._onDidFocus.event;
  434. this._onDidBlur = this._register(new Emitter());
  435. this.onDidBlur = this._onDidBlur.event;
  436. this.history = new HistoryNavigator(options.history, 100);
  437. // Function to append the history suffix to the placeholder if necessary
  438. const addSuffix = () => {
  439. if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
  440. const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
  441. const suffixedPlaceholder = this.placeholder + suffix;
  442. if (options.showPlaceholderOnFocus && document.activeElement !== this.input) {
  443. this.placeholder = suffixedPlaceholder;
  444. }
  445. else {
  446. this.setPlaceHolder(suffixedPlaceholder);
  447. }
  448. }
  449. };
  450. // Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
  451. // and add the history suffix to the placeholder if not yet present
  452. this.observer = new MutationObserver((mutationList, observer) => {
  453. mutationList.forEach((mutation) => {
  454. if (!mutation.target.textContent) {
  455. addSuffix();
  456. }
  457. });
  458. });
  459. this.observer.observe(this.input, { attributeFilter: ['class'] });
  460. this.onfocus(this.input, () => addSuffix());
  461. this.onblur(this.input, () => {
  462. const resetPlaceholder = (historyHint) => {
  463. if (!this.placeholder.endsWith(historyHint)) {
  464. return false;
  465. }
  466. else {
  467. const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
  468. if (options.showPlaceholderOnFocus) {
  469. this.placeholder = revertedPlaceholder;
  470. }
  471. else {
  472. this.setPlaceHolder(revertedPlaceholder);
  473. }
  474. return true;
  475. }
  476. };
  477. if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
  478. resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX);
  479. }
  480. });
  481. }
  482. dispose() {
  483. super.dispose();
  484. if (this.observer) {
  485. this.observer.disconnect();
  486. this.observer = undefined;
  487. }
  488. }
  489. addToHistory() {
  490. if (this.value && this.value !== this.getCurrentValue()) {
  491. this.history.add(this.value);
  492. }
  493. }
  494. showNextValue() {
  495. if (!this.history.has(this.value)) {
  496. this.addToHistory();
  497. }
  498. let next = this.getNextValue();
  499. if (next) {
  500. next = next === this.value ? this.getNextValue() : next;
  501. }
  502. if (next) {
  503. this.value = next;
  504. aria.status(this.value);
  505. }
  506. }
  507. showPreviousValue() {
  508. if (!this.history.has(this.value)) {
  509. this.addToHistory();
  510. }
  511. let previous = this.getPreviousValue();
  512. if (previous) {
  513. previous = previous === this.value ? this.getPreviousValue() : previous;
  514. }
  515. if (previous) {
  516. this.value = previous;
  517. aria.status(this.value);
  518. }
  519. }
  520. onBlur() {
  521. super.onBlur();
  522. this._onDidBlur.fire();
  523. }
  524. onFocus() {
  525. super.onFocus();
  526. this._onDidFocus.fire();
  527. }
  528. getCurrentValue() {
  529. let currentValue = this.history.current();
  530. if (!currentValue) {
  531. currentValue = this.history.last();
  532. this.history.next();
  533. }
  534. return currentValue;
  535. }
  536. getPreviousValue() {
  537. return this.history.previous() || this.history.first();
  538. }
  539. getNextValue() {
  540. return this.history.next() || this.history.last();
  541. }
  542. }