Skip to content

Commit

Permalink
feat(demo): Add per-column features demonstration
Browse files Browse the repository at this point in the history
Closes Enhance the demo page to allow demonstration of per-column features #12
  • Loading branch information
webJose committed Aug 17, 2024
1 parent 7c8709f commit b95b376
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 96 deletions.
34 changes: 34 additions & 0 deletions src/demolib/AllColumnsDropdown.svelte
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>
80 changes: 80 additions & 0 deletions src/demolib/ColumnList.svelte
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>
29 changes: 29 additions & 0 deletions src/demolib/FavButtonMenuItem.svelte
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>
29 changes: 29 additions & 0 deletions src/demolib/FavMenuItem.svelte
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>
26 changes: 26 additions & 0 deletions src/demolib/GlassyDropdownMenu.svelte
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>
182 changes: 182 additions & 0 deletions src/demolib/HeaderCell.svelte
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>
2 changes: 1 addition & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading

0 comments on commit b95b376

Please sign in to comment.