@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
159 lines (158 loc) • 6.39 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { ExportNs } from '../../esl-utils/environment/export-ns';
import { ESLMixinElement } from '../../esl-mixin-element/core';
import { attr, boolAttr, safe, listen, memoize, prop } from '../../esl-utils/decorators';
import { isElement, isRelativeNode } from '../../esl-utils/dom';
import { ESLResizeObserverTarget } from '../../esl-event-listener/core';
import { ESLMediaRuleList } from '../../esl-media-query/core';
function roundLineHeight(value, precision = 0.1) {
return Math.round(value / precision) * precision;
}
/**
* ESLLineClamp mixin element
* @author Dmytro Shovchko
*
* ESLLineClamp is a custom mixin element that applies line clamping to its content based on the specified number of lines.
* It uses CSS custom properties to control the number of lines and applies truncation when necessary.
* The element also handles focus management and scroll behavior to ensure a smooth user experience.
*/
let ESLLineClamp = class ESLLineClamp extends ESLMixinElement {
constructor() {
super(...arguments);
this.lastHeight = 0;
}
/**
* Returns parsed media rule list for line clamping configuration
* @returns ESLMediaRuleList instance for managing responsive line limits
*/
get linesQuery() {
return ESLMediaRuleList.parse(this.lines, this.mask);
}
get lineHeight() {
const styles = getComputedStyle(this.$host);
const lineHeight = styles.lineHeight;
if (lineHeight !== 'normal')
return parseFloat(lineHeight);
return parseFloat(styles.fontSize) * 1.2;
}
get linesExpected() {
const lines = this.linesQuery.value;
if ((lines === null || lines === void 0 ? void 0 : lines.length) === 0)
return null;
if (lines === 'auto') {
const { maxHeight } = getComputedStyle(this.$host);
const parsedLineHeight = roundLineHeight(this.lineHeight);
const parsedMaxHeight = parseFloat(maxHeight);
const autoLines = Math.floor(parsedMaxHeight / parsedLineHeight);
if (!Number.isFinite(autoLines))
return null;
return autoLines > 0 ? String(autoLines) : null;
}
return lines !== null && lines !== void 0 ? lines : null;
}
connectedCallback() {
super.connectedCallback();
this.onQueryChange();
}
disconnectedCallback() {
super.disconnectedCallback();
memoize.clear(this, 'linesQuery');
}
attributeChangedCallback() {
memoize.clear(this, 'linesQuery');
this.$$on(this.onQueryChange);
this.onQueryChange();
}
updateLines() {
const lines = this.linesExpected;
if (lines) {
this.$host.style.setProperty('--esl-line-clamp', lines);
}
else {
this.$host.style.removeProperty('--esl-line-clamp');
}
}
/** Handles query change and sets the CSS custom property for line clamping */
onQueryChange() {
this.updateLines();
}
/** Handles focus out event and resets scroll position */
onFocusOut(e) {
const $relatedTarget = e.relatedTarget;
if (!isElement($relatedTarget) || this.$host.contains($relatedTarget))
return;
const $target = e.target;
if (isElement($target.$target) && $target.$target.contains($relatedTarget))
return;
this.$host.scrollTo(0, 0);
}
/** Handles resize events to update clamped state */
onResize() {
const { height } = this.$host.getBoundingClientRect();
if (this.linesQuery.value === 'auto' && this.lastHeight !== height) {
this.lastHeight = height;
this.updateLines();
}
const { clientHeight, clientWidth, scrollHeight, scrollWidth } = this.$host;
const clampedY = scrollHeight - clientHeight > this.lineHeight * this.TOLERANCE;
const clampedX = scrollWidth - clientWidth > 1;
this.$$attr('clamped', clampedY || clampedX);
}
/** Observes custom broadcast 'esl:refresh' event to force refresh */
onRefresh({ target }) {
if (isElement(target) && !isRelativeNode(target, this.$host))
return;
this.updateLines();
}
/** Handles show request events to scroll target into view */
onShowRequest(e) {
const $target = e.target;
$target.scrollIntoView();
}
};
ESLLineClamp.is = 'esl-line-clamp';
/** Default mask for media query parsing, can be overridden by setting the static property */
ESLLineClamp.DEFAULT_MASK = '';
__decorate([
prop(0.5)
], ESLLineClamp.prototype, "TOLERANCE", void 0);
__decorate([
boolAttr({ name: 'clamped', readonly: true })
], ESLLineClamp.prototype, "clamped", void 0);
__decorate([
attr({ name: ESLLineClamp.is, defaultValue: '' })
], ESLLineClamp.prototype, "lines", void 0);
__decorate([
attr({
name: ESLLineClamp.is + '-mask',
defaultValue: () => ESLLineClamp.DEFAULT_MASK
})
], ESLLineClamp.prototype, "mask", void 0);
__decorate([
memoize(),
safe(ESLMediaRuleList.empty())
], ESLLineClamp.prototype, "linesQuery", null);
__decorate([
listen({ event: 'change', target: ($this) => $this.linesQuery })
], ESLLineClamp.prototype, "onQueryChange", null);
__decorate([
listen('focusout')
], ESLLineClamp.prototype, "onFocusOut", null);
__decorate([
listen({ event: 'resize', target: ESLResizeObserverTarget.for })
], ESLLineClamp.prototype, "onResize", null);
__decorate([
listen({ event: 'esl:refresh', target: window })
], ESLLineClamp.prototype, "onRefresh", null);
__decorate([
listen('esl:show:request')
], ESLLineClamp.prototype, "onShowRequest", null);
ESLLineClamp = __decorate([
ExportNs('LineClamp')
], ESLLineClamp);
export { ESLLineClamp };