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-status
|
||||||
sort-tags
|
sort-tags
|
||||||
sort-type
|
sort-type
|
||||||
|
sortable-fixed
|
||||||
|
sortable-handle
|
||||||
table-tbody
|
table-tbody
|
||||||
tblr-illustrations-computer-fix-a
|
tblr-illustrations-computer-fix-a
|
||||||
tblr-illustrations-computer-fix-b
|
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 SELECTOR_DATA_SORTABLE = `[${DATA_ATTRIBUTE}]`
|
||||||
|
|
||||||
const Default: ComponentConfig = {}
|
const Default: ComponentConfig = {
|
||||||
|
forceFallback: true,
|
||||||
|
}
|
||||||
|
|
||||||
const DefaultType: Record<string, string> = {}
|
const DefaultType: Record<string, string> = {}
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ const DefaultType: Record<string, string> = {}
|
|||||||
* Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately
|
* Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately
|
||||||
* as `window.Sortable`. Without the plugin the component is inert. Options come
|
* as `window.Sortable`. Without the plugin the component is inert. Options come
|
||||||
* from the `data-sortable` attribute as JSON, or from the config object.
|
* 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 {
|
class Sortable extends BaseComponent {
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ describe('Sortable', () => {
|
|||||||
expect(instance.sortable).toBe(plugin.mock.instances[0])
|
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', () => {
|
it('should let the config object win over the attribute', () => {
|
||||||
new Sortable(list(), { animation: 0, group: 'shared' })
|
new Sortable(list(), { animation: 0, group: 'shared' })
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -82,19 +82,19 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "./dist/css/tabler.css",
|
"path": "./dist/css/tabler.css",
|
||||||
"maxSize": "87 kB"
|
"maxSize": "88 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/css/tabler.min.css",
|
"path": "./dist/css/tabler.min.css",
|
||||||
"maxSize": "81 kB"
|
"maxSize": "82 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/css/tabler.rtl.css",
|
"path": "./dist/css/tabler.rtl.css",
|
||||||
"maxSize": "87 kB"
|
"maxSize": "88 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/css/tabler.rtl.min.css",
|
"path": "./dist/css/tabler.rtl.min.css",
|
||||||
"maxSize": "81 kB"
|
"maxSize": "82 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/css/tabler-flags.css",
|
"path": "./dist/css/tabler-flags.css",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
@forward 'ui/chat';
|
@forward 'ui/chat';
|
||||||
@forward 'ui/signature';
|
@forward 'ui/signature';
|
||||||
@forward 'ui/patterns';
|
@forward 'ui/patterns';
|
||||||
|
@forward 'ui/sortable';
|
||||||
|
|
||||||
@forward 'helpers/index';
|
@forward 'helpers/index';
|
||||||
|
|
||||||
|
|||||||
@@ -1837,6 +1837,18 @@ $tracking-border-radius: var(--border-radius) !default;
|
|||||||
$tracking-block-min-width: 0.25rem !default;
|
$tracking-block-min-width: 0.25rem !default;
|
||||||
// scss-docs-end tracking-variables
|
// 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
|
// Progress
|
||||||
// scss-docs-start progress-variables
|
// scss-docs-start progress-variables
|
||||||
$progress-height: 0.5rem !default;
|
$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 DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||||
|
import Avatar from '@ui/Avatar.astro'
|
||||||
import Card from '@ui/Card.astro'
|
import Card from '@ui/Card.astro'
|
||||||
import CardHeader from '@ui/CardHeader.astro'
|
import CardHeader from '@ui/CardHeader.astro'
|
||||||
import CardBody from '@ui/CardBody.astro'
|
import CardBody from '@ui/CardBody.astro'
|
||||||
import CardTitle from '@ui/CardTitle.astro'
|
import CardTitle from '@ui/CardTitle.astro'
|
||||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||||
|
import Icon from '@ui/Icon.astro'
|
||||||
import ListGroup from '@ui/ListGroup.astro'
|
import ListGroup from '@ui/ListGroup.astro'
|
||||||
import ListGroupItem from '@ui/ListGroupItem.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="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">
|
<div class="col-md-6">
|
||||||
<Card>
|
<Card class="h-100">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Basic usage</CardTitle>
|
<CardTitle as="h3">Basic usage</CardTitle>
|
||||||
<CardSubtitle>Drag to reorder items within a single list.</CardSubtitle>
|
<CardSubtitle>Drag to reorder items within a single list.</CardSubtitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -30,11 +74,11 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<Card>
|
<Card class="h-100">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Two lists with drag and drop</CardTitle>
|
<CardTitle as="h3">Two lists with drag and drop</CardTitle>
|
||||||
<CardSubtitle>Move items between two connected lists.</CardSubtitle>
|
<CardSubtitle>Give lists the same <code>group</code> to move items between them.</CardSubtitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -57,5 +101,351 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</DefaultLayout>
|
</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