class PeriodView extends View { constructor(period, dayIndex) { super(null); this.dayIndex = dayIndex; if (period) { this.setPeriodInformation(period); //Add an interval that updates the period view regularly if this is a period without end time. if (!period.end_time) { setInterval(this.displayForTime.bind(this), 30000); } } } build(element) { this.cardTitleLabel = document.createElement("SPAN"); this.cardTitleLabel.className = "card-title-label"; element.appendChild(this.cardTitleLabel); this.movementNotches = [ ]; for (var i = 0; i < 2; i++) { var notch = document.createElement("DIV"); notch.className = "movement-notch require_w__period"; notch.addEventListener("mousedown", this.onNotchMouseDown.bind(this)); notch.addEventListener("click", this.onNotchClicked.bind(this)); element.appendChild(notch); this.movementNotches.push(notch); } document.body.addEventListener("mousemove", this.onMouseMove.bind(this)); document.body.addEventListener("mouseup", this.onMouseUp.bind(this)); element.addEventListener("click", this.onClick.bind(this)); return element; } setPeriodInformation(period) { //We just add a listener for the given period. The listener callback will do the rest. if (this.onPeriodUpdateBound) { TimeCards.dataManager.removeDataListener("period", this.onPeriodUpdateBound); } else { this.onPeriodUpdateBound = this.onPeriodUpdate.bind(this); } TimeCards.dataManager.addDataListener("period", { period_id: period.period_id }, this.onPeriodUpdateBound); } /** * Enables the preview mode for this period view. * In this mode, the view only acts as a mannequin that is controlled by the DayView. * The current time range will be stored temporarily to restore when the preview ends. */ startPreview() { //TODO: For later implementation. We don't implement period pushing yet. } /** * Ends the preview mode. * Will restore the saved */ endPreview() { //TODO: For later implementation. We don't implement period pushing yet. } /** * Sets the location and size for this period view to match the given start and end time. * Pass the start and end time as UNIX timestamps in milliseconds. */ displayForTime() { var startTime = this.period.start_time; var endTime = null; //Get the end time of the period if there is one. //If there is no end time, we want to add the active-period class. if (this.period.end_time) { endTime = this.period.end_time; this.element.classList.remove("active-period"); } else { endTime = new Date().getTime(); if (this.element.className.indexOf("active-period") == -1) { this.element.classList.add("active-period"); } } //Apply the timezone offset. var timezoneOffset = Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); startTime += timezoneOffset; endTime += timezoneOffset; //Get the day index of the start and end time. var startDayIndex = Math.floor(startTime / Time.ONE_DAY); var endDayIndex = Math.floor(endTime / Time.ONE_DAY); //If the period starts on a previous day: if (startDayIndex < this.dayIndex) { //Set the displayed start time to the beginning of the day. startTime = this.dayIndex * Time.ONE_DAY; if (this.element.className.indexOf("overlap-from-previous-day") == -1) { this.element.classList.add("overlap-from-previous-day"); } } else { this.element.classList.remove("overlap-from-previous-day"); } //If the period ends on a following day: if (/*endDayIndex > this.dayIndex*/endTime - (this.dayIndex * Time.ONE_DAY) > Time.ONE_DAY) { //Set the displayed end time to the end of the day. endTime = (this.dayIndex + 1) * Time.ONE_DAY; if (this.element.className.indexOf("overlap-to-next-day") == -1) { this.element.classList.add("overlap-to-next-day"); } } else { this.element.classList.remove("overlap-to-next-day"); } //Convert the displayed time values to relative time since the start of the day. startTime -= this.dayIndex * Time.ONE_DAY; endTime -= this.dayIndex * Time.ONE_DAY; this.displayForTimeRange(startTime, endTime); } /** * Applies the given time range to the DOM element. * The Y position and height are applied in percent values. * The times are relative to the beginning of the day. */ displayForTimeRange(startTime, endTime = null) { //Use the current time if no end time is specified. if (endTime == null) { endTime = new Date().getTime() - (this.dayIndex * Time.ONE_DAY) + Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); } var top = (startTime / Time.ONE_DAY * 100) + "%"; var bottom = ((1 - endTime / Time.ONE_DAY) * 100) + "%"; this.element.style.top = top; this.element.style.bottom = bottom; } /** * Rounds the given time in milliseconds to the current snap time of the day view. */ roundTime(milliseconds) { return Math.round(milliseconds / this.dayView.snapTime) * this.dayView.snapTime; } onNotchMouseDown(event) { this.draggingNotch = event.currentTarget; //Get the current timezone offset. var timezoneOffset = Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); //Store the offset of the mouse position relative to the notch. var notchRect = this.draggingNotch.getBoundingClientRect(); this.mouseDragOffset = /*event.clientY - notchRect.top*/0; //Start counting the duration of the drag. this.dragStartTime = new Date().getTime(); //Use the current time as the initial allowed value as backup. this.lastAllowedDragPosition = (this.draggingNotch == this.movementNotches[0] ? this.period.start_time - (this.dayIndex * Time.ONE_DAY) + timezoneOffset : this.period.end_time - (this.dayIndex * Time.ONE_DAY) + timezoneOffset); //Show the tooltip. this.dayView.periodDraggingTooltip.setText(this.formatTime(this.draggingNotch == this.movementNotches[0] ? this.period.start_time : this.period.end_time)); this.dayView.periodDraggingTooltip.showForElement(this.draggingNotch); } onMouseMove(event) { if (this.draggingNotch) { //Get the current timezone offset. var timezoneOffset = Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); //Make sure there is a day view. if (!this.dayView) { console.error("There is no DayView instance, yet a period is being dragged."); return; } //Calculate the mouse Y position in regard of the drag offset. var mousePosition = event.clientY + this.mouseDragOffset; //Get the reounded time at the mouse Y position in GMT. var roundedTimeAtMousePosition = this.roundTime(this.dayView.verticalPositionToTime(mousePosition)); //Either try the new position time as the start or end of this period. if (this.draggingNotch == this.movementNotches[0]) { if (this.dayView.periodRangeAllowed(this.period.period_id, roundedTimeAtMousePosition + (this.dayIndex * Time.ONE_DAY) - timezoneOffset, this.period.end_time)) { this.displayForTimeRange(roundedTimeAtMousePosition, this.period.end_time ? (this.period.end_time - (this.dayIndex * Time.ONE_DAY) + timezoneOffset) : null); this.lastAllowedDragPosition = roundedTimeAtMousePosition; } } else { if (this.dayView.periodRangeAllowed(this.period.period_id, this.period.start_time, roundedTimeAtMousePosition + (this.dayIndex * Time.ONE_DAY) - timezoneOffset)) { this.displayForTimeRange(this.period.start_time - (this.dayIndex * Time.ONE_DAY) + timezoneOffset, roundedTimeAtMousePosition); this.lastAllowedDragPosition = roundedTimeAtMousePosition; } } //Update the tooltip text. this.dayView.periodDraggingTooltip.setText(this.formatTime(this.lastAllowedDragPosition - timezoneOffset + (this.dayIndex * Time.ONE_DAY))); this.dayView.periodDraggingTooltip.pointAtElement(this.draggingNotch); } } onMouseUp(event) { if (this.draggingNotch && this.lastAllowedDragPosition != null) { //Get the current timezone offset. var timezoneOffset = Time.getTimezoneOffsetForLocalDayIndex(this.dayIndex); var updatedPeriodData = { }; //Update either the start or end time of the period, depending on which notch is being dragged. if (this.draggingNotch == this.movementNotches[0]) { var newStartTime = this.lastAllowedDragPosition - timezoneOffset + (this.dayIndex * Time.ONE_DAY); if (newStartTime != this.period.start_time) { updatedPeriodData.start_time = newStartTime; } } else if (this.draggingNotch == this.movementNotches[1]) { var newEndTime = this.lastAllowedDragPosition - timezoneOffset + (this.dayIndex * Time.ONE_DAY); if (newEndTime != this.period.end_time) { updatedPeriodData.end_time = newEndTime; } } //Make sure there is a day view. if (!this.dayView) { console.error("There is no DayView instance, yet a period is being released."); return; } //Do nothing if there are no changed values. if (("start_time" in updatedPeriodData) || ("end_time" in updatedPeriodData)) { //Put the day view into updating mode. this.dayView.setUpdating(true); //Update the period. TimeCards.dataManager.store("period", this.period.period_id, updatedPeriodData); } //Open the period popover when the drag was short enough. this.showPeriodPopoverAfterDrag(event); //Reset the dragging notch. this.draggingNotch = null; this.lastAllowedDragPosition = null; //Dismiss the tooltip. this.dayView.periodDraggingTooltip.dismiss(); } } onClick(event) { //event.stopPropagation(); //Show the period popover when the period view was clicked. this.showPeriodPopover(); } onNotchClicked(event) { //Stop the propagation of a click on the notches. event.stopPropagation(); } /** * Checks if the drag was short enough and shows the popover if so. * The click or mouse up event must be provided. */ showPeriodPopoverAfterDrag(event) { var notchRect = this.draggingNotch.getBoundingClientRect(); var endOffset = event.clientY - notchRect.top; var movedDistance = this.mouseDragOffset - endOffset; if ((movedDistance <= 5 && movedDistance >= -5) && new Date().getTime() - this.dragStartTime <= 250) { this.showPeriodPopover(); } } showPeriodPopover() { if (!this.periodPopover) { //Get the period popover instance. this.periodPopover = UIKit.getPopoverById("period-popover"); } this.periodPopover.periodView = this; this.periodPopover.showForElement(this.element, "left"); this.periodPopover.setPeriodData(this.period); } /** * Called by the period popover when its delete button was pressed. */ onDeleteButtonPressed() { //Only delete if this period actually exists and is not a preview of a creating one. if (this.period.period_id) { TimeCards.dataManager.store("period", this.period.period_id, null); } } onPeriodUpdate(periodId, period) { if (!period) { //Remove the listeners if the period is deleted. TimeCards.dataManager.removeDataListener("period", this.onPeriodUpdateBound); TimeCards.dataManager.removeDataListener("project", this.onProjectUpdateBound); return; } this.dayView.setUpdating(false); //Re-add the project listener when the period has been updated. //Just add the listener if it is the first time this function is called. if (!this.doneFirstUpdate || this.period.id_project != period.id_project) { this.doneFirstUpdate = true; if (this.onProjectUpdateBound) { TimeCards.dataManager.removeDataListener("project", this.onProjectUpdateBound); } else { this.onProjectUpdateBound = this.onProjectUpdate.bind(this); } TimeCards.dataManager.addDataListener("project", { project_id: period.id_project }, this.onProjectUpdateBound); //Set the period after the callback! this.period = period; } else { //Set the period before the function call! this.period = period; //Trigger a project update to actually update the labels and stuff manually if the project did not change. this.onProjectUpdate(this.period.id_project, TimeCards.dataManager.getEntity("project", this.period.id_project)); } } onProjectUpdate(projectId, project) { this.setDisplayValues(this.period.card_title, project ? project.name : ""); this.displayForTime(); } setDisplayValues(cardTitle, projectName) { this.cardTitleLabel.innerText = cardTitle + (projectName ? " (" + projectName + ")" : ""); } formatTime(timestamp) { var date = new Date(timestamp); var hour = "" + date.getHours(); if (hour.length == 1) { hour = "0" + hour; } var minute = "" + date.getMinutes(); if (minute.length == 1) { minute = "0" + minute; } return hour + ":" + minute; } } UIKit.registerViewType(PeriodView, "period-view");