UNPKG

angular-kanban

Version:

A lightweight and customizable Kanban board library for Angular application

253 lines (243 loc) 20.5 kB
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