kanban/src/index.js

155 lines
3.7 KiB
JavaScript

/**
* @web/kanban
* 原生 ESM 看板引擎
*/
export class Kanban extends HTMLElement {
#data = [];
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.#render();
}
get data() {
return this.#data;
}
set data(value) {
this.#data = value;
this.#render();
}
#render() {
const style = `
:host {
display: flex;
gap: 16px;
padding: 16px;
overflow-x: auto;
font-family: system-ui, -apple-system, sans-serif;
}
.column {
background: #f1f2f4;
border-radius: 8px;
width: 280px;
min-width: 280px;
display: flex;
flex-direction: column;
max-height: 100%;
}
.column-header {
padding: 12px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.items-container {
flex: 1;
padding: 8px;
min-height: 50px;
}
.item {
background: white;
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
cursor: grab;
}
.item:active {
cursor: grabbing;
}
.item.dragging {
opacity: 0.5;
}
.items-container.drag-over {
background: rgba(0,0,0,0.05);
}
`;
this.shadowRoot.innerHTML = `
<style>${style}</style>
${this.#data.map(col => `
<div class="column" data-id="${col.id}">
<div class="column-header">
<span>${col.title}</span>
</div>
<div class="items-container" data-column-id="${col.id}">
${(col.items || []).map(item => `
<div class="item" draggable="true" data-id="${item.id}" data-column-id="${col.id}">
${item.content}
</div>
`).join('')}
</div>
</div>
`).join('')}
`;
this.#initDragAndDrop();
}
#initDragAndDrop() {
const items = this.shadowRoot.querySelectorAll('.item');
const containers = this.shadowRoot.querySelectorAll('.items-container');
items.forEach(item => {
item.addEventListener('dragstart', (e) => {
item.classList.add('dragging');
e.dataTransfer.setData('text/plain', JSON.stringify({
itemId: item.dataset.id,
fromColumnId: item.dataset.columnId
}));
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
});
});
containers.forEach(container => {
container.addEventListener('dragover', (e) => {
e.preventDefault();
container.classList.add('drag-over');
});
container.addEventListener('dragleave', () => {
container.classList.remove('drag-over');
});
container.addEventListener('drop', (e) => {
e.preventDefault();
container.classList.remove('drag-over');
const dragData = JSON.parse(e.dataTransfer.getData('text/plain'));
this.#moveItem(dragData.itemId, dragData.fromColumnId, container.dataset.columnId);
});
});
}
#moveItem(itemId, fromColumnId, toColumnId) {
if (fromColumnId === toColumnId) return;
const fromCol = this.#data.find(c => c.id === fromColumnId);
const toCol = this.#data.find(c => c.id === toColumnId);
const itemIndex = fromCol.items.findIndex(i => i.id === itemId);
const [item] = fromCol.items.splice(itemIndex, 1);
toCol.items.push(item);
this.#render();
this.dispatchEvent(new CustomEvent('@update', {
detail: this.#data,
bubbles: true,
composed: true
}));
}
}
customElements.define('web-kanban', Kanban);