mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 19:03:14 +01:00
f430b66d3b
classes are now uppercase and in their own files. lowercase classes are deprecated.
Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
(cherry picked from commit 5e3c67a5cf
)
2515 lines
71 KiB
TypeScript
2515 lines
71 KiB
TypeScript
/*
|
|
* Egroupware Calendar timegrid
|
|
*
|
|
* @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;
|
|
*/
|
|
|
|
import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget";
|
|
import {ClassWithAttributes} from "../../api/js/etemplate/et2_core_inheritance";
|
|
import {et2_calendar_view} from "./et2_widget_view";
|
|
import {et2_action_object_impl} from "../../api/js/etemplate/et2_core_DOMWidget";
|
|
import {et2_dataview_grid} from "../../api/js/etemplate/et2_dataview_view_grid";
|
|
import {et2_calendar_daycol} from "./et2_widget_daycol";
|
|
import {egw} from "../../api/js/jsapi/egw_global";
|
|
import {et2_no_init} from "../../api/js/etemplate/et2_core_common";
|
|
import {et2_IDetachedDOM, et2_IPrint, et2_IResizeable} from "../../api/js/etemplate/et2_core_interfaces";
|
|
import {et2_calendar_event} from "./et2_widget_event";
|
|
import {egw_getObjectManager, egwActionObject} from "../../api/js/egw_action/egw_action";
|
|
import {et2_compileLegacyJS} from "../../api/js/etemplate/et2_core_legacyJSFunctions";
|
|
import {Et2Dialog} from "../../api/js/etemplate/Et2Dialog/Et2Dialog";
|
|
import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT} from "../../api/js/egw_action/egw_action_constants";
|
|
import {formatDate, formatTime, parseTime} from "../../api/js/etemplate/Et2Date/Et2Date";
|
|
import interact from "@interactjs/interactjs/index";
|
|
import type {InteractEvent} from "@interactjs/core/InteractEvent";
|
|
import {CalendarApp} from "./app";
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export class et2_calendar_timegrid extends et2_calendar_view implements et2_IDetachedDOM, et2_IResizeable,et2_IPrint
|
|
{
|
|
static readonly _attributes : any = {
|
|
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%'
|
|
}
|
|
};
|
|
private gridHeader: JQuery;
|
|
private dayHeader: JQuery;
|
|
private scrolling: JQuery;
|
|
private days: JQuery;
|
|
private owner: any;
|
|
private gridHover: JQuery;
|
|
|
|
private day_list: any[];
|
|
private day_widgets: any[];
|
|
private resize_timer: number;
|
|
private _top_time: number;
|
|
private rowHeight: number;
|
|
private daily_owner: boolean = false;
|
|
private _drop_data: any;
|
|
private day_start: any;
|
|
private day_end: any;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_calendar_timegrid
|
|
*/
|
|
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
|
|
{
|
|
// Call the inherited constructor
|
|
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_timegrid._attributes, _child || {}));
|
|
|
|
|
|
// 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('<div class="calendar_calTimeLabels"></div>');
|
|
|
|
// 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('<div style="height:5px;" class="calendar_calAddEvent drop-hover">');
|
|
|
|
// 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]);
|
|
}
|
|
|
|
destroy( )
|
|
{
|
|
// Stop listening to tab changes
|
|
if(typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab)
|
|
{
|
|
jQuery(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id);
|
|
}
|
|
|
|
super.destroy();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
doLoadingFinished( )
|
|
{
|
|
super.doLoadingFinished();
|
|
|
|
// 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
|
|
const event_info = timegrid._get_event_info(this);
|
|
if(this.classList.contains("resizing") || event_info.whole_day === "true")
|
|
{
|
|
// Currently already resizing
|
|
return;
|
|
}
|
|
|
|
//Resizable event handler
|
|
interact(this).resizable
|
|
({
|
|
distance: 10,
|
|
invert: "reposition",
|
|
edges: {bottom: true},
|
|
startAxis: "y",
|
|
lockAxis: "y",
|
|
containment: 'parent',
|
|
modifiers: [
|
|
interact.modifiers.snapSize({
|
|
targets: [interact.createSnapGrid({width: 10, height: timegrid.rowHeight})]
|
|
})
|
|
],
|
|
|
|
/**
|
|
* 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 {InteractEvent} event
|
|
*/
|
|
onstart: function(event)
|
|
{
|
|
if(timegrid.drag_create.start)
|
|
{
|
|
// Abort drag to create, we're dragging to resize
|
|
timegrid._drag_create_end({});
|
|
}
|
|
event.target.classList.add("resizing");
|
|
},
|
|
|
|
/**
|
|
* Triggered at the end of resizing the calEvent.
|
|
*
|
|
* @param {InteractEvent} event
|
|
*/
|
|
onend: function(event)
|
|
{
|
|
// Remove for re-creation...
|
|
interact(this).unset();
|
|
event.target.classList.remove("resizing");
|
|
|
|
var e = new jQuery.Event('change');
|
|
e.originalEvent = event;
|
|
e.data = {duration: 0};
|
|
var event_data = timegrid._get_event_info(this);
|
|
var event_widget = <et2_calendar_event>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);
|
|
|
|
}
|
|
// 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');
|
|
}.bind(this),
|
|
|
|
/**
|
|
* Triggered during the resize, on the drag of the resize handler
|
|
*
|
|
* @param {InteractEvent} event
|
|
*/
|
|
onmove: function(event)
|
|
{
|
|
event.target.style.height = event.rect.height + "px";
|
|
// Add a bit for better understanding - it will show _to_ the start,
|
|
// covering the 'actual' target
|
|
timegrid._get_time_from_position(event.target.getBoundingClientRect().left, event.target.getBoundingClientRect().bottom + 5);
|
|
timegrid.gridHover.hide();
|
|
var drop = timegrid._drag_helper(this, event.target);
|
|
if(drop && !drop.is(':visible'))
|
|
{
|
|
drop.get(0).scrollIntoView(false);
|
|
}
|
|
}.bind(this)
|
|
});
|
|
});
|
|
|
|
// Customize and override some draggable settings
|
|
this.div
|
|
.on('dragstart', '.calendar_calEvent', function(event)
|
|
{
|
|
// Cancel drag to create, we're dragging an existing event
|
|
timegrid.drag_create.start = null;
|
|
timegrid._drag_create_end();
|
|
timegrid._get_time_from_position(event.clientX, event.clientY);
|
|
})
|
|
.on("dragend", () =>
|
|
{
|
|
timegrid.div.off("drag.timegrid");
|
|
})
|
|
.on('dragover', function(event)
|
|
{
|
|
timegrid._get_time_from_position(event.clientX, event.clientY);
|
|
})
|
|
.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();
|
|
}
|
|
});
|
|
|
|
this.div.get(0).addEventListener("mousedown", this._mouse_down.bind(this));
|
|
this.div.get(0).addEventListener("mouseup", this._mouse_up.bind(this));
|
|
this.div.get(0).addEventListener("click", this.click.bind(this), true);
|
|
|
|
return true;
|
|
}
|
|
|
|
_createNamespace() {
|
|
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
|
|
*/
|
|
_drag_helper(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 = formatTime(parseTime(element.dropEnd.attr('data-hour') + ":" + element.dropEnd.attr('data-minute')));
|
|
}
|
|
element.innerHTML = '<div style="font-size: 1.1em; text-align:center; font-weight: bold; height:100%;"><span class="calendar_timeDemo" >'+time+'</span></div>';
|
|
}
|
|
else
|
|
{
|
|
element.innerHTML = '<div class="calendar_d-n-d_forbiden" style="height:100%"></div>';
|
|
}
|
|
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
|
|
*/
|
|
_event_drop( 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;
|
|
let target_date;
|
|
|
|
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()
|
|
target_date = event_widget._parent.date_helper(
|
|
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)
|
|
{
|
|
target_date.setUTCHours(0);
|
|
target_date.setUTCMinutes(0);
|
|
}
|
|
else if (timegrid.options.granularity === 0)
|
|
{
|
|
// List, not time grid - keep time
|
|
target_date.setUTCHours(event_widget.options.value.start.getUTCHours());
|
|
target_date.setUTCMinutes(event_widget.options.value.start.getUTCMinutes());
|
|
}
|
|
else
|
|
{
|
|
// Non-whole day events, and integrated apps, can change
|
|
target_date.setUTCHours(dropEnd.whole_day ? 0 : dropEnd.hour || 0);
|
|
target_date.setUTCMinutes(dropEnd.whole_day ? 0 : dropEnd.minute || 0);
|
|
}
|
|
|
|
// Leave the helper there until the update is done
|
|
var loading = event_data.event_node;
|
|
// and add a loading icon so user knows something is happening
|
|
jQuery('.calendar_calEventHeader', event_widget.div).addClass('loading');
|
|
|
|
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;
|
|
}
|
|
let duration : string | number | boolean;
|
|
//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, target_date || 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(target_date);
|
|
|
|
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 === Et2Dialog.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}
|
|
*/
|
|
invalidate( 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();
|
|
}
|
|
this.widget._updateNow();
|
|
|
|
// Hide loader
|
|
window.setTimeout(jQuery.proxy(function() {this.loader.hide();},this.widget),200);
|
|
},{widget:this,"trigger":trigger}),et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT);
|
|
}
|
|
|
|
detachFromDOM( )
|
|
{
|
|
// Remove the binding to the change handler
|
|
jQuery(this.div).off(".et2_calendar_timegrid");
|
|
|
|
return super.detachFromDOM();
|
|
}
|
|
|
|
attachToDOM( )
|
|
{
|
|
let result = super.attachToDOM();
|
|
|
|
// 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;
|
|
}
|
|
|
|
getDOMNode( _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;
|
|
}
|
|
}
|
|
|
|
set_disabled( disabled)
|
|
{
|
|
var old_value = this.options.disabled;
|
|
this.disabled = disabled;
|
|
|
|
this.div.get(0).classList.toggle("hideme", 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);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the 'now' line
|
|
* @private
|
|
*/
|
|
// @ts-ignore
|
|
public _updateNow()
|
|
{
|
|
let now = super._updateNow();
|
|
if(now === false || this.options.granularity == 0 || !this.div.is(':visible'))
|
|
{
|
|
this.now_div.hide();
|
|
return false;
|
|
}
|
|
|
|
// Position & show line
|
|
let set_line = function(line, now, day)
|
|
{
|
|
line.appendTo(day.getDOMNode()).show();
|
|
let pos = day._time_to_position(now.getUTCHours() * 60 + now.getUTCMinutes());
|
|
//this.now_div.position({my: 'left', at: 'left', of: day.getDOMNode()});
|
|
line.css('top', pos + '%');
|
|
}
|
|
|
|
// Showing just 1 day, multiple owners - span all
|
|
if(this.daily_owner && this.day_list.length == 1)
|
|
{
|
|
let day = this.day_widgets[0];
|
|
set_line(this.now_div, now, day);
|
|
this.now_div.css('width', (this.day_widgets.length * 100) + '%');
|
|
return true;
|
|
}
|
|
|
|
// Find the day of the week
|
|
for(var i = 0; i < this.day_widgets.length; i++)
|
|
{
|
|
let day = this.day_widgets[i];
|
|
if(day.getDate() >= now)
|
|
{
|
|
day = this.day_widgets[i-1];
|
|
set_line(this.now_div, now, day);
|
|
this.now_div.css('width','100%');
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clear everything, and redraw the whole grid
|
|
*/
|
|
_drawGrid( )
|
|
{
|
|
|
|
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.
|
|
*/
|
|
_drawTimes( )
|
|
{
|
|
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 += '<div class="calendar_calTimeRow' + working_hours + '" style="height: '+(100/row_count)+'%;">';
|
|
// show time for full hours, always for 45min interval and at least on every 3 row
|
|
// @ts-ignore
|
|
let time = formatTime(parseTime((t / 60) + ":" + (t % 60)));
|
|
|
|
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 += '<div class="calendar_calTimeRowTime et2_clickable" data-time="'+time.trim()+'" data-hour="'+Math.floor(t/60)+'" data-minute="'+(t%60)+'">'+time_label+"</div></div>\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.
|
|
*/
|
|
resizeTimes( )
|
|
{
|
|
|
|
// 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.
|
|
*/
|
|
_resizeTimes( )
|
|
{
|
|
|
|
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 && typeof child.resize === 'function')
|
|
{
|
|
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.
|
|
*/
|
|
_drawDays( )
|
|
{
|
|
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));
|
|
this.div.get(0).style.display = "";
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Set 'now' line
|
|
this._updateNow();
|
|
|
|
// 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
|
|
*
|
|
*/
|
|
set_header_classes()
|
|
{
|
|
var day;
|
|
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
|
|
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
|
|
*/
|
|
private _scroll(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
|
|
*/
|
|
_calculate_day_list( start_date, end_date, show_weekend)
|
|
{
|
|
|
|
let day_list = [];
|
|
if(!start_date || !end_date)
|
|
{
|
|
return day_list;
|
|
}
|
|
|
|
let end = this.date_helper(end_date);
|
|
let i = 1;
|
|
let start = this.date_helper(start_date);
|
|
|
|
do
|
|
{
|
|
if(show_weekend || !show_weekend && [0, 6].indexOf(start.getUTCDay()) === -1 || end_date === start_date)
|
|
{
|
|
day_list.push(formatDate(start, {dateFormat: "Ymd"}));
|
|
}
|
|
start.setUTCDate(start.getUTCDate() + 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 >= start && i++ <= 14);
|
|
|
|
return day_list;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
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_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
|
|
let _invite_enabled = function (action, event, target)
|
|
{
|
|
var event = event.iface.getWidget();
|
|
const timegrid = target.iface.getWidget() || false;
|
|
if(timegrid)
|
|
{
|
|
const enabled = timegrid._get_invite_action_enabled(event);
|
|
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;
|
|
}
|
|
|
|
// Hide tooltip or it might throw events too
|
|
egw.tooltipDestroy();
|
|
|
|
/*
|
|
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)
|
|
{
|
|
dropEnd = helper.dropEnd[0].dataset || this.dropEnd
|
|
}
|
|
}
|
|
var drag_listener = function(_event)
|
|
{
|
|
aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], _data.ui.helper[0], 0);
|
|
_invite_enabled(
|
|
widget_object.getActionLink('invite').actionObj,
|
|
_data.ui.selected[0],
|
|
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_ENTER:
|
|
// 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.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
|
|
{
|
|
jQuery(_data.ui.helper).prepend('<div class="calendar_d-n-d_timeCounter" data-count="1"><span></span></div>');
|
|
}
|
|
|
|
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' + 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;
|
|
default:
|
|
// Event starts in its own parent
|
|
if(!time.length)
|
|
{
|
|
jQuery(_data.ui.helper).prepend('<div class="calendar_d-n-d_timeCounter" data-count="1"><span></span></div>');
|
|
}
|
|
drag_listener(_data.ui.selected[0]);
|
|
}
|
|
};
|
|
|
|
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
|
|
*/
|
|
_init_links_dnd( 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.draggable;
|
|
|
|
// 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('<div class="loading"></div>');
|
|
}
|
|
|
|
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(data)
|
|
{
|
|
if(data.type)
|
|
{
|
|
// Make sure to only run once
|
|
return;
|
|
}
|
|
// Need to remove the action from the original timegrid
|
|
source[0].iface.getWidget()?.destroy();
|
|
if(loading)
|
|
{
|
|
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}
|
|
*/
|
|
_get_action_links(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;
|
|
}
|
|
|
|
_get_invite_action_enabled(event : et2_calendar_event)
|
|
{
|
|
if(!event || !event.options || !event.options.value.participants || !this.options.owner)
|
|
{
|
|
return false;
|
|
}
|
|
var owner_match = false;
|
|
var own_timegrid = event.getParent()?.getParent() === this && !this.daily_owner;
|
|
|
|
for(var id in event.options.value.participants)
|
|
{
|
|
if(!this.daily_owner)
|
|
{
|
|
if(this.options.owner === id ||
|
|
this.options.owner.indexOf &&
|
|
this.options.owner.indexOf(id) >= 0)
|
|
{
|
|
owner_match = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.iterateOver(function(col)
|
|
{
|
|
// Check scroll section or header section
|
|
if(col.div.has(this.gridHover).length || col.header.has(this.gridHover).length)
|
|
{
|
|
owner_match = owner_match || col.options.owner.indexOf(id) !== -1;
|
|
own_timegrid = (col === event.getParent());
|
|
}
|
|
}, this, et2_calendar_daycol);
|
|
}
|
|
}
|
|
return !owner_match &&
|
|
// Not inside its own timegrid
|
|
!own_timegrid;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
set_value(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.set_value(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_calendar_view.is_consolidated(this.options.owner, this.day_list.length == 1 ? 'day' : 'week');
|
|
for(var day in events)
|
|
{
|
|
let day_list = [];
|
|
for(var i = 0; i < events[day].length; i++)
|
|
{
|
|
day_list.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);
|
|
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}
|
|
*/
|
|
set_owner(_owner)
|
|
{
|
|
var old = this.options.owner || 0;
|
|
super.set_owner(_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
|
|
*/
|
|
set_label(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
|
|
*/
|
|
set_granularity(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
|
|
*/
|
|
set_show_weekend(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
|
|
*/
|
|
change( )
|
|
{
|
|
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
|
|
*/
|
|
event_change( 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;
|
|
}
|
|
|
|
get_granularity()
|
|
{
|
|
// 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)
|
|
*/
|
|
click(_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.target.closest(".calendar_calEvent"));
|
|
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);
|
|
}
|
|
|
|
_ev.stopImmediatePropagation();
|
|
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
|
|
));
|
|
_ev.preventDefault();
|
|
_ev.stopImmediatePropagation();
|
|
}
|
|
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});
|
|
_ev.stopImmediatePropagation();
|
|
}
|
|
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')) ||
|
|
_ev.target.classList.contains("calendar_calAddEvent")
|
|
)
|
|
{
|
|
// 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.daily_owner ? _ev.target.closest(".calendar_calDayCol").dataset.owner : this.options.owner
|
|
};
|
|
app.calendar.add(options);
|
|
_ev.preventDefault();
|
|
_ev.stopImmediatePropagation();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mousedown handler to support drag to create
|
|
*
|
|
* @param {jQuery.Event} event
|
|
*/
|
|
_mouse_down(event)
|
|
{
|
|
if(event.which !== 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(this.options.readonly)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Skip for events
|
|
if(event.target.closest(".calendar_calEvent"))
|
|
{
|
|
return;
|
|
}
|
|
// Skip for headers
|
|
if(this.dayHeader.has(event.target).length > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let start = {...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
|
|
let date = this.date_helper(start.date);
|
|
if(start.hour)
|
|
{
|
|
date.setUTCHours(start.hour);
|
|
}
|
|
if(start.minute)
|
|
{
|
|
date.setUTCMinutes(start.minute);
|
|
}
|
|
start.date = date;
|
|
|
|
this.gridHover.css('cursor', 'ns-resize');
|
|
|
|
// Start update
|
|
var timegrid = this;
|
|
this.div.on('mousemove.dragcreate', function()
|
|
{
|
|
var end = jQuery.extend({}, timegrid.gridHover[0].dataset);
|
|
let date = timegrid.date_helper(end.date);
|
|
if(end.hour)
|
|
{
|
|
date.setUTCHours(end.hour);
|
|
}
|
|
if(end.minute)
|
|
{
|
|
date.setUTCMinutes(end.minute);
|
|
}
|
|
if(!timegrid.drag_create.event && date.toJSON() != start.date.toJSON())
|
|
{
|
|
timegrid._drag_create_start(start);
|
|
// Create the event immediately
|
|
timegrid._drag_create_event();
|
|
}
|
|
if(timegrid.drag_create.event && timegrid.drag_create.parent && timegrid.drag_create.end)
|
|
{
|
|
|
|
timegrid.drag_create.end.date = date;
|
|
if(timegrid.drag_create.start.date.toJSON() == timegrid.drag_create.end.date.toJSON())
|
|
{
|
|
// Minimum drag size is time granularity or default
|
|
timegrid.drag_create.end.date.setUTCMinutes(timegrid.drag_create.end.date.getUTCMinutes() + (timegrid.options.granularity || timegrid.egw().preference("defaultlength", "calendar")));
|
|
}
|
|
|
|
try
|
|
{
|
|
timegrid._drag_update_event();
|
|
}
|
|
catch(e)
|
|
{
|
|
timegrid._drag_create_end();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mouseup handler to support drag to create
|
|
*
|
|
* @param {jQuery.Event} event
|
|
*/
|
|
_mouse_up(event)
|
|
{
|
|
if(this.options.readonly)
|
|
{
|
|
return;
|
|
}
|
|
let end = {...this.gridHover[0].dataset};
|
|
if(end.date)
|
|
{
|
|
let date = this.date_helper(end.date);
|
|
if(end.hour)
|
|
{
|
|
date.setUTCHours(end.hour);
|
|
}
|
|
if(end.minute)
|
|
{
|
|
date.setUTCMinutes(end.minute);
|
|
}
|
|
end.date = date;
|
|
}
|
|
this.div.off('mousemove.dragcreate');
|
|
this.gridHover.css('cursor', '');
|
|
|
|
if(this.drag_create.end)
|
|
{
|
|
this._drag_create_end(this.drag_create.end);
|
|
}
|
|
else if(this.drag_create.start)
|
|
{
|
|
// Not dragged enough to count, fake a click
|
|
event.stopImmediatePropagation();
|
|
this.gridHover[0].dispatchEvent(new Event("click"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
_get_time_from_position( x,y)
|
|
{
|
|
|
|
x = Math.round(x);
|
|
y = Math.round(y);
|
|
|
|
var path = [];
|
|
var day = null;
|
|
var time = null;
|
|
|
|
let nodes = document.elementsFromPoint(x, y);
|
|
|
|
for(var id in this.gridHover[0].dataset) {
|
|
delete this.gridHover[0].dataset[id];
|
|
}
|
|
if(this.options.granularity == 0)
|
|
{
|
|
this.gridHover.css('height','');
|
|
}
|
|
for(let i = 0; i < nodes.length && nodes[i].tagName != 'FORM'; i++)
|
|
{
|
|
let node = nodes[i];
|
|
let $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'))
|
|
{
|
|
continue;
|
|
}
|
|
if(node.classList.contains('calendar_calDayColHeader'))
|
|
{
|
|
for(var id in node.dataset)
|
|
{
|
|
this.gridHover[0].dataset[id] = node.dataset[id];
|
|
}
|
|
this.gridHover.css({
|
|
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.querySelector(".calendar_calDayColHeader_spacer") ?? node;
|
|
this.gridHover
|
|
.attr('data-non_blocking', 'true');
|
|
break;
|
|
}
|
|
if(node.classList.contains('calendar_calDayCol'))
|
|
{
|
|
day = node;
|
|
this.gridHover
|
|
.attr('data-date', day.dataset.date);
|
|
}
|
|
if(node.classList.contains('calendar_calTimeRowTime'))
|
|
{
|
|
time = node;
|
|
this.gridHover
|
|
.attr('data-hour', time.dataset.hour)
|
|
.attr('data-minute', time.dataset.minute);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!day)
|
|
{
|
|
return [];
|
|
}
|
|
this.gridHover
|
|
.show()
|
|
.css("position", "absolute")
|
|
.appendTo(day);
|
|
if(time)
|
|
{
|
|
this.gridHover
|
|
.height(this.rowHeight)
|
|
.css("top", time.offsetTop + "px");
|
|
}
|
|
this.gridHover.css('left','');
|
|
return this.gridHover;
|
|
}
|
|
|
|
/**
|
|
* Code for implementing et2_IDetachedDOM
|
|
*
|
|
* @param {array} _attrs array to add further attributes to
|
|
*/
|
|
getDetachedAttributes( _attrs)
|
|
{
|
|
_attrs.push('start_date','end_date');
|
|
}
|
|
|
|
getDetachedNodes( )
|
|
{
|
|
return [this.getDOMNode(this)];
|
|
}
|
|
|
|
setDetachedAttributes( _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
|
|
*/
|
|
resize (_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-calendar').last().innerHeight();
|
|
}
|
|
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-calendar').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)
|
|
*/
|
|
beforePrint( )
|
|
{
|
|
|
|
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
|
|
*/
|
|
afterPrint( )
|
|
{
|
|
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_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); |