mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Expand the Sortable demo and add default SortableJS drag styles (#3106)
This commit is contained in:
1 parent
ce523643b2
commit
bd60d5fd4c
11 files changed
+602
-11
No files matched your search
@@ -36,6 +36,8 @@ sort-score
|
||||
sort-status
|
||||
sort-tags
|
||||
sort-type
|
||||
sortable-fixed
|
||||
sortable-handle
|
||||
table-tbody
|
||||
tblr-illustrations-computer-fix-a
|
||||
tblr-illustrations-computer-fix-b
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added grouped Sortable demos: drag handle, table rows, kanban board, clone, multi-drag and swap.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
---
|
||||
|
||||
Added SortableJS drag styles and turned on `forceFallback` by default so `data-sortable` lists drag a styled, tilted copy.
|
||||
@@ -22,7 +22,9 @@ const DATA_ATTRIBUTE = `data-${NAME}`
|
||||
|
||||
const SELECTOR_DATA_SORTABLE = `[${DATA_ATTRIBUTE}]`
|
||||
|
||||
const Default: ComponentConfig = {}
|
||||
const Default: ComponentConfig = {
|
||||
forceFallback: true,
|
||||
}
|
||||
|
||||
const DefaultType: Record<string, string> = {}
|
||||
|
||||
@@ -32,6 +34,7 @@ const DefaultType: Record<string, string> = {}
|
||||
* Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately
|
||||
* as `window.Sortable`. Without the plugin the component is inert. Options come
|
||||
* from the `data-sortable` attribute as JSON, or from the config object.
|
||||
* `forceFallback` is on by default so the dragged copy can be styled with `.sortable-drag`.
|
||||
*/
|
||||
|
||||
class Sortable extends BaseComponent {
|
||||
|
||||
@@ -45,6 +45,19 @@ describe('Sortable', () => {
|
||||
expect(instance.sortable).toBe(plugin.mock.instances[0])
|
||||
})
|
||||
|
||||
it('should drag a styled copy by default', () => {
|
||||
new Sortable(list())
|
||||
|
||||
expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: true })
|
||||
})
|
||||
|
||||
it('should let data-sortable turn the styled copy off', () => {
|
||||
fixtureEl.innerHTML = '<ul data-sortable=\'{"forceFallback":false}\'><li>a</li></ul>'
|
||||
new Sortable(list())
|
||||
|
||||
expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: false })
|
||||
})
|
||||
|
||||
it('should let the config object win over the attribute', () => {
|
||||
new Sortable(list(), { animation: 0, group: 'shared' })
|
||||
|
||||
|
||||
+4
-4
@@ -82,19 +82,19 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "./dist/css/tabler.css",
|
||||
"maxSize": "87 kB"
|
||||
"maxSize": "88 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.min.css",
|
||||
"maxSize": "81 kB"
|
||||
"maxSize": "82 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.rtl.css",
|
||||
"maxSize": "87 kB"
|
||||
"maxSize": "88 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.rtl.min.css",
|
||||
"maxSize": "81 kB"
|
||||
"maxSize": "82 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler-flags.css",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
@forward 'ui/chat';
|
||||
@forward 'ui/signature';
|
||||
@forward 'ui/patterns';
|
||||
@forward 'ui/sortable';
|
||||
|
||||
@forward 'helpers/index';
|
||||
|
||||
|
||||
@@ -1837,6 +1837,18 @@ $tracking-border-radius: var(--border-radius) !default;
|
||||
$tracking-block-min-width: 0.25rem !default;
|
||||
// scss-docs-end tracking-variables
|
||||
|
||||
// Sortable
|
||||
// scss-docs-start sortable-variables
|
||||
$sortable-ghost-bg: var(--primary-lt) !default;
|
||||
$sortable-ghost-border-color: var(--primary) !default;
|
||||
$sortable-ghost-opacity: 0.6 !default;
|
||||
$sortable-outline-width: 1px !default;
|
||||
$sortable-selected-bg: var(--primary-lt) !default;
|
||||
$sortable-drag-bg: var(--bg-surface) !default;
|
||||
$sortable-drag-rotate: 1.5deg !default;
|
||||
$sortable-drag-box-shadow: $box-shadow-lg !default;
|
||||
// scss-docs-end sortable-variables
|
||||
|
||||
// Progress
|
||||
// scss-docs-start progress-variables
|
||||
$progress-height: 0.5rem !default;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Sortable
|
||||
//
|
||||
@use '../config' as *;
|
||||
|
||||
.sortable-ghost,
|
||||
.sortable-swap-highlight,
|
||||
.sortable-selected,
|
||||
.sortable-drag {
|
||||
--sortable-ghost-bg: #{$sortable-ghost-bg};
|
||||
--sortable-ghost-border-color: #{$sortable-ghost-border-color};
|
||||
--sortable-ghost-opacity: #{$sortable-ghost-opacity};
|
||||
--sortable-outline-width: #{$sortable-outline-width};
|
||||
--sortable-selected-bg: #{$sortable-selected-bg};
|
||||
--sortable-drag-bg: #{$sortable-drag-bg};
|
||||
--sortable-drag-rotate: #{$sortable-drag-rotate};
|
||||
--sortable-drag-box-shadow: #{$sortable-drag-box-shadow};
|
||||
}
|
||||
|
||||
.sortable-ghost,
|
||||
.sortable-swap-highlight {
|
||||
background-color: var(--sortable-ghost-bg);
|
||||
outline: var(--sortable-outline-width) dashed var(--sortable-ghost-border-color);
|
||||
outline-offset: calc(var(--sortable-outline-width) * -1);
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: var(--sortable-ghost-opacity);
|
||||
}
|
||||
|
||||
tr.sortable-ghost,
|
||||
tr.sortable-swap-highlight,
|
||||
tr.sortable-selected {
|
||||
> * {
|
||||
--table-bg-state: var(--sortable-ghost-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-selected {
|
||||
background-color: var(--sortable-selected-bg);
|
||||
outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color);
|
||||
outline-offset: calc(var(--sortable-outline-width) * -1);
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
&.sortable-ghost,
|
||||
&.sortable-swap-highlight,
|
||||
&.sortable-selected,
|
||||
&.sortable-drag {
|
||||
@include border-radius(var(--list-group-border-radius));
|
||||
}
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
background-color: var(--sortable-drag-bg);
|
||||
outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color);
|
||||
outline-offset: calc(var(--sortable-outline-width) * -1);
|
||||
box-shadow: var(--sortable-drag-box-shadow);
|
||||
opacity: 1 !important; // stylelint-disable-line declaration-no-important
|
||||
transition: none;
|
||||
|
||||
/* rtl:ignore */
|
||||
rotate: calc(var(--sortable-drag-rotate) * var(--dir));
|
||||
}
|
||||
|
||||
.card.sortable-drag {
|
||||
box-shadow: var(--sortable-drag-box-shadow);
|
||||
}
|
||||
@@ -1,21 +1,65 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import ListGroup from '@ui/ListGroup.astro'
|
||||
import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
import { people, personById } from '@shared/lib/people'
|
||||
import tasks from '@data/tasks.json'
|
||||
|
||||
interface Task {
|
||||
name: string
|
||||
assigned_to: number
|
||||
priority: string
|
||||
}
|
||||
|
||||
interface Column {
|
||||
name: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
const priorities: Record<string, { value: number; color: string }> = {
|
||||
High: { value: 3, color: 'red' },
|
||||
Medium: { value: 2, color: 'orange' },
|
||||
Low: { value: 1, color: 'blue' },
|
||||
}
|
||||
|
||||
const board = (tasks.columns as Column[]).map((column) => ({
|
||||
name: column.name,
|
||||
tasks: column.tasks.slice(0, 4),
|
||||
}))
|
||||
|
||||
const selectedTopics = ['Design', 'Development', 'Marketing']
|
||||
const availableTopics = ['Sales', 'Support', 'Analytics', 'Legal', 'Finance', 'Research', 'Security', 'Hiring']
|
||||
|
||||
const team = people.slice(0, 5)
|
||||
const gallery = people.slice(5, 11)
|
||||
|
||||
const sections = [
|
||||
{ title: 'Basics', description: 'Add data-sortable with SortableJS options as JSON to any list to make its children draggable.' },
|
||||
{ title: 'Handles and tables', description: 'Limit dragging to a grip icon, or reorder the rows of a table.' },
|
||||
{ title: 'Boards', description: 'Connect several lists to build a kanban board, a pick list or a nested tree.' },
|
||||
{ title: 'Selection and swapping', description: 'The bundled SortableJS build also ships the MultiDrag and Swap plugins.' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Sortable" pageHeader="SortableJS" pageMenu="plugins.sortable" pageLibs={['sortablejs']}>
|
||||
<DefaultLayout title="Sortable" pageMenu="plugins.sortable" pageLibs={['sortablejs']} description="Drag and drop to reorder lists, table rows, grids and kanban boards with SortableJS.">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<h2 class="page-section-title">{sections[0]!.title}</h2>
|
||||
<p class="page-section-description">{sections[0]!.description}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle>Basic usage</CardTitle>
|
||||
<CardTitle as="h3">Basic usage</CardTitle>
|
||||
<CardSubtitle>Drag to reorder items within a single list.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -30,11 +74,11 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle>Two lists with drag and drop</CardTitle>
|
||||
<CardSubtitle>Move items between two connected lists.</CardSubtitle>
|
||||
<CardTitle as="h3">Two lists with drag and drop</CardTitle>
|
||||
<CardSubtitle>Give lists the same <code>group</code> to move items between them.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
@@ -57,5 +101,351 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Horizontal lists</CardTitle>
|
||||
<CardSubtitle>Set <code>direction</code> to <code>horizontal</code> for rows of tags, and drag them between the two rows.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="subheader mb-2">Selected</div>
|
||||
<div class="tag-list mb-4" data-sortable='{"group":"topics","animation":150,"direction":"horizontal","emptyInsertThreshold":40}'>
|
||||
{selectedTopics.map((topic) => <span class="tag cursor-move">{topic}</span>)}
|
||||
</div>
|
||||
<div class="subheader mb-2">Available</div>
|
||||
<div class="tag-list" data-sortable='{"group":"topics","animation":150,"direction":"horizontal","emptyInsertThreshold":40}'>
|
||||
{availableTopics.map((topic) => <span class="tag cursor-move">{topic}</span>)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Fixed items</CardTitle>
|
||||
<CardSubtitle>Set <code>filter</code> to a selector to make matching items impossible to drag.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"animation":150,"filter":".sortable-fixed"}'>
|
||||
<ListGroupItem as="li" class="sortable-fixed text-secondary d-flex align-items-center gap-2">
|
||||
<Icon name="lock" /> Inbox (pinned)
|
||||
</ListGroupItem>
|
||||
<ListGroupItem as="li">Projects</ListGroupItem>
|
||||
<ListGroupItem as="li">Reports</ListGroupItem>
|
||||
<ListGroupItem as="li">Archive</ListGroupItem>
|
||||
<ListGroupItem as="li" class="sortable-fixed text-secondary d-flex align-items-center gap-2">
|
||||
<Icon name="lock" /> Trash (pinned)
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<h2 class="page-section-title">{sections[1]!.title}</h2>
|
||||
<p class="page-section-description">{sections[1]!.description}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Drag handle</CardTitle>
|
||||
<CardSubtitle>Set <code>handle</code> so only the grip starts a drag, and the rest of the row stays clickable.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"animation":150,"handle":".sortable-handle"}'>
|
||||
{
|
||||
team.map((person) => (
|
||||
<ListGroupItem as="li">
|
||||
<div class="row align-items-center g-3">
|
||||
<div class="col-auto">
|
||||
<span class="sortable-handle cursor-move text-secondary" aria-hidden="true">
|
||||
<Icon name="grip-vertical" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Avatar person={person} />
|
||||
</div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-reset d-block text-truncate">
|
||||
{person.full_name}
|
||||
</a>
|
||||
<div class="text-secondary text-truncate">{person.job_title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Sortable table</CardTitle>
|
||||
<CardSubtitle>Put <code>data-sortable</code> on the <code>tbody</code> to reorder rows.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><span class="visually-hidden">Drag</span></th>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Country</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-sortable='{"animation":150,"handle":".sortable-handle"}'>
|
||||
{
|
||||
team.map((person) => (
|
||||
<tr>
|
||||
<td>
|
||||
<span class="sortable-handle cursor-move text-secondary" aria-hidden="true">
|
||||
<Icon name="grip-vertical" />
|
||||
</span>
|
||||
</td>
|
||||
<td>{person.full_name}</td>
|
||||
<td class="text-secondary">{person.job_title}</td>
|
||||
<td class="text-secondary">{person.country}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Grid of avatars</CardTitle>
|
||||
<CardSubtitle>Sortable also works on a wrapping layout, not just on vertical lists.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="d-flex flex-wrap gap-3" data-sortable='{"animation":150}'>
|
||||
{
|
||||
gallery.map((person) => (
|
||||
<div class="card card-sm cursor-move flex-fill">
|
||||
<div class="card-body text-center">
|
||||
<Avatar person={person} size="lg" class="mb-2" />
|
||||
<div class="text-truncate">{person.first_name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Disabled list</CardTitle>
|
||||
<CardSubtitle>Set <code>disabled</code> to <code>true</code> to lock a list, for example while the user has no edit rights.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"animation":150,"disabled":true}'>
|
||||
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
|
||||
<Icon name="lock" class="text-secondary" /> Read-only item 1
|
||||
</ListGroupItem>
|
||||
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
|
||||
<Icon name="lock" class="text-secondary" /> Read-only item 2
|
||||
</ListGroupItem>
|
||||
<ListGroupItem as="li" class="d-flex align-items-center gap-2">
|
||||
<Icon name="lock" class="text-secondary" /> Read-only item 3
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<h2 class="page-section-title">{sections[2]!.title}</h2>
|
||||
<p class="page-section-description">{sections[2]!.description}</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Kanban board</CardTitle>
|
||||
<CardSubtitle>Drag tasks between columns that share one <code>group</code>. Items are dragged as a tilted copy with a shadow. Set <code>forceFallback</code> to <code>false</code> to use the browser preview instead.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="row g-3">
|
||||
{
|
||||
board.map((column) => (
|
||||
<div class="col-md-4">
|
||||
<h4 class="mb-2">{column.name}</h4>
|
||||
<div class="d-flex flex-column gap-2" data-sortable='{"group":"kanban","animation":150,"emptyInsertThreshold":40}'>
|
||||
{column.tasks.map((task) => (
|
||||
<div class="card card-sm cursor-move">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">{task.name}</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="d-flex align-items-center gap-2 text-secondary">
|
||||
<Signal value={priorities[task.priority]?.value ?? 0} color={priorities[task.priority]?.color} />
|
||||
{task.priority}
|
||||
</span>
|
||||
<Avatar person={personById(task.assigned_to)} size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Clone from a list</CardTitle>
|
||||
<CardSubtitle>Use <code>pull: "clone"</code> and <code>put: false</code> on the source so items are copied, not moved.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="subheader mb-2">Templates</div>
|
||||
<ListGroup as="ul" data-sortable='{"group":{"name":"clone","pull":"clone","put":false},"sort":false,"animation":150}'>
|
||||
<ListGroupItem as="li">Heading</ListGroupItem>
|
||||
<ListGroupItem as="li">Paragraph</ListGroupItem>
|
||||
<ListGroupItem as="li">Image</ListGroupItem>
|
||||
<ListGroupItem as="li">Button</ListGroupItem>
|
||||
</ListGroup>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="subheader mb-2">Page</div>
|
||||
<ListGroup as="ul" data-sortable='{"group":"clone","animation":150}'>
|
||||
<ListGroupItem as="li">Paragraph</ListGroupItem>
|
||||
</ListGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">One-way transfer</CardTitle>
|
||||
<CardSubtitle>Set <code>put: false</code> on a list to make it a source only, and use it to build a pick list.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="subheader mb-2">Available</div>
|
||||
<ListGroup as="ul" data-sortable='{"group":{"name":"transfer","put":false},"sort":false,"animation":150}'>
|
||||
<ListGroupItem as="li">Email alerts</ListGroupItem>
|
||||
<ListGroupItem as="li">SMS alerts</ListGroupItem>
|
||||
<ListGroupItem as="li">Weekly digest</ListGroupItem>
|
||||
<ListGroupItem as="li">Push notifications</ListGroupItem>
|
||||
</ListGroup>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="subheader mb-2">Selected</div>
|
||||
<ListGroup as="ul" data-sortable='{"group":"transfer","animation":150}'>
|
||||
<ListGroupItem as="li">In-app messages</ListGroupItem>
|
||||
</ListGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-12 col-xl-4">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Nested lists</CardTitle>
|
||||
<CardSubtitle>Put a sortable list inside an item and give all lists one <code>group</code> to move items between levels.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
|
||||
<ListGroupItem as="li">
|
||||
<div>Design</div>
|
||||
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
|
||||
<ListGroupItem as="li">Wireframes</ListGroupItem>
|
||||
<ListGroupItem as="li">Prototype</ListGroupItem>
|
||||
</ListGroup>
|
||||
</ListGroupItem>
|
||||
<ListGroupItem as="li">
|
||||
<div>Development</div>
|
||||
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
|
||||
<ListGroupItem as="li">Frontend</ListGroupItem>
|
||||
<ListGroupItem as="li">Backend</ListGroupItem>
|
||||
<ListGroupItem as="li">Tests</ListGroupItem>
|
||||
</ListGroup>
|
||||
</ListGroupItem>
|
||||
<ListGroupItem as="li">
|
||||
<div>Launch</div>
|
||||
<ListGroup as="ul" class="mt-2" data-sortable='{"group":"nested","animation":150,"fallbackOnBody":true,"swapThreshold":0.65}'>
|
||||
<ListGroupItem as="li">Release notes</ListGroupItem>
|
||||
</ListGroup>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<h2 class="page-section-title">{sections[3]!.title}</h2>
|
||||
<p class="page-section-description">{sections[3]!.description}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Multiple selection</CardTitle>
|
||||
<CardSubtitle>Set <code>multiDrag</code> to <code>true</code>. Hold <kbd>Ctrl</kbd> or <kbd>⌘</kbd> and click to select items, then drag them together.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"multiDrag":true,"animation":150}'>
|
||||
<ListGroupItem as="li">Invoice #1001</ListGroupItem>
|
||||
<ListGroupItem as="li">Invoice #1002</ListGroupItem>
|
||||
<ListGroupItem as="li">Invoice #1003</ListGroupItem>
|
||||
<ListGroupItem as="li">Invoice #1004</ListGroupItem>
|
||||
<ListGroupItem as="li">Invoice #1005</ListGroupItem>
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card class="h-100">
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle as="h3">Swap</CardTitle>
|
||||
<CardSubtitle>Set <code>swap</code> to <code>true</code> to exchange two items instead of shifting the rest.</CardSubtitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ListGroup as="ul" data-sortable='{"swap":true,"animation":150}'>
|
||||
<ListGroupItem as="li">First place</ListGroupItem>
|
||||
<ListGroupItem as="li">Second place</ListGroupItem>
|
||||
<ListGroupItem as="li">Third place</ListGroupItem>
|
||||
<ListGroupItem as="li">Fourth place</ListGroupItem>
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
// Sortable on a kanban board, drawn in the middle of a drag. The card being
|
||||
// moved has left the Upcoming column and shows twice, as SortableJS renders it:
|
||||
// a dashed slot in In Progress where it will land, and the copy that follows the
|
||||
// pointer, lifted with a shadow and an outline. Both carry the real state classes
|
||||
// (`.sortable-ghost`, `.sortable-drag`), so the picture shows the shipped styles.
|
||||
// The fake cursor rests on the floating card.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import Signal from '@ui/Signal.astro'
|
||||
import { personById } from '@shared/lib/people'
|
||||
import tasks from '@data/tasks.json'
|
||||
|
||||
interface Task {
|
||||
name: string
|
||||
assigned_to: number
|
||||
priority: string
|
||||
}
|
||||
|
||||
interface Column {
|
||||
name: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
const [upcoming, progress, completed] = tasks.columns as Column[] as [Column, Column, Column]
|
||||
const priorities: Record<string, { value: number; color: string }> = {
|
||||
High: { value: 3, color: 'red' },
|
||||
Medium: { value: 2, color: 'orange' },
|
||||
Low: { value: 1, color: 'blue' },
|
||||
}
|
||||
|
||||
const moving = upcoming.tasks[2]!
|
||||
|
||||
const columns = [
|
||||
{ name: upcoming.name, cards: [upcoming.tasks[0]!, upcoming.tasks[1]!] },
|
||||
{ name: progress.name, cards: [progress.tasks[0]!, moving, progress.tasks[1]!] },
|
||||
{ name: completed.name, cards: [completed.tasks[0]!, completed.tasks[1]!, completed.tasks[2]!] },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Sortable" columns={3}>
|
||||
<Card>
|
||||
<CardBody class="p-4 position-relative">
|
||||
<div class="row g-3">
|
||||
{
|
||||
columns.map((column) => (
|
||||
<div class="col-4">
|
||||
<h4 class="mb-2">{column.name}</h4>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{column.cards.map((task) => (
|
||||
<Card size="sm" class:list={['cursor-move', task === moving && 'sortable-chosen sortable-ghost']}>
|
||||
<CardBody>
|
||||
<div class="mb-3">{task.name}</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="d-flex align-items-center gap-2 text-secondary">
|
||||
<Signal value={priorities[task.priority]?.value ?? 0} color={priorities[task.priority]?.color} />
|
||||
{task.priority}
|
||||
</span>
|
||||
<Avatar person={personById(task.assigned_to)} size="xs" />
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card card-sm sortable-chosen sortable-fallback sortable-drag" style="position: absolute; top: 11rem; left: 39.5%; width: 28.5%">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">{moving.name}</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="d-flex align-items-center gap-2 text-secondary">
|
||||
<Signal value={priorities[moving.priority]?.value ?? 0} color={priorities[moving.priority]?.color} />
|
||||
{moving.priority}
|
||||
</span>
|
||||
<Avatar person={personById(moving.assigned_to)} size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="cursor" style="top: 55%; left: 55%"></span>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ScreenshotLayout>
|
||||
Reference in new issue
Block a user