@disane/ngx-taskboard
Version:
Yet another angular taskboard
1,066 lines (1,064 loc) • 214 kB
JavaScript
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