155 lines
3.7 KiB
JavaScript
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);
|