Skip to content
Snippets Groups Projects
Verified Commit 0f0ae687 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

wip

# Conflicts:
#	source/components/form/stylesheet/context-help.mjs
#	source/components/time/stylesheet/month-calendar.mjs
#	source/components/time/timeline/stylesheet/segment.mjs
parent a67fed80
No related branches found
No related tags found
No related merge requests found
Showing with 822 additions and 1097 deletions
......@@ -22,37 +22,34 @@
"id": "1",
"startDate": "2025-03-01",
"endDate": "2025-03-01",
"color": "#df0fb6",
"label": "app 1"
"color": "#df0fb6"
},
{
"id": "2",
"startDate": "2025-03-01",
"endDate": "2025-03-01",
"color": "#ffffff",
"label": "app 2"
"color": "#ffffff"
},
{
"id": "3",
"startDate": "2025-03-01",
"endDate": "2025-03-01",
"color": "#ec4c6e",
"label": "app 3"
"color": "#ec4c6e"
},
{
"id": "4",
"startDate": "2025-03-01",
"endDate": "2025-03-01",
"color": "#a32408",
"label": "app 4"
"color": "#a32408"
},
{
"id": "12",
"startDate": "2025-03-01",
"endDate": "2025-04-01",
"color": "#ff3322",
"label": "app 5"
"color": "#ff3322"
},
{
"id": "13",
......
......@@ -24,7 +24,7 @@
}
[data-monster-role="popper"] {
z-index: 1000;
z-index: var(--monster-z-index-tooltip-overlay);
}
:host {
......
......@@ -10,10 +10,10 @@
* For more information about purchasing a commercial license, please contact schukai GmbH.
*/
import {addAttributeToken} from "../../../dom/attributes.mjs";
import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
export {ContextHelpStyleSheet}
export { ContextHelpStyleSheet };
/**
* @private
......@@ -22,10 +22,17 @@ export {ContextHelpStyleSheet}
const ContextHelpStyleSheet = new CSSStyleSheet();
try {
ContextHelpStyleSheet.insertRule(`
ContextHelpStyleSheet.insertRule(
`
@layer contexthelp {
[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{line-height:1em;margin:0;padding:0;position:relative}[data-monster-role=control] [data-monster-role=button]{display:inline-block;position:relative}:is([data-monster-role=control] [data-monster-role=button]) svg{cursor:pointer}:is([data-monster-role=control] [data-monster-role=button]) svg.hidden{cursor:default;visibility:hidden}[data-monster-role=popper]{z-index:1000}:host{display:inline-block;margin:0 .2em;padding:0;position:relative;vertical-align:bottom}
}`, 0);
}`,
0,
);
} catch (e) {
addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
addAttributeToken(
document.getRootNode().querySelector("html"),
ATTRIBUTE_ERRORMESSAGE,
e + "",
);
}
This diff is collapsed.
......@@ -47,14 +47,14 @@
div.popper {
position: absolute;
z-index: var(--monster-theme-control-z-index);
z-index: var(--monster-z-index-dropdown);
background-color: var(--monster-bg-color-primary-1);
color: var(--monster-color-primary-1);
border-radius: var(--monster-theme-control-border-radius);
border-width: var(--monster-theme-control-border-width);
border-color: var(--monster-theme-control-border-color);
border-style: var(--monster-theme-control-border-style);
box-shadow: var(--monster-theme-control-box-shadow);
box-shadow: var(--monster-box-shadow-1);
padding: 0.5em;
display: none;
box-sizing: border-box;
......@@ -63,16 +63,21 @@ div.popper {
div.day-cell {
display: flex;
align-items: start;
justify-content: start;
justify-content: space-between;
box-sizing: border-box;
padding: 0.3em;
position: relative;
flex-direction: column;
transition: background-color 0.3s;
background-color: var(--monster-bg-color-primary-2);
color: var(--monster-color-primary-2);
aspect-ratio: 1 / 1;
aspect-ratio: 1 / 1.26;
}
div.footer {
font-size: xx-small;
}
div.current-month {
......@@ -93,8 +98,9 @@ div.today {
[data-monster-role=appointment-container] {
position: absolute;
top: 0;
box-sizing: border-box;
bottom: 0;
top: 0 ;
left: 0;
width: 100%;
height: 100%;
right: 0;
}
\ No newline at end of file
This diff is collapsed.
// Constants for possible appointment types
import { ID } from "../../../types/id.mjs";
import { isArray } from "../../../types/is.mjs";
/**
* Helper function: Check if two dates are on the same day (ignoring time)
* @private
* @param {string|Date} date1 - First date
* @param {string|Date} date2 - Second date
* @returns {boolean} True if the dates are on the same day, false otherwise
*/
function isSameDay(date1, date2) {
const d1 = new Date(date1);
const d2 = new Date(date2);
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
);
}
class AppointmentCollection {
constructor() {
this.appointments = [];
}
// Add an appointment to the collection
addAppointment(appointment) {
if (!(appointment instanceof TimelineItem)) {
throw new Error("Only instances of Appointment can be added");
}
this.appointments.push(appointment);
}
// Get all appointments that overlap with the given date range
getAppointmentsInRange(rangeStart, rangeEnd) {
const start = new Date(rangeStart);
const end = new Date(rangeEnd);
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
throw new Error("Invalid date range");
}
return this.appointments.filter(
(app) => app.endDate >= start && app.startDate <= end,
);
}
// Get appointments that start on a specific day
getAppointmentsStartingOn(date) {
return this.appointments.filter((app) => isSameDay(app.startDate, date));
}
// Get appointments that end on a specific day
getAppointmentsEndingOn(date) {
return this.appointments.filter((app) => isSameDay(app.endDate, date));
}
// Get appointments active on a specific day (the day falls between startDate and endDate, inclusive)
getAppointmentsForDay(date) {
const d = new Date(date);
return this.appointments.filter(
(app) => app.startDate <= d && app.endDate >= d,
);
}
// Get appointments where the specified date is strictly between the start and end dates
getAppointmentsWithDateAsMiddleDay(date) {
const d = new Date(date);
return this.appointments.filter(
(app) =>
app.startDate < d &&
app.endDate > d &&
!isSameDay(app.startDate, d) &&
!isSameDay(app.endDate, d),
);
}
// Get appointments filtered by type (e.g., all milestones)
getAppointmentsByType(type) {
return this.appointments.filter((app) => app.type === type);
}
// Export the entire collection as JSON
toJSON() {
return this.appointments.map((app) => app.toJSON());
}
/**
* Splits an appointment into slices of a specified duration (in days).
* If the appointment is provided as an ID, it will be looked up in the collection.
* Each slice is returned as an object with the original appointment's properties,
* plus sliceIndex and the new startDate/endDate for the slice.
*
* @param {TimelineItem|string} appointmentOrId - The appointment instance or its ID.
* @param {number} sliceDurationDays - Duration of each slice in days.
* @returns {Array} Array of slice objects.
*/
splitAppointment(appointmentOrId, sliceDurationDays) {
if (typeof sliceDurationDays !== "number" || sliceDurationDays <= 0) {
throw new Error("Slice duration must be a positive number");
}
let appointment;
if (appointmentOrId instanceof TimelineItem) {
appointment = appointmentOrId;
} else {
appointment = this.appointments.find((app) => app.id === appointmentOrId);
if (!appointment) {
throw new Error("Appointment not found");
}
}
const slices = [];
let currentStart = new Date(appointment.startDate);
let sliceIndex = 0;
while (currentStart < appointment.endDate) {
const currentEnd = new Date(currentStart);
currentEnd.setDate(currentEnd.getDate() + sliceDurationDays);
if (currentEnd > appointment.endDate) {
currentEnd.setTime(appointment.endDate.getTime());
}
const slice = {
parentId: appointment.id,
sliceIndex: sliceIndex,
title: appointment.title,
type: appointment.type,
description: appointment.description,
userIds: appointment.userIds,
startDate: currentStart.toISOString(),
endDate: currentEnd.toISOString(),
};
slices.push(slice);
// Prepare for next slice
currentStart = currentEnd;
sliceIndex++;
}
return slices;
}
}
// Example usage
try {
// Create some appointments with user associations
const appointment1 = new TimelineItem({
title: "Team Meeting",
type: AppointmentType.TASK,
startDate: "2025-03-05T09:00:00",
endDate: "2025-03-05T10:00:00",
description: "Weekly team meeting",
userIds: ["user1", "user2"],
});
const appointment2 = new TimelineItem({
title: "Project Kickoff",
type: AppointmentType.MILESTONE,
startDate: "2025-03-06T08:00:00",
endDate: "2025-03-06T12:00:00",
description: "Kickoff for the new project",
userIds: "user3", // single user id; will be converted to an array
});
// An appointment spanning over 30 days
const appointment3 = new TimelineItem({
title: "Long Term Project",
type: AppointmentType.TASK,
startDate: "2025-04-01T09:00:00",
endDate: "2025-05-15T17:00:00",
description: "Project spanning multiple weeks",
userIds: ["user4"],
});
// Create an appointment collection and add appointments
const collection = new AppointmentCollection();
collection.addAppointment(appointment1);
collection.addAppointment(appointment2);
collection.addAppointment(appointment3);
console.log("Appointments on 2025-03-05:");
collection
.getAppointmentsForDay("2025-03-05")
.forEach((app) => console.log(app.toString()));
console.log("\nAppointments in range 2025-03-05 to 2025-03-06:");
collection
.getAppointmentsInRange("2025-03-05T00:00:00", "2025-03-06T23:59:59")
.forEach((app) => console.log(app.toString()));
console.log("\nAll milestones:");
collection
.getAppointmentsByType(AppointmentType.MILESTONE)
.forEach((app) => console.log(app.toString()));
// Split appointment3 into weekly slices (7-day slices)
console.log("\nWeekly slices for Long Term Project:");
const weeklySlices = collection.splitAppointment(appointment3, 7);
weeklySlices.forEach((slice) => {
console.log(
`Slice ${slice.sliceIndex}: ${slice.startDate} to ${slice.endDate}`,
);
});
// Split appointment3 into 14-day slices
console.log("\n14-day slices for Long Term Project:");
const biweeklySlices = collection.splitAppointment(appointment3, 14);
biweeklySlices.forEach((slice) => {
console.log(
`Slice ${slice.sliceIndex}: ${slice.startDate} to ${slice.endDate}`,
);
});
console.log("\nJSON Export of the collection:");
console.log(JSON.stringify(collection.toJSON(), null, 2));
} catch (error) {
console.error("Error:", error.message);
}
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*/
import { ID } from "../../../types/id.mjs";
import { isArray } from "../../../types/is.mjs";
import { BaseWithOptions } from "../../../types/basewithoptions.mjs";
const AppointmentType = {
TODO: "todo",
TASK: "task",
MILESTONE: "milestone",
EVENT: "event",
REMINDER: "reminder",
MEETING: "meeting",
CALL: "call",
APPOINTMENT: "appointment",
DEADLINE: "deadline",
BIRTHDAY: "birthday",
ANNIVERSARY: "anniversary",
HOLIDAY: "holiday",
VACATION: "vacation",
SICKDAY: "sickday",
OOO: "ooo",
CUSTOM: "custom",
};
class Item extends BaseWithOptions {
/**
* Creates a new Item.
*
* @param {Object} params - Parameters for creating the item.
* @param {string} [params.id] - Optional ID. If not provided, one will be generated.
* @param {string} params.title - Title of the item.
* @param {string} params.type - Type of the item (must be one of AppointmentType values).
* @param {string|Date} params.startDate - Start date/time.
* @param {string|Date} params.endDate - End date/time.
* @param {string} [params.description=""] - Optional description.
* @param {string|string[]} [params.userIds=[]] - One or more user IDs associated with this item.
* @throws {Error} If type is invalid or dates are not valid or startDate is after endDate.
*/
constructor({
id,
title,
type,
startDate,
endDate,
description = "",
userIds = [],
}) {
// Validate item type
if (!Object.values(AppointmentType).includes(type)) {
throw new Error(`Invalid appointment type: ${type}`);
}
// Convert startDate and endDate to Date objects
this.startDate = new Date(startDate);
this.endDate = new Date(endDate);
if (isNaN(this.startDate.getTime()) || isNaN(this.endDate.getTime())) {
throw new Error("Invalid start or end date");
}
// Ensure startDate is not after endDate
if (this.startDate > this.endDate) {
throw new Error("Start date cannot be after end date");
}
// Initialize fields
this.id = id || new ID().toString();
this.title = title || "new appointment";
this.type = type || AppointmentType.CUSTOM;
this.description = description || "";
// Ensure userIds is stored as an array
this.userIds = isArray(userIds) ? userIds : [userIds];
}
/**
* Calculates the duration of the item in days.
*
* @returns {number} Duration in days.
*/
getDurationInDays() {
const msPerDay = 1000 * 60 * 60 * 24;
return Math.ceil((this.endDate - this.startDate) / msPerDay);
}
/**
* Calculates the duration of the item in hours.
*
* @returns {number} Duration in hours.
*/
getDurationInHours() {
const msPerHour = 1000 * 60 * 60;
return Math.ceil((this.endDate - this.startDate) / msPerHour);
}
/**
* Calculates the duration of the item in minutes.
*
* @returns {number} Duration in minutes.
*/
getDurationInMinutes() {
const msPerMinute = 1000 * 60;
return Math.ceil((this.endDate - this.startDate) / msPerMinute);
}
/**
* Calculates the duration of the item in seconds.
*
* @returns {number} Duration in seconds.
*/
getDurationInSeconds() {
const msPerSecond = 1000;
return Math.ceil((this.endDate - this.startDate) / msPerSecond);
}
/**
* Checks if the item is active on the specified date.
*
* @param {string|Date} date - The date to check.
* @returns {boolean} True if the item is active on the given date.
* @throws {Error} If the provided date is invalid.
*/
isActiveOn(date) {
const d = new Date(date);
if (isNaN(d.getTime())) {
throw new Error("Invalid date");
}
return this.startDate <= d && this.endDate >= d;
}
/**
* Returns a JSON-compatible object representing the item.
*
* @returns {Object} JSON representation of the item.
*/
toJSON() {
return {
id: this.id,
title: this.title,
type: this.type,
description: this.description,
userIds: this.userIds,
startDate: this.startDate.toISOString(),
endDate: this.endDate.toISOString(),
};
}
/**
* Creates a new Item instance from a JSON object.
*
* @param {Object} json - The JSON object.
* @param {string} json.id - The ID of the item.
* @param {string} json.title - The title of the item.
* @param {string} json.type - The type of the item.
* @param {string} json.startDate - The start date in ISO format.
* @param {string} json.endDate - The end date in ISO format.
* @param {string} [json.description=""] - The description of the item.
* @param {string|string[]} [json.userIds=[]] - One or more user IDs.
* @returns {Item} A new Item instance.
*/
static fromJson(json) {
return new Item({
id: json.id,
title: json.title,
type: json.type,
startDate: json.startDate,
endDate: json.endDate,
description: json.description,
userIds: json.userIds,
});
}
/**
* Returns a readable string representation of the item.
*
* @returns {string} String representation of the item.
*/
toString() {
return `[${this.type}] ${this.title} (${this.startDate.toLocaleString()} - ${this.endDate.toLocaleString()})`;
}
}
export { Item, AppointmentType };
......@@ -62,10 +62,6 @@ class Segment extends CustomElement {
);
}
[initMethodSymbol]() {
super[initMethodSymbol]();
}
/**
*
* @return {Components.Time.Calendar
......@@ -161,7 +157,7 @@ function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<div data-monster-role="appointment" data-monster-replace="path:labels.text"></div>
<div data-monster-role="appointment" data-monster-replace="path:labels.text" part="appointment"></div>
</div>
`;
}
......
......@@ -14,5 +14,13 @@
position: absolute;
left: 0;
height: 0.5rem;
}
}
\ No newline at end of file
[data-monster-role="control"] {
display: flex;
position: relative;
flex-grow: 1;
border: none;
outline: none;
overflow: hidden;
}
This diff is collapsed.
......@@ -31,7 +31,6 @@ export * from "./components/content/viewer.mjs";
export * from "./components/content/copy.mjs";
export * from "./components/content/camera.mjs";
export * from "./components/time/timeline/segment.mjs";
export * from "./components/time/timeline/item.mjs";
export * from "./components/time/month-calendar.mjs";
export * from "./components/form/message-state-button.mjs";
export * from "./components/form/password.mjs";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment