@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
442 lines (440 loc) • 14.1 kB
JavaScript
/**
* 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}
=${this._handleChange}
=${this._handleFocus}
=${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"
=${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}
=${() => 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}
=${() => this._handleSave(false)}
=${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"
=${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
})} =${this._handleBlur}>
${this._renderInput()}
<div
class="${blockClass}__toolbar"
part="actions"
=${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