/* * Egroupware * * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package calendar * @subpackage etemplate * @link https://www.egroupware.org * @author Nathan Gray */ /*egw:uses /calendar/js/et2_widget_view.js; /calendar/js/et2_widget_daycol.js; /calendar/js/et2_widget_event.js; */ import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget"; import {et2_valueWidget} from "../../api/js/etemplate/et2_core_valueWidget"; import {ClassWithAttributes} from "../../api/js/etemplate/et2_core_inheritance"; import {et2_action_object_impl} from "../../api/js/etemplate/et2_core_DOMWidget"; import {et2_calendar_planner} from "./et2_widget_planner"; import {egw_getObjectManager, egwActionObject} from "../../api/js/egw_action/egw_action"; import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT} from "../../api/js/egw_action/egw_action_constants"; import {et2_IResizeable} from "../../api/js/etemplate/et2_core_interfaces"; import {egw} from "../../api/js/jsapi/egw_global"; import {et2_calendar_view} from "./et2_widget_view"; /** * Class for one row of a planner * * This widget is responsible for the label on the side * */ export class et2_calendar_planner_row extends et2_valueWidget implements et2_IResizeable { static readonly _attributes: any = { start_date: { name: "Start date", type: "any" }, end_date: { name: "End date", type: "any" }, value: { type: "any" } }; private div: JQuery; private title: JQuery; private rows: JQuery; private _cached_rows: any[]; private _row_height = 20; private _actionObject: egwActionObject; /** * Constructor */ constructor(_parent, _attrs? : WidgetConfig, _child? : object) { // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_planner_row._attributes, _child || {})); // Main container this.div = jQuery(document.createElement("div")) .addClass("calendar_plannerRowWidget") .css('width',this.options.width); this.title = jQuery(document.createElement('div')) .addClass("calendar_plannerRowHeader") .appendTo(this.div); this.rows = jQuery(document.createElement('div')) .addClass("calendar_eventRows") .appendTo(this.div); this.setDOMNode(this.div[0]); this.set_start_date(this.options.start_date); this.set_end_date(this.options.end_date); this._cached_rows = []; } doLoadingFinished( ) { super.doLoadingFinished(); this.set_label(this.options.label); this._draw(); // Actions are set on the parent, so we need to explicitly get in here // and get ours this._link_actions(this.getParent().options.actions || []); return true; } destroy( ) { super.destroy(); } getDOMNode(_sender) { if(_sender === this || !_sender) { return this.div[0]; } if(_sender._parent === this) { return this.rows[0]; } } /** * Link the actions to the DOM nodes / widget bits. * * @param {object} actions {ID: {attributes..}+} map of egw action information */ _link_actions(actions) { // Get the parent? Might be a grid row, might not. Either way, it is // just a container with no valid actions let objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager; let parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; if(!parent) { 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. let widget_object = this._actionObject || parent.getObjectById(this.id); const aoi = new et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); const planner = this.getParent(); for(let i = 0; i < parent.children.length; i++) { const 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 const _invite_enabled = function (action, event, target) { var event = event.iface.getWidget(); const row = target.iface.getWidget() || false; if(event === row || !event || !row || !row.node || !event.options || !event.options.value.participants ) { return false; } let owner_match = false; const own_row = event.getParent() === row; for (let id in event.options.value.participants) { owner_match = owner_match || (row.node.dataset?.participants ?? false) === '' + id; } const enabled = !owner_match && // Not inside its own timegrid !own_row; 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.classList.contains('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' && widget_object.getActionLink('egw_link_drop').enabled) { const helper = document.body.querySelector(".calendar_d-n-d_helper"); const planner = this.getWidget().getParent(); planner._event_drop.call( helper, planner, event, _data.ui, this.getWidget() ); planner._drop_data = false; } const drag_listener = function(_event) { _event.preventDefault(); let position = {}; if(planner.options.group_by === 'month') { position = {left: _event.clientX, top: _event.clientY}; } else { let style = getComputedStyle(_data.ui.helper); position = { top: parseInt(style.top || "0"), left: _event.clientX - jQuery(this).parent().offset().left } } var event = _data.ui.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 ); } }; const 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_ENTER: // Listen to the drag and update the helper with the time // This part lets us drag between different timegrids planner.div.on('dragover.et2_timegrid_row' + widget_object.id, drag_listener); planner.div.on('dragend.et2_timegrid_row' + widget_object.id, function() { jQuery(_data.ui.draggable).off('drag.et2_timegrid_row' + widget_object.id); planner.div.removeClass(["drop-hover", "et2-dropzone"]); // Remove helper document.body.querySelectorAll(".calendar_d-n-d_helper").forEach(n => n.remove()); }); widget_object.iface.getWidget().div.addClass('drop-hover'); // Disable invite / change actions for same calendar or already participant let event = _data.ui.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); } break; // Triggered once, when something is dragged out of the timegrid case EGW_AI_DRAG_OUT: // Stop listening jQuery(_data.ui.draggable).off('drag.et2_timegrid_row' + widget_object.id); // Remove highlight widget_object.iface.getWidget().div.removeClass('drop-hover'); // 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 const action_links = this._get_action_links(actions); this.getParent()._init_links_dnd(widget_object.manager, action_links); widget_object.updateActionLinks(action_links); } /** * 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} */ _get_action_links(actions) { const action_links = []; // Only these actions are allowed without a selection (empty actions) const empty_actions = ['add']; for(let i in actions) { const action = actions[i]; if(empty_actions.indexOf(action.id) !== -1 || action.type == 'drop') { action_links.push(typeof action.id != 'undefined' ? action.id : i); } } return action_links; } /** * Draw the individual divs for weekends and events */ _draw( ) { // Remove any existing this.rows.remove('.calendar_eventRowsMarkedDay,.calendar_eventRowsFiller').nextAll().remove(); let days = 31; let width = '100'; if (this.getParent().options.group_by === 'month') { days = this.options.end_date.getUTCDate(); if(days < 31) { const diff = 31 - days; width = 'calc('+(diff * 3.23) + '% - ' + (diff * 7) + 'px)'; } } // mark weekends and other special days in yearly planner if (this.getParent().options.group_by == 'month') { this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days)); } if (this.getParent().options.group_by === 'month' && days < 31) { // add a filler for non existing days in that month this.rows.after('
'); } } set_label(label) { this.options.label = label; this.title.text(label); if(this.getParent().options.group_by === 'month') { this.title.attr('data-date', this.options.start_date.toJSON()); this.title.attr('data-sortby', 'user'); this.title.addClass('et2_clickable et2_link'); } else { this.title.attr('data-date',''); this.title.removeClass('et2_clickable'); } } /** * Change the start date * * @param {Date} new_date New end date * @returns {undefined} */ set_start_date(new_date) { if(!new_date || new_date === null) { throw new TypeError('Invalid end date. ' + new_date.toString()); } this.options.start_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); this.options.start_date.setUTCHours(0); this.options.start_date.setUTCMinutes(0); this.options.start_date.setUTCSeconds(0); } /** * Change the end date * * @param {string|number|Date} new_date New end date * @returns {undefined} */ set_end_date(new_date) { if(!new_date || new_date === null) { throw new TypeError('Invalid end date. ' + new_date.toString()); } this.options.end_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); this.options.end_date.setUTCHours(23); this.options.end_date.setUTCMinutes(59); this.options.end_date.setUTCSeconds(59); } /** * Mark special days (birthdays, holidays) on the planner * * @param {Date} start Start of the month * @param {number} days How many days in the month */ _yearlyPlannerMarkDays(start,days) { const day_width = 3.23; const t = new Date(start); let content = ''; for(let i = 0; i < days; i++) { const holidays = []; // TODO: implement this, pull / copy data from et2_widget_timegrid const day_class = (