mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-26 01:14:22 +00:00
812 lines
24 KiB
TypeScript
812 lines
24 KiB
TypeScript
import {
|
|
AfterViewInit,
|
|
Attribute,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
ElementRef,
|
|
HostBinding,
|
|
HostListener,
|
|
Input,
|
|
OnDestroy,
|
|
OnInit,
|
|
Renderer2,
|
|
ViewChild,
|
|
} from '@angular/core';
|
|
import { TaskService } from '../task.service';
|
|
import { EMPTY, Observable, of, ReplaySubject, Subject } from 'rxjs';
|
|
import {
|
|
ShowSubTasksMode,
|
|
TaskAdditionalInfoTargetPanel,
|
|
TaskWithSubTasks,
|
|
} from '../task.model';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { DialogTimeEstimateComponent } from '../dialog-time-estimate/dialog-time-estimate.component';
|
|
import { expandAnimation } from '../../../ui/animations/expand.ani';
|
|
import { GlobalConfigService } from '../../config/global-config.service';
|
|
import { checkKeyCombo } from '../../../util/check-key-combo';
|
|
import { distinctUntilChanged, map, switchMap, take, takeUntil } from 'rxjs/operators';
|
|
import { fadeAnimation } from '../../../ui/animations/fade.ani';
|
|
import { TaskAttachmentService } from '../task-attachment/task-attachment.service';
|
|
import { IssueService } from '../../issue/issue.service';
|
|
import { DialogEditTaskAttachmentComponent } from '../task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component';
|
|
import { swirlAnimation } from '../../../ui/animations/swirl-in-out.ani';
|
|
import { IS_TOUCH_ONLY, isTouchOnly } from '../../../util/is-touch';
|
|
import { DialogAddTaskReminderComponent } from '../dialog-add-task-reminder/dialog-add-task-reminder.component';
|
|
import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component';
|
|
import { ProjectService } from '../../project/project.service';
|
|
import { Project } from '../../project/project.model';
|
|
import { T } from '../../../t.const';
|
|
import { MatMenuTrigger } from '@angular/material/menu';
|
|
import { AddTaskReminderInterface } from '../dialog-add-task-reminder/add-task-reminder-interface';
|
|
import { TODAY_TAG } from '../../tag/tag.const';
|
|
import { DialogEditTagsForTaskComponent } from '../../tag/dialog-edit-tags/dialog-edit-tags-for-task.component';
|
|
import { WorkContextService } from '../../work-context/work-context.service';
|
|
import { environment } from '../../../../environments/environment';
|
|
import { throttle } from 'helpful-decorators';
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
|
@Component({
|
|
selector: 'task',
|
|
templateUrl: './task.component.html',
|
|
styleUrls: ['./task.component.scss'],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
animations: [expandAnimation, fadeAnimation, swirlAnimation],
|
|
})
|
|
export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
|
|
@Input() isBacklog: boolean = false;
|
|
isShowParentTitle: boolean = !!this.showParentTitle;
|
|
|
|
task!: TaskWithSubTasks;
|
|
T: typeof T = T;
|
|
IS_TOUCH_ONLY: boolean = IS_TOUCH_ONLY;
|
|
isDragOver: boolean = false;
|
|
isLockPanLeft: boolean = false;
|
|
isLockPanRight: boolean = false;
|
|
isPreventPointerEventsWhilePanning: boolean = false;
|
|
isActionTriggered: boolean = false;
|
|
ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode;
|
|
contextMenuPosition: { x: string; y: string } = { x: '0px', y: '0px' };
|
|
progress: number = 0;
|
|
isDev: boolean = !(environment.production || environment.stage);
|
|
@ViewChild('contentEditableOnClickEl', { static: true })
|
|
contentEditableOnClickEl?: ElementRef;
|
|
@ViewChild('blockLeftEl') blockLeftElRef?: ElementRef;
|
|
@ViewChild('blockRightEl') blockRightElRef?: ElementRef;
|
|
@ViewChild('innerWrapperEl', { static: true }) innerWrapperElRef?: ElementRef;
|
|
// only works because item comes first in dom
|
|
@ViewChild('contextMenuTriggerEl', { static: true, read: MatMenuTrigger })
|
|
contextMenu?: MatMenuTrigger;
|
|
@ViewChild('projectMenuTriggerEl', { static: false, read: MatMenuTrigger })
|
|
projectMenuTrigger?: MatMenuTrigger;
|
|
@HostBinding('tabindex') tabIndex: number = 1;
|
|
@HostBinding('class.isDone') isDone: boolean = false;
|
|
@HostBinding('id') taskIdWithPrefix: string = 'NO';
|
|
// @see ngOnInit
|
|
@HostBinding('class.isCurrent') isCurrent: boolean = false;
|
|
@HostBinding('class.isSelected') isSelected: boolean = false;
|
|
TODAY_TAG_ID: string = TODAY_TAG.id;
|
|
private _task$: ReplaySubject<TaskWithSubTasks> = new ReplaySubject(1);
|
|
issueUrl$: Observable<string | null> = this._task$.pipe(
|
|
switchMap((v) => {
|
|
return v.issueType && v.issueId && v.projectId
|
|
? this._issueService.issueLink$(v.issueType, v.issueId, v.projectId)
|
|
: of(null);
|
|
}),
|
|
take(1),
|
|
);
|
|
moveToProjectList$: Observable<Project[]> = this._task$.pipe(
|
|
map((t) => t.projectId),
|
|
distinctUntilChanged(),
|
|
switchMap((pid) => this._projectService.getProjectsWithoutId$(pid)),
|
|
);
|
|
|
|
parentTitle$: Observable<string> = this.isShowParentTitle
|
|
? this._task$.pipe(
|
|
take(1),
|
|
switchMap((task) => this._taskService.getByIdLive$(task.parentId as string)),
|
|
map((task) => task.title),
|
|
)
|
|
: EMPTY;
|
|
|
|
projectColor$: Observable<string> = this.isShowParentTitle
|
|
? this._task$.pipe(
|
|
take(1),
|
|
switchMap((task) =>
|
|
task.projectId ? this._projectService.getByIdOnce$(task.projectId) : EMPTY,
|
|
),
|
|
map((project) => project.theme.primary),
|
|
)
|
|
: EMPTY;
|
|
|
|
isFirstLineHover: boolean = false;
|
|
|
|
private _dragEnterTarget?: HTMLElement;
|
|
private _destroy$: Subject<boolean> = new Subject<boolean>();
|
|
private _currentPanTimeout?: number;
|
|
private _isTaskDeleteTriggered: boolean = false;
|
|
|
|
constructor(
|
|
private readonly _taskService: TaskService,
|
|
private readonly _matDialog: MatDialog,
|
|
private readonly _configService: GlobalConfigService,
|
|
private readonly _issueService: IssueService,
|
|
private readonly _attachmentService: TaskAttachmentService,
|
|
private readonly _elementRef: ElementRef,
|
|
private readonly _renderer: Renderer2,
|
|
private readonly _cd: ChangeDetectorRef,
|
|
private readonly _projectService: ProjectService,
|
|
public readonly workContextService: WorkContextService,
|
|
private _route: ActivatedRoute,
|
|
@Attribute('showParentTitle') private showParentTitle: string,
|
|
) {}
|
|
|
|
@Input('task') set taskSet(v: TaskWithSubTasks) {
|
|
this.task = v;
|
|
|
|
this.progress = v && v.timeEstimate && (v.timeSpent / v.timeEstimate) * 100;
|
|
this.taskIdWithPrefix = 't-' + this.task.id;
|
|
this.isDone = v.isDone;
|
|
this._task$.next(v);
|
|
}
|
|
|
|
// methods come last
|
|
@HostListener('keydown', ['$event']) onKeyDown(ev: KeyboardEvent) {
|
|
this._handleKeyboardShortcuts(ev);
|
|
}
|
|
|
|
// @HostListener('focus', ['$event']) onFocus(ev: Event) {
|
|
// if (this._currentFocusId !== this.task.id && ev.target === this._elementRef.nativeElement) {
|
|
// this._taskService.focusTask(this.task.id);
|
|
// this._currentFocusId = this.task.id;
|
|
// }
|
|
// }
|
|
//
|
|
// @HostListener('blur', ['$event']) onBlur(ev: Event) {
|
|
// // console.log('BLUR', this._currentFocusId, this.task.id);
|
|
//
|
|
// // @TODO replace: hacky way to wait for last update
|
|
// setTimeout(() => {
|
|
// if (this._currentFocusId === this.task.id) {
|
|
// this._taskService.focusTask(null);
|
|
// this._currentFocusId = null;
|
|
// }
|
|
// });
|
|
// }
|
|
|
|
@HostListener('dragenter', ['$event']) onDragEnter(ev: DragEvent) {
|
|
this._dragEnterTarget = ev.target as HTMLElement;
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
this.isDragOver = true;
|
|
}
|
|
|
|
@HostListener('dragleave', ['$event']) onDragLeave(ev: DragEvent) {
|
|
if (this._dragEnterTarget === (ev.target as HTMLElement)) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
this.isDragOver = false;
|
|
}
|
|
}
|
|
|
|
@HostListener('drop', ['$event']) onDrop(ev: DragEvent) {
|
|
this._attachmentService.createFromDrop(ev, this.task.id);
|
|
ev.stopPropagation();
|
|
this.isDragOver = false;
|
|
}
|
|
|
|
ngOnInit() {
|
|
this._taskService.currentTaskId$.pipe(takeUntil(this._destroy$)).subscribe((id) => {
|
|
this.isCurrent = this.task && id === this.task.id;
|
|
this._cd.markForCheck();
|
|
});
|
|
this._taskService.selectedTaskId$.pipe(takeUntil(this._destroy$)).subscribe((id) => {
|
|
this.isSelected = this.task && id === this.task.id;
|
|
this._cd.markForCheck();
|
|
});
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
// this._taskService.focusTaskId$
|
|
// .pipe(
|
|
// takeUntil(this._destroy$),
|
|
// )
|
|
// .subscribe((id) => {
|
|
// if (id === this.task.id && document.activeElement !== this._elementRef.nativeElement) {
|
|
// this.focusSelfElement();
|
|
// }
|
|
// });
|
|
|
|
// hacky but relatively performant
|
|
if (this.task.parentId && Date.now() - 100 < this.task.created) {
|
|
setTimeout(() => {
|
|
this.focusTitleForEdit();
|
|
});
|
|
} else {
|
|
this._route.queryParams.subscribe((params) => {
|
|
window.setTimeout(() => {
|
|
if (params.highlightItem === this.task.id) {
|
|
this.focusSelfElement();
|
|
}
|
|
}, 200);
|
|
});
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this._destroy$.next(true);
|
|
this._destroy$.unsubscribe();
|
|
|
|
if (this._currentPanTimeout) {
|
|
window.clearTimeout(this._currentPanTimeout);
|
|
}
|
|
}
|
|
|
|
editReminder() {
|
|
if (this.task.repeatCfgId) {
|
|
return;
|
|
}
|
|
|
|
this._matDialog
|
|
.open(DialogAddTaskReminderComponent, {
|
|
data: { task: this.task } as AddTaskReminderInterface,
|
|
})
|
|
.afterClosed()
|
|
.pipe(takeUntil(this._destroy$))
|
|
.subscribe(() => this.focusSelf());
|
|
}
|
|
|
|
updateIssueData() {
|
|
this._issueService.refreshIssue(this.task, true, true);
|
|
}
|
|
|
|
editTaskRepeatCfg() {
|
|
this._matDialog
|
|
.open(DialogEditTaskRepeatCfgComponent, {
|
|
data: {
|
|
task: this.task,
|
|
},
|
|
})
|
|
.afterClosed()
|
|
.pipe(takeUntil(this._destroy$))
|
|
.subscribe(() => this.focusSelf());
|
|
}
|
|
|
|
handleUpdateBtnClick() {
|
|
this._taskService.setSelectedId(this.task.id);
|
|
}
|
|
|
|
deleteTask(isClick: boolean = false) {
|
|
// NOTE: prevents attempts to delete the same task multiple times
|
|
if (this._isTaskDeleteTriggered) {
|
|
return;
|
|
}
|
|
|
|
this._isTaskDeleteTriggered = true;
|
|
this._taskService.remove(this.task);
|
|
// NOTE: in case we want the focus behaviour on click we could use:
|
|
// this.focusSelf();
|
|
if (!isClick) {
|
|
this.focusNext(true);
|
|
}
|
|
}
|
|
|
|
startTask() {
|
|
this._taskService.setCurrentId(this.task.id);
|
|
this.focusSelf();
|
|
}
|
|
|
|
pauseTask() {
|
|
this._taskService.pauseCurrent();
|
|
}
|
|
|
|
updateTaskTitleIfChanged({
|
|
isChanged,
|
|
newVal,
|
|
}: {
|
|
isChanged: boolean;
|
|
newVal: string;
|
|
$taskEl: HTMLElement | null;
|
|
event: Event;
|
|
}) {
|
|
if (isChanged) {
|
|
this._taskService.update(this.task.id, { title: newVal });
|
|
}
|
|
this.focusSelf();
|
|
}
|
|
|
|
estimateTime() {
|
|
this._matDialog
|
|
.open(DialogTimeEstimateComponent, {
|
|
data: { task: this.task },
|
|
autoFocus: !isTouchOnly(),
|
|
})
|
|
.afterClosed()
|
|
.pipe(takeUntil(this._destroy$))
|
|
.subscribe(() => this.focusSelf());
|
|
}
|
|
|
|
addAttachment() {
|
|
this._matDialog
|
|
.open(DialogEditTaskAttachmentComponent, {
|
|
data: {},
|
|
})
|
|
.afterClosed()
|
|
.pipe(takeUntil(this._destroy$))
|
|
.subscribe((result) => {
|
|
this.focusSelf();
|
|
if (result) {
|
|
this._attachmentService.addAttachment(this.task.id, result);
|
|
}
|
|
});
|
|
}
|
|
|
|
addSubTask() {
|
|
this._taskService.addSubTaskTo(this.task.parentId || this.task.id);
|
|
}
|
|
|
|
@throttle(200, { leading: true, trailing: false })
|
|
toggleDoneKeyboard() {
|
|
this.toggleTaskDone();
|
|
if (!this.task.parentId) {
|
|
this.focusNext(true);
|
|
}
|
|
}
|
|
|
|
toggleTaskDone() {
|
|
if (this.task.parentId) {
|
|
this.focusNext(true);
|
|
}
|
|
|
|
if (this.task.isDone) {
|
|
this._taskService.setUnDone(this.task.id);
|
|
} else {
|
|
this._taskService.setDone(this.task.id);
|
|
}
|
|
}
|
|
|
|
showAdditionalInfos() {
|
|
this._taskService.setSelectedId(this.task.id);
|
|
this.focusSelf();
|
|
}
|
|
|
|
hideAdditionalInfos() {
|
|
this._taskService.setSelectedId(this.task.id);
|
|
this.focusSelf();
|
|
}
|
|
|
|
toggleShowAdditionalInfoOpen() {
|
|
if (this.isSelected) {
|
|
this._taskService.setSelectedId(null);
|
|
} else {
|
|
this._taskService.setSelectedId(this.task.id);
|
|
}
|
|
// this.focusSelf();
|
|
}
|
|
|
|
toggleShowAttachments() {
|
|
this._taskService.setSelectedId(
|
|
this.task.id,
|
|
TaskAdditionalInfoTargetPanel.Attachments,
|
|
);
|
|
this.focusSelf();
|
|
}
|
|
|
|
toggleSubTaskMode() {
|
|
this._taskService.toggleSubTaskMode(this.task.id, true, true);
|
|
this.focusSelf();
|
|
}
|
|
|
|
editTags() {
|
|
this._matDialog
|
|
.open(DialogEditTagsForTaskComponent, {
|
|
data: {
|
|
task: this.task,
|
|
},
|
|
})
|
|
.afterClosed()
|
|
.pipe(takeUntil(this._destroy$))
|
|
.subscribe(() => this.focusSelf());
|
|
}
|
|
|
|
addToMyDay() {
|
|
this._taskService.addTodayTag(this.task);
|
|
}
|
|
|
|
removeFromMyDay() {
|
|
this.onTagsUpdated(this.task.tagIds.filter((tagId) => tagId !== TODAY_TAG.id));
|
|
}
|
|
|
|
convertToMainTask() {
|
|
this._taskService.convertToMainTask(this.task);
|
|
}
|
|
|
|
focusPrevious(isFocusReverseIfNotPossible: boolean = false) {
|
|
if (IS_TOUCH_ONLY) {
|
|
return;
|
|
}
|
|
|
|
const taskEls = Array.from(document.querySelectorAll('task'));
|
|
const currentIndex = taskEls.findIndex((el) => document.activeElement === el);
|
|
const prevEl = taskEls[currentIndex - 1] as HTMLElement;
|
|
|
|
if (prevEl) {
|
|
prevEl.focus();
|
|
// hacky but works
|
|
setTimeout(() => {
|
|
if (document.activeElement !== prevEl) {
|
|
prevEl.focus();
|
|
}
|
|
});
|
|
} else if (isFocusReverseIfNotPossible) {
|
|
this.focusNext();
|
|
}
|
|
}
|
|
|
|
focusNext(isFocusReverseIfNotPossible: boolean = false) {
|
|
if (IS_TOUCH_ONLY) {
|
|
return;
|
|
}
|
|
|
|
const taskEls = Array.from(document.querySelectorAll('task'));
|
|
const currentIndex = taskEls.findIndex((el) => document.activeElement === el);
|
|
const nextEl = taskEls[currentIndex + 1] as HTMLElement;
|
|
|
|
if (nextEl) {
|
|
nextEl.focus();
|
|
// hacky but works
|
|
setTimeout(() => {
|
|
if (document.activeElement !== nextEl) {
|
|
nextEl.focus();
|
|
}
|
|
});
|
|
} else if (isFocusReverseIfNotPossible) {
|
|
this.focusPrevious();
|
|
}
|
|
}
|
|
|
|
focusSelf() {
|
|
if (IS_TOUCH_ONLY) {
|
|
return;
|
|
}
|
|
|
|
this.focusSelfElement();
|
|
// this._taskService.focusTask(this.task.id);
|
|
}
|
|
|
|
focusSelfElement() {
|
|
this._elementRef.nativeElement.focus();
|
|
}
|
|
|
|
focusTitleForEdit() {
|
|
if (!this.contentEditableOnClickEl) {
|
|
throw new Error('No el');
|
|
}
|
|
this.contentEditableOnClickEl.nativeElement.focus();
|
|
}
|
|
|
|
openContextMenu(event: MouseEvent) {
|
|
if (!this.contentEditableOnClickEl || !this.contextMenu) {
|
|
throw new Error('No el');
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
this.contentEditableOnClickEl.nativeElement.blur();
|
|
this.contextMenuPosition.x = event.clientX + 'px';
|
|
this.contextMenuPosition.y = event.clientY + 'px';
|
|
this.contextMenu.openMenu();
|
|
}
|
|
|
|
onTagsUpdated(tagIds: string[]) {
|
|
this._taskService.updateTags(this.task, tagIds, this.task.tagIds);
|
|
}
|
|
|
|
onPanStart(ev: any) {
|
|
if (!IS_TOUCH_ONLY) {
|
|
return;
|
|
}
|
|
if (!this.contentEditableOnClickEl) {
|
|
throw new Error('No el');
|
|
}
|
|
|
|
this._resetAfterPan();
|
|
const targetEl: HTMLElement = ev.target as HTMLElement;
|
|
if (
|
|
(targetEl.className.indexOf && targetEl.className.indexOf('drag-handle') > -1) ||
|
|
Math.abs(ev.deltaY) > Math.abs(ev.deltaX) ||
|
|
document.activeElement === this.contentEditableOnClickEl.nativeElement ||
|
|
ev.isFinal
|
|
) {
|
|
return;
|
|
}
|
|
if (ev.deltaX > 0) {
|
|
this.isLockPanRight = true;
|
|
} else if (ev.deltaX < 0) {
|
|
this.isLockPanLeft = true;
|
|
}
|
|
}
|
|
|
|
onPanEnd() {
|
|
if (!IS_TOUCH_ONLY || (!this.isLockPanLeft && !this.isLockPanRight)) {
|
|
return;
|
|
}
|
|
if (!this.blockLeftElRef || !this.blockRightElRef) {
|
|
throw new Error('No el');
|
|
}
|
|
|
|
this.isPreventPointerEventsWhilePanning = false;
|
|
this._renderer.removeStyle(this.blockLeftElRef.nativeElement, 'transition');
|
|
this._renderer.removeStyle(this.blockRightElRef.nativeElement, 'transition');
|
|
|
|
if (this._currentPanTimeout) {
|
|
window.clearTimeout(this._currentPanTimeout);
|
|
}
|
|
|
|
if (this.isActionTriggered) {
|
|
if (this.isLockPanLeft) {
|
|
this._renderer.setStyle(
|
|
this.blockRightElRef.nativeElement,
|
|
'transform',
|
|
`scaleX(1)`,
|
|
);
|
|
this._currentPanTimeout = window.setTimeout(() => {
|
|
if (this.workContextService.isToday) {
|
|
if (this.task.repeatCfgId) {
|
|
this.editTaskRepeatCfg();
|
|
} else {
|
|
this.editReminder();
|
|
}
|
|
} else {
|
|
if (this.task.parentId) {
|
|
// NOTHING
|
|
} else {
|
|
if (this.task.tagIds.includes(TODAY_TAG.id)) {
|
|
this.removeFromMyDay();
|
|
} else {
|
|
this.addToMyDay();
|
|
}
|
|
}
|
|
}
|
|
this._resetAfterPan();
|
|
}, 100);
|
|
} else if (this.isLockPanRight) {
|
|
this._renderer.setStyle(
|
|
this.blockLeftElRef.nativeElement,
|
|
'transform',
|
|
`scaleX(1)`,
|
|
);
|
|
this._currentPanTimeout = window.setTimeout(() => {
|
|
this.toggleTaskDone();
|
|
this._resetAfterPan();
|
|
}, 100);
|
|
}
|
|
} else {
|
|
this._resetAfterPan();
|
|
}
|
|
}
|
|
|
|
onPanLeft(ev: any) {
|
|
this._handlePan(ev);
|
|
}
|
|
|
|
onPanRight(ev: any) {
|
|
this._handlePan(ev);
|
|
}
|
|
|
|
moveTaskToProject(projectId: string) {
|
|
this._taskService.moveToProject(this.task, projectId);
|
|
}
|
|
|
|
moveToBacklog() {
|
|
this._taskService.moveToBacklog(this.task.id);
|
|
if (this.task.tagIds.includes(TODAY_TAG.id)) {
|
|
this.removeFromMyDay();
|
|
}
|
|
}
|
|
|
|
moveToToday() {
|
|
this._taskService.moveToProjectTodayList(this.task.id);
|
|
}
|
|
|
|
trackByProjectId(i: number, project: Project) {
|
|
return project.id;
|
|
}
|
|
|
|
private _handlePan(ev: any) {
|
|
if (
|
|
!IS_TOUCH_ONLY ||
|
|
(!this.isLockPanLeft && !this.isLockPanRight) ||
|
|
ev.eventType === 8
|
|
) {
|
|
return;
|
|
}
|
|
if (!this.innerWrapperElRef) {
|
|
throw new Error('No el');
|
|
}
|
|
|
|
const targetRef = this.isLockPanRight ? this.blockLeftElRef : this.blockRightElRef;
|
|
|
|
const MAGIC_FACTOR = 2;
|
|
this.isPreventPointerEventsWhilePanning = true;
|
|
// this.contentEditableOnClickEl.nativeElement.blur();
|
|
if (targetRef) {
|
|
let scale = (ev.deltaX / this._elementRef.nativeElement.offsetWidth) * MAGIC_FACTOR;
|
|
scale = this.isLockPanLeft ? scale * -1 : scale;
|
|
scale = Math.min(1, Math.max(0, scale));
|
|
if (scale > 0.5) {
|
|
this.isActionTriggered = true;
|
|
this._renderer.addClass(targetRef.nativeElement, 'isActive');
|
|
} else {
|
|
this.isActionTriggered = false;
|
|
this._renderer.removeClass(targetRef.nativeElement, 'isActive');
|
|
}
|
|
const moveBy = this.isLockPanLeft ? ev.deltaX * -1 : ev.deltaX;
|
|
this._renderer.setStyle(targetRef.nativeElement, 'width', `${moveBy}px`);
|
|
this._renderer.setStyle(targetRef.nativeElement, 'transition', `none`);
|
|
this._renderer.setStyle(
|
|
this.innerWrapperElRef.nativeElement,
|
|
'transform',
|
|
`translateX(${ev.deltaX}px`,
|
|
);
|
|
}
|
|
}
|
|
|
|
private _resetAfterPan() {
|
|
if (
|
|
!this.contentEditableOnClickEl ||
|
|
!this.blockLeftElRef ||
|
|
!this.blockRightElRef ||
|
|
!this.innerWrapperElRef
|
|
) {
|
|
throw new Error('No el');
|
|
}
|
|
|
|
this.isPreventPointerEventsWhilePanning = false;
|
|
this.isActionTriggered = false;
|
|
this.isLockPanLeft = false;
|
|
this.isLockPanRight = false;
|
|
// const scale = 0;
|
|
// this._renderer.setStyle(this.blockLeftEl.nativeElement, 'transform', `scaleX(${scale})`);
|
|
// this._renderer.setStyle(this.blockRightEl.nativeElement, 'transform', `scaleX(${scale})`);
|
|
this._renderer.removeClass(this.blockLeftElRef.nativeElement, 'isActive');
|
|
this._renderer.removeClass(this.blockRightElRef.nativeElement, 'isActive');
|
|
this._renderer.setStyle(this.innerWrapperElRef.nativeElement, 'transform', ``);
|
|
}
|
|
|
|
private _handleKeyboardShortcuts(ev: KeyboardEvent) {
|
|
if (ev.target !== this._elementRef.nativeElement) {
|
|
return;
|
|
}
|
|
|
|
const cfg = this._configService.cfg;
|
|
if (!cfg) {
|
|
throw new Error();
|
|
}
|
|
const keys = cfg.keyboard;
|
|
const isShiftOrCtrlPressed = ev.shiftKey || ev.ctrlKey;
|
|
|
|
if (checkKeyCombo(ev, keys.taskEditTitle) || ev.key === 'Enter') {
|
|
this.focusTitleForEdit();
|
|
// prevent blur
|
|
ev.preventDefault();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskToggleAdditionalInfoOpen)) {
|
|
this.toggleShowAdditionalInfoOpen();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskOpenEstimationDialog)) {
|
|
this.estimateTime();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskSchedule)) {
|
|
this.editReminder();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskToggleDone)) {
|
|
this.toggleDoneKeyboard();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskAddSubTask)) {
|
|
this.addSubTask();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskMoveToProject)) {
|
|
if (!this.projectMenuTrigger) {
|
|
throw new Error('No el');
|
|
}
|
|
this.projectMenuTrigger.openMenu();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskOpenContextMenu)) {
|
|
if (!this.contextMenu) {
|
|
throw new Error('No el');
|
|
}
|
|
this.contextMenu.openMenu();
|
|
}
|
|
|
|
if (checkKeyCombo(ev, keys.togglePlay)) {
|
|
if (this.isCurrent) {
|
|
this.pauseTask();
|
|
} else {
|
|
this.startTask();
|
|
}
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskEditTags)) {
|
|
this.editTags();
|
|
}
|
|
if (checkKeyCombo(ev, keys.taskDelete)) {
|
|
this.deleteTask();
|
|
}
|
|
|
|
if (checkKeyCombo(ev, keys.moveToBacklog)) {
|
|
if (!this.task.parentId) {
|
|
this.focusPrevious(true);
|
|
this._taskService.moveToBacklog(this.task.id);
|
|
}
|
|
}
|
|
|
|
if (checkKeyCombo(ev, keys.moveToTodaysTasks)) {
|
|
if (!this.task.parentId) {
|
|
this.focusNext(true);
|
|
this._taskService.moveToProjectTodayList(this.task.id);
|
|
}
|
|
}
|
|
|
|
// move focus up
|
|
if (
|
|
(!isShiftOrCtrlPressed && ev.key === 'ArrowUp') ||
|
|
checkKeyCombo(ev, keys.selectPreviousTask)
|
|
) {
|
|
ev.preventDefault();
|
|
this.focusPrevious();
|
|
}
|
|
// move focus down
|
|
if (
|
|
(!isShiftOrCtrlPressed && ev.key === 'ArrowDown') ||
|
|
checkKeyCombo(ev, keys.selectNextTask)
|
|
) {
|
|
ev.preventDefault();
|
|
this.focusNext();
|
|
}
|
|
|
|
// collapse sub tasks
|
|
if (ev.key === 'ArrowLeft' || checkKeyCombo(ev, keys.collapseSubTasks)) {
|
|
const hasSubTasks = this.task.subTasks && (this.task.subTasks as any).length > 0;
|
|
if (this.isSelected) {
|
|
this.hideAdditionalInfos();
|
|
} else if (
|
|
hasSubTasks &&
|
|
this.task._showSubTasksMode !== ShowSubTasksMode.HideAll
|
|
) {
|
|
this._taskService.toggleSubTaskMode(this.task.id, true, false);
|
|
// TODO find a solution
|
|
// } else if (this.task.parentId) {
|
|
// this._taskService.focusTask(this.task.parentId);
|
|
} else {
|
|
this.focusPrevious();
|
|
}
|
|
}
|
|
|
|
// expand sub tasks
|
|
if (ev.key === 'ArrowRight' || checkKeyCombo(ev, keys.expandSubTasks)) {
|
|
const hasSubTasks = this.task.subTasks && (this.task.subTasks as any).length > 0;
|
|
if (hasSubTasks && this.task._showSubTasksMode !== ShowSubTasksMode.Show) {
|
|
this._taskService.toggleSubTaskMode(this.task.id, false, false);
|
|
} else if (!this.isSelected) {
|
|
this.showAdditionalInfos();
|
|
} else {
|
|
this.focusNext();
|
|
}
|
|
}
|
|
|
|
// moving items
|
|
// move task up
|
|
if (checkKeyCombo(ev, keys.moveTaskUp)) {
|
|
this._taskService.moveUp(this.task.id, this.task.parentId, this.isBacklog);
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
// timeout required to let changes take place @TODO hacky
|
|
setTimeout(this.focusSelf.bind(this));
|
|
}
|
|
if (checkKeyCombo(ev, keys.moveTaskDown)) {
|
|
this._taskService.moveDown(this.task.id, this.task.parentId, this.isBacklog);
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
}
|