/** * A DayView is a view that displays the time span of a day vertically. * A day view may contain a list of PeriodViews to display work time periods. */ class DayView extends DragTargetView { constructor(element) { super(element); TimeCards.dataManager.addStoreErrorListener("period", this.onStoreError.bind(this)); } build(element) { super.build(element); //The list of available snapping time scales in milliseconds. this.snapTimes = [ 60000, 300000, 600000, 900000, 1800000, Time.ONE_HOUR ]; this.snapTimeBlockSizes = [ 150, 100, 85, 70, 50, 40 ]; this.displayingPeriods = { }; //Set the time in milliseconds the time snaps to. var storedSnapTime = localStorage.getItem("day_view_snap_time"); if (storedSnapTime) { this.setSnapTime(parseInt(storedSnapTime), true); } else { this.setDefaultSnapTime(); } //The time area is used to display the time markings and periods in the day view. this.timeArea = document.createElement("TIME-AREA"); element.appendChild(this.timeArea); //The decoration container is the background of the day view. this.decorationContainer = document.createElement("DIV"); this.decorationContainer.className = "decoration-container"; this.timeArea.appendChild(this.decorationContainer); //This element will contain the period views. this.periodsContainer = document.createElement("DIV"); this.periodsContainer.className = "periods-container"; this.timeArea.appendChild(this.periodsContainer); //Instantiate the period preview. this.periodPreview = new PeriodView(null, Time.getCurrentLocalDayIndex()); this.periodPreview.element.className = "preview"; this.dragPreviewVisible = false; //Add the current time line. this.currentTimeLine = document.createElement("DIV"); this.currentTimeLine.className = "current-time-line"; this.decorationContainer.appendChild(this.currentTimeLine); //Add the tools container. this.toolsContainer = document.createElement("DIV"); this.toolsContainer.className = "tools-container"; element.parentNode.appendChild(this.toolsContainer); //Add the time scale tool element. this.timeScaleContainer = document.createElement("DIV"); this.timeScaleContainer.className = "tool time-scale-container"; this.toolsContainer.appendChild(this.timeScaleContainer); //Add the time scale slider. this.timeScaleSlider = document.createElement("INPUT"); this.timeScaleSlider.type = "range"; this.timeScaleSlider.setAttribute("orient", "vertical"); this.timeScaleSlider.min = "0"; this.timeScaleSlider.max = "" + (this.snapTimes.length - 1); this.timeScaleSlider.value = "" + this.snapTimes.indexOf(this.snapTime); this.timeScaleSlider.className = "time-scale-slider"; this.timeScaleSlider.addEventListener("input", this.onTimeScaleChanged.bind(this)); this.timeScaleSlider.addEventListener("mousedown", this.onTimeScaleMouseDown.bind(this)); this.timeScaleSlider.addEventListener("mouseup", this.onTimeScaleMouseUp.bind(this)); this.timeScaleContainer.appendChild(this.timeScaleSlider); //Start the time interval that updates the current time. this.updateCurrentTime(); setInterval(this.updateCurrentTime.bind(this), 30000); //Create a tooltip for the time scale slider. this.timeScaleSliderTooltip = Tooltip.createTooltip("time-scale-slider-tooltip"); this.timeScaleSliderTooltip.setText("Snapping: " + (this.snapTime == 60000 ? "none" : (this.snapTime / 1000 / 60) + " minutes")); //Create a tooltip for the period dragging. this.periodDraggingTooltip = Tooltip.createTooltip("period-dragging-tooltip"); return element; } /** * Sets the snap time to the default value. */ setDefaultSnapTime() { this.setSnapTime(this.snapTimes[this.snapTimes.length - 2], true); } viewDidAppear() { this.updateCurrentTime(); } /** * Sets the snap time and recalculates the hour block size of the decoration layer. * Will refresh the periods if the second parameter is omitted or set to false. */ setSnapTime(snapTime, skipRefresh = false) { this.snapTime = snapTime; this.hourBlockSize = this.snapTimeBlockSizes[this.snapTimes.indexOf(snapTime)]; //Store the snap time in the local storage. localStorage.setItem("day_view_snap_time", this.snapTime); if (!skipRefresh) { this.refresh(); } } onTimeScaleMouseDown(event) { this.timeScaleSliderTooltip.showForCursor(); } onTimeScaleMouseUp(event) { this.timeScaleSliderTooltip.dismiss(); } onTimeScaleChanged(event) { this.setSnapTime(this.snapTimes[parseInt(this.timeScaleSlider.value)]); this.timeScaleSliderTooltip.setText("Snapping: " + (this.snapTime == 60000 ? "none" : (this.snapTime / 1000 / 60) + " minutes")); this.timeScaleSliderTooltip.pointToCursor(); } onDragOver(draggedObject, validType, sourceView, x, y) { if (validType) { //Get the bounds of the actual periods container. var periodsContainerBounds = this.periodsContainer.getBoundingClientRect(); if (this.dragPreviewVisible) { //If the periods container bounds do no longer contain the cursor position, hide the preview. if (periodsContainerBounds.left < x && periodsContainerBounds.left + periodsContainerBounds.width > x && periodsContainerBounds.top < y && periodsContainerBounds.top + periodsContainerBounds.height > y) { var hoverData = this.getHoverRangeForPosition(x, y); this.periodPreview.period = { start_time: hoverData.start, end_time: hoverData.end }; this.periodPreview.dayIndex = this.dayIndex; this.periodPreview.displayForTime(); } else { this.dragPreviewVisible = false; this.timeArea.removeChild(this.periodPreview.element); } } else { //If the periods container bounds contain the cursor position, show the preview. if (periodsContainerBounds.left < x && periodsContainerBounds.left + periodsContainerBounds.width > x && periodsContainerBounds.top < y && periodsContainerBounds.top + periodsContainerBounds.height > y) { this.dragPreviewVisible = true; /*this.periodPreview.setPeriodInformation({ card_title: draggedObject.title, id_project: draggedObject.id_project });*/ var project = draggedObject.id_project ? TimeCards.dataManager.getEntity("project", draggedObject.id_project) : null; this.periodPreview.setDisplayValues(draggedObject.title, project ? project.name : ""); this.periodPreview.dayView = this; this.timeArea.appendChild(this.periodPreview.element); } } } } onDragEnter(draggedObject, validType, sourceView) { } onDragExit() { this.hideDragPreviewPeriod(); } onDragEnd() { //Only hide the period if there is currently no newly creating period having the period popover opened. if (!this.updating) { this.hideDragPreviewPeriod(); } } onDrop(droppedObject, sourceView, x, y) { //Only proceed if the drag preview is being shown and the day view is not updating something. if (this.dragPreviewVisible && !this.updating) { var hoverData = this.getHoverRangeForPosition(x, y); var hoveringPeriod = hoverData.hoveringPeriod; var hoveringPeriodStartTime = hoverData.hoveringPeriodStartTime; var hoveringPeriodEndTime = hoverData.hoveringPeriodEndTime; //Get the dropped card information. var droppedCard = TimeCards.dataManager.getEntity("card", droppedObject.card_id); //Create the new period object. This will not be entirely used in any case, as sometimes only some information is needed. var period = { id_card: droppedObject.card_id, id_project: droppedObject.id_project, card_title: droppedCard.title, start_time: hoverData.start, end_time: hoverData.end, notes: droppedCard.description, id_user: this.viewController.selectedUser }; if (hoveringPeriod) { //Use the project ID of the existing card if the new card has no project assigned to it. var projectId = droppedCard.id_project ?? hoveringPeriod.id_project; //Full replacement. if (hoverData.action == "Replace") { //Only do the change if anything is different. if (hoveringPeriod.id_card != droppedObject.card_id || hoveringPeriod.id_project != projectId || hoveringPeriod.card_title != droppedCard.title || (hoveringPeriod.notes ?? "") != droppedCard.description || hoveringPeriod.id_user != this.viewController.selectedUser) { this.setUpdating(true); TimeCards.dataManager.store("period", hoveringPeriod.period_id, { id_card: droppedObject.card_id, //id_project: projectId, //EDIT: When replacing a card, the project should remain the one of the replaced card. Therefore, this line was disabled. card_title: droppedCard.title, notes: droppedCard.description, id_user: this.viewController.selectedUser }); } else { //Just hide the drag period if no change was made. this.hideDragPreviewPeriod(); } } //Partial replacement. else if (hoverData.action == "Replace partially") { this.setUpdating(true); //Top. if (period.start_time == hoveringPeriodStartTime) { TimeCards.dataManager.store("period", hoveringPeriod.period_id, { start_time: period.end_time }); //It is unknown for what case this code existed. //if (hoveringPeriod.start_time < hoveringPeriodStartTime) { /*TimeCards.workTimeManager.addPeriod({ id_card: hoveringPeriod.id_card, card_title: droppedCard.title, start_time: hoveringPeriod.start_time, end_time: hoveringPeriodStartTime - 1, notes: droppedCard.description, id_user: this.viewController.selectedUser });*/ /*TimeCards.dataManager.store("period", { id_card: hoveringPeriod.id_card, card_title: droppedCard.title, start_time: hoveringPeriod.start_time, end_time: hoveringPeriodStartTime - 1, notes: droppedCard.description, id_user: this.viewController.selectedUser }); }*/ } //Bottom. else if (period.end_time == hoveringPeriodEndTime) { TimeCards.dataManager.store("period", hoveringPeriod.period_id, { end_time: period.start_time }); if (!hoveringPeriod.end_time) { period.end_time = null; } //It is unknown for what case this code existed. //if (hoveringPeriod.end_time > hoveringPeriodEndTime) { /*TimeCards.workTimeManager.addPeriod({ id_card: hoveringPeriod.id_card, card_title: droppedCard.title, start_time: hoveringPeriodEndTime + 1, end_time: hoveringPeriod.end_time, notes: droppedCard.description, id_user: this.viewController.selectedUser });*/ /*TimeCards.dataManager.store("period", { id_card: hoveringPeriod.id_card, card_title: droppedCard.title, start_time: hoveringPeriodEndTime + 1, end_time: hoveringPeriod.end_time, notes: droppedCard.description, id_user: this.viewController.selectedUser }); }*/ } period.id_project = projectId; TimeCards.dataManager.store("period", period); } //Hide the drag period. this.hideDragPreviewPeriod(); } else { //Just add the new period if we don't hover on any other period. //New periods will not be saved immediately, but will trigger the period popover, which will save the period as soon as a project is selected. this.addNewPeriod(period); } } else { //Hide the drag period. //It is unknown whether there is a use case that leads here. this.hideDragPreviewPeriod(); } } /** * Shows the period popover with the given period. * The period popover will save the period as soon as a project is selected and the popover is dismissed. * Only after the popover is dismissed will the period preview be hidden. */ addNewPeriod(period) { this.setUpdating(true); this.periodPreview.period = period; this.periodPreview.showPeriodPopover(); //TimeCards.dataManager.store("period", period); } /** * Called by the period popover if no project was selected and the popover is dismissed. * Also called when period popover did successfully create the new period. * Will end the creation of the new card. */ endPeriodCreation() { this.setUpdating(false); this.hideDragPreviewPeriod(); } updateCurrentTime() { var currentTime = new Date().getTime(); currentTime -= (Time.getCurrentLocalDayIndex() * Time.ONE_DAY); currentTime += Time.TIMEZONE_OFFSET; this.currentTimeLine.style.top = ((currentTime / Time.ONE_DAY) * this.decorationContainer.clientHeight) + "px"; } hideDragPreviewPeriod() { if (this.dragPreviewVisible) { this.timeArea.removeChild(this.periodPreview.element); this.dragPreviewVisible = false; } } acceptsType(draggableType) { //Refuse the card if the user has no write permission for periods. return draggableType == "card" && Authentication.isResourceAvailable("period", "w"); } /** * Renders the time area again. */ refresh() { //this.renderPeriods(); this.renderTimeArea(); } /** * Displays the work time periods for the given day and user. * The time of the date object is ignored. */ displayDay(dayIndex, userId = null) { this.dayIndex = dayIndex; this.userId = userId; //Show or hide the current time line, depending on whether the displayed day is today or not. this.currentTimeLine.style.display = this.dayIndex == Time.getCurrentLocalDayIndex() ? "" : "none"; //Remove the periods from the previous day. this.clear(); //We use a data listener with a filter for this day to obtain the periods. if (!this.onPeriodUpdateBound) { this.onPeriodUpdateBound = this.onPeriodUpdate.bind(this); } else { TimeCards.dataManager.removeDataListener("period", this.onPeriodUpdateBound); } var dayStartGmt = Time.getTimestampForLocalDayIndex(this.dayIndex); var nextDayStartGmt = Time.getTimestampForLocalDayIndex(this.dayIndex + 1); TimeCards.dataManager.addDataListener("period", [ { "start_time": { cond: ">=", value: dayStartGmt }, "§start_time": { cond: "<", value: nextDayStartGmt }, id_user: this.userId }, { "end_time": { cond: ">", value: dayStartGmt }, "§end_time": { cond: "<=", value: nextDayStartGmt }, id_user: this.userId }, { start_time: { cond: "<", value: dayStartGmt }, end_time: { cond: ">", value: nextDayStartGmt }, id_user: this.userId } ], this.onPeriodUpdateBound); this.refresh(); } /** * Removes all periods from the day view. */ clear() { while (this.periodsContainer.children.length > 0) { this.periodsContainer.removeChild(this.periodsContainer.firstChild); } this.displayingPeriods = { }; } /** * Builds the time lines and hour labels. */ renderTimeArea() { for (var i = this.decorationContainer.children.length - 1; i >= 0; i--) { if (this.decorationContainer.children[i] != this.currentTimeLine) { this.decorationContainer.removeChild(this.decorationContainer.children[i]); } } for (var i = 0; i < 25; i++) { var hourBlock = document.createElement("DIV"); hourBlock.className = "hour-block"; if (i == 23) { hourBlock.className = "hour-block last"; } hourBlock.style.height = this.hourBlockSize + "px"; var timeLabelContainer = document.createElement("DIV"); timeLabelContainer.className = "time-label-container"; var timeLabel = document.createElement("SPAN"); timeLabel.className = "time-label"; timeLabel.innerText = (i == 24 ? 0 : i) + ":00"; timeLabelContainer.appendChild(timeLabel); hourBlock.appendChild(timeLabelContainer); this.decorationContainer.appendChild(hourBlock); } this.updateCurrentTime(); } /** * Checks whether the given time range for a period is allowed or not. * The times are absolute timestamps. */ periodRangeAllowed(periodId, start, end = null) { if (this.updating) { return false; } //Use the current time if no end time is specified. if (end == null || end < 0) { end = new Date().getTime(); } //Do not allow the range if it is smaller than the snap time. //EDIT: Now the minimum size of a period is 1 second. if (end - start < /*this.snapTime*/1000) { return false; } for (var checkingPeriodId in this.displayingPeriods) { var checkingPeriod = this.displayingPeriods[checkingPeriodId].period; if (checkingPeriodId == periodId) { continue; } var endTime = checkingPeriod.end_time; if (!endTime) { endTime = new Date().getTime(); } if ((start > checkingPeriod.start_time && start < endTime) || (end > checkingPeriod.start_time && end < endTime) || (checkingPeriod.start_time >= start && endTime <= end)) { return false; } } return true; } periodOnThisDay(period) { var dayStartGmt = Time.getTimestampForLocalDayIndex(this.dayIndex); var endTime = period.end_time; if (!endTime) { endTime = new Date().getTime(); } if ((period.start_time >= dayStartGmt && period.start_time < dayStartGmt + Time.ONE_DAY && endTime != dayStartGmt) || (endTime > dayStartGmt && endTime <= dayStartGmt + Time.ONE_DAY && period.start_time != dayStartGmt + Time.ONE_DAY) || (period.start_time < dayStartGmt && endTime > dayStartGmt + Time.ONE_DAY)) { return true; } return false; } onPeriodUpdate(periodId, period) { var userCausedUpdate = this.updating; this.setUpdating(false); if (!period) { //Handle the deleted period. if (!(periodId in this.displayingPeriods)) { //Do nothing if the period is not in the displaying periods. return; } this.periodsContainer.removeChild(this.displayingPeriods[periodId].element); delete this.displayingPeriods[periodId]; } else if (!(periodId in this.displayingPeriods)) { //Handle the added period. var newPeriodView = new PeriodView(period, this.dayIndex); newPeriodView.dayView = this; this.displayingPeriods[periodId] = newPeriodView; this.periodsContainer.appendChild(this.displayingPeriods[periodId].element); this.displayingPeriods[periodId].period = period; this.displayingPeriods[periodId].dayIndex = this.dayIndex; //Show the period popover for new periods. if (userCausedUpdate) { this.displayingPeriods[periodId].element.click(); } } else { //Handle the changed period. this.displayingPeriods[periodId].dayIndex = this.dayIndex; } } setUpdating(updating) { if (updating) { if (this.element.className.indexOf("updating") == -1) { this.element.classList.add("updating"); } } else { this.element.classList.remove("updating"); } this.updating = updating; } /** * Converts an amount of pixels from the top edge of the periods container to milliseconds since the start of the day. */ verticalPositionToTime(y) { var periodsContainerBounds = this.periodsContainer.getBoundingClientRect(); return Math.round(((y - periodsContainerBounds.top) / this.periodsContainer.clientHeight) * Time.ONE_DAY); } /** * Converts an amount of milliseconds since the start of the day to pixels relative to the top edge of the periods container. */ timeToVerticalPosition(milliseconds) { return Math.round((milliseconds / Time.ONE_DAY) * this.periodsContainer.clientHeight); } getHoverRangeForPosition(x, y) { var start = 0; var end = 0; var action = "None"; var timezoneOffset = Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); var dayStartGmt = Time.getTimestampForLocalDayIndex(this.dayIndex); var hoverTime = this.verticalPositionToTime(y) + dayStartGmt; //Get the period the cursor is hovering over, if there is one. var hoveringPeriod = null; var hoveringPeriodEndTime = 0; var hoveringPeriodStartTime = 0; for (var periodId in this.displayingPeriods) { var checkingPeriod = this.displayingPeriods[periodId].period; var startTime = checkingPeriod.start_time; var endTime = checkingPeriod.end_time; if (!endTime) { endTime = new Date().getTime(); } var startTimeOffset = startTime + timezoneOffset; var endTimeOffset = endTime + timezoneOffset; var startDayIndex = Math.floor(startTimeOffset / Time.ONE_DAY); var endDayIndex = Math.floor(endTimeOffset / Time.ONE_DAY); //If the period starts on a previous day: if (startDayIndex < this.dayIndex) { startTime = (this.dayIndex * Time.ONE_DAY) - timezoneOffset; } //If the period ends on a following day: if (endDayIndex > this.dayIndex) { endTime = ((this.dayIndex + 1) * Time.ONE_DAY) - timezoneOffset; } if (hoverTime > startTime && hoverTime <= endTime) { hoveringPeriod = checkingPeriod; hoveringPeriodStartTime = startTime; hoveringPeriodEndTime = endTime; break; } } var previousLine = (Math.floor((hoverTime - (this.snapTime / 2)) / this.snapTime) * this.snapTime); var nextLine = previousLine + Time.ONE_HOUR; //If the cursor is not over any period: the end of the previous period or the previous line before, the start of the next period or the next line after. if (!hoveringPeriod) { start = previousLine; end = nextLine; action = "Insert"; //Check if this range is partially over another period. for (var periodId in this.displayingPeriods) { var checkingPeriod = this.displayingPeriods[periodId].period; var endTime = checkingPeriod.end_time; if (!endTime) { endTime = new Date().getTime(); } if (start < checkingPeriod.start_time && end > checkingPeriod.start_time) { end = checkingPeriod.start_time; } if (start < endTime && end > endTime) { start = endTime; } } } else { var periodSize = hoveringPeriodEndTime - hoveringPeriodStartTime; //If the cursor is over a period which is smaller than three times the snap size, only allow replacement. if (periodSize < (this.snapTime * 3)) { start = hoveringPeriodStartTime; end = hoveringPeriodEndTime; action = "Replace"; } //If the cursor is over a period which is at least three times the snap size, allow for replacement of the first or last part. else if (periodSize >= (this.snapTime * 3)) { var thirdSize = Math.round(periodSize / 3 / this.snapTime) * this.snapTime; //Top area. if (hoverTime < hoveringPeriodStartTime + thirdSize) { start = hoveringPeriodStartTime; end = hoveringPeriodStartTime + thirdSize; action = "Replace partially"; } //Bottom area. else if (hoverTime > hoveringPeriodEndTime - thirdSize) { start = hoveringPeriodEndTime - thirdSize; end = hoveringPeriodEndTime; action = "Replace partially"; } //Center area. else { start = hoveringPeriodStartTime; end = hoveringPeriodEndTime; action = "Replace"; } } } return { start, end, action, hoveringPeriod, hoveringPeriodStartTime, hoveringPeriodEndTime } } onStoreError(periodId, period, error) { //Set updating to false on error so that another store attempt can be made. Otherwise, the day view would be softlocked. this.setUpdating(false); } } UIKit.registerViewType(DayView, "day-view");