UNPKG

@carbon/ibm-products-web-components

Version:
442 lines (440 loc) 14.1 kB
/** * Copyright IBM Corp. 2020, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import "../../globals/settings.js"; import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.js"; import edit_in_place_default$1 from "./edit-in-place.scss.js"; import { classMap } from "lit/directives/class-map.js"; import { LitElement, html } from "lit"; import { property, query, state } from "lit/decorators.js"; import { carbonElement } from "@carbon/web-components/es/globals/decorators/carbon-element.js"; import Close from "@carbon/icons/es/close/16"; import { iconLoader } from "@carbon/web-components/es/globals/internal/icon-loader.js"; import '@carbon/web-components/es-custom/components/icon-button/index.js'; import '@carbon/web-components/es-custom/components/tooltip/index.js'; import Edit16 from "@carbon/icons/es/edit/16"; import EditOff16 from "@carbon/icons/es/edit--off/16"; import Checkmark16 from "@carbon/icons/es/checkmark/16"; import WarningFilled16 from "@carbon/icons/es/warning--filled/16"; //#region src/components/edit-in-place/edit-in-place.ts /** * @license * * Copyright IBM Corp. 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const blockClass = `c4p--edit-in-place`; /** * EditInPlace component for inline text editing. * * @element c4p-edit-in-place * @csspart input - The input element * @csspart actions - The button container * @csspart invalid-text - The error message container * @fires c4p-edit-in-place-change - Fired when the input value changes * @fires c4p-edit-in-place-save - Fired when the save button is clicked or Enter is pressed * @fires c4p-edit-in-place-cancel - Fired when the cancel button is clicked or Escape is pressed * @fires c4p-edit-in-place-blur - Fired when the input loses focus (if onBlur handler is used) */ let C4PEditInPlace = class C4PEditInPlace extends LitElement { constructor(..._args) { super(..._args); this.cancelLabel = "Cancel"; this.editAlwaysVisible = false; this.editLabel = "Edit"; this.id = ""; this.inheritTypography = false; this.invalid = false; this.invalidText = ""; this.labelText = ""; this.placeholder = ""; this.readOnly = false; this.readOnlyLabel = "Edit off"; this.readOnlyToggleTipText = "This field is read-only and cannot be edited"; this.saveLabel = "Save"; this.size = "sm"; this.toggleTipAlignment = "bottom"; this.tooltipAlignment = "top"; this.value = ""; this.defaultValue = ""; this._focused = false; this._internalValue = ""; this._initialValue = ""; this._dirtyInput = false; this._escaping = false; this._clickingWithin = false; this._boundHandleDocumentClick = null; } /** * Check if value has changed from initial */ get _hasValueChanged() { return this._internalValue.trim() !== this._initialValue.trim(); } /** * Check if we can save (value changed and not invalid) */ get _canSave() { return this._hasValueChanged && !this.invalid; } /** * Check if we can cancel (value has changed) */ get _canCancel() { return this._hasValueChanged; } /** * Handle input change */ _handleChange(e) { const input = e.target; if (!this._dirtyInput) this._dirtyInput = true; this._internalValue = input.value; const init = { bubbles: true, composed: true, detail: { value: this._internalValue } }; this.dispatchEvent(new CustomEvent(`c4p-edit-in-place-change`, init)); } /** * Handle focus */ _handleFocus(e) { const relatedTarget = e.relatedTarget; if (!this.contains(relatedTarget)) requestAnimationFrame(() => { this._inputElement?.focus(); }); this._focused = true; } /** * Handle save */ _handleSave(exitEditMode = false) { this._initialValue = this._internalValue; this._dirtyInput = false; const init = { bubbles: true, composed: true, detail: { value: this._internalValue } }; this.dispatchEvent(new CustomEvent(`c4p-edit-in-place-save`, init)); if (exitEditMode) this._focused = false; else requestAnimationFrame(() => { this._inputElement?.focus(); }); } /** * Handle cancel */ _handleCancel(exitEditMode = false) { this._dirtyInput = false; this._internalValue = this._initialValue; const init = { bubbles: true, composed: true, detail: { value: this._initialValue } }; this.dispatchEvent(new CustomEvent(`c4p-edit-in-place-cancel`, init)); if (exitEditMode) this._focused = false; else requestAnimationFrame(() => { if (this._inputElement) { this._inputElement.focus(); const length = this._inputElement.value.length; this._inputElement.setSelectionRange(length, length); } }); } /** * Handle blur */ _handleBlur(e) { const relatedTarget = e.relatedTarget; const clickedWithin = this._clickingWithin; const targetingChild = this.contains(relatedTarget); if (clickedWithin) { this._clickingWithin = false; if (targetingChild) return; } if (!clickedWithin && targetingChild) return; if (this._escaping) return; if (this._canSave) this._handleSave(true); else this._handleCancel(true); const init = { bubbles: true, composed: true, detail: { value: this._initialValue } }; this.dispatchEvent(new CustomEvent(`c4p-edit-in-place-blur`, init)); } /** * Handle keyboard events */ _handleKeyDown(e) { switch (e.key) { case "Escape": this._escaping = true; this._inputElement?.blur(); this._handleCancel(false); this._escaping = false; break; case "Enter": this._escaping = true; this._inputElement?.blur(); if (this._canSave) this._handleSave(false); this._escaping = false; break; case "Tab": if (!e.shiftKey && !this._hasValueChanged) this._handleCancel(true); break; default: break; } } /** * Handle toolbar mousedown to track button clicks * Uses composedPath() to properly handle Shadow DOM boundaries */ _handleToolbarMouseDown(e) { const foundButton = e.composedPath().find((el) => el instanceof HTMLElement && (el.tagName === "BUTTON" || el.tagName === "CDS-ICON-BUTTON")); if (foundButton) { this._clickingWithin = true; if (foundButton.hasAttribute("disabled") || foundButton.getAttribute("aria-disabled") === "true") e.preventDefault(); } } /** * Handle keydown on save button to exit edit mode on Tab */ _handleSaveButtonKeyDown(e) { if (e.key === "Tab" && !e.shiftKey) { this._focused = false; this._inputElement?.blur(); } } /** * Handle clicks outside the component */ _handleDocumentClick(e) { if (!this._focused) return; if (!e.composedPath().includes(this)) if (this._canSave) this._handleSave(true); else this._handleCancel(true); } /** * Initialize component */ connectedCallback() { super.connectedCallback(); this._internalValue = this.value || this.defaultValue; this._initialValue = this._internalValue; this._boundHandleDocumentClick = this._handleDocumentClick.bind(this); document.addEventListener("click", this._boundHandleDocumentClick, true); } /** * Cleanup component state when removed from DOM */ disconnectedCallback() { super.disconnectedCallback(); this._escaping = false; this._clickingWithin = false; this._focused = false; if (this._boundHandleDocumentClick) { document.removeEventListener("click", this._boundHandleDocumentClick, true); this._boundHandleDocumentClick = null; } } /** * Render the input element */ _renderInput() { const inputClasses = { [`${blockClass}__text-input`]: true, [`cds-custom--text-input`]: true, [`cds-custom--text-input--${this.size}`]: true }; const inputElement = html` <input id=${this.id} class=${classMap(inputClasses)} type="text" part="input" placeholder=${this.placeholder} .value=${this._internalValue} @input=${this._handleChange} @focus=${this._handleFocus} @keydown=${this._handleKeyDown} aria-label=${this.labelText} aria-invalid=${this.invalid ? "true" : "false"} ?readonly=${this.readOnly} /> `; if (this.readOnly) return html` <cds-custom-tooltip align=${this.toggleTipAlignment} class="${blockClass}__toggletip-wrapper" > ${inputElement} <cds-custom-tooltip-content >${this.readOnlyToggleTipText}</cds-custom-tooltip-content > </cds-custom-tooltip> `; return inputElement; } /** * Render action buttons */ _renderActions() { if (this.readOnly) return html` <cds-custom-icon-button class="${blockClass}__btn-readonly" size=${this.size} align=${this.tooltipAlignment} kind="ghost" @click=${this._handleFocus} > ${iconLoader(EditOff16, { slot: "icon" })} <span slot="tooltip-content">${this.readOnlyLabel}</span> </cds-custom-icon-button> `; if (this._focused) return html` <cds-custom-icon-button class="${blockClass}__btn ${blockClass}__btn-cancel" size=${this.size} align=${this.tooltipAlignment} kind="ghost" ?disabled=${!this._canCancel} @click=${() => this._handleCancel(false)} > ${iconLoader(Close, { slot: "icon" })} <span slot="tooltip-content">${this.cancelLabel}</span> </cds-custom-icon-button> <cds-custom-icon-button class="${blockClass}__btn ${blockClass}__btn-save" size=${this.size} align=${this.tooltipAlignment} kind="ghost" ?disabled=${!this._canSave} @click=${() => this._handleSave(false)} @keydown=${this._handleSaveButtonKeyDown} > ${iconLoader(Checkmark16, { slot: "icon" })} <span slot="tooltip-content">${this.saveLabel}</span> </cds-custom-icon-button> `; return html` <cds-custom-icon-button class=${classMap({ [`${blockClass}__btn`]: true, [`${blockClass}__btn-edit`]: true, [`${blockClass}__btn-edit--always-visible`]: this.editAlwaysVisible })} size=${this.size} align=${this.tooltipAlignment} kind="ghost" @click=${this._handleFocus} > ${iconLoader(Edit16, { slot: "icon" })} <span slot="tooltip-content">${this.editLabel}</span> </cds-custom-icon-button> `; } render() { return html` <div class=${classMap({ [`${blockClass}__container`]: true, [`${blockClass}--${this.size}`]: true, [`${blockClass}--focused`]: this._focused, [`${blockClass}--invalid`]: this.invalid, [`${blockClass}--inherit-type`]: this.inheritTypography, [`${blockClass}--readonly`]: this.readOnly })} @blur=${this._handleBlur}> ${this._renderInput()} <div class="${blockClass}__toolbar" part="actions" @mousedown=${this._handleToolbarMouseDown} > ${this.invalid ? iconLoader(WarningFilled16, { class: `${blockClass}__warning-icon` }) : ""} ${this._renderActions()} </div> </div> ${this.invalid ? html`<p class="${blockClass}__warning-text" part="invalid-text"> ${this.invalidText} </p>` : ""} `; } static { this.styles = edit_in_place_default$1; } }; __decorate([property({ type: String, attribute: "cancel-label" })], C4PEditInPlace.prototype, "cancelLabel", void 0); __decorate([property({ type: Boolean, attribute: "edit-always-visible" })], C4PEditInPlace.prototype, "editAlwaysVisible", void 0); __decorate([property({ type: String, attribute: "edit-label" })], C4PEditInPlace.prototype, "editLabel", void 0); __decorate([property({ type: String })], C4PEditInPlace.prototype, "id", void 0); __decorate([property({ type: Boolean, attribute: "inherit-typography" })], C4PEditInPlace.prototype, "inheritTypography", void 0); __decorate([property({ type: Boolean })], C4PEditInPlace.prototype, "invalid", void 0); __decorate([property({ type: String, attribute: "invalid-text" })], C4PEditInPlace.prototype, "invalidText", void 0); __decorate([property({ type: String, attribute: "label-text" })], C4PEditInPlace.prototype, "labelText", void 0); __decorate([property({ type: String })], C4PEditInPlace.prototype, "placeholder", void 0); __decorate([property({ type: Boolean, attribute: "read-only" })], C4PEditInPlace.prototype, "readOnly", void 0); __decorate([property({ type: String, attribute: "read-only-label" })], C4PEditInPlace.prototype, "readOnlyLabel", void 0); __decorate([property({ type: String, attribute: "read-only-toggletip-text" })], C4PEditInPlace.prototype, "readOnlyToggleTipText", void 0); __decorate([property({ type: String, attribute: "save-label" })], C4PEditInPlace.prototype, "saveLabel", void 0); __decorate([property({ type: String })], C4PEditInPlace.prototype, "size", void 0); __decorate([property({ type: String, attribute: "toggletip-alignment" })], C4PEditInPlace.prototype, "toggleTipAlignment", void 0); __decorate([property({ type: String, attribute: "tooltip-alignment" })], C4PEditInPlace.prototype, "tooltipAlignment", void 0); __decorate([property({ type: String })], C4PEditInPlace.prototype, "value", void 0); __decorate([property({ type: String, attribute: "default-value" })], C4PEditInPlace.prototype, "defaultValue", void 0); __decorate([state()], C4PEditInPlace.prototype, "_focused", void 0); __decorate([state()], C4PEditInPlace.prototype, "_internalValue", void 0); __decorate([state()], C4PEditInPlace.prototype, "_initialValue", void 0); __decorate([state()], C4PEditInPlace.prototype, "_dirtyInput", void 0); __decorate([query("input")], C4PEditInPlace.prototype, "_inputElement", void 0); C4PEditInPlace = __decorate([carbonElement(`c4p-edit-in-place`)], C4PEditInPlace); var edit_in_place_default = C4PEditInPlace; //#endregion export { edit_in_place_default as default }; //# sourceMappingURL=edit-in-place.js.map