angular-kanban
Version:
A lightweight and customizable Kanban board library for Angular application
253 lines (243 loc) • 20.5 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, EventEmitter, Output, Input } from '@angular/core';
class NgKanbanService {
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NgKanbanService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NgKanbanService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NgKanbanService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class NgKanbanComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NgKanbanComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: NgKanbanComponent, isStandalone: true, selector: "lib-ng-kanban", ngImport: i0, template: `
<p>
ng-kanban works!
</p>
`, isInline: true, styles: [""] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NgKanbanComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-ng-kanban', imports: [], template: `
<p>
ng-kanban works!
</p>
` }]
}] });
class KanbanCardComponent {
/** The card data */
card;
/** Whether the card is draggable (default: true) */
draggable = true;
/** Whether to show the default title (default: true) */
showTitle = true;
/** Whether to show the default description (default: true) */
showDescription = true;
/** Custom CSS class for the card */
customClass = '';
/** Event emitter for card click */
cardClick = new EventEmitter();
/** Track if custom header/content is provided */
hasCustomHeader = false;
hasCustomBody = false;
onDragStart(event) {
if (this.draggable && event.dataTransfer && this.card) {
// Store the card ID in the drag data
event.dataTransfer.setData('cardId', this.card.id);
event.dataTransfer.effectAllowed = 'move';
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.2", type: KanbanCardComponent, isStandalone: true, selector: "lib-kanban-card", inputs: { card: "card", draggable: "draggable", showTitle: "showTitle", showDescription: "showDescription", customClass: "customClass" }, outputs: { cardClick: "cardClick" }, ngImport: i0, template: "<!-- Card Id is Set to the card, \n on drag start emit the event which emit which card is dragging and there source and target source\n Click event emit which card is clicked and emit that card details -->\n\n <div\n class=\"kanban-card\"\n [attr.data-id]=\"card.id\"\n draggable=\"true\"\n (dragstart)=\"onDragStart($event)\"\n (click)=\"cardClick.emit(card)\"\n>\n <!-- Projected content for the card header -->\n <ng-content select=\"[card-header]\"></ng-content>\n\n <!-- Default card title (if no custom header is provided) -->\n @if (!hasCustomHeader) {\n <div class=\"card-title\">{{ card.title }}</div>\n }\n\n <!-- Projected content for the card body -->\n <ng-content select=\"[card-body]\"></ng-content>\n\n <!-- Default card description (if no custom body is provided) -->\n @if (!hasCustomBody && card.description) {\n <div class=\"card-description\">\n {{ card.description }}\n </div>\n }\n\n <!-- Projected content for the card footer -->\n <ng-content select=\"[card-footer]\"></ng-content>\n</div>", styles: [".kanban-card{background-color:#fff;border-radius:12px;box-shadow:0 4px 8px #0000001a;padding:16px;margin-bottom:12px;transition:transform .2s ease,box-shadow .2s ease;cursor:pointer;border:1px solid #e0e0e0}.kanban-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px #0003}.card-title{font-size:18px;font-weight:600;color:#333;margin-bottom:8px}.card-description{font-size:14px;color:#666;line-height:1.5;margin-bottom:12px}::ng-deep [card-header]{font-size:20px;font-weight:700;color:#007bff;margin-bottom:12px}::ng-deep [card-body]{font-size:14px;color:#444;line-height:1.6;margin-bottom:12px}::ng-deep [card-footer]{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}::ng-deep [card-footer] button{background-color:#007bff;color:#fff;border:none;padding:8px 16px;border-radius:6px;font-size:14px;cursor:pointer;transition:background-color .2s ease}::ng-deep [card-footer] button:hover{background-color:#0056b3}.kanban-card[draggable=true]{-webkit-user-select:none;user-select:none}.kanban-card[draggable=true]:active{opacity:.7}\n"] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanCardComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-kanban-card', imports: [], template: "<!-- Card Id is Set to the card, \n on drag start emit the event which emit which card is dragging and there source and target source\n Click event emit which card is clicked and emit that card details -->\n\n <div\n class=\"kanban-card\"\n [attr.data-id]=\"card.id\"\n draggable=\"true\"\n (dragstart)=\"onDragStart($event)\"\n (click)=\"cardClick.emit(card)\"\n>\n <!-- Projected content for the card header -->\n <ng-content select=\"[card-header]\"></ng-content>\n\n <!-- Default card title (if no custom header is provided) -->\n @if (!hasCustomHeader) {\n <div class=\"card-title\">{{ card.title }}</div>\n }\n\n <!-- Projected content for the card body -->\n <ng-content select=\"[card-body]\"></ng-content>\n\n <!-- Default card description (if no custom body is provided) -->\n @if (!hasCustomBody && card.description) {\n <div class=\"card-description\">\n {{ card.description }}\n </div>\n }\n\n <!-- Projected content for the card footer -->\n <ng-content select=\"[card-footer]\"></ng-content>\n</div>", styles: [".kanban-card{background-color:#fff;border-radius:12px;box-shadow:0 4px 8px #0000001a;padding:16px;margin-bottom:12px;transition:transform .2s ease,box-shadow .2s ease;cursor:pointer;border:1px solid #e0e0e0}.kanban-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px #0003}.card-title{font-size:18px;font-weight:600;color:#333;margin-bottom:8px}.card-description{font-size:14px;color:#666;line-height:1.5;margin-bottom:12px}::ng-deep [card-header]{font-size:20px;font-weight:700;color:#007bff;margin-bottom:12px}::ng-deep [card-body]{font-size:14px;color:#444;line-height:1.6;margin-bottom:12px}::ng-deep [card-footer]{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}::ng-deep [card-footer] button{background-color:#007bff;color:#fff;border:none;padding:8px 16px;border-radius:6px;font-size:14px;cursor:pointer;transition:background-color .2s ease}::ng-deep [card-footer] button:hover{background-color:#0056b3}.kanban-card[draggable=true]{-webkit-user-select:none;user-select:none}.kanban-card[draggable=true]:active{opacity:.7}\n"] }]
}], propDecorators: { card: [{
type: Input
}], draggable: [{
type: Input
}], showTitle: [{
type: Input
}], showDescription: [{
type: Input
}], customClass: [{
type: Input
}], cardClick: [{
type: Output
}] } });
class KanbanColumnComponent {
/** Column data: {
id: string;
title: string;
cards: KanbanCard[];
} */
column;
/** Emit event when a card is clicked */
cardClick = new EventEmitter();
/** Emit event when a card is moved */
cardMoved = new EventEmitter();
/** Fires continuously while a card is being dragged over the column. */
onDragOver(event) {
/**
* Prevents the default behavior (which blocks dropping).
* Sets the dropEffect to 'move' to indicate that the card will be moved.
**/
event.preventDefault();
if (event.dataTransfer) {
// drop effect alwayse set to none by default we explicitly set it to move
event.dataTransfer.dropEffect = 'move';
}
}
/**
* @param event Fires when a dragged card enters the column.
* Adds a CSS class (drag-over) to the column's content area to provide visual feedback (e.g., highlighting the column).
*/
onDragEnter(event) {
if (event.target instanceof Element) {
// Check for the class column-content exist in classList or not if exist add drag-over class
if (event.target.classList.contains('column-content')) {
event.target.classList.add('drag-over');
}
}
}
/**
* @param event Fires when a dragged card leaves the column.
* Removes the drag-over CSS class to reset the visual feedback.
*/
onDragLeave(event) {
if (event.target instanceof Element) {
if (event.target.classList.contains('column-content')) {
// Remove drag-over class
event.target.classList.remove('drag-over');
}
}
}
/**
*
* @param event Fires when a card is dropped on the column.
* Prevents the default behavior.
* Removes the drag-over styling.
* Gets the cardId from the dataTransfer object.
* Finds the sourceColumnId (the column the card is coming from).
* Emits a cardMoved event to notify the parent component that the card has been moved.
* @returns
*/
onDrop(event) {
event.preventDefault();
// Remove the drag-over styling
if (event.target instanceof Element) {
const columnContent = event.target.closest('.column-content');
if (columnContent) {
columnContent.classList.remove('drag-over');
}
}
if (event.dataTransfer) {
// Get the cardId
const cardId = event.dataTransfer.getData('cardId');
if (!cardId)
return;
// Find the source column (the column the card is coming from)
let sourceColumnId = '';
// Get All the columns
const columnElements = document.querySelectorAll('.kanban-column');
// Get all the cards
columnElements.forEach(colEl => {
const cards = colEl.querySelectorAll('.kanban-card');
cards.forEach(card => {
// Check if the cardId matches
if (card.getAttribute('data-id') === cardId) {
// If Card Id matches set the sourceColumnId
// Get source column Id ( eg: in progress )
sourceColumnId = colEl.getAttribute('data-id') || '';
}
});
});
if (sourceColumnId && sourceColumnId !== this.column.id) {
// Emit an event to move the card
this.cardMoved.emit({
cardId,
sourceColumnId,
targetColumnId: this.column.id
});
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.2", type: KanbanColumnComponent, isStandalone: true, selector: "lib-kanban-column", inputs: { column: "column" }, outputs: { cardClick: "cardClick", cardMoved: "cardMoved" }, ngImport: i0, template: "<div class=\"kanban-column\" [attr.data-id]=\"column.id\">\n <div class=\"column-header\">\n <h3>{{ column.title }}</h3>\n </div>\n <div\n class=\"column-content\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n >\n @for (card of column.cards; track card.id) {\n <lib-kanban-card [card]=\"card\" (cardClick)=\"cardClick.emit($event)\">\n </lib-kanban-card>\n }\n </div>\n</div>\n", styles: [".kanban-column{background-color:#fff;border-radius:8px;box-shadow:0 4px 8px #0000001a;width:280px;min-width:280px;margin-right:12px;height:100%;display:flex;flex-direction:column;transition:transform .2s ease,box-shadow .2s ease;border:1px solid #e0e0e0}.column-header{padding:12px 16px;border-bottom:2px solid #007bff;background-color:#f8f9fa;border-top-left-radius:8px;border-top-right-radius:8px}.column-header h3{margin:0;font-size:18px;font-weight:600;color:#333}.column-content{padding:12px;flex:1;margin:2px;overflow-y:auto;background-color:#fff;border-bottom-left-radius:8px;border-bottom-right-radius:8px}.column-content.drag-over{background-color:#e3f2fd;border:2px dashed #007bff;margin:2px;border-radius:6px;padding:10px}.column-content::-webkit-scrollbar{width:8px}.column-content::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}.column-content::-webkit-scrollbar-thumb{background:#007bff;border-radius:4px}.column-content::-webkit-scrollbar-thumb:hover{background:#0056b3}\n"], dependencies: [{ kind: "component", type: KanbanCardComponent, selector: "lib-kanban-card", inputs: ["card", "draggable", "showTitle", "showDescription", "customClass"], outputs: ["cardClick"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanColumnComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-kanban-column', imports: [KanbanCardComponent], template: "<div class=\"kanban-column\" [attr.data-id]=\"column.id\">\n <div class=\"column-header\">\n <h3>{{ column.title }}</h3>\n </div>\n <div\n class=\"column-content\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n >\n @for (card of column.cards; track card.id) {\n <lib-kanban-card [card]=\"card\" (cardClick)=\"cardClick.emit($event)\">\n </lib-kanban-card>\n }\n </div>\n</div>\n", styles: [".kanban-column{background-color:#fff;border-radius:8px;box-shadow:0 4px 8px #0000001a;width:280px;min-width:280px;margin-right:12px;height:100%;display:flex;flex-direction:column;transition:transform .2s ease,box-shadow .2s ease;border:1px solid #e0e0e0}.column-header{padding:12px 16px;border-bottom:2px solid #007bff;background-color:#f8f9fa;border-top-left-radius:8px;border-top-right-radius:8px}.column-header h3{margin:0;font-size:18px;font-weight:600;color:#333}.column-content{padding:12px;flex:1;margin:2px;overflow-y:auto;background-color:#fff;border-bottom-left-radius:8px;border-bottom-right-radius:8px}.column-content.drag-over{background-color:#e3f2fd;border:2px dashed #007bff;margin:2px;border-radius:6px;padding:10px}.column-content::-webkit-scrollbar{width:8px}.column-content::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}.column-content::-webkit-scrollbar-thumb{background:#007bff;border-radius:4px}.column-content::-webkit-scrollbar-thumb:hover{background:#0056b3}\n"] }]
}], propDecorators: { column: [{
type: Input
}], cardClick: [{
type: Output
}], cardMoved: [{
type: Output
}] } });
class KanbanBoardComponent {
board;
cardClick = new EventEmitter();
cardMoved = new EventEmitter();
onCardMoved(event) {
this.cardMoved.emit(event);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.2", type: KanbanBoardComponent, isStandalone: true, selector: "lib-kanban-board", inputs: { board: "board" }, outputs: { cardClick: "cardClick", cardMoved: "cardMoved" }, ngImport: i0, template: "<div class=\"kanban-board\">\n @for (column of board.columns; track column.id) {\n <lib-kanban-column\n [column]=\"column\"\n (cardClick)=\"cardClick.emit($event)\"\n (cardMoved)=\"onCardMoved($event)\">\n </lib-kanban-column>\n }\n</div>", styles: [".kanban-board{display:flex;overflow-x:auto;padding:16px;height:100%;min-height:400px;background-color:#f8f9fa;gap:16px;border-radius:12px;box-shadow:0 4px 8px #0000001a}.kanban-board::-webkit-scrollbar{height:8px}.kanban-board::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}.kanban-board::-webkit-scrollbar-thumb{background:#007bff;border-radius:4px}.kanban-board::-webkit-scrollbar-thumb:hover{background:#0056b3}\n"], dependencies: [{ kind: "component", type: KanbanColumnComponent, selector: "lib-kanban-column", inputs: ["column"], outputs: ["cardClick", "cardMoved"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanBoardComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-kanban-board', imports: [KanbanColumnComponent], template: "<div class=\"kanban-board\">\n @for (column of board.columns; track column.id) {\n <lib-kanban-column\n [column]=\"column\"\n (cardClick)=\"cardClick.emit($event)\"\n (cardMoved)=\"onCardMoved($event)\">\n </lib-kanban-column>\n }\n</div>", styles: [".kanban-board{display:flex;overflow-x:auto;padding:16px;height:100%;min-height:400px;background-color:#f8f9fa;gap:16px;border-radius:12px;box-shadow:0 4px 8px #0000001a}.kanban-board::-webkit-scrollbar{height:8px}.kanban-board::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}.kanban-board::-webkit-scrollbar-thumb{background:#007bff;border-radius:4px}.kanban-board::-webkit-scrollbar-thumb:hover{background:#0056b3}\n"] }]
}], propDecorators: { board: [{
type: Input
}], cardClick: [{
type: Output
}], cardMoved: [{
type: Output
}] } });
class KanbanStateService {
// Move a card from one column to another
moveCard(board, cardId, sourceColumnId, targetColumnId) {
// Create a deep copy of the board to prevent direct mutation
const updatedBoard = JSON.parse(JSON.stringify(board));
// Find source and target columns
const sourceColumn = updatedBoard.columns.find(col => col.id === sourceColumnId);
const targetColumn = updatedBoard.columns.find(col => col.id === targetColumnId);
if (!sourceColumn || !targetColumn) {
return board; // Return original if columns not found
}
// Find and remove card from source column
const cardIndex = sourceColumn.cards.findIndex(card => card.id === cardId);
if (cardIndex === -1) {
return board; // Return original if card not found
}
// Remove the card from source column and get a reference to it
const [card] = sourceColumn.cards.splice(cardIndex, 1);
// Add card to target column
targetColumn.cards.push(card);
return updatedBoard;
}
log(board) {
console.log(board);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanStateService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: KanbanStateService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/*
* Public API Surface of ng-kanban
*/
/**
* Generated bundle index. Do not edit.
*/
export { KanbanBoardComponent, KanbanCardComponent, KanbanColumnComponent, KanbanStateService, NgKanbanComponent, NgKanbanService };
//# sourceMappingURL=angular-kanban.mjs.map