shoelace/src/components/split-panel/split-panel.ts

258 wiersze
7.7 KiB
TypeScript
Czysty Zwykły widok Historia

import { html, LitElement } from 'lit';
2021-12-22 23:32:27 +00:00
import { customElement, property, query } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
2022-03-24 11:48:03 +00:00
import { drag } from '~/internal/drag';
import { emit } from '~/internal/event';
import { clamp } from '~/internal/math';
import { watch } from '~/internal/watch';
import { LocalizeController } from '~/utilities/localize';
2021-12-22 23:32:27 +00:00
import styles from './split-panel.styles';
/**
* @since 2.0
* @status experimental
*
2021-12-23 16:23:14 +00:00
* @event sl-reposition - Emitted when the divider's position changes.
2021-12-22 23:32:27 +00:00
*
* @csspart start - The start panel.
* @csspart end - The end panel.
* @csspart panel - Targets both the start and end panels.
2021-12-30 23:14:07 +00:00
* @csspart divider - The divider that separates the start and end panels.
2021-12-23 00:07:16 +00:00
*
2021-12-22 23:32:27 +00:00
* @slot start - The start panel.
* @slot end - The end panel.
2021-12-23 00:07:16 +00:00
* @slot handle - An optional handle to render at the center of the divider.
2021-12-22 23:32:27 +00:00
*
* @cssproperty [--divider-width=4px] - The width of the visible divider.
2021-12-30 23:14:07 +00:00
* @cssproperty [--divider-hit-area=12px] - The invisible region around the divider where dragging can occur. This is
* usually wider than the divider to facilitate easier dragging.
2021-12-23 15:07:37 +00:00
* @cssproperty [--min=0] - The minimum allowed size of the primary panel.
* @cssproperty [--max=100%] - The maximum allowed size of the primary panel.
2021-12-22 23:32:27 +00:00
*/
@customElement('sl-split-panel')
export default class SlSplitPanel extends LitElement {
static styles = styles;
2021-12-30 23:14:07 +00:00
private cachedPositionInPixels: number;
private readonly localize = new LocalizeController(this);
2021-12-22 23:32:27 +00:00
private resizeObserver: ResizeObserver;
private size: number;
@query('.divider') divider: HTMLElement;
/**
2021-12-30 23:14:07 +00:00
* The current position of the divider from the primary panel's edge as a percentage 0-100. Defaults to 50% of the
2022-01-01 01:39:16 +00:00
* container's initial size.
2021-12-22 23:32:27 +00:00
*/
2021-12-30 23:14:07 +00:00
@property({ type: Number, reflect: true }) position = 50;
/**
* The current position of the divider from the primary panel's edge in pixels.
*/
@property({ attribute: 'position-in-pixels', type: Number }) positionInPixels: number;
2021-12-22 23:32:27 +00:00
/** Draws the split panel in a vertical orientation with the start and end panels stacked. */
@property({ type: Boolean, reflect: true }) vertical = false;
2021-12-30 23:14:07 +00:00
/** Disables resizing. Note that the position may still change as a result of resizing the host element. */
2021-12-22 23:32:27 +00:00
@property({ type: Boolean, reflect: true }) disabled = false;
/**
2021-12-30 23:14:07 +00:00
* If no primary panel is designated, both panels will resize proportionally when the host element is resized. If a
* primary panel is designated, it will maintain its size and the other panel will grow or shrink as needed when the
* host element is resized.
2021-12-22 23:32:27 +00:00
*/
@property() primary?: 'start' | 'end';
2021-12-22 23:32:27 +00:00
/**
* One or more space-separated values at which the divider should snap. Values can be in pixels or percentages, e.g.
* `"100px 50%"`.
*/
@property() snap?: string;
2021-12-22 23:32:27 +00:00
/** How close the divider must be to a snap point until snapping occurs. */
@property({ type: Number, attribute: 'snap-threshold' }) snapThreshold = 12;
connectedCallback() {
super.connectedCallback();
this.resizeObserver = new ResizeObserver(entries => this.handleResize(entries));
this.updateComplete.then(() => this.resizeObserver.observe(this));
2021-12-22 23:32:27 +00:00
2021-12-30 23:14:07 +00:00
this.detectSize();
this.cachedPositionInPixels = this.percentageToPixels(this.position);
2021-12-22 23:32:27 +00:00
}
2021-12-30 18:09:50 +00:00
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver.unobserve(this);
}
2021-12-22 23:32:27 +00:00
2021-12-30 23:14:07 +00:00
private detectSize() {
const { width, height } = this.getBoundingClientRect();
this.size = this.vertical ? height : width;
}
private percentageToPixels(value: number) {
return this.size * (value / 100);
}
private pixelsToPercentage(value: number) {
return (value / this.size) * 100;
}
2021-12-23 00:07:16 +00:00
handleDrag(event: Event) {
if (this.disabled) {
return;
}
// Prevent text selection when dragging
event.preventDefault();
drag(this, (x, y) => {
2021-12-30 23:14:07 +00:00
let newPositionInPixels = this.vertical ? y : x;
2021-12-23 00:07:16 +00:00
// Flip for end panels
2021-12-23 13:24:44 +00:00
if (this.primary === 'end') {
2021-12-30 23:14:07 +00:00
newPositionInPixels = this.size - newPositionInPixels;
2021-12-23 00:07:16 +00:00
}
2021-12-22 23:32:27 +00:00
// Check snap points
2022-01-19 14:37:07 +00:00
if (this.snap) {
2021-12-22 23:32:27 +00:00
const snaps = this.snap.split(' ');
snaps.forEach(value => {
2021-12-22 23:32:27 +00:00
let snapPoint: number;
if (value.endsWith('%')) {
snapPoint = this.size * (parseFloat(value) / 100);
} else {
snapPoint = parseFloat(value);
}
2021-12-30 23:14:07 +00:00
if (
newPositionInPixels >= snapPoint - this.snapThreshold &&
newPositionInPixels <= snapPoint + this.snapThreshold
) {
newPositionInPixels = snapPoint;
2021-12-22 23:32:27 +00:00
}
});
}
2021-12-30 23:14:07 +00:00
this.position = clamp(this.pixelsToPercentage(newPositionInPixels), 0, 100);
2021-12-23 00:07:16 +00:00
});
2021-12-22 23:32:27 +00:00
}
handleKeyDown(event: KeyboardEvent) {
if (this.disabled) {
return;
}
2021-12-27 15:31:57 +00:00
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
2021-12-30 23:14:07 +00:00
let newPosition = this.position;
const incr = (event.shiftKey ? 10 : 1) * (this.primary === 'end' ? -1 : 1);
2021-12-22 23:32:27 +00:00
event.preventDefault();
2021-12-27 15:31:57 +00:00
if ((event.key === 'ArrowLeft' && !this.vertical) || (event.key === 'ArrowUp' && this.vertical)) {
2021-12-30 23:14:07 +00:00
newPosition -= incr;
2021-12-27 15:31:57 +00:00
}
if ((event.key === 'ArrowRight' && !this.vertical) || (event.key === 'ArrowDown' && this.vertical)) {
2021-12-30 23:14:07 +00:00
newPosition += incr;
2021-12-27 15:31:57 +00:00
}
if (event.key === 'Home') {
2021-12-30 23:14:07 +00:00
newPosition = this.primary === 'end' ? 100 : 0;
2021-12-27 15:31:57 +00:00
}
if (event.key === 'End') {
2021-12-30 23:14:07 +00:00
newPosition = this.primary === 'end' ? 0 : 100;
2021-12-27 15:31:57 +00:00
}
2021-12-30 23:14:07 +00:00
this.position = clamp(newPosition, 0, 100);
2021-12-22 23:32:27 +00:00
}
}
@watch('position')
handlePositionChange() {
2021-12-30 23:14:07 +00:00
this.cachedPositionInPixels = this.percentageToPixels(this.position);
this.positionInPixels = this.percentageToPixels(this.position);
2021-12-22 23:32:27 +00:00
emit(this, 'sl-reposition');
}
2021-12-30 23:14:07 +00:00
@watch('positionInPixels')
handlePositionInPixelsChange() {
this.position = this.pixelsToPercentage(this.positionInPixels);
}
2022-03-11 19:31:25 +00:00
@watch('vertical')
handleVerticalChange() {
this.detectSize();
}
2021-12-22 23:32:27 +00:00
handleResize(entries: ResizeObserverEntry[]) {
const { width, height } = entries[0].contentRect;
this.size = this.vertical ? height : width;
2021-12-30 23:14:07 +00:00
// Resize when a primary panel is set
2022-01-19 14:37:07 +00:00
if (this.primary) {
2021-12-30 23:14:07 +00:00
this.position = this.pixelsToPercentage(this.cachedPositionInPixels);
2021-12-23 16:23:14 +00:00
}
2021-12-22 23:32:27 +00:00
}
render() {
2021-12-23 15:07:37 +00:00
const gridTemplate = this.vertical ? 'gridTemplateRows' : 'gridTemplateColumns';
2022-03-11 19:31:25 +00:00
const gridTemplateAlt = this.vertical ? 'gridTemplateColumns' : 'gridTemplateRows';
2021-12-23 15:07:37 +00:00
const primary = `
clamp(
0%,
clamp(
var(--min),
2021-12-30 23:14:07 +00:00
${this.position}% - var(--divider-width) / 2,
2021-12-23 15:07:37 +00:00
var(--max)
),
calc(100% - var(--divider-width))
)
`;
const secondary = 'auto';
2021-12-22 23:32:27 +00:00
2021-12-23 13:24:44 +00:00
if (this.primary === 'end') {
2021-12-23 15:07:37 +00:00
this.style[gridTemplate] = `${secondary} var(--divider-width) ${primary}`;
2021-12-22 23:32:27 +00:00
} else {
2021-12-23 15:07:37 +00:00
this.style[gridTemplate] = `${primary} var(--divider-width) ${secondary}`;
2021-12-22 23:32:27 +00:00
}
2022-03-11 19:31:25 +00:00
// Unset the alt grid template property
this.style[gridTemplateAlt] = '';
2021-12-22 23:32:27 +00:00
return html`
<div part="panel start" class="start">
2021-12-22 23:32:27 +00:00
<slot name="start"></slot>
</div>
<div
2021-12-23 00:07:16 +00:00
part="divider"
2021-12-22 23:32:27 +00:00
class="divider"
tabindex=${ifDefined(this.disabled ? undefined : '0')}
2021-12-23 00:07:16 +00:00
role="separator"
2021-12-23 16:23:14 +00:00
aria-label=${this.localize.term('resize')}
2021-12-22 23:32:27 +00:00
@keydown=${this.handleKeyDown}
@mousedown=${this.handleDrag}
@touchstart=${this.handleDrag}
2021-12-23 00:07:16 +00:00
>
<slot name="handle"></slot>
</div>
2021-12-22 23:32:27 +00:00
<div part="panel end" class="end">
2021-12-22 23:32:27 +00:00
<slot name="end"></slot>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'sl-split-panel': SlSplitPanel;
}
}