time_table 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,569 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="dropdown"
4
+ export default class extends Controller {
5
+ static targets = ["canvas", "eventsList", "event", "eventName", "eventTime", "timestamp"]
6
+
7
+ // static SCOPE = {
8
+ // days: Symbol("days"),
9
+ // hours: Symbol("hours"),
10
+ // minutes: Symbol("minutes"),
11
+ // seconds: Symbol("seconds")
12
+ // }
13
+
14
+ static UNITS = {
15
+ hours: 3_600_000,
16
+ minutes: 60_000,
17
+ seconds: 1_000
18
+ }
19
+
20
+ connect() {
21
+ this.startTime = new Date(this.element.dataset.timetableStartTime);
22
+ this.endTime = new Date(this.element.dataset.timetableEndTime);
23
+ this.extendedStartTime = this.startTime;
24
+ this.extendedEndTime = this.endTime;
25
+ this.duration = (this.endTime - this.startTime) / 1000;
26
+ this.clipSize = this.element.dataset.timetableClipSize;
27
+ this.scrollHeight = this.element.scrollHeight - 20; // -20 because of the padding
28
+ this.dragging = false;
29
+ this.dragEvent = {};
30
+ this.scrollEvent = {};
31
+ // this.setScope(this.constructor.SCOPE.hours);
32
+ this.timeScope = this.constructor.UNITS.hours;
33
+ this.timeMetric = this.constructor.UNITS.minutes;
34
+ this.metricConversion = this.timeScope / this.timeMetric;
35
+
36
+ this.scale = 60;
37
+
38
+ this.eventTargets.forEach((event) => {
39
+ this.#setSpecialClasses(event);
40
+ });
41
+ }
42
+
43
+ startDragEvent(event) {
44
+ event.preventDefault();
45
+ event.stopPropagation();
46
+
47
+ this.dragging = true;
48
+
49
+ // this.dragEvent.reset();
50
+
51
+ this.dragEvent.type = event.params.dragEvent;
52
+ this.dragEvent.frame = this.element.getBoundingClientRect();
53
+ this.dragEvent.originScrollY = this.#relativeCursorPosition(event).y;
54
+ this.dragEvent.originScrollYTime = this.#timeAtPosition(this.dragEvent.originScrollY);
55
+ this.dragEvent.currentScrollY = this.dragEvent.originScrollY;
56
+ this.dragEvent.currentScrollYTime = this.dragEvent.originScrollYTime;
57
+
58
+ this.dragEvent.originFrameY = event.clientY - this.dragEvent.frame.top;
59
+ this.dragEvent.currentFrameY = this.dragEvent.originFrameY;
60
+
61
+ this.#setDragEventTarget(event.currentTarget.closest(".event"));
62
+ this.#setDragEventListeners();
63
+ }
64
+
65
+ stopDragEvent(event) {
66
+ event.preventDefault();
67
+
68
+ this.dragging = false;
69
+ this.dragEvent = {};
70
+
71
+ this.stopScroll();
72
+ this.#removeDragEventListeners();
73
+ }
74
+
75
+ drag(event) {
76
+ // this.dragging = true; // For test purposes only
77
+ if(this.dragging) {
78
+ this.dragEvent.frame = this.element.getBoundingClientRect();
79
+ this.dragEvent.previousScrollY = this.dragEvent.currentScrollY;
80
+ this.dragEvent.currentScrollY = this.#relativeCursorPosition(event).y;
81
+ this.dragEvent.currentScrollYTime = this.#timeAtPosition(this.dragEvent.currentScrollY);
82
+ this.dragEvent.currentFrameY = event.clientY - this.dragEvent.frame.top;
83
+ this.dragEvent.event = event;
84
+
85
+ this.#invokeDragMethodFor(this.dragEvent.type); // calls dragCreate, dragMove or dragResize
86
+
87
+ if(this.dragEvent.type != undefined && this.dragEvent.type != "rescale") {
88
+ this.checkScroll();
89
+ }
90
+ }
91
+ }
92
+
93
+ //////////////////
94
+ // Drag Methods //
95
+ //////////////////
96
+
97
+ dragCreate(event) {
98
+ let timeBoundaries = this.#timeBoundaries(this.dragEvent.currentScrollYTime.clipped, this.dragEvent.originScrollYTime.clipped);
99
+ let eventDiv = this.#createEventDiv(timeBoundaries.start, timeBoundaries.end);
100
+ this.#setDragEventTarget(eventDiv);
101
+ this.dragEvent.type = "resize";
102
+ }
103
+
104
+ dragResize(event) {
105
+ let timeBoundaries = this.#timeBoundaries(this.dragEvent.currentScrollYTime.clipped, this.dragEvent.originEventStartTime.real);
106
+ this.#updateEventDiv(this.dragEvent.target, timeBoundaries.start, timeBoundaries.end);
107
+ }
108
+
109
+ dragMove(event) {
110
+ let timeChange = (this.dragEvent.currentScrollYTime.clipped - this.dragEvent.originScrollYTime.clipped);
111
+ let updatedStartTime = new Date(this.dragEvent.originEventStartTime.real.getTime() + timeChange)
112
+ let updatedEndTime = new Date(this.dragEvent.originEventEndTime.real.getTime() + timeChange);
113
+
114
+ let timeBoundaries = this.#timeBoundaries(updatedStartTime, updatedEndTime);
115
+
116
+ this.#updateEventDiv(this.dragEvent.target, timeBoundaries.start, timeBoundaries.end);
117
+ }
118
+
119
+ dragRescale(event) {
120
+ let delta = this.dragEvent.previousScrollY - this.dragEvent.currentScrollY;
121
+ // let currentScale = this.element.style.getPropertyValue("--hour-scale");
122
+ let newScale = Math.max(30, Math.min(300, this.scale - (delta)));
123
+ this.element.style.setProperty("--hour-scale", newScale);
124
+ let scaleChange = newScale / this.scale;
125
+
126
+ this.scrollHeight = this.element.scrollHeight - 20;
127
+ this.scale = newScale;
128
+ this.setClipSize(newScale);
129
+
130
+ this.eventTargets.forEach((event) => {
131
+ event.style.setProperty("height", `${event.offsetHeight * (scaleChange)}px`);
132
+ event.style.setProperty("top", `${event.offsetTop * (scaleChange)}px`);
133
+ this.#setSpecialClasses(event);
134
+ });
135
+ }
136
+
137
+ ////////////////////
138
+ // Scroll Methods //
139
+ ////////////////////
140
+
141
+ checkScroll() {
142
+ // TODO - Refactor
143
+ if(this.dragEvent.currentFrameY < 30) {
144
+ let distance = Math.abs(this.dragEvent.currentFrameY - 30);
145
+ let direction = -1;
146
+ this.setScroll(distance, direction);
147
+ } else if (this.dragEvent.currentFrameY > this.dragEvent.frame.height - 30) {
148
+ let distance = Math.abs(this.dragEvent.currentFrameY - (this.dragEvent.frame.height - 30));
149
+ let direction = +1;
150
+ this.setScroll(distance, direction);
151
+ } else {
152
+ this.stopScroll();
153
+ }
154
+ }
155
+
156
+ static SCROLL_SPEEDS = [
157
+ { max: 30, speed: 1 },
158
+ { max: 50, speed: 2 },
159
+ { max: 100, speed: 3 },
160
+ { max: 150, speed: 5 },
161
+ { max: 250, speed: 10 },
162
+ { max: Infinity, speed: 15}
163
+ ]
164
+
165
+ setScroll(distance, direction) {
166
+ this.scrollEvent.step = (this.constructor.SCROLL_SPEEDS.find(range => distance < range.max)?.speed || 10) * direction;
167
+
168
+ // if(distance < 30) {
169
+ // this.scrollEvent.step = 1 * direction;
170
+ // } else if(distance < 50) {
171
+ // this.scrollEvent.step = 2 * direction;
172
+ // } else if(distance < 70) {
173
+ // this.scrollEvent.step = 3 * direction;
174
+ // }
175
+
176
+ if(!this.scrollEvent.timeout) {
177
+ this.scroll();
178
+ }
179
+ }
180
+
181
+ scroll() {
182
+ this.scrollEvent.timeout = setTimeout(() => {
183
+ this.element.scrollTop += this.scrollEvent.step;
184
+ if(this.element.scrollTop == 0) {
185
+ this.insertTimestampAbove();
186
+ }
187
+
188
+ if(this.element.scrollTop + this.element.clientHeight >= this.scrollHeight) {
189
+ this.insertTimestampBelow();
190
+ }
191
+
192
+ this.scroll();
193
+ this.drag(this.dragEvent.event);
194
+ }, 1);
195
+ }
196
+
197
+ insertTimestampAbove() {
198
+ }
199
+
200
+ insertTimestampBelow() {
201
+ // this.dragEvent.inserting = true;
202
+ // Add 1 hour
203
+ let time = new Date(this.endTime.getTime());
204
+ time.setHours(time.getHours() + 1);
205
+ this.endTime = time;
206
+ let timestampDiv = this.timestamp(time);
207
+ console.log(timestampDiv);
208
+ this.element.querySelector(".timestamps").appendChild(timestampDiv);
209
+ this.scrollHeight = this.element.scrollHeight - 20;
210
+ }
211
+
212
+ timestamp(time) {
213
+ let timestampDiv = document.createElement("div");
214
+ timestampDiv.classList.add("timestamp");
215
+ timestampDiv.classList.add("out-of-bounds");
216
+ timestampDiv.setAttribute("data-time-table-target", "timestamp");
217
+ timestampDiv.setAttribute("data-action", "mousedown->time-table#startDragEvent");
218
+ timestampDiv.setAttribute("data-time-table-drag-event-param", "rescale");
219
+
220
+ timestampDiv.innerHTML = `
221
+ <div class="label" data-action="mousedown->time-table#startDragEvent" data-time-table-drag-event-param="rescale">
222
+ ${this.#timeString(time, "short")}
223
+ </div>
224
+ `;
225
+
226
+ return timestampDiv;
227
+ }
228
+
229
+ stopScroll() {
230
+ if(this.scrollEvent.timeout) {
231
+ clearTimeout(this.scrollEvent.timeout);
232
+ this.scrollEvent = {};
233
+ }
234
+ }
235
+
236
+ /////////////////////
237
+ // Time & Position //
238
+ /////////////////////
239
+
240
+ #relativeCursorPosition(event) {
241
+ return {
242
+ x: event.clientX - this.dragEvent.frame.left,
243
+ y: (event.clientY - this.dragEvent.frame.top - 20) + this.element.scrollTop
244
+ };
245
+ }
246
+
247
+ #relativeElementPosition(element) {
248
+ let rect = element.getBoundingClientRect();
249
+ return {
250
+ x: rect.left - this.dragEvent.frame.left,
251
+ y: (rect.top - this.dragEvent.frame.top - 20) + this.element.scrollTop
252
+ };
253
+ }
254
+
255
+ #timeBoundaries(timeA, timeB) {
256
+ let startTime = new Date(Math.min(timeA, timeB));
257
+ let endTime = new Date(Math.max(timeA, timeB));
258
+
259
+ if (endTime - startTime < this.clipSize * 60 * 1000) {
260
+ endTime = new Date(startTime.getTime() + (this.clipSize * 60 * 1000));
261
+ }
262
+
263
+ return { start: startTime, end: endTime };
264
+ }
265
+
266
+ #oldTimeAtPosition(y) {
267
+ let realSecond = ((y / this.scrollHeight) * this.duration) + 1;
268
+ let clippedSecond = (Math.floor((realSecond / 60).toFixed(0) / this.clipSize) * this.clipSize) * 60;
269
+
270
+ // No need for seconds and minutes, should convert to one unit only, probably seconds or milliseconds
271
+ return {
272
+ real: new Date(this.startTime.getTime() + (realSecond * 1000)), // For some reason 1 second is added to real each time you change it.
273
+ clipped: new Date(this.startTime.getTime() + (clippedSecond * 1000)),
274
+ }
275
+ }
276
+
277
+ #timeAtPosition(y) {
278
+ let scopeOrdinal = Math.trunc(y / this.scale); // Eg "Hours from start"
279
+ let positionInScope = (y % this.scale) / this.scale;
280
+ let scopeMeasure = Math.trunc(positionInScope * this.metricConversion)
281
+ let clippedScopeMeasure = Math.floor(scopeMeasure / this.clipSize) * this.clipSize;
282
+ let millisecondsFromStart = (scopeOrdinal * this.timeScope) + (scopeMeasure * this.timeMetric);
283
+ let clippedMillisecondsFromStart = (scopeOrdinal * this.timeScope) + (clippedScopeMeasure * this.timeMetric);
284
+
285
+ return {
286
+ real: new Date(this.startTime.getTime() + millisecondsFromStart), // For some reason 1 second is added to real each time you change it.
287
+ clipped: new Date(this.startTime.getTime() + clippedMillisecondsFromStart),
288
+ }
289
+ }
290
+
291
+ #positionOfTime(time) {
292
+ let timeFromStart = time - this.startTime;
293
+ let scopeFromStart = Math.floor(timeFromStart / this.timeScope);
294
+ let metricFromScope = Math.floor((timeFromStart % this.timeScope) / this.timeMetric);
295
+ let pixelsFromStart = (scopeFromStart * this.scale) + (metricFromScope * this.scale / this.metricConversion);
296
+ return pixelsFromStart;
297
+ }
298
+
299
+ #oldPositionOfTime(time) {
300
+ let positionInDuration = (time - this.startTime) / (this.endTime - this.startTime + 1000);
301
+ let positionInCanvas = (positionInDuration * this.scrollHeight)
302
+ return positionInCanvas;
303
+ }
304
+
305
+ /////////////////////////////////////////
306
+ // Event Methods //
307
+ // (eventually switch to use template) //
308
+ /////////////////////////////////////////
309
+
310
+ #createEventDiv(startTime, endTime){
311
+ const eventDiv = document.createElement("div");
312
+
313
+ eventDiv.classList.add("event");
314
+ eventDiv.setAttribute("data-time-table-target", "event");
315
+ eventDiv.setAttribute("data-action", "mousedown->time-table#startDragEvent");
316
+ eventDiv.setAttribute("data-time-table-drag-event-param", "move");
317
+
318
+ eventDiv.innerHTML = `
319
+ <div class="event-name"><span style="font-weight: 400; color: gray;">(No Title)</span></div>
320
+ <div class="event-time" data-action="mousedown->time-table#startDragEvent" data-time-table-drag-event-param="resize">
321
+ ${ this.#durationString(startTime, endTime) }
322
+ </div>
323
+ `;
324
+
325
+ this.#updateEventDiv(eventDiv, startTime, endTime);
326
+ this.eventsListTarget.appendChild(eventDiv);
327
+
328
+ return eventDiv;
329
+ }
330
+
331
+ #updateEventDiv(target, startTime, endTime) {
332
+ let startLocation = this.#positionOfTime(startTime);
333
+ let endLocation = this.#positionOfTime(endTime);
334
+
335
+ target.style.setProperty("top", `${startLocation}px`);
336
+ target.style.setProperty("height", `${endLocation - startLocation}px`);
337
+
338
+ this.#setSpecialClasses(target);
339
+
340
+ target.querySelector(".event-time").innerHTML = this.#durationString(startTime, endTime);
341
+ }
342
+
343
+ #setSpecialClasses(eventDiv) {
344
+ eventDiv.classList.toggle("tiny", eventDiv.offsetHeight < 15);
345
+ eventDiv.classList.toggle("small", eventDiv.offsetHeight < 45);
346
+ }
347
+
348
+ /////////////////////
349
+ // Time Formatters //
350
+ /////////////////////
351
+
352
+ #timeString(date, format = "long") {
353
+ let hours = date.getUTCHours();
354
+ let minutes = date.getUTCMinutes();
355
+ let ampm = hours >= 12 ? 'PM' : 'AM';
356
+ hours = hours % 12;
357
+ hours = hours ? hours : 12; // the hour '0' should be '12'
358
+
359
+ minutes = minutes < 10 ? '0' + minutes : minutes;
360
+ if(format == "long") {
361
+ return hours + ':' + minutes + ' ' + ampm;
362
+ } else if (format == "short") {
363
+ return hours + ampm
364
+ }
365
+ }
366
+
367
+ #durationString(startTime, endTime) {
368
+ return `${this.#timeString(startTime)} - ${this.#timeString(endTime)}`;
369
+ }
370
+
371
+ ////////////////
372
+ // Invokation //
373
+ ////////////////
374
+
375
+ #invokeDragMethodFor(type) {
376
+ if(!type) {
377
+ // console.error("No drag type provided");
378
+ return;
379
+ }
380
+ let capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
381
+
382
+ if (this[`drag${capitalizedType}`]) {
383
+ this[`drag${capitalizedType}`]();
384
+ } else {
385
+ console.error(`No drag method found for type: ${type}`);
386
+ }
387
+ }
388
+
389
+ /////////////
390
+ // Helpers //
391
+ /////////////
392
+
393
+ static SCALE_RANGES = [
394
+ { max: 40, clipSize: 60 },
395
+ { max: 50, clipSize: 30 },
396
+ { max: 150, clipSize: 15 },
397
+ { max: 280, clipSize: 5 },
398
+ { max: Infinity, clipSize: 1}
399
+ ]
400
+
401
+ setClipSize(scale) {
402
+ this.clipSize = this.constructor.SCALE_RANGES.find(range => scale < range.max)?.clipSize || 15;
403
+ }
404
+
405
+ #setDragEventTarget(target) {
406
+ if(target) {
407
+ this.dragEvent.target = target;
408
+ this.dragEvent.originEventPosition = this.#relativeElementPosition(target);
409
+ this.dragEvent.originEventStartTime = this.#timeAtPosition(this.dragEvent.originEventPosition.y);
410
+ this.dragEvent.originEventEndTime = this.#timeAtPosition(this.dragEvent.originEventPosition.y + target.offsetHeight);
411
+ }
412
+ }
413
+
414
+ #setDragEventListeners() {
415
+ document.addEventListener("mousemove", this.drag.bind(this));
416
+ document.addEventListener("mouseup", this.stopDragEvent.bind(this));
417
+ document.addEventListener("mouseleave", this.stopDragEvent.bind(this));
418
+ }
419
+
420
+ #removeDragEventListeners() {
421
+ document.removeEventListener("mousemove", this.drag.bind(this));
422
+ document.removeEventListener("mouseup", this.stopDragEvent.bind(this));
423
+ document.removeEventListener("mouseleave", this.stopDragEvent.bind(this));
424
+ }
425
+ }
426
+
427
+ // this.dragEvent = new DragEvent();
428
+ // this.scrollFrame = new ScrollFrame(this.element);
429
+
430
+
431
+ // class DragHandler {
432
+ // constructor() {
433
+ // this.dragging = false;
434
+ // this.dragEvent = {};
435
+ // }
436
+
437
+ // startDrag(event) {
438
+ // event.preventDefault();
439
+ // event.stopPropagation();
440
+
441
+ // this.dragging = true;
442
+ // this.dragEvent.reset();
443
+ // this.dragEvent.type = event.params.dragEvent;
444
+ // }
445
+
446
+ // stopDrag(event) {
447
+ // event.preventDefault();
448
+ // this.dragging = false;
449
+ // this.dragEvent = {};
450
+ // }
451
+
452
+ // }
453
+
454
+
455
+ // this.scrollFrame = new ScrollFrame(this.element);
456
+ // this.scrollFrame.startBoundary = 0;
457
+
458
+ // Major Purpose - Calculate position of of a coordinate on screen and convert it to position relative to frame and scroll of element.
459
+ class ScrollFrame {
460
+ constructor(element, { origin = 0 }) {
461
+ this.element = element;
462
+ this.updateFrame();
463
+ this.origin = origin;
464
+ // this.endBoundary = this.element.scrollHeight;
465
+ }
466
+
467
+ get frame() {
468
+ return this.element.getBoundingClientRect();
469
+ }
470
+
471
+ updateFrame() {
472
+ // console.log("Updating frame");
473
+ this.frame = this.element.getBoundingClientRect();
474
+ }
475
+
476
+ relativeCursorPosition(event) {
477
+ return this.relativePosition(event.clientX, event.clientY);
478
+ }
479
+
480
+ relativePosition(x, y) {
481
+ return {
482
+ x: this.relativeXPosition(x),
483
+ y: this.relativeYPosition(y)
484
+ }
485
+ }
486
+
487
+ relativeScrollPosition(x, y) {
488
+ return {
489
+ x: this.relativeXScrollPosition(x),
490
+ y: this.relativeYScrollPosition(y)
491
+ }
492
+ }
493
+
494
+ relativeFramePosition(x, y) {
495
+ return {
496
+ x: this.relativeXFramePosition(x),
497
+ y: this.relativeYFramePosition(y)
498
+ }
499
+ }
500
+
501
+ relativeYPosition(y) {
502
+ return {
503
+ inFrame: this.relativeYFramePosition(y),
504
+ inScroll: this.relativeYScrollPosition(y)
505
+ }
506
+ }
507
+
508
+ relativeXPosition(x) {
509
+ return {
510
+ inFrame: this.relativeXFramePosition(x),
511
+ inScroll: this.relativeXScrollPosition(x)
512
+ }
513
+ }
514
+
515
+ relativeYFramePosition(y) {
516
+ return y - this.frame.top;
517
+ }
518
+
519
+ relativeYScrollPosition(y) {
520
+ return (this.relativeYFramePosition(y) - this.origin) + this.element.scrollTop;
521
+ }
522
+
523
+ relativeXFramePosition(x) {
524
+ return x - this.frame.left;
525
+ }
526
+
527
+ relativeXScrollPosition(x) {
528
+ return this.relativeXFramePosition(x) + this.element.scrollLeft;
529
+ }
530
+
531
+ #relativeYPosition(y) {
532
+ // return {
533
+ // frame:
534
+ // }
535
+ }
536
+
537
+ #relativePosition(x, y) {
538
+ // return
539
+ }
540
+
541
+
542
+ #relativeCursorPosition(event) {
543
+ this.updateFrame();
544
+ return {
545
+ x: event.clientX - this.dragEvent.frame.left,
546
+ y: (event.clientY - this.dragEvent.frame.top - 20) + this.element.scrollTop
547
+ };
548
+ }
549
+
550
+ #relativeElementPosition(element) {
551
+ this.updateFrame();
552
+ let rect = element.getBoundingClientRect();
553
+ return {
554
+ x: rect.left - this.dragEvent.frame.left,
555
+ y: (rect.top - this.dragEvent.frame.top - 20) + this.element.scrollTop
556
+ };
557
+ }
558
+ }
559
+
560
+ // relativePosition = {
561
+ // frame: {
562
+ // x: event.clientX - this.dragEvent.frame.left,
563
+ // y: (event.clientY - this.dragEvent.frame.top - 20) + this.element.scrollTop
564
+ // },
565
+ // scroll: {
566
+ // x: event.clientX - this.dragEvent.frame.left,
567
+ // y: (event.clientY - this.dragEvent.frame.top - 20) + this.element.scrollTop
568
+ // }
569
+ // }