egroupware/calendar/js/et2_widget_view.ts
nathan b2935ba4a3 Calendar: Fix days offset by one
Depending on your timezone and current time, your calendar could show the wrong days (Tuesday -> Monday)
2023-09-06 13:32:52 -10:00

735 lines
19 KiB
TypeScript

/*
* 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
/etemplate/js/et2_core_valueWidget;
*/
import {et2_createWidget, et2_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_calendar_event} from "./et2_widget_event";
import {formatDate} from "../../api/js/etemplate/Et2Date/Et2Date";
/**
* Parent class for the various calendar views to reduce copied code
*
*
* et2_calendar_view is responsible for its own loader div, which is displayed while
* the times & days are redrawn.
*
* @augments et2_valueWidget
*/
export class et2_calendar_view extends et2_valueWidget
{
static readonly _attributes : any = {
owner: {
name: "Owner",
type: "any", // Integer, or array of integers, or string like r13 (resources, addressbook)
default: [egw.user('account_id')],
description: "Account ID number of the calendar owner, if not the current user"
},
start_date: {
name: "Start date",
type: "any"
},
end_date: {
name: "End date",
type: "any"
}
};
protected dataStorePrefix: string = 'calendar';
protected loader: JQuery;
protected div: JQuery;
protected now_div: JQuery;
protected update_timer: number = null;
protected now_timer: number = null;
protected drag_create: { parent: et2_widget; start: any; end: any; event: et2_calendar_event };
protected value: any;
protected _actionObject: egwActionObject;
/**
* Constructor
*
*/
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
{
// Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_view._attributes, _child || {}));
this.loader = jQuery('<div class="egw-loading-prompt-container ui-front loading"></div>');
this.now_div = jQuery('<div class="calendar_now"/>');
this.update_timer = null;
this.now_timer = null;
// Used to support dragging on empty space to create an event
this.drag_create = {
start: null,
end: null,
parent: null,
event: null
};
}
destroy() {
super.destroy();
// Stop the invalidate timer
if(this.update_timer)
{
window.clearTimeout(this.update_timer);
}
// Stop the 'now' line
if(this.now_timer)
{
window.clearInterval(this.now_timer);
}
}
doLoadingFinished( )
{
super.doLoadingFinished();
this.loader.hide(0).prependTo(this.div);
this.div.append(this.now_div);
if(this.options.owner) this.set_owner(this.options.owner);
// Start moving 'now' line
this.now_timer = window.setInterval(this._updateNow.bind(this), 60000);
return true;
}
/**
* Something changed, and the view need to be re-drawn. We wait a bit to
* avoid re-drawing twice if start and end date both changed, then recreate
* as needed.
*
* @param {boolean} [trigger_event=false] Trigger an event once things are done.
* Waiting until invalidate completes prevents 2 updates when changing the date range.
* @returns {undefined}
*
* @memberOf et2_calendar_view
*/
invalidate(trigger_event) {
// If this wasn't a stub, we'd set this.update_timer
}
/**
* Returns the current start date
*
* @returns {Date}
*
* @memberOf et2_calendar_view
*/
get_start_date() {
return new Date(this.options.start_date);
}
/**
* Returns the current start date
*
* @returns {Date}
*
* @memberOf et2_calendar_view
*/
get_end_date() {
return new Date(this.options.end_date);
}
/**
* Change the start date
*
* Changing the start date will invalidate the display, and it will be redrawn
* after a timeout.
*
* @param {string|number|Date} new_date New starting date. Strings can be in
* any format understood by et2_widget_date, or Ymd (eg: 20160101).
* @returns {undefined}
*
* @memberOf et2_calendar_view
*/
set_start_date(new_date)
{
if(!new_date || new_date === null)
{
new_date = new Date();
}
let old_date = this.options.start_date;
this.options.start_date = this.date_helper(new_date)
if(old_date !== this.options.start_date && this.isAttached())
{
this.invalidate(true);
}
}
/**
* Change the end date
*
* Changing the end date will invalidate the display, and it will be redrawn
* after a timeout.
*
* @param {string|number|Date} new_date - New end date. Strings can be in
* any format understood by et2_widget_date, or Ymd (eg: 20160101).
* @returns {undefined}
*
* @memberOf et2_calendar_view
*/
set_end_date(new_date)
{
if(!new_date || new_date === null)
{
new_date = new Date();
}
let old_date = this.options.end_date;
this.options.end_date = this.date_helper(new_date);
if(old_date !== this.options.end_date && this.isAttached())
{
this.invalidate(true);
}
}
/**
* Set which users to display
*
* Changing the owner will invalidate the display, and it will be redrawn
* after a timeout.
*
* @param {number|number[]|string|string[]} _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.
*
* @memberOf et2_calendar_view
*/
set_owner(_owner)
{
var old = this.options.owner;
// 0 means current user, but that causes problems for comparison,
// so we'll just switch to the actual ID
if(_owner == '0')
{
_owner = [egw.user('account_id')];
}
if(!jQuery.isArray(_owner))
{
if(typeof _owner === "string")
{
_owner = _owner.split(',');
}
else
{
_owner = [_owner];
}
}
else
{
_owner = jQuery.extend([],_owner);
}
this.options.owner = _owner;
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);
}
}
/**
* Provide specific data to be displayed.
* This is a way to set start and end dates, owner and event data in one call.
*
* If events are not provided in the array,
* @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;
if(events.length && events.length > 0 || !jQuery.isEmptyObject(events))
{
this.set_disabled(false);
}
if(events.id)
{
this.set_id(events.id);
delete events.id;
}
if(events.start_date)
{
this.set_start_date(events.start_date);
delete events.start_date;
}
if(events.end_date)
{
this.set_end_date(events.end_date);
delete events.end_date;
}
// set_owner() wants start_date set to get the correct week number
// for the corner label
if(events.owner)
{
this.set_owner(events.owner);
delete events.owner;
}
this.value = events || {};
// None of the above changed anything, hide the loader
if(!this.update_timer)
{
window.setTimeout(jQuery.proxy(function() {this.loader.hide();}, this), 200);
}
}
/**
* Parse something that we think is a date into an actual date.
*
* The passed value could be a Date, or a string in an unknown format, or a timestamp
*
* @param {Date | string | number} _value
*/
date_helper(_value : Date | string | number)
{
if(_value === null || _value === "" || _value === undefined || _value === 0)
{
return undefined;
}
if(_value instanceof Date)
{
// Return a copy, because often modifications are made after
return new Date(_value.getTime());
}
let date = new Date();
if(typeof _value === "string" && _value.length == 8)
{
// Ymd format: 20000101
date.setFullYear(parseInt(_value.substring(0, 4)));
// Avoid overflow into next month since it already has a value
date.setUTCDate(1);
date.setUTCMonth(parseInt(_value.substring(4, 6)) - 1);
date.setUTCDate(parseInt(_value.substring(6, 8)));
date.setUTCHours(0);
date.setUTCMinutes(0);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
}
// Check for full timestamp
else if(typeof _value == 'string' && _value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2})|)$/))
{
date = new Date(_value);
}
// timestamp in usertime
else if(typeof _value == 'number' || typeof _value == 'string' && !isNaN(<number><unknown>_value) && _value[0] != '+' && _value[0] != '-')
{
date = new Date((typeof _value == "number" ? _value : parseInt(_value)) * 1000);
}
// @ts-ignore
else if(typeof _value == 'object' && typeof _value.date !== "undefined")
{
// @ts-ignore
return this.date_helper(_value.date);
}
// @ts-ignore
else if(typeof _value == 'object' && typeof _value.valueOf !== "undefined")
{
date = _value;
}
else
{
// string starting with + or - --> add/substract number of seconds from current value
date.setTime(date.getTime() + 1000 * parseInt("" + _value));
}
return date;
}
_createNamespace()
{
return true;
}
/**
* Update the 'now' line
*
* Here we just do some limit checks and return the current date/time.
* Extending widgets should handle position.
*
* @private
*/
public _updateNow()
{
var tempDate = new Date();
var now = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),tempDate.getHours(),tempDate.getMinutes()-tempDate.getTimezoneOffset(),0);
now = this.date_helper(now.toJSON());
if(this.get_start_date() <= now && this.get_end_date() >= now)
{
return now;
}
this.now_div.hide();
return false;
}
/**
* Calendar supports many different owner types, including users & resources.
* This translates an ID to a user-friendly name.
*
* @param {string} user
* @returns {string}
*
* @memberOf et2_calendar_view
*/
_get_owner_name(user) {
var label = undefined;
if(parseInt(user) === 0)
{
// 0 means current user
user = egw.user('account_id');
}
if(et2_calendar_view.owner_name_cache[user])
{
return et2_calendar_view.owner_name_cache[user];
}
if (!isNaN(user))
{
user = parseInt(user);
var accounts = <any[]>egw.accounts('both');
for(var j = 0; j < accounts.length; j++)
{
if(accounts[j].value === user)
{
label = accounts[j].label;
break;
}
}
}
if(typeof label === 'undefined')
{
// Not found? Ask the sidebox owner widget (it gets updated) or the original arrayMgr
let options = false;
if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
{
options = app.calendar.sidebox_et2.getWidgetById('owner').select_options;
}
else
{
options = this.getArrayMgr("sel_options").getRoot().getEntry('owner');
}
if(options && options.find)
{
var found = options.find(function(element) {return element.value == user;}) || {};
if(found && found.label && found.label !== user)
{
label = found.label;
}
}
if(!label)
{
// No sidebox? Must be in home or sitemgr (no caching) - ask directly
label = '?';
egw.jsonq('calendar_owner_etemplate_widget::ajax_owner',user,function(data) {
et2_calendar_view.owner_name_cache[user] = data;
this.invalidate(true);
// Set owner to make sure labels get set
if(this.owner && typeof this.owner.set_value === 'function')
{
this.owner.set_value(data);
}
}.bind(this), this);
}
}
if(label)
{
et2_calendar_view.owner_name_cache[user] = label;
}
return label;
}
/**
* Find the event information linked to a given DOM node
*
* @param {HTMLElement} dom_node - It should have something to do with an event
* @returns {Object}
*/
_get_event_info(dom_node)
{
// Determine as much relevant info as can be found
var event_node = jQuery(dom_node).closest('[data-id]',this.div)[0];
var day_node = jQuery(event_node).closest('[data-date]',this.div)[0];
var result = jQuery.extend({
event_node: event_node,
day_node: day_node
},
event_node ? event_node.dataset : {},
day_node ? day_node.dataset : {}
);
// Widget ID should be the DOM node ID without the event_ prefix
if(event_node && event_node.id)
{
var widget_id = event_node.id || '';
widget_id = widget_id.split('event_');
widget_id.shift();
result.widget_id = 'event_' + widget_id.join('');
const widget = this.getWidgetById(result.widget_id);
if(widget)
{
result.title = widget.options.value.title;
}
}
return result;
}
/**
* Starting (mousedown) handler to support drag to create
*
* Extending classes need to set this.drag_create.parent, which is the
* parent container (child of extending class) that will directly hold the
* event.
*
* @param {String} start Date string (JSON format)
*/
_drag_create_start(start)
{
this.drag_create.start = jQuery.extend({}, start);
if(!this.drag_create.start.date)
{
this.drag_create.start = null;
}
this.drag_create.end = {...start, date: new Date(start.date.valueOf())};
// Begin at default duration
this.drag_create.end.date.setUTCMinutes(this.drag_create.end.date.getUTCMinutes() + (this.egw().preference("defaultlength", "calendar") ?? 60));
// Clear some stuff, if last time did not complete
if(this.drag_create.event)
{
if(this.drag_create.event.destroy)
{
this.drag_create.event.destroy();
}
this.drag_create.event = null;
}
// Wait a bit before adding an "event", it may be just a click
window.setTimeout(() =>
{
// Create event
this._drag_create_event()
}, 100);
}
/**
* Create or update an event used for feedback while dragging on empty space,
* so user can see something is happening
*/
_drag_create_event()
{
if(!this.drag_create.parent || !this.drag_create.start)
{
return;
}
if(!this.drag_create.event)
{
var value = jQuery.extend({},
this.drag_create.start,
this.drag_create.end,
{
start: this.drag_create.start.date,
end: this.drag_create.end && this.drag_create.end.date || this.drag_create.start.date,
date: "" + formatDate(this.date_helper(this.drag_create.start.date), {dateFormat: "Ymd"}),
title: '',
description: '',
owner: this.options.owner,
participants: this.options.owner,
app: 'calendar',
whole_day_on_top: this.drag_create.start.whole_day
}
);
this.drag_create.event = <et2_calendar_event>et2_createWidget('calendar-event', {
id: 'event_drag',
value: value
}, this.drag_create.parent);
this.drag_create.event._values_check(value);
this.drag_create.event.doLoadingFinished();
if(this.drag_create.parent && typeof this.drag_create.parent.position_event == "function")
{
this.drag_create.parent.position_event(this.drag_create.event);
}
}
}
_drag_update_event()
{
if(!this.drag_create.event || !this.drag_create.start || !this.drag_create.end
|| !this.drag_create.parent || !this.drag_create.event._type)
{
return;
}
else if (this.drag_create.end)
{
this.drag_create.event.options.value.end = this.drag_create.end.date;
this.drag_create.event._values_check(this.drag_create.event.options.value);
}
this.drag_create.event._update();
this.drag_create.parent.position_event(this.drag_create.event);
}
/**
* Ending (mouseup) handler to support drag to create
*
* @param {String} end Date string (JSON format)
*/
_drag_create_end(end?)
{
this.div.css('cursor','');
if(typeof end === 'undefined')
{
end = {};
}
if(this.drag_create.start && end.date)
{
// Drag from start to end, open dialog
var options = {
start: this.drag_create.start.date < end.date ? this.drag_create.start.date : end.date,
end: this.drag_create.start.date < end.date ? end.date : this.drag_create.start.date
};
// Whole day needs to go from 00:00 to 23:59
if(end.whole_day || this.drag_create.start.whole_day)
{
var start = new Date(options.start);
start.setUTCHours(0);
start.setUTCMinutes(0);
options.start = start.toJSON();
var end = new Date(options.end);
end.setUTCHours(23);
end.setUTCMinutes(59);
options.end = end.toJSON();
}
// Add anything else that was set, but not date
jQuery.extend(options,this.drag_create.start, end);
delete(options.date);
// Make sure parent is set, if needed
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
if (this.drag_create.parent && this.drag_create.parent.options.owner !== app_calendar.state.owner && !options.owner)
{
options.owner = this.drag_create.parent.options.owner;
}
// Remove empties
for(var key in options)
{
if(!options[key])
{
delete options[key];
}
if(options[key] instanceof Date)
{
options[key] = options[key].toJSON();
}
}
app.calendar.add(options, this.drag_create.event);
// Wait a bit, having these stops the click
window.setTimeout(jQuery.proxy(function() {
this.drag_create.start = null;
this.drag_create.end = null;
this.drag_create.parent = null;
if(this.drag_create.event)
{
try
{
if(this.drag_create.event.destroy)
{
this.drag_create.event.destroy();
}
}
catch(e)
{
}
this.drag_create.event = null;
}
},this),100);
return false;
}
this.drag_create.start = null;
this.drag_create.end = null;
this.drag_create.parent = null;
if(this.drag_create.event)
{
try
{
if(this.drag_create.event.destroy)
{
this.drag_create.event.destroy();
}
} catch(e) {}
this.drag_create.event = null;
}
return true;
}
/**
* Check if the view should be consolidated into one, or listed seperately
* based on the user's preferences
*
* @param {string[]} owners List of owners
* @param {string} view Name of current view (day, week)
* @returns {boolean} True of only one is needed, false if each owner needs
* to be listed seperately.
*/
static is_consolidated(owners, view)
{
// Seperate owners, or consolidated?
return !(
owners.length > 1 &&
(view === 'day' && owners.length < parseInt(''+egw.preference('day_consolidate','calendar')) ||
view === 'week' && owners.length < parseInt(''+egw.preference('week_consolidate','calendar')))
);
}
/**
* Cache to map owner & resource IDs to names, helps cut down on server requests
*/
static owner_name_cache = {};
}