226 lines
6.5 KiB
Plaintext
226 lines
6.5 KiB
Plaintext
// LayoutSplitter -- two panes with a divider the reader can move.
|
|
//
|
|
// <LayoutSplitter size={40} minSize={20}>
|
|
// <div data-slot="start">...</div>
|
|
// <div data-slot="end">...</div>
|
|
// </LayoutSplitter>
|
|
//
|
|
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
|
|
// moves fire far too often to route through a client function, and a state
|
|
// write made inside a pointermove callback is dropped, so the resolved size is
|
|
// held in the DOM as the --wrn-split custom property and these styles read it.
|
|
//
|
|
// This component previously declared resizeStart, resize and resizeEnd with no
|
|
// pointer handling whatsoever: a caller connected up @resize and received nothing,
|
|
// for ever, with no error.
|
|
//
|
|
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
|
|
// and silently swallows the rule that follows it.
|
|
component LayoutSplitter {
|
|
outputs {
|
|
// Named sizeChange rather than resize so it cannot be confused with the
|
|
// native window event a caller may already be listening for. An earlier
|
|
// comment here claimed a natively-named output could never reach a parent
|
|
// binding; that was wrong, and the rename was never what fixed anything.
|
|
sizeChange(payload: { size: number })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: number = 50
|
|
orientation: string = "horizontal"
|
|
// Smallest share either pane may take, as a percentage. It also fixes the
|
|
// upper bound at 100 - minSize, so neither pane can be dragged away to
|
|
// nothing and left unrecoverable by pointer.
|
|
minSize: number = 15
|
|
step: number = 5
|
|
label: string = "Resize panels"
|
|
class: string = ""
|
|
}
|
|
|
|
functions {
|
|
// The runtime resolves the size and announces it; this turns that into the
|
|
// declared output so a parent @resize binding receives it.
|
|
client function reportResize(sourceEvent) {
|
|
var detail = sourceEvent ? sourceEvent.detail : null
|
|
if (!detail) {
|
|
return
|
|
}
|
|
output.sizeChange({ size: detail.size })
|
|
}
|
|
|
|
shared function isVertical() {
|
|
return orientation === "vertical"
|
|
}
|
|
|
|
shared function lowerBound() {
|
|
var value = Number(minSize)
|
|
if (!value || value < 0) {
|
|
return 15
|
|
}
|
|
return Math.min(45, value)
|
|
}
|
|
|
|
shared function upperBound() {
|
|
return 100 - lowerBound()
|
|
}
|
|
|
|
// Sizes arrive as HTML attributes, so a value outside the bounds is
|
|
// routine rather than exceptional. Clamp instead of rendering something
|
|
// the reader cannot undo.
|
|
shared function currentSize() {
|
|
var value = Number(size)
|
|
if (!value || value < 0) {
|
|
return 50
|
|
}
|
|
return Math.min(upperBound(), Math.max(lowerBound(), value))
|
|
}
|
|
|
|
shared function stepSize() {
|
|
var value = Number(step)
|
|
return value && value > 0 ? value : 5
|
|
}
|
|
}
|
|
|
|
view {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="LayoutSplitter"
|
|
class='wrn-splitter {class}'
|
|
data-color='{color}'
|
|
data-orientation='{orientation}'
|
|
data-wrn-splitter='{isVertical() ? "vertical" : "horizontal"}'
|
|
data-wrn-splitter-min='{lowerBound()}'
|
|
data-wrn-splitter-step='{stepSize()}'
|
|
style='--wrn-split:{currentSize()}%;'
|
|
@wrnexus:splitter:resize='reportResize(event)'
|
|
>
|
|
<div class="wrn-splitter__pane wrn-splitter__pane--start">
|
|
<slot name="start"></slot>
|
|
</div>
|
|
|
|
<div
|
|
class="wrn-splitter__handle"
|
|
data-wrn-splitter-handle="true"
|
|
role="separator"
|
|
tabindex="0"
|
|
aria-label='{label}'
|
|
aria-orientation='{isVertical() ? "horizontal" : "vertical"}'
|
|
aria-valuenow='{currentSize()}'
|
|
aria-valuemin='{lowerBound()}'
|
|
aria-valuemax='{upperBound()}'
|
|
>
|
|
<span class="wrn-splitter__grip" aria-hidden="true"></span>
|
|
</div>
|
|
|
|
<div class="wrn-splitter__pane wrn-splitter__pane--end">
|
|
<slot name="end"></slot>
|
|
</div>
|
|
|
|
<slot />
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wrn-splitter {
|
|
--splitter-accent: var(--wrn-color-primary);
|
|
display: grid;
|
|
/* The first track follows the size the runtime resolves. */
|
|
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
|
|
align-items: stretch;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrn-splitter[data-color="secondary"] {
|
|
--splitter-accent: var(--wrn-color-secondary);
|
|
}
|
|
|
|
.wrn-splitter[data-color="success"] {
|
|
--splitter-accent: var(--wrn-color-success);
|
|
}
|
|
|
|
.wrn-splitter[data-color="danger"] {
|
|
--splitter-accent: var(--wrn-color-danger);
|
|
}
|
|
|
|
.wrn-splitter[data-color="info"] {
|
|
--splitter-accent: var(--wrn-color-info);
|
|
}
|
|
|
|
.wrn-splitter[data-orientation="vertical"] {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
|
|
}
|
|
|
|
.wrn-splitter__pane {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
}
|
|
|
|
.wrn-splitter__handle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
padding: 0 0.25rem;
|
|
border: 0;
|
|
background: transparent;
|
|
cursor: col-resize;
|
|
/* Without this the pointer drag selects the text in both panes. */
|
|
touch-action: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__handle {
|
|
padding: 0.25rem 0;
|
|
cursor: row-resize;
|
|
}
|
|
|
|
.wrn-splitter__grip {
|
|
display: block;
|
|
width: 2px;
|
|
height: 100%;
|
|
min-height: 1.5rem;
|
|
border-radius: 999px;
|
|
background: var(--wrn-color-border);
|
|
transition: background 140ms ease;
|
|
}
|
|
|
|
.wrn-splitter[data-orientation="vertical"] .wrn-splitter__grip {
|
|
width: 100%;
|
|
min-width: 1.5rem;
|
|
height: 2px;
|
|
}
|
|
|
|
.wrn-splitter__handle:hover .wrn-splitter__grip,
|
|
.wrn-splitter[data-wrn-splitter-dragging="true"] .wrn-splitter__grip {
|
|
background: var(--splitter-accent);
|
|
}
|
|
|
|
.wrn-splitter__handle:focus-visible {
|
|
outline: 2px solid var(--splitter-accent);
|
|
outline-offset: -2px;
|
|
border-radius: var(--wrn-radius-sm);
|
|
}
|
|
|
|
/*
|
|
* Two panes side by side stop making sense on a phone. Stacking them keeps
|
|
* both readable, and the divider stops being draggable because the grid
|
|
* no longer has a second track to trade against.
|
|
*/
|
|
@media (max-width: 639px) {
|
|
.wrn-splitter,
|
|
.wrn-splitter[data-orientation="vertical"] {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
grid-template-rows: auto auto auto;
|
|
}
|
|
|
|
.wrn-splitter__handle {
|
|
cursor: default;
|
|
}
|
|
}
|
|
}
|
|
}
|