-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(demo): Add per-column features demonstration
Closes Enhance the demo page to allow demonstration of per-column features #12
- Loading branch information
Showing
13 changed files
with
494 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<script lang="ts"> | ||
import ColumnList, { type Column } from "./ColumnList.svelte"; | ||
import GlassyDropdownMenu from "./GlassyDropdownMenu.svelte"; | ||
type Props = { | ||
columns: Column[]; | ||
}; | ||
let { | ||
columns = $bindable(), | ||
}: Props = $props(); | ||
let btn: HTMLButtonElement; | ||
function getDd() { | ||
return (globalThis as any).bootstrap.Dropdown.getInstance(btn); | ||
} | ||
</script> | ||
<div class="dropdown"> | ||
<button | ||
class="btn btn-neutral btn-sm" | ||
type="button" | ||
data-bs-toggle="dropdown" | ||
title="Columns" | ||
aria-expanded="false" | ||
data-bs-auto-close="false" | ||
bind:this={btn} | ||
> | ||
<i class="bi bi-layout-sidebar-inset"></i> | ||
</button> | ||
<GlassyDropdownMenu shadow> | ||
<ColumnList bind:columns onClose={() => getDd().hide()} /> | ||
</GlassyDropdownMenu> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<script context="module" lang="ts"> | ||
export type Column = { key: string; text: string; hidden?: boolean }; | ||
</script> | ||
|
||
<script lang="ts"> | ||
import { nextControlId } from '$lib/utils.js'; | ||
type Props = { | ||
columns: Column[]; | ||
newColumnThreshold?: number; | ||
maxColumns?: number; | ||
onClose?: () => void; | ||
}; | ||
let { columns = $bindable(), newColumnThreshold = 5, maxColumns = 4, onClose }: Props = $props(); | ||
let thisId = nextControlId(); | ||
let numColumns = $derived( | ||
Math.min( | ||
maxColumns, | ||
Math.floor(columns.length / newColumnThreshold) + (columns.length % newColumnThreshold !== 0 ? 1 : 0), | ||
), | ||
); | ||
let numRows = $derived(Math.floor(columns.length / numColumns) + (columns.length % numColumns !== 0 ? 1 : 0)); | ||
function onInputHandler(col: Column, checked: boolean) { | ||
col.hidden = !checked; | ||
} | ||
</script> | ||
|
||
<div class="d-flex flex-row flex-nowrap px-4 pt-2 align-items-baseline"> | ||
<h6 class="me-3">Available Columns</h6> | ||
<button | ||
type="button" | ||
class="btn btn-sm btn-secondary ms-auto me-2" | ||
onclick="{() => columns.forEach((c) => (c.hidden = false))}">Select all</button | ||
> | ||
<button type="button" class="btn-close align-self-center" aria-label="Close" onclick="{() => onClose?.()}"></button> | ||
</div> | ||
<div class="px-4 py-2"> | ||
<table class="table table-sm table-borderless"> | ||
<tbody> | ||
{#each { length: numRows } as _, rowIndex} | ||
<tr> | ||
{#each { length: numColumns } as _, colIndex} | ||
{@const remainder = columns.length % numColumns} | ||
{@const remainderConsumed = colIndex >= remainder} | ||
{@const col = | ||
remainderConsumed && remainder > 0 && rowIndex + 1 === numRows | ||
? undefined | ||
: columns[ | ||
(remainderConsumed ? remainder : colIndex) * numRows + | ||
(remainderConsumed ? colIndex - remainder : 0) * | ||
(numRows - (remainder > 0 ? 1 : 0)) + | ||
rowIndex | ||
]} | ||
<td> | ||
{#if col} | ||
<input | ||
type="checkbox" | ||
class="form-check-input" | ||
id="{thisId}_{col.key}" | ||
checked="{!col.hidden}" | ||
oninput="{(ev) => onInputHandler(col, ev.currentTarget.checked)}" | ||
/> | ||
<label for="{thisId}_{col.key}" class="me-3">{col.text}</label> | ||
{/if} | ||
</td> | ||
{/each} | ||
</tr> | ||
{/each} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
<style> | ||
table { | ||
--bs-table-bg: transparent; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<script lang="ts"> | ||
import { combineClasses } from "$lib/utils.js"; | ||
import type { Snippet } from "svelte"; | ||
import FavMenuItem from "./FavMenuItem.svelte"; | ||
type Props = { | ||
pinPreference: boolean | undefined; | ||
class?: string; | ||
children: Snippet; | ||
onClick?: () => void; | ||
}; | ||
let { | ||
pinPreference = $bindable(), | ||
class: cssClass, | ||
children, | ||
onClick, | ||
}: Props = $props(); | ||
</script> | ||
|
||
<FavMenuItem bind:pinPreference> | ||
<button | ||
type="button" | ||
class={combineClasses("btn btn-neutral rounded-0 text-start flex-fill pe-4", cssClass)} | ||
onclick={() => onClick?.()} | ||
> | ||
{@render children()} | ||
</button> | ||
</FavMenuItem> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<script lang="ts"> | ||
import { nextControlId } from "$lib/utils.js"; | ||
import type { Snippet } from "svelte"; | ||
type Props = { | ||
pinPreference: boolean | undefined; | ||
noMenuItem?: boolean; | ||
children: Snippet; | ||
}; | ||
let { | ||
pinPreference = $bindable(), | ||
noMenuItem = false, | ||
children, | ||
}: Props = $props(); | ||
let thisId = nextControlId(); | ||
</script> | ||
|
||
<div class="d-flex flex-row p-0" class:dropdown-item={!noMenuItem}> | ||
{@render children()} | ||
<input type="checkbox" class="btn-check" bind:checked={pinPreference} id="{thisId}_pinpref"> | ||
<!-- svelte-ignore a11y_click_events_have_key_events --> | ||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> | ||
<label for="{thisId}_pinpref" class="btn btn-neutral rounded-0 flex-grow-0" onclick={(e) => e.stopPropagation()}> | ||
<!-- <i class="bi bi-{pinPreference ? 'star-fill' : 'star'}"></i> --> | ||
<i class="bi bi-stars"></i> | ||
</label> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<script lang="ts"> | ||
import type { Snippet } from "svelte"; | ||
import type { HTMLAttributes } from "svelte/elements"; | ||
type Props = HTMLAttributes<HTMLDivElement> & { | ||
children?: Snippet; | ||
shadow?: boolean; | ||
}; | ||
let { | ||
children, | ||
shadow, | ||
...restProps | ||
}: Props = $props(); | ||
</script> | ||
|
||
<div class="dropdown-menu bg-glass" class:shadow {...restProps}> | ||
{@render children?.()} | ||
</div> | ||
|
||
<style lang="scss"> | ||
.bg-glass { | ||
background-color: rgba(var(--bs-body-bg-rgb), 0.3); | ||
backdrop-filter: blur(7px) saturate(110%); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<script context="module"> | ||
export type HeaderColumn<TRow extends Record<string, any> = Record<string, any>, TCol extends Record<string, any> = Record<string, any>> = | ||
WjDvColumn<TRow, TCol> & { | ||
alignment: ColAlignment; | ||
pinnedFunctions: { | ||
pin?: boolean; | ||
hide?: boolean; | ||
align?: boolean; | ||
textWrap?: boolean; | ||
}; | ||
}; | ||
const allAlignments: ColAlignment[] = [ | ||
'start', | ||
'center', | ||
'end' | ||
]; | ||
const alignmentIcons: Record<ColAlignment, string> = { | ||
'center': 'text-center', | ||
'end': 'text-right', | ||
'start': 'text-left' | ||
}; | ||
</script> | ||
<script lang="ts" generics="TRow extends Record<string, any> = Record<string, any>, TCol extends Record<string, any> = Record<string, any>"> | ||
import { nextControlId } from "$lib/utils.js"; | ||
import type { ColAlignment, WjDvColumn } from "$lib/WjDataView/WjDataView.svelte"; | ||
import FavButtonMenuItem from "./FavButtonMenuItem.svelte"; | ||
import FavMenuItem from "./FavMenuItem.svelte"; | ||
import GlassyDropdownMenu from "./GlassyDropdownMenu.svelte"; | ||
import keyStateStore from './keyStateStore.svelte.js'; | ||
type Props = { | ||
col: HeaderColumn<TRow, TCol>; | ||
maxWidth?: string; | ||
}; | ||
let { | ||
col = $bindable(), | ||
maxWidth, | ||
}: Props = $props(); | ||
let id = nextControlId(); | ||
let pinIcon = $derived(`bi-pin-${col.pinned ? 'fill' : 'angle'}`); | ||
let textWrap = $state(!col.noTextWrap); | ||
let colAlignmentIndex = $derived(allAlignments.findIndex(a => a === (col.alignment ?? 'start'))); | ||
let alignmentIcon = $derived(alignmentIcons[allAlignments[keyStateStore.ctrl ? previousAlignmentIndex() : nextAlignmentIndex()]]); | ||
$effect.pre(() => { | ||
col.noTextWrap = !textWrap; | ||
}); | ||
$effect.pre(() => { | ||
if (typeof col.minWidth === 'number' && col.minWidth > (col.width ?? Number.MAX_VALUE)) { | ||
col.width = col.minWidth | ||
} | ||
}); | ||
function nextAlignmentIndex() { | ||
return (colAlignmentIndex + 1) % allAlignments.length; | ||
} | ||
function previousAlignmentIndex() { | ||
return (colAlignmentIndex - 1 + allAlignments.length) % allAlignments.length; | ||
} | ||
function changeAlignment(ev: MouseEvent) { | ||
col.alignment = allAlignments[ev.ctrlKey ? previousAlignmentIndex() : nextAlignmentIndex()]; | ||
} | ||
</script> | ||
|
||
<div class="d-flex flex-row ps-2"> | ||
<div class="dropdown"> | ||
<button | ||
type="button" | ||
data-bs-toggle="dropdown" | ||
data-bs-target="{id}_ddmenu" | ||
aria-expanded="false" | ||
class="btn btn-sm btn-neutral dropdown-toggle flex-shrink-1" | ||
> | ||
<span class="fw-semibold text-nowrap text-truncate">{col.text}</span> | ||
</button> | ||
<GlassyDropdownMenu shadow id="{id}_ddmenu"> | ||
<h6 class="px-3">Minimum Width</h6> | ||
<div class="d-flex flex-column flex-nowrap px-3 py-1 fs-6"> | ||
<input | ||
type="range" | ||
class="form-range" | ||
list="{id}_{col.key}_minwidth_dl" | ||
id="{id}_{col.key}_minwidth" | ||
min="3" | ||
max="15" | ||
step="0.1" | ||
bind:value={col.minWidth} | ||
> | ||
<datalist id="{id}_{col.key}_minwidth_dl"> | ||
<option value="3">3</option> | ||
<option value="5">5</option> | ||
<option value="7">7</option> | ||
</datalist> | ||
<span class="ms-1 force-max-content font-monospace fw-bold text-center"> | ||
{typeof col.minWidth !== 'number' ? '(not set)' : `${col.minWidth.toFixed(1)} em`} | ||
</span> | ||
</div> | ||
<div class="dropdown-divider"></div> | ||
<h6 class="px-3">Alignment</h6> | ||
<FavMenuItem noMenuItem bind:pinPreference={col.pinnedFunctions.align}> | ||
<div class="btn-toolbar px-3 flex-fill align-self-center"> | ||
<div class="btn-group btn-group-sm me-1"> | ||
<input type="radio" name="{id}_{col.key}_alignment" bind:group={col.alignment} value='start' class="btn-check" id="{id}_{col.key}_align_left"> | ||
<label for="{id}_{col.key}_align_left" title="Align left" class="btn btn-outline-primary"> | ||
<i class="bi bi-text-left"></i> | ||
</label> | ||
<input type="radio" name="{id}_{col.key}_alignment" bind:group={col.alignment} value='center' class="btn-check" id="{id}_{col.key}_align_center"> | ||
<label for="{id}_{col.key}_align_center" title="Align center" class="btn btn-outline-primary"> | ||
<i class="bi bi-text-center"></i> | ||
</label> | ||
<input type="radio" name="{id}_{col.key}_alignment" bind:group={col.alignment} value='end' class="btn-check" id="{id}_{col.key}_align_right"> | ||
<label for="{id}_{col.key}_align_right" title="Align right" class="btn btn-outline-primary"> | ||
<i class="bi bi-text-right"></i> | ||
</label> | ||
</div> | ||
</div> | ||
</FavMenuItem> | ||
<div class="dropdown-divider"></div> | ||
<FavButtonMenuItem | ||
class={textWrap ? 'active' : undefined} | ||
bind:pinPreference={col.pinnedFunctions.textWrap} | ||
onClick={() => textWrap = !textWrap} | ||
> | ||
<i class="bi bi-text-wrap"></i> | ||
Text wrap | ||
</FavButtonMenuItem> | ||
<div class="dropdown-divider"></div> | ||
<FavButtonMenuItem bind:pinPreference={col.pinnedFunctions.hide} onClick={() => col.hidden = true}> | ||
<i class="bi bi-eye-slash me-2"></i> | ||
Hide column | ||
</FavButtonMenuItem> | ||
<FavButtonMenuItem bind:pinPreference={col.pinnedFunctions.pin} onClick={() => col.pinned = !col.pinned}> | ||
<i class="bi {pinIcon} me-2"></i> | ||
{col.pinned ? 'Unpin' : 'Pin'} column | ||
</FavButtonMenuItem> | ||
</GlassyDropdownMenu> | ||
</div> | ||
<div class="d-flex flex-row flex-nowrap ms-auto"> | ||
{#if col.pinnedFunctions.align} | ||
<button | ||
type="button" | ||
class="btn btn-neutral btn-sm ms-auto" | ||
title="Click: Next alignment; Ctrl + Click: Previous alignment" | ||
onclick={changeAlignment} | ||
> | ||
<i class="bi bi-{alignmentIcon}"></i> | ||
</button> | ||
{/if} | ||
{#if col.pinnedFunctions.textWrap} | ||
<input type="checkbox" class="btn-check" id="{id}_textwrap" bind:checked={textWrap}> | ||
<label for="{id}_textwrap" class="btn btn-neutral btn-sm"> | ||
<i class="bi bi-text-wrap"></i> | ||
</label> | ||
{/if} | ||
{#if col.pinnedFunctions.hide} | ||
<button type="button" class="btn btn-sm btn-neutral" onclick={() => col.hidden = true}> | ||
<span title="Click to {col.pinned ? 'un' : ''}pin"> | ||
<i class="bi bi-eye-slash"></i> | ||
</span> | ||
</button> | ||
{/if} | ||
{#if col.pinnedFunctions.pin} | ||
<button type="button" class="btn btn-sm btn-neutral" onclick={() => col.pinned = !col.pinned}> | ||
<span title="Click to {col.pinned ? 'un' : ''}pin"> | ||
<i class="bi {pinIcon}"></i> | ||
</span> | ||
</button> | ||
{/if} | ||
</div> | ||
</div> | ||
|
||
<style> | ||
.force-max-content { | ||
min-width: max-content; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,6 @@ | |
</head> | ||
<body data-sveltekit-preload-data="hover"> | ||
<div>%sveltekit.body%</div> | ||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script> | ||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | ||
</body> | ||
</html> |
Oops, something went wrong.