super-productivity/app-src/scripts/task-list/task-list-d.js
2017-10-28 18:52:02 +02:00

450 lines
14 KiB
JavaScript

/**
* @ngdoc component
* @name superProductivity.directive:taskList
* @description
* # taskList
*/
(function() {
'use strict';
const CONTROLLER_AS = '$ctrl';
const KEY_LEFT = 37;
const KEY_UP = 38;
const KEY_RIGHT = 39;
const KEY_DOWN = 40;
class TaskListCtrl {
/* @ngInject */
constructor(Dialogs, $localStorage, $mdToast, $timeout, Tasks, EDIT_ON_CLICK_TOGGLE_EV, $scope, ShortSyntax, $element, Jira, CheckShortcutKeyCombo, Util) {
this.Dialogs = Dialogs;
this.$mdToast = $mdToast;
this.$timeout = $timeout;
this.Tasks = Tasks;
this.EDIT_ON_CLICK_TOGGLE_EV = EDIT_ON_CLICK_TOGGLE_EV;
this.$scope = $scope;
this.ShortSyntax = ShortSyntax;
this.$element = $element;
this.Jira = Jira;
this.$localStorage = $localStorage;
this.lastFocusedTaskEl = undefined;
this.checkKeyCombo = CheckShortcutKeyCombo;
this.Util = Util;
// this.selectCurrentTaskTimeout;
this.boundHandleKeyPress = this.handleKeyPress.bind(this);
this.boundFocusLastTaskEl = this.focusLastFocusedTaskEl.bind(this);
}
$onDestroy() {
this.$timeout.cancel(this.animationReadyTimeout);
this.$timeout.cancel(this.selectCurrentTaskTimeout);
}
$onInit() {
// only allow after short delay
this.animationReadyTimeout = this.$timeout(() => {
this.$element.addClass('is-animation-ready');
}, 400);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// NOTE: Take good care not to update the dom (scope) structure
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
function getParenTaskFromDestScope(destSortableScope) {
return destSortableScope.$parent[CONTROLLER_AS].parentTask;
}
this.dragControlListeners = {
accept: (sourceItemHandleScope, destSortableScope) => {
if (this.disableDropInto) {
return false;
} else {
// disallow parent tasks to be dropped into parent tasks
let draggedTask = sourceItemHandleScope.itemScope.task;
return !(draggedTask.subTasks && draggedTask.subTasks.length > 0 && getParenTaskFromDestScope(destSortableScope));
}
},
itemMoved: function(event) {
let currentlyMovedTask = event.dest.sortableScope.modelValue[event.dest.index];
let parentTask = getParenTaskFromDestScope(event.dest.sortableScope);
if (parentTask) {
currentlyMovedTask.parentId = parentTask.id;
} else {
if (!angular.isUndefined(currentlyMovedTask.parentId)) {
delete currentlyMovedTask.parentId;
}
}
if (angular.isFunction(this.onItemMoved)) {
this.onItemMoved({
currentlyMovedTask,
parentTask,
$event: event
});
}
},
orderChanged: function(event) {
if (angular.isFunction(this.onOrderChanged)) {
this.onOrderChanged({ $event: event });
}
},
allowDuplicates: false,
containment: '#board'
};
}
focusPreviousInListOrParent($index) {
let taskEls = angular.element(this.$element.children().children());
let focusTaskEl;
// NOTE!!! element has not yet been removed from the dom
if (taskEls.length > 1) {
// if is last
if (taskEls.length === $index + 1) {
focusTaskEl = angular.element(taskEls[$index - 1]);
} else {
focusTaskEl = angular.element(taskEls[$index + 1]);
}
} else if (this.parentTask) {
focusTaskEl = angular.element(this.$element.parent()).parent();
}
if (focusTaskEl) {
focusTaskEl.focus();
}
}
focusLastFocusedTaskEl() {
if (this.lastFocusedTaskEl) {
this.lastFocusedTaskEl.focus();
}
}
focusCurrentTask() {
if (this.selectCurrentTaskTimeout) {
this.$timeout.cancel(this.selectCurrentTaskTimeout);
}
this.selectCurrentTaskTimeout = this.$timeout(() => {
document.querySelectorAll('.task.is-current')[0].focus();
});
}
estimateTime(task) {
this.Dialogs('TIME_ESTIMATE', { task })
.then(this.boundFocusLastTaskEl, this.boundFocusLastTaskEl);
}
deleteTask(task, $index) {
const that = this;
// create copy for undo
let taskCopy = angular.copy(task);
if (!$index && $index !== 0) {
$index = _.findIndex(this.tasks, (taskFromAllTasks) => {
return taskFromAllTasks.id === task.id;
});
}
//delete
this.tasks.splice($index, 1);
this.focusPreviousInListOrParent($index);
// check if current task was deleted and unset current task if so
if (this.$localStorage.currentTask && this.$localStorage.currentTask.id === task.id) {
this.Tasks.updateCurrent(undefined);
}
// show toast for undo
let toast = this.$mdToast.simple()
.textContent('You deleted "' + task.title + '"')
.action('UNDO')
.hideDelay(15000)
.position('bottom');
this.$mdToast.show(toast)
.then(function(response) {
if (response === 'ok') {
// re-add task on undo
that.tasks.splice($index, 0, taskCopy);
}
})
// we need an empty catch to prevent the unhandled rejection error
.catch(() => {
});
}
onChangeTitle(task, isChanged, newVal) {
if (isChanged && newVal) {
// we need to do this, as the pointer might not have been updated yet
task.title = newVal;
this.ShortSyntax(task);
}
}
onTaskNotesChanged(newNoteVal, task) {
if (task.originalKey) {
// for some reason, this isn't updated for the task, so we do it here once more
task.notes = newNoteVal;
this.Jira.updateIssueDescription(task);
}
}
onTaskDoneChanged(task) {
if (task.isDone) {
this.Tasks.markAsDone(task);
}
if (angular.isFunction(this.onTaskDoneChangedCallback)) {
this.onTaskDoneChangedCallback({ task, taskList: this.tasks });
}
}
focusPrevTask(currentTaskEl) {
const taskEls = document.querySelectorAll('.task');
const index = Array.prototype.indexOf.call(taskEls, currentTaskEl[0]);
const nextEl = taskEls[index - 1] || taskEls[0];
nextEl.focus();
}
focusNextTask(currentTaskEl) {
const taskEls = document.querySelectorAll('.task');
const index = Array.prototype.indexOf.call(taskEls, currentTaskEl[0]);
const nextEl = taskEls[index + 1] || currentTaskEl[0];
nextEl.focus();
}
onFocus($event) {
let taskEl = $event.currentTarget || $event.srcElement || $event.originalTarget;
taskEl = angular.element(taskEl);
this.lastFocusedTaskEl = taskEl;
taskEl.on('keydown', this.boundHandleKeyPress);
}
onBlur($event) {
let taskEl = $event.currentTarget || $event.srcElement || $event.originalTarget;
taskEl = angular.element(taskEl);
taskEl.off('keydown', this.boundHandleKeyPress);
}
handleKeyPress($ev) {
let taskEl = $ev.currentTarget || $ev.srcElement || $ev.originalTarget;
taskEl = angular.element(taskEl);
const task = this.lastFocusedTaskEl.scope().modelValue;
const lsKeys = this.$localStorage.keys;
const isShiftOrCtrlPressed = ($ev.shiftKey === false && $ev.ctrlKey === false);
const taskIndex = _.findIndex(this.tasks, (cTask) => {
return cTask.id === task.id;
});
if (this.checkKeyCombo($ev, lsKeys.taskEditTitle) || $ev.key === 'Enter') {
this.$scope.$broadcast(this.EDIT_ON_CLICK_TOGGLE_EV, task.id);
}
if (this.checkKeyCombo($ev, lsKeys.taskToggleNotes)) {
task.showNotes = !task.showNotes;
}
if (this.checkKeyCombo($ev, lsKeys.taskOpenEstimationDialog)) {
this.estimateTime(task);
}
if (this.checkKeyCombo($ev, lsKeys.taskToggleDone)) {
task.isDone = !task.isDone;
this.onTaskDoneChanged(task);
}
if (this.checkKeyCombo($ev, lsKeys.taskAddSubTask)) {
this.addSubTask(task);
}
if (this.checkKeyCombo($ev, lsKeys.moveToBacklog)) {
this.Tasks.moveTaskFromTodayToBackLog(task);
}
if (this.checkKeyCombo($ev, lsKeys.taskOpenOriginalLink)) {
this.Util.openExternalUrl(task.originalLink);
}
if (this.checkKeyCombo($ev, lsKeys.togglePlay)) {
this.expandSubTasks(task);
this.togglePlay(task);
}
if (this.checkKeyCombo($ev, lsKeys.taskDelete)) {
this.deleteTask(task);
// don't propagate to next focused element (important to allow global keyboard shortcuts)
$ev.preventDefault();
$ev.stopPropagation();
}
if (this.checkKeyCombo($ev, lsKeys.moveToTodaysTasks)) {
this.Tasks.moveTaskFromBackLogToToday(task);
}
// move focus up
if ((isShiftOrCtrlPressed && $ev.keyCode === KEY_UP) || this.checkKeyCombo($ev, lsKeys.selectPreviousTask)) {
this.focusPrevTask(taskEl);
// stop propagation to prevent from occurring twice (important to allow global keyboard shortcuts)
$ev.preventDefault();
$ev.stopPropagation();
}
// move focus down
if ((isShiftOrCtrlPressed && $ev.keyCode === KEY_DOWN) || this.checkKeyCombo($ev, lsKeys.selectNextTask)) {
this.focusNextTask(taskEl);
// stop propagation to prevent from occurring twice (important to allow global keyboard shortcuts)
$ev.preventDefault();
$ev.stopPropagation();
}
// expand sub tasks
if (($ev.keyCode === KEY_RIGHT) || this.checkKeyCombo($ev, lsKeys.expandSubTasks)) {
// if already opened or is sub task select next task
if ((task.subTasks && task.subTasks.length > 0 && task.isHideSubTasks === false) || this.parentTask) {
this.focusNextTask(taskEl);
// stop propagation to prevent from occurring twice (important to allow global keyboard shortcuts)
$ev.preventDefault();
$ev.stopPropagation();
}
this.expandSubTasks(task);
}
// collapse sub tasks
if (($ev.keyCode === KEY_LEFT) || this.checkKeyCombo($ev, lsKeys.collapseSubTasks)) {
if (task.subTasks) {
this.collapseSubTasks(task);
}
if (this.parentTask) {
this.focusPrevTask(taskEl);
}
}
// moving items
// move task up
if (this.checkKeyCombo($ev, lsKeys.moveTaskUp)) {
if (taskIndex > 0) {
TaskListCtrl.moveItem(this.tasks, taskIndex, taskIndex - 1);
// we need to manually re-add focus after timeout
this.$timeout(() => {
taskEl.focus();
});
}
}
// move task down
if (this.checkKeyCombo($ev, lsKeys.moveTaskDown)) {
if (taskIndex < this.tasks.length - 1) {
TaskListCtrl.moveItem(this.tasks, taskIndex, taskIndex + 1);
}
}
// finally apply
this.$scope.$apply();
}
expandSubTasks(task) {
if (task.subTasks) {
task.isHideSubTasks = false;
}
}
collapseSubTasks(task) {
if (task.subTasks) {
const hasCurrentTaskAsSubTask = !!(task.subTasks.find((task) => this.currentTaskId === task.id));
if (!hasCurrentTaskAsSubTask) {
task.isHideSubTasks = true;
}
}
}
addSubTask(task) {
// use parent task if the current task is a sub task itself
if (this.parentTask) {
task = this.parentTask;
}
this.expandSubTasks(task);
// only allow if task is not done
if (!task.isDone) {
if (!task.subTasks) {
task.subTasks = [];
// save original values for potential later re-initialization
task.mainTaskTimeEstimate = task.timeEstimate;
task.mainTaskTimeSpent = task.timeSpent;
task.mainTaskTimeSpentOnDay = task.timeSpentOnDay;
}
let subTask = this.Tasks.createTask({
title: '',
parentId: task.id
});
// edit title right away
task.subTasks.push(subTask);
// focus the new element to edit it right away
// timeout is needed to wait for dom to update
this.$timeout(() => {
this.$scope.$broadcast(this.EDIT_ON_CLICK_TOGGLE_EV, subTask.id);
});
// if parent was current task, mark sub task as current now
if (this.currentTaskId === task.id) {
this.Tasks.updateCurrent(subTask);
}
}
}
togglePlay(task) {
if (this.currentTaskId === task.id) {
this.Tasks.updateCurrent(undefined);
} else {
if (task.subTasks) {
const firstUndone = task.subTasks.find((cTask) => !cTask.isDone);
if (firstUndone) {
this.Tasks.updateCurrent(firstUndone);
}
} else {
this.Tasks.updateCurrent(task);
}
}
if (this.currentTaskId) {
this.focusCurrentTask();
}
}
static moveItem(array, oldIndex, newIndex) {
if (newIndex >= array.length) {
let k = newIndex - array.length;
while ((k--) + 1) {
array.push(undefined);
}
}
array.splice(newIndex, 0, array.splice(oldIndex, 1)[0]);
return array; // for testing purposes
}
}
// hacky fix for ff
TaskListCtrl.$$ngIsClass = true;
angular
.module('superProductivity')
.controller('TaskListCtrl', TaskListCtrl)
.component('taskList', {
templateUrl: 'scripts/task-list/task-list-d.html',
bindToController: true,
controller: 'TaskListCtrl',
controllerAs: CONTROLLER_AS,
bindings: {
tasks: '=',
currentTaskId: '<',
limitTo: '@',
filter: '<',
isSubTasksDisabled: '@',
allowTaskSelection: '@',
disableDropInto: '@',
onItemMoved: '&',
onOrderChanged: '&',
onTaskDoneChangedCallback: '&onTaskDoneChanged',
parentTask: '='
}
});
})();