"use strict"; /* * Egroupware Calendar timegrid * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link http://www.egroupware.org * @author Nathan Gray * @version $Id$ */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses /calendar/js/et2_widget_view.js; */ var et2_core_widget_1 = require("../../api/js/etemplate/et2_core_widget"); var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); var et2_widget_view_1 = require("./et2_widget_view"); var et2_core_DOMWidget_1 = require("../../api/js/etemplate/et2_core_DOMWidget"); var et2_dataview_view_grid_1 = require("../../api/js/etemplate/et2_dataview_view_grid"); /** * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days * * This widget is responsible for the times on the side, and it is also the * controller for both positioning and setting the day columns. Day columns are * recycled rather than removed and re-created to reduce reloading. Similarly, * the horizontal time grid (when used - see granularity attribute) is only * redrawn or resized when needed. Unfortunately resizing is needed every time * the all day section has an event added or removed so the full work day from * start time to end time is properly displayed. * * * @augments et2_calendar_view */ var et2_calendar_timegrid = /** @class */ (function (_super) { __extends(et2_calendar_timegrid, _super); /** * Constructor * * @memberOf et2_calendar_timegrid */ function et2_calendar_timegrid(_parent, _attrs, _child) { var _this = // Call the inherited constructor _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_calendar_timegrid._attributes, _child || {})) || this; _this.daily_owner = false; // Main container _this.div = jQuery(document.createElement("div")) .addClass("calendar_calTimeGrid") .addClass("calendar_TimeGridNoLabel"); // Headers _this.gridHeader = jQuery(document.createElement("div")) .addClass("calendar_calGridHeader") .appendTo(_this.div); _this.dayHeader = jQuery(document.createElement("div")) .appendTo(_this.gridHeader); // Contains times / rows _this.scrolling = jQuery(document.createElement('div')) .addClass("calendar_calTimeGridScroll") .appendTo(_this.div) .append('
'); // Contains days / columns _this.days = jQuery(document.createElement("div")) .addClass("calendar_calDayCols") .appendTo(_this.scrolling); // Used for owners _this.owner = et2_createWidget('description', {}, _this); _this._labelContainer = jQuery(document.createElement("label")) .addClass("et2_label et2_link") .appendTo(_this.gridHeader); _this.gridHover = jQuery('
'); // List of dates in Ymd // The first one should be start_date, last should be end_date _this.day_list = []; _this.day_widgets = []; // Timer to re-scale time to fit _this.resize_timer = null; _this.setDOMNode(_this.div[0]); return _this; } et2_calendar_timegrid.prototype.destroy = function () { // Stop listening to tab changes if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { jQuery(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id); } _super.prototype.destroy.call(this); // Delete all old objects this._actionObject.clear(); this._actionObject.unregisterActions(); this._actionObject.remove(); this._actionObject = null; this.div.off(); this.div = null; this.gridHeader = null; this.dayHeader = null; this.days = null; this.scrolling = null; this._labelContainer = null; // Stop the resize timer if (this.resize_timer) { window.clearTimeout(this.resize_timer); } }; et2_calendar_timegrid.prototype.doLoadingFinished = function () { _super.prototype.doLoadingFinished.call(this); // Listen to tab show to make sure we scroll to the day start, not top if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { jQuery(framework.getApplicationByName('calendar').tab.contentDiv) .on('show.' + this.id, jQuery.proxy(function () { if (this.scrolling) { this.scrolling.scrollTop(this._top_time); } }, this)); } // Need to get the correct internal sizing this.resize(); this._drawGrid(); // Actions may be set on a parent, so we need to explicitly get in here // and get ours this._link_actions(this.options.actions || this.getParent().options.actions || []); // Automatically bind drag and resize for every event using jQuery directly // - no action system - var timegrid = this; /** * If user puts the mouse over an event, then we'll set up resizing so * they can adjust the length. Should be a little better on resources * than binding it for every calendar event, and we won't need exceptions * for planner view to resize horizontally. */ this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function () { // Only resize in timegrid if (timegrid.options.granularity === 0) return; // Load the event timegrid._get_event_info(this); var that = this; //Resizable event handler jQuery(this).resizable({ distance: 10, // Grid matching preference grid: [10000, timegrid.rowHeight], autoHide: false, handles: 's,se', containment: 'parent', /** * Triggered when the resizable is created. * * @param {event} event * @param {Object} ui */ create: function (event, ui) { var resizeHelper = event.target.getAttribute('data-resize'); if (resizeHelper == 'WD' || resizeHelper == 'WDS') { jQuery(this).resizable('destroy'); } }, /** * If dragging to resize an event, abort drag to create * * @param {jQuery.Event} event * @param {Object} ui */ start: function (event, ui) { if (timegrid.drag_create.start) { // Abort drag to create, we're dragging to resize timegrid._drag_create_end({}); } }, /** * Triggered at the end of resizing the calEvent. * * @param {event} event * @param {Object} ui */ stop: function (event, ui) { var e = new jQuery.Event('change'); e.originalEvent = event; e.data = { duration: 0 }; var event_data = timegrid._get_event_info(this); var event_widget = timegrid.getWidgetById(event_data.widget_id); var sT = event_widget.options.value.start_m; if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) { var eT = (parseInt(timegrid._drop_data.hour) * 60) + parseInt(timegrid._drop_data.minute); e.data.duration = ((eT - sT) / 60) * 3600; if (event_widget) { event_widget.options.value.end_m = eT; event_widget.options.value.duration = e.data.duration; } jQuery(this).trigger(e); event_widget._update(event_widget.options.value); // That cleared the resize handles, so remove for re-creation... if (jQuery(this).resizable('instance')) { jQuery(this).resizable('destroy'); } } // Clear the helper, re-draw if (event_widget && event_widget._parent) { event_widget._parent.position_event(event_widget); } timegrid.div.children('.drop-hover').removeClass('.drop-hover'); }, /** * Triggered during the resize, on the drag of the resize handler * * @param {event} event * @param {Object} ui */ resize: function (event, ui) { // Add a bit for better understanding - it will show _to_ the start, // covering the 'actual' target timegrid._get_time_from_position(ui.helper[0].getBoundingClientRect().left, ui.helper[0].getBoundingClientRect().bottom + 5); timegrid.gridHover.hide(); var drop = timegrid._drag_helper(this, ui.element[0]); if (drop && !drop.is(':visible')) { drop.get(0).scrollIntoView(false); } } }); }); // Customize and override some draggable settings this.div .on('dragcreate', '.calendar_calEvent', function (event, ui) { jQuery(this).draggable('option', 'cancel', '.rowNoEdit'); // Act like you clicked the header, makes it easier to position // but put it to the side (-5) so we can still do the hover jQuery(this).draggable('option', 'cursorAt', { top: 5, left: -5 }); }) .on('dragstart', '.calendar_calEvent', function (event, ui) { jQuery('.calendar_calEvent', ui.helper).width(jQuery(this).width()) .height(jQuery(this).outerHeight()) .css('top', '').css('left', '') .appendTo(ui.helper); ui.helper.width(jQuery(this).width()); // Cancel drag to create, we're dragging an existing event timegrid.drag_create.start = null; timegrid._drag_create_end(); }) .on('mousemove', function (event) { timegrid._get_time_from_position(event.clientX, event.clientY); }) .on('mouseout', function (event) { if (timegrid.div.has(event.relatedTarget).length === 0) { timegrid.gridHover.hide(); } }) .on('mousedown', jQuery.proxy(this._mouse_down, this)) .on('mouseup', jQuery.proxy(this._mouse_up, this)); return true; }; et2_calendar_timegrid.prototype._createNamespace = function () { return true; }; /** * Show the current time while dragging * Used for resizing as well as drag & drop * * @param {type} element * @param {type} helper * @param {type} height */ et2_calendar_timegrid.prototype._drag_helper = function (element, helper, height) { if (!element) return; element.dropEnd = this.gridHover; if (element.dropEnd.length) { this._drop_data = jQuery.extend({}, element.dropEnd[0].dataset || {}); } if (typeof element.dropEnd != 'undefined' && element.dropEnd.length) { // Make sure the target is visible in the scrollable day if (this.gridHover.is(':visible')) { if (this.scrolling.scrollTop() > 0 && this.scrolling.scrollTop() >= this.gridHover.position().top - this.rowHeight) { this.scrolling.scrollTop(this.gridHover.position().top - this.rowHeight); } else if (this.scrolling.scrollTop() + this.scrolling.height() <= this.gridHover.position().top + (2 * this.rowHeight)) { this.scrolling.scrollTop(this.scrolling.scrollTop() + this.rowHeight); } } var time = ''; if (this._drop_data.whole_day) { time = this.egw().lang('Whole day'); } else if (this.options.granularity === 0) { // No times, keep what's in the event // Add class to helper to keep formatting jQuery(helper).addClass('calendar_calTimeGridList'); } else { // @ts-ignore time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { hour: element.dropEnd.attr('data-hour'), minute: element.dropEnd.attr('data-minute'), seconds: 0, timezone: 0 }, { "ampm": (egw.preference("timeformat") == "12") }); } element.innerHTML = '
' + time + '
'; } else { element.innerHTML = '
'; } jQuery(element).width(jQuery(helper).width()); return element.dropEnd; }; /** * Handler for dropping an event on the timegrid * * @param {type} timegrid * @param {type} event * @param {type} ui * @param {type} dropEnd */ et2_calendar_timegrid.prototype._event_drop = function (timegrid, event, ui, dropEnd) { var e = new jQuery.Event('change'); e.originalEvent = event; e.data = { start: 0 }; if (typeof dropEnd != 'undefined' && dropEnd) { var drop_date = dropEnd.date || false; var event_data = timegrid._get_event_info(ui.draggable); var event_widget = timegrid.getWidgetById(event_data.widget_id); if (!event_widget) { // Widget was moved across weeks / owners event_widget = timegrid.getParent().getWidgetById(event_data.widget_id); } if (event_widget) { // Send full string to avoid rollover between months using set_month() event_widget._parent.date_helper.set_value(drop_date.substring(0, 4) + '-' + drop_date.substring(4, 6) + '-' + drop_date.substring(6, 8) + 'T00:00:00Z'); // Make sure whole day events stay as whole day events by ignoring drop time if (event_data.app == 'calendar' && event_widget.options.value.whole_day) { event_widget._parent.date_helper.set_hours(0); event_widget._parent.date_helper.set_minutes(0); } else if (timegrid.options.granularity === 0) { // List, not time grid - keep time event_widget._parent.date_helper.set_hours(event_widget.options.value.start.getUTCHours()); event_widget._parent.date_helper.set_minutes(event_widget.options.value.start.getUTCMinutes()); } else { // Non-whole day events, and integrated apps, can change event_widget._parent.date_helper.set_hours(dropEnd.whole_day ? 0 : dropEnd.hour || 0); event_widget._parent.date_helper.set_minutes(dropEnd.whole_day ? 0 : dropEnd.minute || 0); } // Leave the helper there until the update is done var loading = ui.helper.clone(true).appendTo(jQuery('body')); // and add a loading icon so user knows something is happening if (jQuery('.calendar_timeDemo', loading).length == 0) { jQuery('.calendar_calEventHeader', loading).addClass('loading'); } else { jQuery('.calendar_timeDemo', loading).after('
'); } event_widget.recur_prompt(function (button_id) { if (button_id === 'cancel' || !button_id) { // Need to refresh the event with original info to clean up var app_id = event_widget.options.value.app_id ? event_widget.options.value.app_id : event_widget.options.value.id + (event_widget.options.value.recur_type ? ':' + event_widget.options.value.recur_date : ''); egw().dataStoreUID('calendar::' + app_id, egw.dataGetUIDdata('calendar::' + app_id).data); loading.remove(); return; } var duration; //Get infologID if in case if it's an integrated infolog event if (event_data.app === 'infolog') { // Duration - infologs are always non-blocking duration = dropEnd.whole_day ? 86400 - 1 : (event_widget.options.value.whole_day ? (egw().preference('defaultlength', 'calendar') * 60) : false); // If it is an integrated infolog event we need to edit infolog entry egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.app_id, event_widget._parent.date_helper.getValue() || false, duration], function () { loading.remove(); }).sendRequest(true); } else { //Edit calendar event // Duration - check for whole day dropped on a time, change it to full days duration = event_widget.options.value.whole_day && dropEnd.hour ? // Make duration whole days, less 1 second (Math.round((event_widget.options.value.end - event_widget.options.value.start) / (1000 * 86400)) * 86400) - 1 : false; // Event (whole day or not) dropped on whole day section, change to whole day non blocking if (dropEnd.whole_day) duration = 'whole_day'; // Send the update var _send = function (series_instance) { var start = new Date(event_widget._parent.date_helper.getValue()); egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ button_id === 'series' ? event_data.id : event_data.app_id, event_data.owner, start, timegrid.options.owner || egw.user('account_id'), duration, series_instance ], function () { loading.remove(); }).sendRequest(true); }; // Check for modifying a series that started before today if (event_widget.options.value.recur_type && button_id === 'series') { event_widget.series_split_prompt(function (_button_id) { if (_button_id === et2_dialog.OK_BUTTON) { _send(event_widget.options.value.recur_date); } else { loading.remove(); } }); } else { _send(event_widget.options.value.recur_date); } } }); } } }; /** * Something changed, and the days need to be re-drawn. We wait a bit to * avoid re-drawing twice if start and end date both changed, then recreate * the days. * The whole grid is not regenerated because times aren't expected to change, * just the days. * * @param {boolean} [trigger=false] Trigger an event once things are done. * Waiting until invalidate completes prevents 2 updates when changing the date range. * @returns {undefined} */ et2_calendar_timegrid.prototype.invalidate = function (trigger) { // Reset the list of days this.day_list = []; // Wait a bit to see if anything else changes, then re-draw the days if (this.update_timer) { window.clearTimeout(this.update_timer); } this.update_timer = window.setTimeout(jQuery.proxy(function () { this.widget.update_timer = null; window.clearTimeout(this.resize_timer); this.widget.loader.hide().show(); // Update actions if (this.widget._actionManager) { this.widget._link_actions(this.widget._actionManager.children); } this.widget._drawDays(); // We have to completely re-do times, as they may have changed in // scale to the point where more labels are needed / need to be removed this.widget._drawTimes(); if (this.trigger) { this.widget.change(); } // Hide loader window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this.widget), 200); }, { widget: this, "trigger": trigger }), et2_dataview_view_grid_1.et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); }; et2_calendar_timegrid.prototype.detachFromDOM = function () { // Remove the binding to the change handler jQuery(this.div).off(".et2_calendar_timegrid"); return _super.prototype.detachFromDOM.call(this); }; et2_calendar_timegrid.prototype.attachToDOM = function () { var result = _super.prototype.attachToDOM.call(this); // Add the binding for the event change handler jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function (e) { // Make sure function gets a reference to the widget var args = Array.prototype.slice.call(arguments); if (args.indexOf(this) == -1) args.push(this); return e.data.event_change.apply(e.data, args); }); // Add the binding for the change handler jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function (e) { return e.data.change.call(e.data, e, this); }); // Catch resize and prevent it from bubbling further, triggering // etemplate's resize this.div.on('resize', this, function (e) { e.stopPropagation(); }); return result; }; et2_calendar_timegrid.prototype.getDOMNode = function (_sender) { if (_sender === this || !_sender) { return this.div ? this.div[0] : null; } else if (_sender.instanceOf(et2_calendar_daycol)) { return this.days ? this.days[0] : null; } else if (_sender) { return this.gridHeader ? this.gridHeader[0] : null; } }; et2_calendar_timegrid.prototype.set_disabled = function (disabled) { var old_value = this.options.disabled; _super.prototype.set_disabled.call(this, disabled); if (disabled) { this.loader.show(); } else if (old_value !== disabled) { // Scroll to start of day - stops jumping in FF // For some reason on Chrome & FF this doesn't quite get the day start // to the top, so add 2px; this.scrolling.scrollTop(this._top_time + 2); } }; /** * Clear everything, and redraw the whole grid */ et2_calendar_timegrid.prototype._drawGrid = function () { this.div.css('height', this.options.height) .empty(); this.loader.prependTo(this.div).show(); // Draw in the horizontal - the times this._drawTimes(); // Draw in the vertical - the days this.invalidate(); }; /** * Creates the DOM nodes for the times in the left column, and the horizontal * lines (mostly via CSS) that span the whole date range. */ et2_calendar_timegrid.prototype._drawTimes = function () { jQuery('.calendar_calTimeRow', this.div).remove(); this.div.toggleClass('calendar_calTimeGridList', this.options.granularity === 0); this.gridHeader .attr('data-date', this.options.start_date) .attr('data-owner', this.options.owner) .append(this._labelContainer) .append(this.owner.getDOMNode()) .append(this.dayHeader) .appendTo(this.div); // Max with 18 avoids problems when it's not shown var header_height = Math.max(this.gridHeader.outerHeight(true), 18); this.scrolling .appendTo(this.div) .off(); // No time grid - list if (this.options.granularity === 0) { this.scrolling.css('height', '100%'); this.days.css('height', '100%'); this.iterateOver(function (day) { day.resize(); }, this, et2_calendar_daycol); return; } var wd_start = 60 * this.options.day_start; var wd_end = 60 * this.options.day_end; var granularity = this.options.granularity; var totalDisplayMinutes = wd_end - wd_start; var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / granularity); var row_count = (1440 / this.options.granularity); this.scrolling .on('scroll', jQuery.proxy(this._scroll, this)); // Percent var rowHeight = (100 / rowsToDisplay).toFixed(1); // Pixels this.rowHeight = this.scrolling.height() / rowsToDisplay; // We need a reasonable bottom limit here, but resize will handle it // if we get too small if (this.rowHeight < 5 && this.div.is(':visible')) { if (this.rowHeight === 0) { // Something is not right... this.rowHeight = 5; } } // the hour rows var show = { 5: [0, 15, 30, 45], 10: [0, 30], 15: [0, 30], 45: [0, 15, 30, 45] }; var html = ''; var line_height = parseInt(this.div.css('line-height')); this._top_time = 0; for (var t = 0, i = 0; t < 1440; t += granularity, ++i) { if (t <= wd_start && t + granularity > wd_start) { this._top_time = this.rowHeight * (i + 1 + (wd_start - (t + granularity)) / granularity); } var working_hours = (t >= wd_start && t < wd_end) ? ' calendar_calWorkHours' : ''; html += '
'; // show time for full hours, always for 45min interval and at least on every 3 row // @ts-ignore var time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { hour: t / 60, minute: t % 60, seconds: 0, timezone: 0 }, { "ampm": (egw.preference("timeformat") === "12") }); var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; if (time_label && egw.preference("timeformat") == "12" && time_label.split(':')[0] < 10) { time_label = '  ' + time_label; } html += '
' + time_label + "
\n"; } // Set heights in pixels for scrolling jQuery('.calendar_calTimeLabels', this.scrolling) .empty() .height(this.rowHeight * i) .append(html); this.days.css('height', (this.rowHeight * i) + 'px'); this.gridHover.css('height', this.rowHeight); // Scroll to start of day this.scrolling.scrollTop(this._top_time); }; /** * As window size and number of all day non-blocking events change, we need * to re-scale the time grid to make sure the full working day is shown. * * We use a timeout to avoid doing it multiple times if redrawing or resizing. */ et2_calendar_timegrid.prototype.resizeTimes = function () { // Hide resizing from user this.loader.show(); // Wait a bit to see if anything else changes, then re-draw the times if (this.resize_timer) { window.clearTimeout(this.resize_timer); } // No point if it is just going to be redone completely if (this.update_timer) return; this.resize_timer = window.setTimeout(jQuery.proxy(function () { if (this._resizeTimes) { this.resize_timer = null; this._resizeTimes(); } }, this), 1); }; /** * Re-scale the time grid to make sure the full working day is shown. * This is the timeout callback that does the actual re-size immediately. */ et2_calendar_timegrid.prototype._resizeTimes = function () { if (!this.div.is(':visible')) { return; } var wd_start = 60 * this.options.day_start; var wd_end = 60 * this.options.day_end; var totalDisplayMinutes = wd_end - wd_start; var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / this.options.granularity); var row_count = (1440 / this.options.granularity); var new_height = this.scrolling.height() / rowsToDisplay; var old_height = this.rowHeight; this.rowHeight = new_height; jQuery('.calendar_calTimeLabels', this.scrolling).height(this.rowHeight * row_count); this.days.css('height', this.options.granularity === 0 ? '100%' : (this.rowHeight * row_count) + 'px'); // Scroll to start of day this._top_time = (wd_start * this.rowHeight) / this.options.granularity; // For some reason on Chrome & FF this doesn't quite get the day start // to the top, so add 2px; this.scrolling.scrollTop(this._top_time + 2); if (this.rowHeight != old_height) { this.iterateOver(function (child) { if (child === this) return; child.resize(); }, this, et2_IResizeable); } this.loader.hide(); }; /** * Set up the needed day widgets to correctly display the selected date * range. First we calculate the needed dates, then we create any needed * widgets. Existing widgets are recycled rather than discarded. */ et2_calendar_timegrid.prototype._drawDays = function () { this.scrolling.append(this.days); // If day list is still empty, recalculate it from start & end date if (this.day_list.length === 0 && this.options.start_date && this.options.end_date) { this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend); } // For a single day, we show each owner in their own daycol this.daily_owner = this.day_list.length === 1 && this.options.owner.length > 1 && this.options.owner.length < (parseInt('' + egw.preference('day_consolidate', 'calendar')) || 6); var daycols_needed = this.daily_owner ? this.options.owner.length : this.day_list.length; var day_width = (Math.min(jQuery(this.getInstanceManager().DOMContainer).width(), this.days.width()) / daycols_needed); if (!day_width || !this.day_list) { // Hidden on another tab, or no days for some reason var dim = egw.getHiddenDimensions(this.days, false); day_width = (dim.w / Math.max(daycols_needed, 1)); } // Create any needed widgets - otherwise, we'll just recycle // Add any needed day widgets (now showing more days) var add_index = 0; var before = true; while (daycols_needed > this.day_widgets.length) { var existing_index = this.day_widgets[add_index] && !this.daily_owner ? this.day_list.indexOf(this.day_widgets[add_index].options.date) : -1; before = existing_index > add_index; var day = et2_createWidget('calendar-daycol', { owner: this.options.owner, width: (before ? 0 : day_width) + "px" }, this); if (this.isInTree()) { day.doLoadingFinished(); } if (existing_index != -1 && parseInt(this.day_list[add_index]) < parseInt(this.day_list[existing_index])) { this.day_widgets.unshift(day); jQuery(this.getDOMNode(day)).prepend(day.getDOMNode(day)); } else { this.day_widgets.push(day); } add_index++; } // Remove any extra day widgets (now showing less) var delete_index = this.day_widgets.length - 1; before = false; while (this.day_widgets.length > daycols_needed) { // If we're going down to an existing one, just keep it for cool CSS animation while (delete_index > 1 && this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1) { delete_index--; before = true; } if (delete_index < 0) delete_index = 0; // Widgets that are before our date shrink, after just get pushed out if (before) { this.day_widgets[delete_index].set_width('0px'); } this.day_widgets[delete_index].div.hide(); this.day_widgets[delete_index].header.hide(); this.day_widgets[delete_index].destroy(); this.day_widgets.splice(delete_index--, 1); } this.set_header_classes(); // Create / update day widgets with dates and data for (var i = 0; i < this.day_widgets.length; i++) { day = this.day_widgets[i]; // Position day.set_left((day_width * i) + 'px'); day.title.removeClass('blue_title'); if (this.daily_owner) { // Each 'day' is the same date, different user day.set_id(this.day_list[0] + '-' + this.options.owner[i]); day.set_date(this.day_list[0], false); day.set_owner(this.options.owner[i]); day.set_label(this._get_owner_name(this.options.owner[i])); day.title.addClass('blue_title'); } else { // Show user name in day header even if only one if (this.day_list.length === 1) { day.set_label(this._get_owner_name(this.options.owner)); day.title.addClass('blue_title'); } else { // Go back to self-calculated date by clearing the label day.set_label(''); } day.set_id(this.day_list[i]); day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); day.set_owner(this.options.owner); } day.set_width(day_width + 'px'); } // Adjust and scroll to start of day this.resizeTimes(); // Don't hold on to value any longer, use the data cache for best info this.value = {}; if (this.daily_owner) { this.set_label(''); } // Handle not fully visible elements this._scroll(); // TODO: Figure out how to do this with detached nodes /* var nodes = this.day_col.getDetachedNodes(); var supportedAttrs = []; this.day_col.getDetachedAttributes(supportedAttrs); supportedAttrs.push("id"); for(var i = 0; i < day_count; i++) { this.day_col.setDetachedAttributes(nodes.clone(),) } */ }; /** * Set header classes * */ et2_calendar_timegrid.prototype.set_header_classes = function () { var day; for (var i = 0; i < this.day_widgets.length; i++) { day = this.day_widgets[i]; // Classes if (app.calendar && app.calendar.state && this.day_list[i] && parseInt(this.day_list[i].substr(4, 2)) !== new Date(app.calendar.state.date).getUTCMonth() + 1) { day.set_class('calendar_differentMonth'); } else { day.set_class(''); } } }; /** * Update UI while scrolling within the selected time * * Toggles out of view indicators and adjusts not visible headers * @param {Event} event Scroll event */ et2_calendar_timegrid.prototype._scroll = function (event) { if (!this.day_widgets) return; // Loop through days, let them deal with it for (var day = 0; day < this.day_widgets.length; day++) { this.day_widgets[day]._out_of_view(); } }; /** * Calculate a list of days between start and end date, skipping weekends if * desired. * * @param {Date|string} start_date Date that et2_date widget can understand * @param {Date|string} end_date Date that et2_date widget can understand * @param {boolean} show_weekend If not showing weekend, Saturday and Sunday * will not be in the returned list. * * @returns {string[]} List of days in Ymd format */ et2_calendar_timegrid.prototype._calculate_day_list = function (start_date, end_date, show_weekend) { var day_list = []; this.date_helper.set_value(end_date); var end = this.date_helper.date.getTime(); var i = 1; this.date_helper.set_value(new Date(start_date)); do { if (show_weekend || !show_weekend && [0, 6].indexOf(this.date_helper.date.getUTCDay()) === -1 || end_date === start_date) { day_list.push('' + this.date_helper.get_year() + sprintf('%02d', this.date_helper.get_month()) + sprintf('%02d', this.date_helper.get_date())); } this.date_helper.set_date(this.date_helper.get_date() + 1); } // Limit it to 14 days to avoid infinite loops in case something is mis-set, // though the limit is more based on how wide the screen is while (end >= this.date_helper.date.getTime() && i++ <= 14); return day_list; }; /** * Link the actions to the DOM nodes / widget bits. * * @param {object} actions {ID: {attributes..}+} map of egw action information */ et2_calendar_timegrid.prototype._link_actions = function (actions) { // Get the parent? Might be a grid row, might not. Either way, it is // just a container with no valid actions var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; var parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; if (!parent) { debugger; egw.debug('error', 'No parent objectManager found'); return; } // This binds into the egw action system. Most user interactions (drag to move, resize) // are handled internally using jQuery directly. var widget_object = this._actionObject || parent.getObjectById(this.id); var aoi = new et2_core_DOMWidget_1.et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); for (var i = 0; i < parent.children.length; i++) { var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); if (parent_finder.length > 0) { parent = parent.children[i]; break; } } // Determine if we allow a dropped event to use the invite/change actions var _invite_enabled = function (action, event, target) { var event = event.iface.getWidget(); var timegrid = target.iface.getWidget() || false; if (event === timegrid || !event || !timegrid || !event.options || !event.options.value.participants || !timegrid.options.owner) { return false; } var owner_match = false; var own_timegrid = event.getParent().getParent() === timegrid && !timegrid.daily_owner; for (var id in event.options.value.participants) { if (!timegrid.daily_owner) { if (timegrid.options.owner === id || timegrid.options.owner.indexOf && timegrid.options.owner.indexOf(id) >= 0) { owner_match = true; } } else { timegrid.iterateOver(function (col) { // Check scroll section or header section if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { owner_match = owner_match || col.options.owner.indexOf(id) !== -1; own_timegrid = (col === event.getParent()); } }, this, et2_calendar_daycol); } } var enabled = !owner_match && // Not inside its own timegrid !own_timegrid; widget_object.getActionLink('invite').enabled = enabled; widget_object.getActionLink('change_participant').enabled = enabled; // If invite or change participant are enabled, drag is not widget_object.getActionLink('egw_link_drop').enabled = !enabled; }; aoi.doTriggerEvent = function (_event, _data) { // Determine target node var event = _data.event || false; if (!event) return; if (_data.ui.draggable.hasClass('rowNoEdit')) return; /* We have to handle the drop in the normal event stream instead of waiting for the egwAction system so we can get the helper, and destination */ if (event.type === 'drop') { var dropEnd = false; var helper = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0]; if (helper && helper.dropEnd && helper.dropEnd.length >= 1) if (typeof this.dropEnd !== 'undefined' && this.dropEnd.length >= 1) { dropEnd = helper.dropEnd[0].dataset || false; } this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget(), event, _data.ui, dropEnd); } var drag_listener = function (_event, ui) { aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], ui.helper[0], 0); if (aoi.getWidget().daily_owner) { _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); } }; var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); switch (_event) { // Triggered once, when something is dragged into the timegrid's div case EGW_AI_DRAG_OVER: // Listen to the drag and update the helper with the time // This part lets us drag between different timegrids _data.ui.draggable.on('drag.et2_timegrid' + widget_object.id, drag_listener); _data.ui.draggable.on('dragend.et2_timegrid' + widget_object.id, function () { _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); }); // Remove formatting for out-of-view events (full day non-blocking) jQuery('.calendar_calEventHeader', _data.ui.helper).css('top', ''); jQuery('.calendar_calEventBody', _data.ui.helper).css('padding-top', ''); // Disable invite / change actions for same calendar or already participant var event = _data.ui.draggable.data('selected')[0]; if (!event || event.id && event.id.indexOf('calendar') !== 0) { event = false; } if (event) { _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); } if (time.length) { // The out will trigger after the over, so we count time.data('count', time.data('count') + 1); } else { _data.ui.helper.prepend('
'); } break; // Triggered once, when something is dragged out of the timegrid case EGW_AI_DRAG_OUT: // Stop listening _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); // Remove highlighted time square var timegrid = aoi.getWidget(); timegrid.gridHover.hide(); timegrid.scrolling.scrollTop(timegrid._top_time); // Out triggers after the over, count to not accidentally remove time.data('count', time.data('count') - 1); if (time.length && time.data('count') <= 0) { time.remove(); } break; } }; if (widget_object == null) { // Add a new container to the object manager which will hold the widget // objects widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager)); } else { widget_object.setAOI(aoi); } this._actionObject = widget_object; // Delete all old objects widget_object.clear(); widget_object.unregisterActions(); // Go over the widget & add links - this is where we decide which actions are // 'allowed' for this widget at this time var action_links = this._get_action_links(actions); this._init_links_dnd(widget_object.manager, action_links); widget_object.updateActionLinks(action_links); }; /** * Automatically add dnd support for linking * * @param {type} mgr * @param {type} actionLinks */ et2_calendar_timegrid.prototype._init_links_dnd = function (mgr, actionLinks) { if (this.options.readonly) return; var self = this; var drop_link = mgr.getActionById('egw_link_drop'); var drop_change_participant = mgr.getActionById('change_participant'); var drop_invite = mgr.getActionById('invite'); var drag_action = mgr.getActionById('egw_link_drag'); // Check if this app supports linking if (!egw.link_get_registry(this.dataStorePrefix, 'query') || egw.link_get_registry(this.dataStorePrefix, 'title')) { if (drop_link) { drop_link.remove(); if (actionLinks.indexOf(drop_link.id) >= 0) { actionLinks.splice(actionLinks.indexOf(drop_link.id), 1); } } if (drag_action) { drag_action.remove(); if (actionLinks.indexOf(drag_action.id) >= 0) { actionLinks.splice(actionLinks.indexOf(drag_action.id), 1); } } return; } // Don't re-add if (drop_link == null) { // Create the drop action that links entries drop_link = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function (action, source, target) { // Extract link IDs var links = []; var id = ''; for (var i = 0; i < source.length; i++) { // Check for no ID (invalid) or same manager (dragging an event) if (!source[i].id) continue; if (source[i].manager === target.manager) { // Find the timegrid, could have dropped on an event var timegrid = target.iface.getWidget(); while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { target = target.parent; timegrid = target.iface.getWidget(); } if (timegrid && timegrid._drop_data) { timegrid._event_drop.call(source[i].iface.getDOMNode(), timegrid, null, action.ui, timegrid._drop_data); } timegrid._drop_data = false; // Ok, stop. return false; } id = source[i].id.split('::'); links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); } if (links.length && target && target.iface.getWidget() && target.iface.getWidget().instanceOf(et2_calendar_event)) { // Link the entries egw.json(self.egw().getAppName() + ".etemplate_widget_link.ajax_link.etemplate", target.id.split('::').concat([links]), function (result) { if (result) { this.egw().message('Linked'); } }, self, true, self).sendRequest(); } else if (links.length) { // Get date and time var params = jQuery.extend({}, jQuery('.drop-hover[data-date]', target.iface.getDOMNode())[0].dataset || {}); // Add link IDs var app_registry = egw.link_get_registry('calendar'); params[app_registry.add_app] = []; params[app_registry.add_id] = []; for (var n in links) { params[app_registry.add_app].push(links[n].app); params[app_registry.add_id].push(links[n].id); } app.calendar.add(params); } }, true); drop_link.acceptedTypes = ['default', 'link']; drop_link.hideOnDisabled = true; // Create the drop action for moving events between calendars var invite_action = function (action, source, target) { // Extract link IDs var links = []; var id = ''; for (var i = 0; i < source.length; i++) { // Check for no ID (invalid) or same manager (dragging an event) if (!source[i].id) continue; if (source[i].manager === target.manager) { // Find the timegrid, could have dropped on an event var timegrid = target.iface.getWidget(); while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { target = target.parent; timegrid = target.iface.getWidget(); } // Leave the helper there until the update is done var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); // and add a loading icon so user knows something is happening if (jQuery('.calendar_timeDemo', loading).length == 0) { jQuery('.calendar_calEventHeader', loading).addClass('loading'); } else { jQuery('.calendar_timeDemo', loading).after('
'); } var event_data = egw.dataGetUIDdata(source[i].id).data; et2_calendar_event.recur_prompt(event_data, function (button_id) { if (button_id === 'cancel' || !button_id) { return; } var add_owner = jQuery.extend([], timegrid.options.owner); if (timegrid.daily_owner) { timegrid.iterateOver(function (col) { if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { add_owner = col.options.owner; } }, this, et2_calendar_daycol); } egw().json('calendar.calendar_uiforms.ajax_invite', [ button_id === 'series' ? event_data.id : event_data.app_id, add_owner, action.id === 'change_participant' ? jQuery.extend([], source[i].iface.getWidget().getParent().options.owner) : [] ], function () { loading.remove(); }).sendRequest(true); }); // Ok, stop. return false; } } }; drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action, true); drop_change_participant.acceptedTypes = ['calendar']; drop_change_participant.hideOnDisabled = true; drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action, true); drop_invite.acceptedTypes = ['calendar']; drop_invite.hideOnDisabled = true; } if (actionLinks.indexOf(drop_link.id) < 0) { actionLinks.push(drop_link.id); } actionLinks.push(drop_invite.id); actionLinks.push(drop_change_participant.id); // Don't re-add if (drag_action == null) { // Create drag action that allows linking drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function (action, selected) { // Drag helper - list titles. // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links // TODO: Need to decide if we need to create a customized helper interface for links anyway //return helper; return null; }, true); } // The timegrid itself is not draggable, so don't add a link. // The action is there for the children (events) to use if (false && actionLinks.indexOf(drag_action.id) < 0) { actionLinks.push(drag_action.id); } drag_action.set_dragType(['link', 'calendar']); }; /** * Get all action-links / id's of 1.-level actions from a given action object * * Here we are only interested in drop events. * * @param actions * @returns {Array} */ et2_calendar_timegrid.prototype._get_action_links = function (actions) { var action_links = []; // TODO: determine which actions are allowed without an action (empty actions) for (var i in actions) { var action = actions[i]; if (action.type == 'drop') { action_links.push(typeof action.id != 'undefined' ? action.id : i); } } return action_links; }; /** * Provide specific data to be displayed. * This is a way to set start and end dates, owner and event data in one call. * * Events will be retrieved automatically from the egw.data cache, so there * is no great need to provide them. * * @param {Object[]} events Array of events, indexed by date in Ymd format: * { * 20150501: [...], * 20150502: [...] * } * Days should be in order. * {string|number|Date} events.start_date - New start date * {string|number|Date} events.end_date - New end date * {number|number[]|string|string[]} event.owner - Owner ID, which can * be an account ID, a resource ID (as defined in calendar_bo, not * necessarily an entry from the resource app), or a list containing a * combination of both. */ et2_calendar_timegrid.prototype.set_value = function (events) { if (typeof events !== 'object') return false; var use_days_sent = true; if (events.start_date) { use_days_sent = false; } if (events.end_date) { use_days_sent = false; } _super.prototype.set_value.call(this, events); if (use_days_sent) { var day_list = Object.keys(events); if (day_list.length) { this.set_start_date(day_list[0]); this.set_end_date(day_list[day_list.length - 1]); } // Sub widgets actually get their own data from egw.data, so we'll // stick it there var consolidated = et2_widget_view_1.et2_calendar_view.is_consolidated(this.options.owner, this.day_list.length == 1 ? 'day' : 'week'); for (var day in events) { var day_list_1 = []; for (var i = 0; i < events[day].length; i++) { day_list_1.push(events[day][i].row_id); egw.dataStoreUID('calendar::' + events[day][i].row_id, events[day][i]); } // Might be split by user, so we have to check that too for (var i = 0; i < this.options.owner.length; i++) { var owner = consolidated ? this.options.owner : this.options.owner[i]; var day_id = CalendarApp._daywise_cache_id(day, owner); egw.dataStoreUID(day_id, day_list_1); if (consolidated) break; } } } // Reset and calculate instead of just use the keys so we can get the weekend preference this.day_list = []; // None of the above changed anything, hide the loader if (!this.update_timer) { window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this), 200); } }; /** * Set which user owns this. Owner is passed along to the individual * days. * * @param {number|number[]} _owner Account ID * @returns {undefined} */ et2_calendar_timegrid.prototype.set_owner = function (_owner) { var old = this.options.owner || 0; _super.prototype.set_owner.call(this, _owner); this.owner.set_label(''); this.div.removeClass('calendar_TimeGridNoLabel'); // Check to see if it's our own calendar, with just us showing if (typeof _owner == 'object' && _owner.length == 1) { var rowCount = 0; this.getParent().iterateOver(function (widget) { if (!widget.disabled) rowCount++; }, this, et2_calendar_timegrid); // Just us, show week number if (rowCount == 1 && _owner.length == 1 && _owner[0] == egw.user('account_id') || rowCount != 1) _owner = false; } var day_count = this.day_list.length ? this.day_list.length : this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend).length; // @ts-ignore if (typeof _owner == 'string' && isNaN(_owner)) { this.set_label(''); this.owner.set_value(this._get_owner_name(_owner)); // Label is empty, but give extra space for the owner name this.div.removeClass('calendar_TimeGridNoLabel'); } else if (!_owner || typeof _owner == 'object' && _owner.length > 1 || // Single owner, single day _owner.length === 1 && day_count === 1) { // Don't show owners if more than one, show week number this.owner.set_value(''); if (this.options.start_date) { this.set_label(egw.lang('wk') + ' ' + (app.calendar ? app.calendar.date.week_number(this.options.start_date) : '')); } } else { this.owner.options.application = 'api-accounts'; this.owner.set_value(this._get_owner_name(_owner)); this.set_label(''); jQuery(this.getDOMNode(this.owner)).prepend(this.owner.getDOMNode()); } if (this.isAttached() && (typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || // Array of ids will not compare as equal ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || // Strings typeof old === 'string' && '' + old !== '' + this.options.owner)) { this.invalidate(true); } }; /** * Set a label for this week * * May conflict with owner, which is displayed when there's only one owner. * * @param {string} label */ et2_calendar_timegrid.prototype.set_label = function (label) { this.options.label = label; this._labelContainer.html(label); this.gridHeader.prepend(this._labelContainer); // If it's a short label (eg week number), don't give it an extra line // but is empty, but give extra space for a single owner name this.div.toggleClass('calendar_TimeGridNoLabel', label.trim().length > 0 && label.trim().length <= 6 || this.options.owner.length > 1); }; /** * Set how big the time divisions are * * Setting granularity to 0 will remove the time divisions and display * each days events in a list style. This 'gridlist' is not to be confused * with the list view, which uses a nextmatch. * * @param {number} minutes */ et2_calendar_timegrid.prototype.set_granularity = function (minutes) { // Avoid < 0 minutes = Math.max(0, minutes); if (this.options.granularity !== minutes) { if (this.options.granularity === 0 || minutes === 0) { this.options.granularity = minutes; // Need to re-do a bunch to make sure this is propagated this.invalidate(); } else { this.options.granularity = minutes; this._drawTimes(); } } else if (!this.update_timer) { this.resizeTimes(); } }; /** * Turn on or off the visibility of weekends * * @param {boolean} weekends */ et2_calendar_timegrid.prototype.set_show_weekend = function (weekends) { weekends = weekends ? true : false; if (this.options.show_weekend !== weekends) { this.options.show_weekend = weekends; if (this.isAttached()) { this.invalidate(); } } }; /** * Call change handler, if set */ et2_calendar_timegrid.prototype.change = function () { if (this.onchange) { if (typeof this.onchange == 'function') { // Make sure function gets a reference to the widget var args = Array.prototype.slice.call(arguments); if (args.indexOf(this) == -1) args.push(this); return this.onchange.apply(this, args); } else { return (et2_compileLegacyJS(this.options.onchange, this, _node))(); } } }; /** * Call event change handler, if set * * @param {type} event * @param {type} dom_node */ et2_calendar_timegrid.prototype.event_change = function (event, dom_node) { if (this.onevent_change) { var event_data = this._get_event_info(dom_node); var event_widget = this.getWidgetById(event_data.widget_id); et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function (button_id, event_data) { // No need to continue if (button_id === 'cancel') return false; if (typeof this.onevent_change == 'function') { // Make sure function gets a reference to the widget var args = Array.prototype.slice.call(arguments); if (args.indexOf(event_widget) == -1) args.push(event_widget); // Put button ID in event event.button_id = button_id; return this.onevent_change.apply(this, [event, event_widget, button_id]); } else { return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); } }, this)); } return false; }; et2_calendar_timegrid.prototype.get_granularity = function () { // get option, or user's preference if (typeof this.options.granularity === 'undefined') { this.options.granularity = egw.preference('interval', 'calendar') || 30; } return parseInt(this.options.granularity); }; /** * Click handler calling custom handler set via onclick attribute to this.onclick * * This also handles all its own actions, including navigation. If there is * an event associated with the click, it will be found and passed to the * onclick function. * * @param {Event} _ev * @returns {boolean} Continue processing event (true) or stop (false) */ et2_calendar_timegrid.prototype.click = function (_ev) { var result = true; if (this.options.readonly) return; // Drag to create in progress if (this.drag_create.start !== null) return; // Is this click in the event stuff, or in the header? if (_ev.target.dataset.id || jQuery(_ev.target).parents('.calendar_calEvent').length) { // Event came from inside, maybe a calendar event var event = this._get_event_info(_ev.originalEvent.target); if (typeof this.onclick == 'function') { // Make sure function gets a reference to the widget, splice it in as 2. argument if not var args = Array.prototype.slice.call(arguments); if (args.indexOf(this) == -1) args.splice(1, 0, this); result = this.onclick.apply(this, args); } var event_node = jQuery(event.event_node); if (event.id && result && !this.disabled && !this.options.readonly && // Permissions - opening will fail if we try event_node && !(event_node.hasClass('rowNoView'))) { if (event.widget_id && this.getWidgetById(event.widget_id)) { this.getWidgetById(event.widget_id).recur_prompt(); } else { et2_calendar_event.recur_prompt(event); } return false; } return result; } else if (this.gridHeader.is(_ev.target) && _ev.target.dataset || this._labelContainer.is(_ev.target) && this.gridHeader[0].dataset) { app.calendar.update_state(jQuery.extend({ view: 'week' }, this._labelContainer.is(_ev.target) ? this.gridHeader[0].dataset : _ev.target.dataset)); } else if (this.options.owner.length === 1 && jQuery(this.owner.getDOMNode()).is(_ev.target)) { // Click on the owner in header, show just that owner app.calendar.update_state({ owner: this.options.owner }); } else if (this.dayHeader.has(_ev.target).length) { // Click on a day header - let day deal with it // First child is a selectAccount for (var i = 1; i < this._children.length; i++) { if (this._children[i].header && (this._children[i].header.has(_ev.target).length || this._children[i].header.is(_ev.target))) { return this._children[i].click(_ev); } } } // No time grid, click on a day else if (this.options.granularity === 0 && (jQuery(_ev.target).hasClass('event_wrapper') || jQuery(_ev.target).hasClass('.calendar_calDayCol'))) { // Default handler to open a new event at the selected time var target = jQuery(_ev.target).hasClass('event_wrapper') ? _ev.target.parentNode : _ev.target; var options = { date: target.dataset.date || this.options.date, hour: target.dataset.hour || this._parent.options.day_start, minute: target.dataset.minute || 0, owner: this.options.owner }; app.calendar.add(options); return false; } }; /** * Mousedown handler to support drag to create * * @param {jQuery.Event} event */ et2_calendar_timegrid.prototype._mouse_down = function (event) { if (event.which !== 1) return; if (this.options.readonly) return; var start = jQuery.extend({}, this.gridHover[0].dataset); if (start.date) { // Set parent for event if (this.daily_owner) { // Each 'day' is the same date, different user // Find the correct row so we know the parent var col = event.target.closest('.calendar_calDayCol'); for (var i = 0; i < this._children.length && col; i++) { if (this._children[i].node === col) { this.drag_create.parent = this._children[i]; break; } } } else { this.drag_create.parent = this.getWidgetById(start.date); } // Format date this.date_helper.set_year(start.date.substring(0, 4)); this.date_helper.set_month(start.date.substring(4, 6)); this.date_helper.set_date(start.date.substring(6, 8)); if (start.hour) { this.date_helper.set_hours(start.hour); } if (start.minute) { this.date_helper.set_minutes(start.minute); } start.date = this.date_helper.get_value(); this.gridHover.css('cursor', 'ns-resize'); // Start update var timegrid = this; this.div.on('mousemove.dragcreate', function () { if (timegrid.drag_create.event && timegrid.drag_create.parent && timegrid.drag_create.end) { var end = jQuery.extend({}, timegrid.gridHover[0].dataset); if (end.date) { timegrid.date_helper.set_year(end.date.substring(0, 4)); timegrid.date_helper.set_month(end.date.substring(4, 6)); timegrid.date_helper.set_date(end.date.substring(6, 8)); if (end.hour) { timegrid.date_helper.set_hours(end.hour); } if (end.minute) { timegrid.date_helper.set_minutes(end.minute); } timegrid.drag_create.end.date = timegrid.date_helper.get_value(); } try { timegrid._drag_update_event(); } catch (e) { timegrid._drag_create_end(); } } }); } return this._drag_create_start(start); }; /** * Mouseup handler to support drag to create * * @param {jQuery.Event} event */ et2_calendar_timegrid.prototype._mouse_up = function (event) { if (this.options.readonly) return; var end = jQuery.extend({}, this.gridHover[0].dataset); if (end.date) { this.date_helper.set_year(end.date.substring(0, 4)); this.date_helper.set_month(end.date.substring(4, 6)); this.date_helper.set_date(end.date.substring(6, 8)); if (end.hour) { this.date_helper.set_hours(end.hour); } if (end.minute) { this.date_helper.set_minutes(end.minute); } end.date = this.date_helper.get_value(); } this.div.off('mousemove.dragcreate'); this.gridHover.css('cursor', ''); return this._drag_create_end(end); }; /** * Get time from position for drag and drop * * This does not return an actual time on a clock, but finds the closest * time node (.calendar_calAddEvent or day column) to the given position. * * @param {number} x * @param {number} y * @returns {DOMNode[]} time node(s) for the given position */ et2_calendar_timegrid.prototype._get_time_from_position = function (x, y) { x = Math.round(x); y = Math.round(y); var path = []; var day = null; var time = null; var node = document.elementFromPoint(x, y); var $node = jQuery(node); // Ignore high level & non-time (grid itself, header parent & week label) if ([this.node, this.gridHeader[0], this._labelContainer[0]].indexOf(node) !== -1 || // Day labels this.gridHeader.has(node).length && !$node.hasClass("calendar_calDayColAllDay") && !$node.hasClass('calendar_calDayColHeader')) { return []; } for (var id in this.gridHover[0].dataset) { delete this.gridHover[0].dataset[id]; } if (this.options.granularity == 0) { this.gridHover.css('height', ''); } while (node && node != this.node && node.tagName != 'BODY' && path.length < 10) { path.push(node); node.style.display = 'none'; $node = jQuery(node); if ($node.hasClass('calendar_calDayColHeader')) { for (var id in node.dataset) { this.gridHover[0].dataset[id] = node.dataset[id]; } this.gridHover.css({ position: 'absolute', top: '', bottom: '0px', // Use 100% height if we're hiding the day labels to avoid // any remaining space from the hidden labels height: $node.height() > parseInt($node.css('line-height')) ? $node.css('padding-bottom') : '100%' }); day = node; this.gridHover .attr('data-non_blocking', 'true'); break; } if ($node.hasClass('calendar_calDayCol')) { day = node; this.gridHover .attr('data-date', day.dataset.date); } if ($node.hasClass('calendar_calTimeRowTime')) { time = node; this.gridHover .attr('data-hour', time.dataset.hour) .attr('data-minute', time.dataset.minute); break; } node = document.elementFromPoint(x, y); } for (var i = 0; i < path.length; i++) { path[i].style.display = ''; } if (!day) { return []; } this.gridHover .show() .appendTo(day); if (time) { this.gridHover .height(this.rowHeight) .position({ my: 'left top', at: 'left top', of: time }); } this.gridHover.css('left', ''); return this.gridHover; }; /** * Code for implementing et2_IDetachedDOM * * @param {array} _attrs array to add further attributes to */ et2_calendar_timegrid.prototype.getDetachedAttributes = function (_attrs) { _attrs.push('start_date', 'end_date'); }; et2_calendar_timegrid.prototype.getDetachedNodes = function () { return [this.getDOMNode(this)]; }; et2_calendar_timegrid.prototype.setDetachedAttributes = function (_nodes, _values) { this.div = jQuery(_nodes[0]); if (_values.start_date) { this.set_start_date(_values.start_date); } if (_values.end_date) { this.set_end_date(_values.end_date); } }; // Resizable interface /** * @param {boolean} [_too_small=null] Force the widget to act as if it was too small */ et2_calendar_timegrid.prototype.resize = function (_too_small) { if (this.disabled || !this.div.is(':visible')) { return; } /* We expect the timegrid to be in a table with 0 or more other timegrids, 1 per row. We want each timegrid to be as large as possible, but space shared equally. Height can't be set to a percentage on the rows, because that doesn't work. However, if any timegrid is too small (1/2 hour < 1 line height), we change to showing only the working hours with no vertical scrollbar. Each week gets as much space as it needs, and all scroll together. */ // How many rows? var rowCount = 0; this.getParent().iterateOver(function (widget) { if (!widget.disabled) rowCount++; }, this, et2_calendar_timegrid); // Take the whole tab height, or home portlet if (this.getInstanceManager().app === 'home') { var height = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last().parent().innerHeight(); // Allow for portlet header height -= jQuery('.ui-widget-header', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); } else { var height = jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight(); // Allow for toolbar height -= jQuery('#calendar-toolbar', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); } this.options.height = Math.floor(height / rowCount); // Allow for borders & padding this.options.height -= 2 * ((this.div.outerWidth(true) - this.div.innerWidth()) + parseInt(this.div.parent().css('padding-top'))); // Calculate how much space is needed, and // if too small be bigger var needed = ((this.day_end - this.day_start) / (this.options.granularity / 60) * parseInt(this.div.css('line-height'))) + this.gridHeader.outerHeight(); var too_small = needed > this.options.height && this.options.granularity != 0; if (this.getInstanceManager().app === 'home') { var modify_node = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last(); } else { var modify_node = jQuery(this.getInstanceManager().DOMContainer); } modify_node .css({ 'overflow-y': too_small || _too_small ? 'auto' : 'hidden', 'overflow-x': 'hidden', 'height': too_small || _too_small ? height : '100%' }); if (too_small || _too_small) { this.options.height = Math.max(this.options.height, needed); // Set all others to match if (!_too_small && rowCount > 1 && this.getParent()) { window.setTimeout(jQuery.proxy(function () { if (!this._parent) return; this._parent.iterateOver(function (widget) { if (!widget.disabled) widget.resize(true); }, this, et2_calendar_timegrid); }, this), 1); return; } this.div.addClass('calendar_calTimeGridFixed'); } else { this.div.removeClass('calendar_calTimeGridFixed'); } this.div.css('height', this.options.height); // Re-do time grid if (!this.update_timer) { this.resizeTimes(); } // Try to resize width, though animations cause problems var total_width = modify_node.parent().innerWidth() - this.days.position().left; // Space for todos, if there total_width -= jQuery(this.getInstanceManager().DOMContainer).siblings().has(':visible').not('#calendar-toolbar').outerWidth(); var day_width = (total_width > 0 ? total_width : modify_node.width()) / this.day_widgets.length; // update day widgets for (var i = 0; i < this.day_widgets.length; i++) { var day = this.day_widgets[i]; // Position day.set_left((day_width * i) + 'px'); day.set_width(day_width + 'px'); } }; /** * Set up for printing * * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up * (waiting for data) */ et2_calendar_timegrid.prototype.beforePrint = function () { if (this.disabled || !this.div.is(':visible')) { return; } var height_check = this.div.height(); this.div.css('max-height', '17cm'); if (this.div.height() != height_check) { this.div.height('17cm'); this._resizeTimes(); } // update day widgets, if not on single day view // // TODO: Find out why don't we update single day view // Let the single day view participate in print calculation. if (this.day_widgets.length > 0) { var day_width = (100 / this.day_widgets.length); for (var i = 0; i < this.day_widgets.length; i++) { var day = this.day_widgets[i]; // Position day.set_left((i * day_width) + '%'); day.set_width(day_width + '%'); // For some reason the column's method does not set it correctly in Chrome day.header[0].style.width = day_width + '%'; } } // Stop Firefox from scrolling the day to the top - this would break printing in Chrome if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { var height = this.scrolling.scrollTop() + this.scrolling.height(); this.scrolling // Disable scroll event, or it will recalculate out of view events .off('scroll') // Explicitly transform to the correct place .css({ 'transform': 'translateY(-' + this.scrolling.scrollTop() + 'px)', 'margin-bottom': '-' + this.scrolling.scrollTop() + 'px', 'height': height + 'px' }); this.div.css({ 'height': '', 'max-height': '' }); } }; /** * Reset after printing */ et2_calendar_timegrid.prototype.afterPrint = function () { this.div.css('maxHeight', ''); this.scrolling.children().css({ 'transform': '', 'overflow': '' }); this.div.height(this.options.height); if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { this._resizeTimes(); this.scrolling // Re-enable out-of-view formatting on scroll .on('scroll', jQuery.proxy(this._scroll, this)) // Remove translation .css({ 'transform': '', 'margin-bottom': '' }); } }; et2_calendar_timegrid._attributes = { value: { type: "any", description: "An array of events, indexed by date (Ymd format)." }, day_start: { name: "Day start time", type: "string", default: parseInt('' + egw.preference('workdaystarts', 'calendar')) || 9, description: "Work day start time. If unset, this will default to the current user's preference" }, day_end: { name: "Day end time", type: "string", default: parseInt('' + egw.preference('workdayends', 'calendar')) || 17, description: "Work day end time. If unset, this will default to the current user's preference" }, show_weekend: { name: "Weekends", type: "boolean", // @ts-ignore default: egw.preference('days_in_weekview', 'calendar') != 5, description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." }, granularity: { name: "Granularity", type: "integer", default: parseInt('' + egw.preference('interval', 'calendar')) || 30, description: "How many minutes per row, or 0 to display events as a list" }, "onchange": { "name": "onchange", "type": "js", "default": et2_no_init, "description": "JS code which is executed when the date range changes." }, "onevent_change": { "name": "onevent_change", "type": "js", "default": et2_no_init, "description": "JS code which is executed when an event changes." }, height: { "default": '100%' } }; return et2_calendar_timegrid; }(et2_widget_view_1.et2_calendar_view)); exports.et2_calendar_timegrid = et2_calendar_timegrid; et2_core_widget_1.et2_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); //# sourceMappingURL=et2_widget_timegrid.js.map