UNPKG

@disane/ngx-taskboard

Version:
1,066 lines (1,064 loc) 214 kB
import { EventEmitter, Injectable, ɵɵdefineInjectable, Component, ChangeDetectionStrategy, Renderer2, ElementRef, ChangeDetectorRef, NgZone, Input, Output, HostListener, Directive, NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { timer, Subject } from 'rxjs'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { fab } from '@fortawesome/free-brands-svg-icons'; import { far } from '@fortawesome/free-regular-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; /** * @fileoverview added by tsickle * Generated from: lib/taskboard.service.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TaskboardService { constructor() { this.filterChanged$ = new EventEmitter(); this.objectProperties = []; } } TaskboardService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ TaskboardService.ɵprov = ɵɵdefineInjectable({ factory: function TaskboardService_Factory() { return new TaskboardService(); }, token: TaskboardService, providedIn: "root" }); if (false) { /** @type {?} */ TaskboardService.prototype.filterChanged$; /** @type {?} */ TaskboardService.prototype.objectProperties; } /** * @fileoverview added by tsickle * Generated from: lib/board/board.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class BoardComponent { /** * @param {?} renderer * @param {?} elRef * @param {?} cd * @param {?} taskboardService * @param {?} ngZone */ constructor(renderer, elRef, cd, taskboardService, ngZone) { this.renderer = renderer; this.elRef = elRef; this.cd = cd; this.taskboardService = taskboardService; this.ngZone = ngZone; /** * Shows the blacklog on onit */ this.showBacklog = true; /** * Name of the backlog row */ this.backlogName = 'Backlog'; /** * Grouping keys for columns (if not passed, the keys will be determined out of the items) * Caution: If you don't pass any headings manually, only the columns are shown, which have data. * If you want to show emtpy rows, please set them */ this.hGroupKeys = []; /** * Grouping keys for rows (if not passed, the keys will be determined out of the items) * Caution: If you don't pass any headings manually, only the rows are shown, which have data. * If you want to show emtpy rows, please set them */ this.vGroupKeys = []; /** * Show add buttons on the column headings */ this.hAddNewItems = true; /** * Show add buttons on the row headings */ this.vAddNewItems = true; /** * Show add buttons in the cells for columns and rows */ this.cellAddNewItems = true; /** * Key to group data for rows */ this.vGroupKey = ''; /** * Key to group data for columns */ this.hGroupKey = ''; /** * Sort items by property */ this.sortBy = ''; /** * Board name to show between row and column header */ this.boardName = ''; /** * Invert rows and columns */ this.invertGroupDirection = false; /** * All items which can't be grouped into rows and columns are stored into the backlog */ this.showUngroupedInBacklog = true; /** * Decrease overall font size */ this.smallText = false; /** * Template for items to render. "item" object ist passed (see examples) */ this.itemTemplate = null; /** * Template for collapsed rows to render. "count" object ist passed (see examples) */ this.noElementsTemplate = null; /** * Template for column headers. Current groupName will be passed (see examples) */ this.hHeaderTemplate = null; /** * Template for row headers. Current groupName will be passed (see examples) */ this.vHeaderTemplate = null; /** * Template for actions, add and collapse buttons (see examples) */ this.actionsTemplate = null; /** * Template for the placeholder element which will be generated when an item is draged over a cell */ this.dragoverPlaceholderTemplate = null; /** * Icon template for add icons */ this.addIconTemplate = null; /** * Icon template for collapse icons */ this.collapseIconTemplate = null; /** * Icon template for expand icons */ this.expandIconTemplate = null; /** * Default css class for row header */ this.vHeaderClass = 'card-header'; /** * Default css class for column header */ this.hHeaderClass = 'card-header card-header-bg'; /** * If set to true, the horizontal group keys are fixed positioned to the top and remain at the top while scrolling. Only applied when scrollable is true */ this.stickyHorizontalHeaderKeys = true; /** * If set to true, the vertical group keys are fixed positioned to the top and remain at the top while scrolling. Only applied when scrollable is true */ this.stickyVerticalHeaderKeys = false; /** * Default css class for cell header */ this.cellClass = 'card-header'; /** * Column width (in px) which is applied to the columns when the content is scollable */ this.columnWidth = 200; /** * Width of the backlog row, when activated. You can use all valid css units. Default is columnWidth */ this.backlogWidth = `${this.columnWidth}px`; /** * Allow to collapse the rows */ this.vCollapsable = true; /** * Rows are collapsed or not on init */ this.vCollapsed = false; /** * Columns are collapsed or not on init */ this.hCollapsed = false; /** * Shows the filter row to search items by filter in filterOnProperties array */ this.showFilterRow = true; /** * Placeholder for the input with the filter row */ this.filterRowPlaceholder = 'Search for items'; /** * Predefined filter for the searchbar. If set, the items are filtered by the term on init. */ this.filter = ''; /** * Specify the properties which will be searched for the given term * in filter. If not properties are given, all will be searched */ this.filterOnProperties = []; /** * The collapse state which is applied when set initially */ this.initialCollapseState = []; // OUTPUTS /** * Fired when the user drags an item. Current item is passed */ this.dragStarted = new EventEmitter(); /** * Fired when an item is dropped. Current item is passed */ this.dropped = new EventEmitter(); /** * Fired when an add action is click. Current ClickEvent is passed */ this.elementCreateClick = new EventEmitter(); /** * Fired when a heading is collapsed. CollapseEvent is emitted */ this.headingCollapsed = new EventEmitter(); this.isScrolling = new EventEmitter(); this.scrolledToEnd = new EventEmitter(); this.scrollEnded = new EventEmitter(); /** * Column headings */ this.hHeadings = []; /** * Row headings */ this.vHeadings = []; /** * If set to true, the rows and columns are scrollable and will be out of the viewport. * If not set, all rows and column will only use 100% of the parent element (aligned by flex/flex-fill) */ this.scrollable = false; /** * If set to true, rows are scrollable */ this.verticalScrolling = false; /** * If set to true, columns are scrollable */ this.horizontalScrolling = false; /** * Items to display */ // tslint:disable-next-line: variable-name this._items = []; this.collapseStates = []; this.placeholderSet = false; this.isScrollingTimeout = 0; this.scrollableContainer = '.column-cards'; } /** * @param {?} items * @return {?} */ set items(items) { this._items = items; if (items.length > 0) { this.prepareBoard(); } } /** * @return {?} */ get items() { return this._items; } /** * @return {?} */ onResize() { this.checkIfContentNeedsToScroll(); } /** * @return {?} */ ngOnInit() { if (this.items.length > 0) { this.prepareBoard(); } } /** * @return {?} */ executeChangeDetection() { // this.checkIfContentNeedsToScroll(); this.cd.detectChanges(); } /** * @return {?} */ ngDoCheck() { } /** * @return {?} */ ngAfterViewInit() { // Called after ngAfterContentInit when the component's view has been initialized. Applies to components only. // Add 'implements AfterViewInit' to the class. } /** * Checks if content needs to scroll and restore the scrollstate if needed * @private * @return {?} */ checkIfContentNeedsToScroll() { const { hScroll: h, vScroll: v } = this.containerIsScrollable(this.scrollableContainer); this.horizontalScrolling = h; this.verticalScrolling = v; this.scrollable = v || h; // this.executeChangeDetection(); if (this.scrollStates && (this.horizontalScrolling || this.verticalScrolling)) { setTimeout((/** * @return {?} */ () => { if (this.restoreScrollState(h, v, this.scrollStates)) { this.scrollStates = null; } }), 500); } } /** * Prepares board * @private * @return {?} */ prepareBoard() { this.checkPrerequisites().then((/** * @return {?} */ () => { this.generateHeadings(); this.collapseStates.push(...this.generateCollapseStates(this.hHeadings, 'h'), ...this.generateCollapseStates(this.vHeadings, 'v')); this.matchAndSetInitialCollapseState(); this.taskboardService.filterChanged$.subscribe((/** * @param {?} filter * @return {?} */ filter => this.filter = filter)); this.checkIfContentNeedsToScroll(); this.executeChangeDetection(); })); } /** * @private * @param {?} hScrollable * @param {?} vScrollable * @param {?} scrollState * @return {?} */ restoreScrollState(hScrollable, vScrollable, scrollState) { /** @type {?} */ const scrollContainer = this.elRef.nativeElement.querySelector(this.scrollableContainer); if (scrollContainer && scrollState) { if (vScrollable) { scrollContainer.scrollTop = scrollState.scrollTop; } if (hScrollable) { scrollContainer.scrollLeft = scrollState.scrollLeft; } return true; } return false; } /** * Matches and set initial collapse state * @private * @return {?} */ matchAndSetInitialCollapseState() { this.initialCollapseState.forEach((/** * @param {?} item * @return {?} */ item => { /** @type {?} */ const foundCollapseState = this.collapseStates.find((/** * @param {?} cS * @return {?} */ cS => cS.name.toLowerCase() === item.name.toLowerCase())); if (foundCollapseState && foundCollapseState.collapsed !== item.collapsed) { foundCollapseState.collapsed = item.collapsed; } })); } /** * Checks prerequisites * @private * @return {?} prerequisites */ checkPrerequisites() { if (this.checkIfPropIsObject(this.hGroupKeys[0])) { /** @type {?} */ const hasValueProperty = this.hGroupKeys.every((/** * @param {?} item * @return {?} */ (item) => item.value != null)); if (!hasValueProperty) { throw new Error((`Column headers are objects but field 'value' is missing in one or more items.`)); } } if (this.checkIfPropIsObject(this.vGroupKeys[0])) { /** @type {?} */ const hasValueProperty = this.vGroupKeys.every((/** * @param {?} item * @return {?} */ (item) => item.value != null)); if (!hasValueProperty) { throw new Error((`Row headers are objects but field 'value' is missing in one or more items.`)); } } return Promise.resolve(true); } /** * Checks if prop is object * @private * @param {?} prop * @return {?} true if if prop is object */ checkIfPropIsObject(prop) { return typeof (prop) === 'object'; } /** * Generates the appropiate headings * \@memberOf BoardComponent * @private * @return {?} */ generateHeadings() { if (this.invertGroupDirection) { /** @type {?} */ const vGkey = this.vGroupKey; /** @type {?} */ const hGkey = this.hGroupKey; this.hGroupKey = vGkey; this.vGroupKey = hGkey; } this.vHeadings = this.getHeadings(this.vGroupKeys, this.vGroupKey); this.hHeadings = this.getHeadings(this.hGroupKeys, this.hGroupKey); } /** * Generates collapse states * @private * @param {?} array Array of collapse states * @param {?} direction * @return {?} collapse states */ generateCollapseStates(array, direction) { return array.map((/** * @param {?} item * @return {?} */ item => ({ name: this.getValue(item), collapsed: (direction === 'h') ? this.hCollapsed : this.vCollapsed }))); } /** * Gets all items of a cell (row / col) * * \@memberOf BoardComponent * @param {?} vValue Value of the row * @param {?} hValue Value of the column * @return {?} Array of all items of a cell * */ getItemsOfGroup(vValue, hValue) { /** @type {?} */ let items = this.items.filter((/** * @param {?} item * @return {?} */ item => { if (this.taskboardService.objectProperties.length === 0) { this.taskboardService.objectProperties = Object.keys(item); } /** @type {?} */ const groupKeys = this.determineCorrectGroupKeys(); /** @type {?} */ const vItem = this.getValue(item[groupKeys.vGroupKey]); /** @type {?} */ const hItem = this.getValue(item[groupKeys.hGroupKey]); if (hItem === undefined || hItem === undefined && vItem === undefined || vItem === undefined) { return false; } /** @type {?} */ const found = vItem.toString().toLowerCase() === vValue.toString().toLowerCase() && hItem.toString().toLowerCase() === hValue.toString().toLowerCase(); return found; })); // console.log('getItemsOfGroup', items); if (this.showUngroupedInBacklog) { items = items.filter((/** * @param {?} item * @return {?} */ item => item[this.vGroupKey] !== '' && item[this.hGroupKey] !== '')); } if (this.sortBy !== '') { /** @type {?} */ const fieldType = typeof (items.some((/** * @return {?} */ () => items[0][this.sortBy] !== undefined && items[0][this.sortBy] !== null))[this.sortBy]); if (fieldType) { items = items.sort((/** * @param {?} a * @param {?} b * @return {?} */ (a, b) => { /** @type {?} */ const aField = a[this.sortBy]; /** @type {?} */ const bField = b[this.sortBy]; if (fieldType === 'number') { return bField - aField; } if (fieldType === 'string') { if (aField < bField) { return -1; } if (aField > bField) { return 1; } return 0; } })); } } return (this.filter !== '') ? items.filter((/** * @param {?} item * @return {?} */ (item) => (this.filterOnProperties.length > 0 ? this.filterOnProperties : Object.keys(item)).some((/** * @param {?} key * @return {?} */ key => { /** @type {?} */ const found = item[key] !== null && typeof (item[key]) !== 'number' && (((/** @type {?} */ (item[key]))).indexOf(this.filter) > -1 ? true : false); // found && console.info(`Searching "${item[key]}" for "${this.filter}" | Found ${found}`); return found; })))) : items; } /** * Toggles in entire group (all rows or all columns) * * \@memberOf BoardComponent * @param {?} direction Direction to toggle * @param {?} collapsed Current collapse state * * @return {?} */ toggleCollapseGroup(direction, collapsed) { this.ngZone.runOutsideAngular((/** * @return {?} */ () => { /** @type {?} */ const groupKeysToToggle = this.collapseStates.filter((/** * @param {?} item * @return {?} */ item => (direction === 'vertical' ? this.vHeadings : this.hHeadings) .some((/** * @param {?} i * @return {?} */ i => this.getValue(i).toString() .toLowerCase() === item.name.toString().toLowerCase())))); groupKeysToToggle.forEach((/** * @param {?} item * @return {?} */ item => item.collapsed = !collapsed)); if (groupKeysToToggle.length > 0) { if (direction === 'vertical') { this.vCollapsed = !collapsed; } else { this.hCollapsed = !collapsed; } } this.headingCollapsed.emit({ group: direction, collapsed: !collapsed, overallCollapseState: this.collapseStates }); })); // debugger; setTimeout((/** * @return {?} */ () => { this.executeChangeDetection(); this.checkIfContentNeedsToScroll(); }), 100); } /** * Gets the value of an item * * \@memberOf BoardComponent * @param {?} item Item to get the value from * @return {?} Value of item * */ getValue(item) { if ((/** @type {?} */ (item))) { return (((/** @type {?} */ (item))).value ? ((/** @type {?} */ (item))).value : (/** @type {?} */ (item))); } return ''; } /** * Determines correct group keys * @private * @return {?} correct group keys */ determineCorrectGroupKeys() { return { hGroupKey: this.getCaseInsensitivePropKey(this.items[0], this.hGroupKey), vGroupKey: this.getCaseInsensitivePropKey(this.items[0], this.vGroupKey) }; } /** * Gets case insensitive prop key * @private * @param {?} item Item object * @param {?} propKey property key * @return {?} case insensitive prop key */ getCaseInsensitivePropKey(item, propKey) { if (item) { return Object.keys(item) .find((/** * @param {?} key * @return {?} */ key => (key !== '' && key !== undefined && key !== undefined) ? key.toLowerCase() === propKey.toLowerCase() : false)); } return ''; } /** * Gets headings from items * @private * @param {?=} groupKey * @return {?} headings from items */ getHeadingsFromItems(groupKey = this.vGroupKey) { /** @type {?} */ const keys = ((/** @type {?} */ (this.items))).map((/** * @param {?} item * @return {?} */ (item) => item[Object.keys(item) .find((/** * @param {?} key * @return {?} */ key => key.toLowerCase() === groupKey.toLowerCase()))])); return keys.filter((/** * @param {?} elem * @param {?} pos * @param {?} arr * @return {?} */ (elem, pos, arr) => arr.indexOf(elem) === pos && (this.showUngroupedInBacklog && (elem !== '' && elem !== undefined)))); } /** * Gets ungrouped items (which could not be put into rows or cols) * * \@memberOf BoardComponent * @return {?} Array of ungrouped items * */ getUngroupedItems() { if (this.showUngroupedInBacklog) { return this.items.filter((/** * @param {?} item * @return {?} */ item => { /** @type {?} */ const groupKeys = this.determineCorrectGroupKeys(); /** @type {?} */ const isUngrouped = (item[groupKeys.vGroupKey] === '' && item[groupKeys.hGroupKey] === '') || (item[groupKeys.vGroupKey] === null && item[groupKeys.hGroupKey] === null); return isUngrouped; })); } return []; } /** * Toggles an elements collapse state * * \@memberOf BoardComponent * @param {?} group Column and row value * * @return {?} */ toggleCollapse(group) { this.ngZone.runOutsideAngular((/** * @return {?} */ () => { /** @type {?} */ const part = this.getValue(group.hGroup || group.vGroup); // console.log("Toggle: " + part); /** @type {?} */ const collapseState = this.collapseState(part); this.collapseStates.find((/** * @param {?} item * @return {?} */ item => item.name === part)).collapsed = !collapseState; this.headingCollapsed.emit({ group: group.hGroup || group.vGroup, collapsed: !collapseState, overallCollapseState: this.collapseStates }); // this.ngZone.run(() => { // }); // ; })); timer(100).subscribe((/** * @param {?} t * @return {?} */ t => { this.executeChangeDetection(); this.checkIfContentNeedsToScroll(); })); // this.checkIfContentNeedsToScroll(); } /** * Gets the current collapse state of a specific item * * \@memberOf BoardComponent * @param {?} collapseItem Item to get the collapse state * @return {?} true if collapsed, false when expanded * */ collapseState(collapseItem) { if (typeof (collapseItem) === 'object') { collapseItem = (collapseItem).value; } /** @type {?} */ const foundItem = this.collapseStates.find((/** * @param {?} item * @return {?} */ item => item.name === this.getValue(collapseItem))); if (foundItem) { /** @type {?} */ const foundCollapsedState = foundItem.collapsed; // console.log('collapseState', part, foundCollapsedState); return foundCollapsedState; } return false; } /** * Handler which is called when an item starts to drag * * \@memberOf BoardComponent * @param {?} event Native drag event * @param {?} item CardItem which is dragged * * @return {?} */ dragStart(event, item) { this.dragItem = item; this.nativeDragItem = ((/** @type {?} */ (event.currentTarget))); this.dragStarted.emit(this.dragItem); this.cd.detach(); } /** * Handler which is called, when the drag of an item ends * * \@memberOf BoardComponent * @return {?} */ dragEnd() { this.dragItem = undefined; } /** * Handler which is called, when a new item should be created (click on a add icon) * * \@memberOf BoardComponent * @param {?} group Row and column value * * @return {?} */ createElement(group) { this.elementCreateClick.emit(group); } /** * Handler which is called when an item is dropped * * \@memberOf BoardComponent * @param {?} event Native drag event * @param {?} vRow Row item * @param {?} hRow Column item * * @return {?} */ drop(event, vRow, hRow) { event.preventDefault(); if (event.currentTarget) { /** @type {?} */ const placeholderEl = ((/** @type {?} */ (event.currentTarget))).querySelector('.placeholder'); if (placeholderEl) { this.renderer.removeChild(placeholderEl.parentNode, placeholderEl); } this.currentDragZone = ''; this.placeholderSet = false; } /** @type {?} */ const groupKeys = this.determineCorrectGroupKeys(); /** @type {?} */ const dragItemBeforeChange = Object.assign({}, this.dragItem); this.dragItem[groupKeys.vGroupKey] = this.getValue(vRow); this.dragItem[groupKeys.hGroupKey] = this.getValue(hRow); this.dropped.emit({ hGroup: hRow, vGroup: vRow, item: this.dragItem, itemBeforeChange: dragItemBeforeChange, nativeItemElement: this.nativeDragItem }); this.dragItem = undefined; this.cd.reattach(); this.executeChangeDetection(); } /** * Handler which is called when an item is dragged over a cell * * \@memberOf BoardComponent * @param {?} event Native html drag event * @param {?} vRow Row item * @param {?} hRow Column item * * @return {?} */ dragOver(event, vRow, hRow) { if (this.dragItem) { event.preventDefault(); if (vRow === undefined) { vRow = ''; } if (hRow === undefined) { hRow = ''; } vRow = this.getValue(vRow).toString(); hRow = this.getValue(hRow).toString(); /** @type {?} */ const dragZone = `${vRow}-${hRow.replace(' ', '')}`.toLowerCase(); if (dragZone !== this.currentDragZone && this.currentDragZone !== '') { /** @type {?} */ const lastPlaceholder = document.getElementById(this.currentDragZone); if (lastPlaceholder) { this.renderer.removeChild(lastPlaceholder.parentNode, lastPlaceholder); this.placeholderSet = false; } } this.currentDragZone = `${vRow}-${hRow.replace(' ', '')}`.toLowerCase(); if (!this.placeholderSet) { /** @type {?} */ const placeholderElement = this.createPlaceholderElement(); this.renderer.appendChild(event.currentTarget, placeholderElement); this.placeholderSet = true; } } } /** * Checks if container is scrollable * @private * @param {?} containerName Container to check if scrollable * @return {?} is scrollable */ containerIsScrollable(containerName) { /** @type {?} */ const container = this.elRef.nativeElement.querySelector(containerName); if (container) { /** @type {?} */ const hasHorizontalScrollbar = container.scrollWidth > container.clientWidth; /** @type {?} */ const hasVerticalScrollbar = container.scrollHeight > container.clientHeight; return { hScroll: hasHorizontalScrollbar, vScroll: hasVerticalScrollbar }; } return null; } /** * Determines the style of a container which includes the scrollbar * * \@memberOf BoardComponent * @return {?} Style of the container the scrollbar is applied to * */ scrollBarStyle() { return { 'padding-right': `${this.calculateScrollBarWidth()}px` }; } /** * Gets the current width of a scrollbar * * \@memberOf BoardComponent * @return {?} Object with native css style * */ getColumnWidth() { if (!this.scrollable) { return {}; } return { 'min-width': `${this.columnWidth}px` }; } /** * @private * @return {?} */ calculateScrollBarWidth() { /** @type {?} */ const headingsRowWidth = this.elRef.nativeElement.querySelector('.headings').clientWidth; /** @type {?} */ const contentWidth = this.elRef.nativeElement.querySelector('.row-content').clientWidth; return headingsRowWidth - contentWidth; } /** * Creates placeholder element * @private * @return {?} placeholder element */ createPlaceholderElement() { if (this.dragoverPlaceholderTemplate) { return this.dragoverPlaceholderTemplate.elementRef.nativeElement.cloneNode(true); } /** @type {?} */ const placeholderElement = this.renderer.createElement('div'); this.renderer.setStyle(placeholderElement, 'border', '1px dashed gray'); this.renderer.setStyle(placeholderElement, 'width', '100%'); this.renderer.setStyle(placeholderElement, 'height', '50px'); this.renderer.setAttribute(placeholderElement, 'id', this.currentDragZone); this.renderer.setAttribute(placeholderElement, 'class', 'placeholder'); return placeholderElement; } /** * Gets headings * @private * @param {?} keys key array * @param {?} key key to check * @return {?} headings */ getHeadings(keys, key) { if ((keys.length > 0 && ((/** @type {?} */ (keys[0]))).value !== '')) { return keys.sort((/** * @param {?} a * @param {?} b * @return {?} */ (a, b) => a.orderId - b.orderId)); } return this.getHeadingsFromItems(key); } /** * Scrolling board component * @param {?} event Event * @return {?} */ scrolling(event) { /** @type {?} */ const target = ((/** @type {?} */ (event.currentTarget))); // Clear our timeout throughout the scroll this.detectIfUserHasEndedScrolling().then((/** * @return {?} */ () => { /** @type {?} */ const scrollStateEnded = this.getScrollState(target); scrollStateEnded.hasReachedEnd = false; scrollStateEnded.isScrolling = false; if (Math.round(scrollStateEnded.distance) !== Math.round(scrollStateEnded.maxDistance)) { scrollStateEnded.hasReachedEnd = false; this.scrollEnded.emit(scrollStateEnded); } else { scrollStateEnded.hasReachedEnd = true; this.scrolledToEnd.emit(scrollStateEnded); this.scrollEnded.emit(scrollStateEnded); } })); /** @type {?} */ const scrollState = this.getScrollState(target); scrollState.hasReachedEnd = false; scrollState.isScrolling = true; this.isScrolling.emit(scrollState); } /** * @private * @param {?} target * @return {?} */ getScrollState(target) { /** @type {?} */ const scrollTop = target.scrollTop; /** @type {?} */ const scrollAxis = (scrollTop > 0) ? 'y' : 'x'; /** @type {?} */ const currentDistance = (scrollAxis === 'y' ? target.scrollTop : target.scrollWidth); /** @type {?} */ const maximumDistance = (scrollAxis === 'y' ? target.scrollHeight - target.clientHeight : target.scrollWidth - target.clientWidth); return { axis: scrollAxis, distance: currentDistance, maxDistance: maximumDistance }; } /** * Detects if user has ended scrolling * Got from: https://gomakethings.com/detecting-when-a-visitor-has-stopped-scrolling-with-vanilla-javascript/ * @private * @return {?} if user has ended scrolling */ detectIfUserHasEndedScrolling() { return new Promise((/** * @param {?} res * @return {?} */ (res) => { window.clearTimeout(this.isScrollingTimeout); // Set a timeout to run after scrolling ends this.isScrollingTimeout = window.setTimeout((/** * @return {?} */ () => { // Run the callback return res(true); }), 66); })); } } BoardComponent.decorators = [ { type: Component, args: [{ // tslint:disable-next-line: component-selector selector: 'ngx-taskboard', template: "<div class=\"h-100 d-flex flex-column align-items-stretch border-0\" [class.small]=\"smallText\" style=\"overflow: hidden\">\n\t<ngx-taskboard-filter-search-bar [placeholder]=\"filterRowPlaceholder\"></ngx-taskboard-filter-search-bar>\n\t\n\t<div class=\"d-flex flex-row align-items-stretch flex-fill overflow-y-auto\">\n\t\t<ng-container *ngTemplateOutlet=\"backlogColumn\"></ng-container>\n\n\t\t<div class=\"column-cards d-flex flex-column flex-fill \" [class.overflow-x-auto]=\"horizontalScrolling\"\n\t\t\t[class.overflow-y-auto]=\"verticalScrolling\" (outSideEventHandler)=\"scrolling($event)\"\n\t\t\t[class.border-bottom]=\"(scrollable && verticalScrolling)\">\n\n\t\t\t<ng-container *ngTemplateOutlet=\"columnHeadings\"></ng-container>\n\n\t\t\t<div class=\"d-flex flex-column row-content flex-fill align-items-stretch\"\n\t\t\t\t[class.flex-fill]=\"!(scrollable && verticalScrolling)\">\n\t\t\t\t<ng-container *ngFor=\"let vGroup of vHeadings\">\n\n\t\t\t\t\t<div class=\"d-flex flex-row taskboard-row\" [class.collapsed]=\"!collapseState(vGroup)\" [class.flex-fill]=\"!collapseState(vGroup)\">\n\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"rowHeadings; context: { vGroup: vGroup }\"></ng-container>\n\n\t\t\t\t\t\t<ng-container *ngFor=\"let hGroup of hHeadings; let hLast = last\">\n\n\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t*ngTemplateOutlet=\"cellItem; context: { hGroup: hGroup, vGroup: vGroup, hLast: hLast } \">\n\t\t\t\t\t\t\t</ng-container>\n\n\t\t\t\t\t\t</ng-container>\n\t\t\t\t\t</div>\n\t\t\t\t</ng-container>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n\n<!-- Templates -->\n<ng-template #cardAction let-withNew=\"withNew\" let-group=\"group\" let-collapsed=\"collapsed\"\n\tlet-collapser=\"collapser\">\n\n\t<ng-container [ngIf]=\"actionsTemplate\"\n\t\t*ngTemplateOutlet=\"actionsTemplate; context: { actionConfig: { group: group, collapsed: collapsed, collapser: collapser } }\">\n\t</ng-container>\n\n\t<ng-container *ngIf=\"!actionsTemplate\">\n\t\t<div class=\"actions ml-3\" *ngIf=\"vGroupKey\">\n\n\t\t\t<div class=\"new-action\" *ngIf=\"withNew\" (click)=\"createElement(group)\">\n\t\t\t\t<ng-container *ngTemplateOutlet=\"addIcon\"></ng-container>\n\t\t\t</div>\n\n\t\t\t<div class=\"collapse-action\" *ngIf=\"(collapser != null ? collapser : true)\" (click)=\"toggleCollapse(group)\">\n\t\t\t\t<ng-container *ngIf=\"collapsed\">\n\t\t\t\t\t<ng-container *ngTemplateOutlet=\"expandIconTemplate\"></ng-container>\n\t\t\t\t</ng-container>\n\n\t\t\t\t<ng-container *ngIf=\"!collapsed\">\n\t\t\t\t\t<ng-container *ngTemplateOutlet=\"collapseIconTemplate\"></ng-container>\n\t\t\t\t</ng-container>\n\t\t\t</div>\n\t\t\t\n\t\t\t<!-- <fa-icon [icon]=\"['fas','plus-square']\" [fixedWidth]=\"true\" >\n\t\t\t</fa-icon> -->\n\t\t\t<!-- <fa-icon [icon]=\"['fas',chevronIcon ? chevronIcon : 'chevron-left']\" [fixedWidth]=\"true\"\n\t\t\t\t(click)=\"toggleCollapse(group)\" *ngIf=\"(collapser != null ? collapser : true)\"></fa-icon> -->\n\t\t</div>\n\t</ng-container>\n</ng-template>\n\n<ng-template #defaultItemTemplate let-item=\"item\">\n\t<div class=\"card mb-1 border-bottom-0 border-top-0\" style=\"border-left: 5px solid; border-radius: 0;\"\n\t\t[style.border-left-color]=\"item?.color\">\n\t\t<div class=\"card-header border-top d-flex flex-row justify-content-between\">\n\t\t\t<div class=\"\">\n\t\t\t\t<span class=\"font-weight-bold\">#{{item.id}}</span>\n\t\t\t\t<span class=\"ml-3\">{{ item.name }}</span>\n\t\t\t\t<span class=\"ml-3\">{{ item.priority }}</span>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</ng-template>\n\n\n<!-- Layout parts -->\n<ng-template #backlogColumn>\n\t<div class=\"backlog border-right\" *ngIf=\"showBacklog\" [style.width]=\"backlogWidth\">\n\t\t<div class=\"card border-right-0 border-top-0 border-left-0 w-100 h-100\">\n\t\t\t<div class=\"{{hHeaderClass}} d-flex flex-row justify-content-between\">\n\n\t\t\t\t{{ backlogName }}\n\n\t\t\t\t<div class=\"actions ml-3\">\n\t\t\t\t\t<ng-container *ngTemplateOutlet=\"addIcon\"></ng-container>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<ul class=\"list-group list-group-flush p-3 h-100\" (outSideEventHandler)=\"dragOver($event, null, null)\">\n\t\t\t\t<div class=\" ungrouped-item\" *ngFor=\"let item of getUngroupedItems()\" draggable=\"true\"\n\t\t\t\t\t(outSideEventHandler)=\"dragStart($event, item)\" (outSideEventHandler)=\"dragEnd()\">\n\t\t\t\t\t<ng-container *ngIf=\"itemTemplate\">\n\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"itemTemplate; context { item: item }\"></ng-container>\n\t\t\t\t\t</ng-container>\n\n\t\t\t\t\t<ng-container *ngIf=\"!itemTemplate\">\n\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"defaultItemTemplate; context: { item: item }\"></ng-container>\n\t\t\t\t\t</ng-container>\n\t\t\t\t</div>\n\t\t\t</ul>\n\t\t</div>\n\t</div>\n</ng-template>\n\n<ng-template #columnHeadings>\n\t<div class=\"headings d-flex flex-row align-items-stretch w-100\" [ngStyle]=\"scrollBarStyle()\"\n\t\t[class.sticky-top]=\"stickyHorizontalHeaderKeys\">\n\t\t<div class=\"col-2 border-left {{hHeaderClass}} border-left-0 d-flex flex-row\" *ngIf=\"vGroupKey\">\n\t\t\t<div class=\"d-flex flex-row w-100\">\n\t\t\t\t<div class=\"boardname flex-fill\">{{ boardName }}</div>\n\t\t\t\t<div class=\"h-v-actions align-self-end\" (click)=\"toggleCollapseGroup('vertical', vCollapsed)\">\n\n\t\t\t\t\t<ng-container *ngIf=\"vCollapsed\">\n\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"collapseIcon\" [ngIf]=\"vCollapsed\"></ng-container>\n\t\t\t\t\t</ng-container>\n\n\t\t\t\t\t<ng-container *ngIf=\"!vCollapsed\">\n\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"expandIcon\" [ngIf]=\"vCollapsed\"></ng-container>\n\t\t\t\t\t</ng-container>\n\t\t\t\t\t<!-- <fa-icon [icon]=\"['fas', vCollapsed ? 'chevron-down' : 'chevron-up']\" [fixedWidth]=\"true\"\n\t\t\t\t\t\t(click)=\"toggleCollapseGroup('vertical', vCollapsed)\"></fa-icon> -->\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"border-left {{hHeaderClass}} d-flex flex-row justify-content-between horizontal-group-header\"\n\t\t\t[class.bg-white]=\"collapseState(hGroup)\" [class.border-bottom-0]=\"collapseState(hGroup)\"\n\t\t\t[class.col]=\"!collapseState(hGroup) && !(scrollable && horizontalScrolling)\"\n\t\t\t*ngFor=\"let hGroup of hHeadings; let hLast = last\" [class.h-collapsed]=\"collapseState(hGroup)\"\n\t\t\t[ngStyle]=\"getColumnWidth()\" [class.border-right]=\"(scrollable && horizontalScrolling) && hLast\">\n\t\t\t<div class=\"\" *ngIf=\"!collapseState(hGroup)\">\n\t\t\t\t<ng-container [ngIf]=\"hHeaderTemplate\"\n\t\t\t\t\t*ngTemplateOutlet=\"hHeaderTemplate; context: { groupName: hGroup }\">\n\t\t\t\t</ng-container>\n\t\t\t\t<ng-container *ngIf=\"!hHeaderTemplate\">\n\t\t\t\t\t{{ hGroup?.display || getValue(hGroup) || 'Ungrouped' }}\n\t\t\t\t</ng-container>\n\t\t\t</div>\n\t\t\t<div *ngIf=\"!collapseState(hGroup)\">\n\t\t\t\t<ng-content\n\t\t\t\t\t*ngTemplateOutlet=\"cardAction; context: { withNew: hAddNewItems, group: { hGroup: hGroup, vGroup: null}, collapser: false, collapsed: !collapseState(hGroup)}\">\n\t\t\t\t</ng-content>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</ng-template>\n\n<ng-template #rowHeadings let-vGroup=\"vGroup\">\n\t<div class=\"col-2 {{vHeaderClass}} border-left-0 d-flex flex-row justify-content-between\" *ngIf=\"vGroupKey\">\n\t\t<div class=\"vertical-group-header h-100 border-bottom-0 border-left-0 p-0 m-0 flex-fill\">\n\n\t\t\t<ng-container [ngIf]=\"vHeaderTemplate\" *ngTemplateOutlet=\"vHeaderTemplate; context: { groupName: vGroup }\">\n\t\t\t</ng-container>\n\t\t\t<ng-container *ngIf=\"!vHeaderTemplate\">\n\t\t\t\t{{ vGroup?.display || getValue(vGroup) || 'Ungrouped' }}\n\t\t\t</ng-container>\n\t\t</div>\n\n\t\t<div class=\"vertical-actions d-flex flex-column\" [class.flex-column]=\"!collapseState(vGroup)\"\n\t\t\t[class.flex-row]=\"collapseState(vGroup)\" [class.flex-row-reverse]=\"collapseState(vGroup)\">\n\t\t\t<div class=\"align-self-start flex-fill\">\n\t\t\t\t<ng-content\n\t\t\t\t\t*ngTemplateOutlet=\"cardAction; context: { withNew: false, group: { hGroup: null, vGroup: vGroup}, collapser: true, collapsed: !collapseState(vGroup)}\">\n\t\t\t\t</ng-content>\n\t\t\t</div>\n\t\t\t<div class=\"align-self-start\">\n\t\t\t\t<ng-content class=\"align-self-end\"\n\t\t\t\t\t*ngTemplateOutlet=\"cardAction; context: { withNew: vAddNewItems, group: { hGroup: null, vGroup: vGroup}, collapser: false }\">\n\t\t\t\t</ng-content>\n\t\t\t</div>\n\t\t</div>\n\n\t</div>\n</ng-template>\n\n<ng-template #cellItem let-vGroup=\"vGroup\" let-hGroup=\"hGroup\" let-hLast=\"hLast\">\n\t<div class=\"cell-item border-left {{cellClass}} bg-white d-flex flex-column\"\n\t\t[class.col]=\"!collapseState(hGroup) && !(scrollable && horizontalScrolling)\"\n\t\t[class.border-bottom-0]=\"collapseState(hGroup)\" [class.v-collapsed]=\"collapseState(vGroup)\"\n\t\t[class.h-collapsed]=\"collapseState(hGroup)\" [ngStyle]=\"getColumnWidth()\"\n\t\t[class.border-right]=\"(scrollable && horizontalScrolling) && hLast\">\n\n\n\t\t<ng-container *ngIf=\"!collapseState(vGroup) && !collapseState(hGroup)\">\n\n\t\t\t<div class=\"cell-items flex-fill\" (dragover)=\"dragOver($event, vGroup, hGroup)\"\n\t\t\t\t(drop)=\"drop($event, vGroup, hGroup)\">\n\t\t\t\t<ng-container *ngFor=\"let item of getItemsOfGroup(getValue(vGroup), getValue(hGroup))\">\n\n\t\t\t\t\t<div class=\"item-container p-0 m-0\" draggable=\"true\" (dragstart)=\"dragStart($event, item)\"\n\t\t\t\t\t\t(dragend)=\"dragEnd()\">\n\t\t\t\t\t\t<ng-container *ngIf=\"itemTemplate\">\n\t\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"itemTemplate; context { item: item }\"></ng-container>\n\t\t\t\t\t\t</ng-container>\n\n\t\t\t\t\t\t<ng-container *ngIf=\"!itemTemplate\">\n\t\t\t\t\t\t\t<ng-container *ngTemplateOutlet=\"defaultItemTemplate; context: { item: item }\">\n\t\t\t\t\t\t\t</ng-container>\n\t\t\t\t\t\t</ng-container>\n\t\t\t\t\t</div>\n\n\t\t\t\t</ng-container>\n\t\t\t</div>\n\t\t\t<div class=\"cell-actions d-flex flex-row align-self-end\">\n\t\t\t\t<ng-content\n\t\t\t\t\t*ngTemplateOutlet=\"cardAction; context: { withNew: cellAddNewItems, group: { hGroup: hGroup, vGroup: vGroup}, collapser: false }\">\n\t\t\t\t</ng-content>\n\t\t\t</div>\n\n\t\t</ng-container>\n\n\t\t<ng-container *ngIf=\"collapseState(vGroup) && !collapseState(hGroup)\">\n\t\t\t<div class=\"text-center\" (dragover)=\"dragOver($event, vGroup, hGroup)\"\n\t\t\t\t(drop)=\"drop($event, vGroup, hGroup)\">\n\t\t\t\t<ng-container [ngIf]=\"noElementsTemplate\"\n\t\t\t\t\t*ngTemplateOutlet=\"noElementsTemplate; context: { count: getItemsOfGroup(getValue(vGroup), getValue(hGroup)).length, filter: this.filter }\">\n\t\t\t\t</ng-container>\n\t\t\t\t<ng-container *ngIf=\"!noElementsTemplate\">\n\t\t\t\t\t{{ getItemsOfGroup(getValue(vGroup), getValue(hGroup)).length }} elements\n\t\t\t\t\t<span *ngIf=\"filter !== ''\" class=\"code\"> (filtered by <code>{{filter}}</code>)</span>\n\t\t\t\t</ng-container>\n\t\t\t</div>\n\t\t</ng-container>\n\t\t<ng-container *ngIf=\"collapseState(hGroup)\">\n\n\t\t</ng-container>\n\n\t</div>\n\n</ng-template>\n\n<!-- Icons -->\n<ng-template #addIcon>\n\t<ng-container *ngTemplateOutlet=\"icon; context: { template: addIconTemplate, defaultIcon: ['fas','plus-square'] }\"></ng-container>\n</ng-template>\n\n<ng-template #collapseIcon>\n\t<ng-container *ngTemplateOutlet=\"icon; context: { template: collapseIconTemplate, defaultIcon: ['fas','chevron-up'] }\"></ng-container>\n</ng-template>\n\n<ng-template #expandIcon>\n\t<ng-container *ngTemplateOutlet=\"icon; context: { template: expandIconTemplate, defaultIcon: ['fas','chevron-up'] }\"></ng-container>\n</ng-template>\n\n<ng-template #icon let-template=\"template\" let-defaultIcon=\"defaultIcon\">\n\t<ng-container *ngIf=\"template else noIcon\">\n\t\t<ng-container *ngTemplateOutlet=\"template; context: { defaultIcon: defaultIcon }\"></ng-container>\n\t</ng-container> \n\t<ng-template #noIcon>\n\t\t<fa-icon [icon]=\"defaultIcon\" [fixedWidth]=\"true\"></fa-icon>\n\t</ng-template>\n</ng-template>", changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";\n/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */:root{--blue:#007bff;--breakpoint-lg:992px;--breakpoint-md:768px;--breakpoint-sm:576px;--breakpoint-xl:1200px;--breakpoint-xs:0;--cyan:#17a2b8;--danger:#dc3545;--dark:#343a40;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--gray:#6c757d;--gray-dark:#343a40;--green:#28a745;--indigo:#6610f2;--info:#17a2b8;--light:#f8f9fa;--orange:#fd7e14;--pink:#e83e8c;--primary:#007bff;--purple:#6f42c1;--red:#dc3545;--secondary:#6c757d;--success:#28a745;--teal:#20c997;--warning:#ffc107;--white:#fff;--yellow:#ffc107}*,:after,:before{box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:100%;font-family:sans-serif;line-height:1.15}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{background-color:#fff;color:#212529;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1rem;font-weight:400;line-height:1.5;margin:0;text-align:left}[tabindex=\"-1\"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;margin-top:0}p{margin-bottom:1rem;margin-top:0}abbr[data-original-title],abbr[title]{-webkit-text-decoration:underline dotted;-webkit-text-decoration-skip-ink:none;border-bottom:0;cursor:help;text-decoration:underline;text-decoration:underline dotted;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{background-color:transparent;color:#007bff;text-decoration:none}a:hover{color:#0056b3;text-decoration:underline}a:not([href]),a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace