· 6 years ago · Jan 13, 2020, 05:14 PM
1/*!
2 * jQuery Timespace Plugin
3 * Author: Michael S. Howard
4 * Email: codingadvent@gmail.com
5 * License: MIT
6 */
7
8/*global jQuery*/
9'use strict';
10
11/**
12 * jQuery Timespace Plugin
13 * Important: This Plugin uses features that are not supported by any Internet Explorer version.
14 * @author Michael S. Howard
15 * @requires jQuery 1.7+
16 * @param $ The jQuery object
17 * @param global The global Window object
18 * @return void
19 */
20(($, global) => {
21
22 // When in debug mode, errHandler will throw the Error
23 const debug = false;
24
25 /**
26 * The Time Event Object Type
27 * @typedef {Object} TimeEvent
28 * @property {number} start The start time for the event
29 * @property {number?} end The optional end time for the event
30 * @property {string} title The text for the event title
31 * @property {string?|jQuery} description The optional text or jQuery Object for the event description
32 * @property {number?} width The optional width for the event <p> element
33 * @property {bool} noDetails If the time event should not have a display
34 (If noDetails and a description string exists, it will be used for the event's title attribute)
35 * @property {string} class The optional CSS class to use for the event's <p> element
36 * @property {Function?} callback The optional callback to run on event selection
37 The callback Cannot be an arrow function if calling any API methods within the callback
38 */
39 /**
40 * The Time Heading Object Type
41 * @typedef {Object} TimeHeading
42 * @property {number} start The start time for the heading
43 * @property {number} end The end time for the heading / Optional only for the last heading
44 * @property {string} title The text for the heading
45 */
46 /**
47 * The Data Object Type
48 * @typedef {Object} Data
49 * @property {TimeHeading[]} headings The array of heading objects
50 * @property {TimeEvent[]]} events The array of event objects
51 */
52 /**
53 * The ControlText Object Type
54 * @typedef {Object} ControlText
55 * @property {string} navLeft The title text for the left navigation arrow
56 * @property {string} navRight The title text for the right navigation arrow
57 * @property {string} drag The title text for the time table
58 * @property {string} eventLeft The title text for the display box previous event arrow
59 * @property {string} eventRight The title text for the display box next event arrow
60 */
61 /**
62 * The Default Options Object Type
63 * @typedef {Object} Defaults
64 * @property {Data|string|null} data The data to use for the Timespace instance, or a URL for loading the data object with jQuery.get()
65 * @property {number} startTime The starting time of the time table
66 * @property {number} endTime The ending time of the time table
67 * @property {number} markerAmount The amount of time markers to use (0 to calculate from startTime, endTime, and markerIncrement)
68 * @property {number} markerIncrement The amount of time each marker spans
69 * @property {number} markerWidth The width of each time marker td element (0 to calculate from maxWidth and markerAmount)
70 * @property {number} maxWidth The maximum width for the time table container
71 * @property {number} maxHeight The maximum height for the time table container
72 * @property {number} navigateAmount The amount of pixels to move the time table on navigation (0 to disable)
73 * @property {number} dragXMultiplier The multiplier to use with navigateAmount when dragging the time table horizontally
74 * @property {number} dragYMultiplier The multiplier to use with navigateAmount when dragging the time table vertically
75 * @property {number} selectedEvent The index number of the event to start on (0 for first event, -1 to disable)
76 * @property {bool} shiftOnEventSelect If the time table should shift when an event is selected
77 * @property {bool} scrollToDisplayBox If the window should scroll to the display box on event selection
78 (only applies if the time table height is greater than the window height, and if the event has a description)
79 * @property {Object} customEventDisplay The jQuery Object of the element to use for the display box
80 * @property {string} timeType Use 'hour' or 'date' for the type of time being used
81 * @property {bool} use12HourTime If using 12-Hour time (e.g. '2:00 PM' instead of '14:00')
82 * @property {bool} useTimeSuffix If a suffix should be added to the displayed time (e.g. '12 AM' or '300 AD')
83 No time suffix is used if timeType is hour and use12HourTime is false
84 * @property {Function} timeSuffixFunction A function that receives the lowercase suffix string and returns a formatted string
85 * @property {ControlText} controlText The object of title texts for the various control elements
86 */
87 const defaults = {
88 data: null,
89 startTime: 0,
90 endTime: 24,
91 markerAmount: 0,
92 markerIncrement: 1,
93 markerWidth: 100,
94 maxWidth: 1000,
95 maxHeight: 280,
96 navigateAmount: 400,
97 dragXMultiplier: 1,
98 dragYMultiplier: 1,
99 selectedEvent: 0,
100 shiftOnEventSelect: true,
101 scrollToDisplayBox: true,
102 customEventDisplay: null,
103 timeType: 'hour',
104 use12HourTime: true,
105 useTimeSuffix: true,
106 timeSuffixFunction: s => ' ' + s[0].toUpperCase() + s[1].toUpperCase(),
107 controlText: {
108 navLeft: 'Move Left',
109 navRight: 'Move Right',
110 drag: 'Drag',
111 eventLeft: 'Previous Event',
112 eventRight: 'Next Event',
113 },
114 };
115
116 /** The error constants for error handling */
117 const errors = {
118 NULL: { code: '', msg: '' },
119 OPTS : { code: '001', msg: 'Invalid options argument supplied to the jQuery Timespace Plugin.' },
120 CALLBACK: { code: '002', msg: 'Invalid callback function supplied to the jQuery Timespace Plugin.' },
121 DATA_ERR: { code: '003', msg: 'Failure to load the Timespace data URL.' },
122 INV_INSTANCE: { code: '002', msg: 'The Timespace Plugin instance is invalid.' },
123 INV_EVENT_CB: { code: '010', msg: 'Invalid callback supplied for event in data argument.' },
124 INV_HEADING_START: { code: '011', msg: 'A heading\'s start time is less than the Timespace start time.' },
125 INV_HEADING_END: { code: '012', msg: 'A heading\'s end time is greater than the Timespace end time.' },
126 EVENT_OOR: { code: '013', msg: 'An event\'s start time is outside of the Timespace start and end time range.' },
127 };
128
129 /**
130 * The error handler for the Plugin
131 * @param {Error} err The Error object (used for line number where error occurred)
132 * @param {string} name The error name in the errors constant
133 * @param {Object} target The jQuery object to display the error
134 * @throws {Error} Only in debug mode
135 * @return void
136 */
137 const errHandler = (err, name, target) => {
138
139 target = (!target) ? $('body') : target;
140
141 const e = (errors.hasOwnProperty(name)) ? errors[name] : errors.NULL,
142 msg = 'An error has occurred. ' + e.code + ': ' + e.msg;
143
144 let errElem = $(`<p class="jqTimespaceError">${msg}</p>`),
145 errExists = (target) ? (target.find('.jqTimespaceError').length > 0) : false;
146
147 if (debug) {
148 throw err;
149 } else {
150
151 if (errExists) {
152 target.find('.jqTimespaceError').text(msg);
153 } else {
154 target.prepend(errElem);
155 }
156
157 }
158
159 };
160
161 const classes = {
162 animated: 'jqTimespaceAnimated',
163 column: 'jqTimespaceColumn',
164 dummySpan: 'jqTimespaceDummySpan',
165 event: 'jqTimespaceEvent',
166 eventBorder: 'jqTimespaceEventBorder',
167 eventRev: 'jqTimespaceEventRev',
168 eventSelected: 'jqTimespaceEventSelected',
169 heading: 'jqTimespaceHeading',
170 noDisplay: 'jqTimespaceNoDisplay',
171 shifting: 'jqTimespaceShifting',
172 timeframe: 'jqTimespaceTimeframe',
173 };
174
175 let inst = [],
176 Timespace = null,
177 API = null,
178 APILoader = null,
179 utility = null;
180
181 /**
182 * jQuery Timespace Plugin Method
183 * @param {Defaults} options The Plugin options
184 * @param {Function} callback A callback function to execute on completion
185 If using URL for plugin data and it fails to load, the callback will receive the jqxhr object.
186 * @return {Object} The jQuery object used to call this method
187 */
188 $.fn.timespace = function (options, callback) {
189
190 if ($.isFunction(options)) {
191
192 callback = options;
193 options = {};
194
195 }
196 if (options && !$.isPlainObject(options)) {
197
198 errHandler(new Error(errors.OPTS.msg), 'OPTS', $(this[0]));
199 return this;
200
201 }
202 if (callback && !$.isFunction(callback)) {
203
204 errHandler(new Error(errors.CALLBACK.msg), 'CALLBACK', $(this[0]));
205 callback = $.noop;
206
207 }
208
209 // Create the instance
210 $.data(this, 'Timespace', Object.create(Timespace));
211
212 if (typeof options.data === 'string') {
213
214 // Use Async loader for URL data
215 inst.push($.data(this, 'Timespace').loadAsync(
216 this, options, callback || $.noop)
217 );
218
219 } else {
220
221 // Store and load the instance, and run the callback
222 inst.push($.data(this, 'Timespace').load(this, options));
223 if (callback) { callback.call(inst[inst.length - 1]['API']); }
224
225 }
226
227 return this;
228
229 };
230
231 /***************************/
232 /* Timespace Plugin Object */
233 /***************************/
234
235 /*
236 * DO NOT INITIATE VALUES WITH OBJECTS OR ARRAYS,
237 * OR THEY WILL BE SHARED BY INSTANCES
238 */
239 Timespace = {
240
241 options: null,
242 data: null,
243 API: null,
244
245 // Calculations
246 totalTime: 0,
247 markers: null,
248 shiftXEnabled: true,
249 shiftYEnabled: true,
250 shiftPosX: null,
251 shiftPosY: null,
252 shiftDirX: '=',
253 shiftDirY: '=',
254 shiftDiffX: 0,
255 shiftDiffY: 0,
256 lastMousePosX: 0,
257 lastMousePosY: 0,
258 navInterval: null,
259 transition: -1,
260 transitionEase: null,
261 viewData: null,
262
263 // Elements
264 container: '<div class="jqTimepsaceContainer"></div>',
265 error: '<div class="jqTimespaceErrors"></div>',
266 titleClamp: '<div class="jqTimespaceTitleClamp"></div>',
267 timeTableLine: '<div class="jqTimespaceLine"></div>',
268 navLeft: '<div class="jqTimespaceLeft"><</div>',
269 navRight: '<div class="jqTimespaceRight">></div>',
270 dataContainer: '<div class="jqTimespaceDataContainer"></div>',
271 timeTable: '<aside></aside>',
272 timeTableHead: '<header></header>',
273 timeTableBody: '<section></section>',
274 display: '<article class="jqTimespaceDisplay"></article>',
275 displayWrapper: '<div></div>',
276 displayTitle: '<h1></h1>',
277 displayTimeDiv: '<div class="jqTimespaceDisplayTime"></div>',
278 displayTime: '<time></time>',
279 displayBody: '<section></section>',
280 displayLeft: '<div class="jqTimespaceDisplayLeft"><</div>',
281 displayRight: '<div class="jqTimespaceDisplayRight">></div>',
282 displayObserver: null,
283 timeMarkers: null,
284 timeEvents: null,
285 wideHeadings: null,
286 curWideHeading: null,
287 curEvent: null,
288
289 /**
290 * The main method to load the Plugin with async data
291 * @param {Object} target The jQuery Object that the plugin was called on
292 * @param {Object} options The user-defined options
293 * @param {Function} callback The callback to run when loaded
294 * @return {Object} The Plugin instance
295 */
296 loadAsync: function (target, options, callback) {
297
298 const id = inst.length;
299
300 $.get(options.data, (data) => {
301
302 options.data = data;
303 this.load(target, options, id);
304 callback.call(this.API);
305
306 }).fail((err) => {
307
308 errHandler(new Error(err.status + ': ' + err.statusText + '. '
309 + errors.DATA_ERR.msg), 'DATA_ERR');
310 callback.call(this.API, err);
311
312 });
313
314 return this;
315
316 },
317
318 /**
319 * The main method to load the Plugin
320 * @param {Object} target The jQuery Object that the plugin was called on
321 * @param {Object} options The user-defined options
322 * @param {Number?} id The optional instance id
323 * @return {Object} The Plugin instance
324 */
325 load: function (target, options, id) {
326
327 let opts = {};
328
329 this.API = new APILoader((!utility.isEmpty(id)) ? id : inst.length);
330 this.options = Object.assign(opts, defaults, options);
331 this.data = opts.data || {};
332 this.totalTime = (opts.endTime - opts.startTime) || 1;
333 this.navInterval = {
334 dir: 'left',
335 timer: null,
336 engaged: false,
337 };
338
339 // Setup Base Elements
340 this.container = $(this.container).appendTo(target)
341 .on('resize.jqTimespace', this.updateDynamicData.bind(this));
342 this.error = $(this.error).appendTo(this.container);
343 this.dataContainer = $(this.dataContainer)
344 .css({
345 maxWidth: opts.maxWidth,
346 maxHeight: opts.maxHeight,
347 })
348 .appendTo(this.container);
349 this.navLeft = $(this.navLeft)
350 .attr('title', opts.controlText.navLeft)
351 .appendTo(this.dataContainer);
352 this.navRight = $(this.navRight)
353 .attr('title', opts.controlText.navRight)
354 .appendTo(this.dataContainer);
355 this.titleClamp = $(this.titleClamp).appendTo(this.dataContainer);
356
357 // Values are updated once elements are built
358 this.viewData = {
359 left: 0,
360 top: 0,
361 width: 0,
362 height: 0,
363 heightOverhang: 0,
364 halfX: 0,
365 halfY: 0,
366 offsetX: 0,
367 offsetY: 0,
368 tableWidth: 0,
369 tableOffsetX: 0,
370 tableOffsetY: 0,
371 shiftOriginX: 0,
372 shiftOriginY: 0,
373 };
374
375 this.calculateMarkers()
376 .buildTimeTable()
377 .buildTimeEvents()
378 .buildTimeDisplay()
379 .updateStaticData()
380 .updateDynamicData()
381 .setDOMEvents();
382
383 // Select first event if needed & prevent scrolling / or hide display
384 if (this.timeEvents.length > 0 && opts.selectedEvent >= 0) {
385 this.timeEvents.eq(opts.selectedEvent).trigger('mouseup', [true]);
386 } else {
387 this.display.hide();
388 }
389
390 return this;
391
392 },
393
394 /**
395 * Calculate the amount and width needed for time markers
396 * @return {Object} The Plugin instance
397 */
398 calculateMarkers: function () {
399
400 const opts = this.options;
401
402 if (opts.markerAmount === 0) {
403 opts.markerAmount = (Math.floor(this.totalTime / opts.markerIncrement)) || 0;
404 }
405 if (opts.markerWidth === 0) {
406 opts.markerWidth = (Math.floor(opts.maxWidth / opts.markerAmount)) || 100;
407 }
408
409 return this;
410
411 },
412
413 /**
414 * Build the time table
415 * @return {Object} The Plugin instance
416 */
417 buildTimeTable: function () {
418
419 let opts = this.options;
420
421 // Time table width is used to force marker widths
422 this.viewData.tableWidth = opts.markerAmount * opts.markerWidth || 'auto';
423 this.timeTableLine = $(this.timeTableLine)
424 .attr('title', opts.controlText.drag)
425 .appendTo(this.dataContainer);
426 this.timeTable = $(this.timeTable)
427 .width(this.viewData.tableWidth)
428 .appendTo(this.dataContainer);
429 this.timeTableHead = $(this.timeTableHead)
430 .attr('title', opts.controlText.drag)
431 .appendTo(this.timeTable);
432 this.timeTableBody = $(this.timeTableBody)
433 .attr('title', opts.controlText.drag)
434 .appendTo(this.timeTable);
435
436 this.buildTimeHeadings()
437 .buildTimeMarkers();
438
439 this.viewData.width = Math.ceil(this.dataContainer.innerWidth());
440 this.viewData.left = Math.ceil(this.dataContainer.offset().left);
441
442 return this;
443
444 },
445
446 /**
447 * Build the heading titles for the time markers
448 * @return {Object} The Plugin instance
449 */
450 buildTimeHeadings: function () {
451
452 const opts = this.options;
453
454 let h1 = `<h1><span class="${classes.heading}"></span></h1>`,
455 dummy = `<div class="${classes.dummySpan}"></div>`,
456 headings = $('<div></div>'),
457 curSpan = 0;
458
459 this.wideHeadings = $();
460
461 if (this.data.headings) {
462 this.data.headings.forEach((v, i, a) => {
463
464 const start = parseFloat(v.start),
465 title = utility.sanitize(v.title);
466 let end = (utility.isEmpty(v.end)) ? null : parseFloat(v.end);
467
468 // Check for timeline start and heading start error
469 if (opts.startTime > start) {
470 errHandler(new Error(errors.INV_HEADING_START.msg), 'INV_HEADING_START', this.error);
471 }
472
473 // Create dummy span before first heading if needed
474 if (i === 0 && utility.compareTime(start, opts.startTime, opts.markerIncrement) === 1) {
475
476 curSpan = utility.getTimeSpan(start, opts.startTime, opts.markerIncrement, opts.markerWidth);
477 headings.append(
478 $(dummy).width(curSpan)
479 );
480
481 }
482
483 // Create dummy span to cover time in between headings if needed
484 if (i > 0 && utility.compareTime(start, a[i - 1]['end'], opts.markerIncrement) === 1) {
485
486 curSpan = utility.getTimeSpan(start, a[i - 1]['end'], opts.markerIncrement, opts.markerWidth);
487 headings.append(
488 $(dummy).width(curSpan)
489 );
490
491 }
492
493 // Check heading end time
494 if (utility.isEmpty(end)) {
495 end = opts.endTime;
496 } else if (end > opts.endTime) {
497
498 errHandler(new Error(errors.INV_HEADING_END.msg), 'INV_HEADING_END', this.error);
499 end = opts.endTime;
500
501 }
502
503 // Add current heading
504 curSpan = utility.getTimeSpan(start, end, opts.markerIncrement, opts.markerWidth) || 0;
505 headings.append(
506 $(h1).children('span').text(title)
507 .end().width(curSpan)
508 );
509
510 // Check if heading needs a title clamp
511 if (curSpan > opts.maxWidth * 1.75) {
512 this.wideHeadings = this.wideHeadings.add(
513 headings.children().last().data({
514 span: curSpan,
515 textSpan: 0, // Updated after headings are appended to table
516 })
517 );
518 }
519
520 // Create dummy span to cover ending if needed
521 if (i === a.length - 1
522 && utility.compareTime(end, opts.endTime, opts.markerIncrement) === -1) {
523
524 // Create ending dummy span
525 curSpan = utility.getTimeSpan(end, opts.endTime, opts.markerIncrement, opts.markerWidth);
526 headings.append(
527 $(dummy).width(curSpan)
528 );
529
530 }
531
532 });
533 }
534
535 if (headings.length > 0) {
536
537 headings.appendTo(this.timeTableHead);
538 this.titleClamp.css('top', headings.innerHeight() / 2);
539
540 }
541
542 // Update heading text widths for any wide headings
543 this.wideHeadings.each(function (i, elem) {
544 $(elem).data('textSpan', $(elem).children('span').outerWidth());
545 });
546
547 return this;
548
549 },
550
551 /**
552 * Build the time markers
553 * @return {Object} The Plugin instance
554 */
555 buildTimeMarkers: function () {
556
557 const opts = this.options;
558 let curTime = opts.startTime,
559 markers = $('<div></div>');
560
561 this.markers = []; // The header time markers
562 this.timeMarkers = $(); // The section divs that hold the event boxes
563
564 // Iterate and build time markers using increment
565 for (let i = 0; i < opts.markerAmount; i += 1) {
566
567 curTime = (i === 0) ? opts.startTime : curTime + opts.markerIncrement;
568 this.markers.push(curTime);
569 this.timeMarkers = this.timeMarkers.add(
570 $(`<div class="${classes.column}"></div>`).width(opts.markerWidth)
571 );
572 markers.append(
573 $(`<time>${this.getDisplayTime(curTime)}</time>`).width(opts.markerWidth)
574 );
575
576 }
577
578 markers.appendTo(this.timeTableHead);
579 this.timeMarkers.appendTo(this.timeTableBody);
580
581 return this;
582
583 },
584
585 /**
586 * Build the time table events
587 * @return {Object} The Plugin instance
588 */
589 buildTimeEvents: function () {
590
591 let opts = this.options,
592 markers = this.markers,
593 events = $(),
594 rowData = {
595 rows: [],
596 curRow: 0,
597 marginOrigin: 0,
598 marginTop: 0,
599 event: null,
600 eventElem: null,
601 };
602
603 if (this.data.events) {
604 this.data.events.forEach((v, i) => {
605
606 const start = parseFloat(v.start) || null,
607 end = parseFloat(v.end) || null,
608 title = utility.sanitize(v.title),
609 description = (v.description instanceof $)
610 ? v.description
611 : (!utility.isEmpty(v.description))
612 ? $(`<p>${utility.sanitize(v.description)}</p>`)
613 : $(),
614 width = parseInt(v.width),
615 noDetails = !!v.noDetails,
616 evtClass = (!utility.isEmpty(v.class))
617 ? ` class="${utility.sanitize(v.class)}"` : '',
618 eventCallback = (utility.isEmpty(v.callback))
619 ? $.noop : v.callback.bind(this.API);
620
621 rowData.event = $(`<div class="${classes.event}"></div>`);
622 rowData.eventElem = $(`<p${evtClass}><span>${title}</span></p>`).prependTo(rowData.event);
623
624 const rounded = utility.roundToIncrement('floor', opts.markerIncrement, start),
625 index = markers.indexOf(rounded),
626 eventElemSpan = rowData.eventElem.children('span');
627
628 if (!$.isFunction(eventCallback)) {
629
630 errHandler(new Error(errors.INV_EVENT_CB.msg), 'INV_EVENT_CB', this.error);
631 rowData.eventElem.data('eventCallback', $.noop);
632
633 }
634
635 if (start < opts.startTime || start > opts.endTime) {
636 errHandler(new Error(errors.EVENT_OOR.msg), 'EVENT_OOR', this.error);
637 }
638
639 let pos = 0,
640 eventOffset = 0,
641 eventOverhang = false,
642 eventWidth = 0,
643 eventElemWidth = 0,
644 eventElemSpanWidth = 0;
645
646 if (index >= 0) {
647
648 // Find the position based on percentage of starting point to the increment amount
649 pos = (((start - markers[index]) / opts.markerIncrement) * opts.markerWidth);
650 rowData.event.css('left', pos).appendTo(this.timeMarkers[index]);
651 eventOffset = Math.floor(rowData.event.offset().left);
652
653 // Immediately invoke arrow function to return best width
654 eventElemWidth = (() => {
655
656 const endWidth = (end) ? ((end - start) / opts.markerIncrement) * opts.markerWidth : 0;
657
658 let styles = [
659 parseFloat(rowData.eventElem.css('borderLeftWidth')) || 0,
660 parseFloat(rowData.eventElem.css('borderRightWidth')) || 0,
661 parseFloat(rowData.eventElem.css('paddingLeft')) || 0,
662 parseFloat(rowData.eventElem.css('paddingRight')) || 0,
663 ],
664 extra = styles.reduce((t, v) => t + v), // Add all style values
665 tableLength = this.viewData.tableWidth + this.getTablePosition(true),
666 result = opts.markerWidth - extra;
667
668 eventElemSpanWidth = eventElemSpan.width();
669 eventOverhang = (tableLength < eventOffset + eventElemSpanWidth + extra);
670
671 if (eventOverhang) {
672 result = eventElemSpanWidth; // Text width
673 } else if (width) {
674 result = width - extra; // User-defined width
675 } else if (eventElemSpanWidth > endWidth - extra && eventElemSpanWidth > result) {
676 result = eventElemSpanWidth; // Text width
677 } else if (endWidth - extra > result) {
678 result = endWidth - extra; // Timespan width
679 }
680
681 return result;
682
683 })();
684
685 rowData.eventElem.width(eventElemWidth)
686 .data({
687 time: this.getFullDate(start, end),
688 title: title,
689 description: description,
690 noDetails: noDetails,
691 eventCallback: eventCallback,
692 })
693 .attr('title', rowData.eventElem.data('time'));
694
695 events = events.add(rowData.eventElem);
696 eventWidth = rowData.eventElem.outerWidth();
697 rowData.event.width(eventWidth);
698
699 // Prevent display for noDetails, and use description on event title
700 if (noDetails) {
701
702 rowData.event.addClass(classes.noDisplay);
703 rowData.eventElem.attr('title', (i, t) => (!utility.isEmpty(description.text()))
704 ? `${t} - ${description.text()}` : t
705 );
706
707 } else {
708
709 $(`<div class="${classes.eventBorder}"
710 style="left:${eventOffset - this.viewData.left - 1}px;"></div>`)
711 .appendTo(this.timeMarkers[index]);
712
713 }
714
715 // Reverse event if it extends past the time table width
716 if (eventOverhang) {
717
718 rowData.event.css('left', pos - eventWidth)
719 .addClass(classes.eventRev);
720 eventOffset = Math.floor(rowData.event.offset().left);
721 rowData.event.next('.' + classes.eventBorder)
722 .css('left', eventOffset - this.viewData.left + eventWidth - 1);
723
724 }
725
726 this.updateEventOverlap(i, rowData, eventOffset);
727
728 // Change offset to start at table offset in case of window resize
729 eventOffset -= this.viewData.left;
730
731 // Update event's span position if the event width extends the container viewport
732 if (eventElemWidth > this.viewData.width) {
733 this.container.on('shiftX.jqTimespace', () => {
734 this.updateWideEvent(
735 eventOffset,
736 eventElemWidth,
737 eventElemSpan,
738 eventElemSpanWidth
739 );
740 });
741 }
742
743 }
744
745 });
746 }
747
748 if (events.length <= 1) {
749 this.displayLeft.add(this.displayRight).hide();
750 }
751
752 this.timeEvents = events;
753
754 return this;
755
756 },
757
758 /**
759 * Build the time display
760 * @return {Object} The Plugin instance
761 */
762 buildTimeDisplay: function () {
763
764 const opts = this.options;
765
766 this.display = (opts.customEventDisplay)
767 ? $(this.display).appendTo($(opts.customEventDisplay))
768 : $(this.display).appendTo(this.container)
769 .css('maxWidth', opts.maxWidth);
770 this.displayWrapper = $(this.displayWrapper).appendTo(this.display);
771 this.displayTitle = $(this.displayTitle).appendTo(this.displayWrapper);
772 this.displayTimeDiv = $(this.displayTimeDiv).appendTo(this.displayWrapper);
773 this.displayLeft = $(this.displayLeft)
774 .attr('title', opts.controlText.eventLeft)
775 .appendTo(this.displayTimeDiv);
776 this.displayTime = $(this.displayTime).appendTo(this.displayTimeDiv);
777 this.displayRight = $(this.displayRight)
778 .attr('title', opts.controlText.eventRight)
779 .appendTo(this.displayTimeDiv);
780 this.displayBody = $(this.displayBody).appendTo(this.displayWrapper);
781
782 this.displayObserver = (new MutationObserver(
783 this.updateDisplayHeight.bind(this)
784 )).observe(this.displayWrapper[0], {
785 childList: true,
786 subtree: true,
787 });
788
789 this.updateDisplayHeight();
790
791 return this;
792
793 },
794
795 /**
796 * Set up the element DOM events
797 * @return {Object} The Plugin instance
798 */
799 setDOMEvents: function () {
800
801 const ts = this;
802
803 // Window Events
804 $(global).on('mouseup touchend', () => {
805
806 $(global).off('mousemove.jqTimespace touchmove.jqTimespace');
807
808 // Clear nav button interval if needed
809 this.clearNavInterval();
810
811 // Run timeShift once more on completion and animate movement
812 if (this.timeTable.hasClass(classes.shifting)) {
813
814 this.setTimeShiftState(false);
815 this.timeShift(null, null, true, true);
816
817 }
818
819 }).on('resize', () => {
820
821 this.container.trigger('resize.jqTimespace');
822
823 });
824
825 // Navigation Events
826 this.navLeft.on('mousedown', () => {
827
828 if (this.options.navigateAmount > 0) {
829
830 this.updateDynamicData()
831 .setNavInterval('left');
832
833 }
834 });
835 this.navRight.on('mousedown', () => {
836 if (this.options.navigateAmount > 0) {
837
838 this.updateDynamicData()
839 .setNavInterval('right');
840
841 }
842 });
843
844 // Time Table Events
845 this.timeTable
846 .add(this.timeTableLine)
847 .add(this.titleClamp)
848 .on('mousedown touchstart', (e) => {
849
850 e.preventDefault();
851 let touch = utility.getTouchCoords(e);
852
853 if (this.shiftXEnabled || this.shiftYEnabled) {
854
855 this.lastMousePosX = (touch) ? touch.x : e.pageX;
856 this.lastMousePosY = (touch) ? touch.y : e.pageY;
857 this.updateDynamicData()
858 .setTimeShiftState(true);
859
860 $(global).on('mousemove.jqTimespace touchmove.jqTimespace', (e) => {
861
862 e.preventDefault();
863 this.timeShift(e);
864
865 });
866
867 }
868
869 });
870
871 // Event Marker Events
872 this.timeEvents.each(function () {
873
874 const elem = $(this);
875
876 if (!elem.data('noDetails')) {
877 elem.on('mouseup touchend', (e, preventScroll) => {
878
879 // Allow if event is not selected and time table has not shifted too much
880 if (!elem.hasClass(classes.eventSelected)
881 && Math.abs(ts.viewData.shiftOriginX - ts.getTablePosition()) < 10
882 && Math.abs(ts.viewData.shiftOriginY - ts.getTableBodyPosition()) < 10) {
883
884 ts.updateDynamicData()
885 .displayEvent(elem, preventScroll);
886
887 }
888
889 });
890 }
891
892 });
893
894 // Event Display Nav
895 this.displayLeft.on('click', () => {
896
897 const len = -ts.timeEvents.length,
898 index = ts.timeEvents.index(ts.curEvent);
899
900 // Check for the next or previous event that doesn't have noDetails
901 if (index >= 0) {
902 for (let i = -1; i >= len; i -= 1) {
903 if (!ts.timeEvents.eq(index + i).data('noDetails')) {
904
905 ts.updateDynamicData()
906 .displayEvent(ts.timeEvents.eq(index + i));
907 break;
908
909 }
910 }
911 }
912
913 });
914 this.displayRight.on('click', () => {
915
916 const len = ts.timeEvents.length;
917 let index = ts.timeEvents.index(ts.curEvent);
918
919 // Check for the next event that doesn't have noDetails
920 if (index >= 0) {
921 for (let i = 1; i <= len; i += 1) {
922 // If reached the end of collection, start again at 0 (index + i === 0)
923 if (index + i === len) { index = -i; }
924 if (!ts.timeEvents.eq(index + i).data('noDetails')) {
925
926 ts.updateDynamicData()
927 .displayEvent(ts.timeEvents.eq(index + i));
928 break;
929
930 }
931 }
932 }
933
934 });
935
936 return this;
937
938 },
939
940 /**
941 * Set up navigation interval for holding down left or right nav buttons
942 * @param {string} dir 'left' or 'right'
943 * @return {Object} The Plugin instance
944 */
945 setNavInterval: function (dir) {
946
947 this.navInterval.dir = dir;
948 this.navigate(dir, -1);
949 this.navInterval.timer = setInterval(() => {
950
951 this.navInterval.engaged = true;
952 this.navigate(dir, -1, 'linear');
953
954 }, 200);
955
956 return this;
957
958 },
959
960 /**
961 * Clear navigation interval
962 * @return {Object} The Plugin instance
963 */
964 clearNavInterval: function () {
965
966 if (this.navInterval.timer) {
967
968 clearInterval(this.navInterval.timer);
969 this.navInterval.timer = null;
970
971 if (this.navInterval.engaged) {
972
973 this.navInterval.engaged = false;
974 this.navigate((this.navInterval.dir === 'left')
975 ? -this.options.markerWidth : this.options.markerWidth, -1);
976
977 }
978
979 }
980
981 return this;
982
983 },
984
985 /**
986 * Navigate the time table in a direction or by a specified amount
987 * @param {string|number|Array} direction 'left', 'right', a positive or negative amount, or [x, y]
988 * @param {number} duration The duration in seconds, or -1
989 * @param {string?} ease The transition ease type
990 * @param {bool?} isTableShift If the direction amount is the actual time table shiftPos
991 * @return {Object} The Plugin instance
992 */
993 navigate: function (dir, duration, ease, isTableShift) {
994
995 let x = dir,
996 y = 0,
997 shift = null;
998
999 this.transition = duration;
1000 this.transitionEase = ease;
1001 this.setTimeShiftState(false);
1002
1003 if (Array.isArray(dir)) {
1004
1005 x = dir[0];
1006 y = dir[1];
1007
1008 }
1009
1010 if (typeof x === 'number') {
1011 if (isTableShift) {
1012
1013 // Shifting time table
1014 this.shiftDirX = (x > 0) ? '>' : '<';
1015 this.shiftPosX = x;
1016
1017 } else {
1018
1019 // Navigating by an amount
1020 this.shiftDirX = (x > 0) ? '<' : '>';
1021 this.shiftPosX = this.getTablePosition() - x;
1022
1023 }
1024 } else {
1025
1026 // If direction is left, the time table is shifted to a greater amount
1027 if (shift === null) { shift = [0, 0]; }
1028 shift[0] = (x === 'left') ? '>' : '<';
1029
1030 }
1031 if (y) {
1032 if (typeof y === 'number') {
1033
1034 this.shiftDirY = (y > 0) ? '<' : '>';
1035 this.shiftPosY = this.getTableBodyPosition() - y;
1036
1037 } else {
1038
1039 // If direction is up, the time table is shifted to a greater amount
1040 if (shift === null) { shift = [0, 0]; }
1041 shift[1] = (y === 'up') ? '>' : '<';
1042
1043 }
1044 }
1045
1046 this.timeShift(null, shift, true);
1047
1048 return this;
1049
1050 },
1051
1052 /**
1053 * Set the time table and container states for shifting
1054 * @return {Object} The Plugin instance
1055 */
1056 setTimeShiftState: function (on) {
1057
1058 const elems = this.dataContainer
1059 .add(this.timeTable)
1060 .add(this.timeTableBody)
1061 .add(this.timeEvents.map(function () {
1062 return $(this).find('span')[0];
1063 }));
1064
1065 // Reset Transitions
1066 elems.removeClass(classes.animated)
1067 .css({
1068 transitionDuration: '',
1069 transitionTimingFunction: '',
1070 });
1071
1072 if (on) {
1073
1074 this.timeTable.addClass(classes.shifting);
1075 this.transition = -1; // Reset the custom transition duration
1076
1077 } else {
1078
1079 elems.addClass(classes.animated);
1080 this.timeTable.removeClass(classes.shifting);
1081
1082 // Check if custom transition time is used
1083 if (this.transition >= 0) {
1084 elems.css('transitionDuration', this.transition + 's');
1085 }
1086
1087 // Check if custom transition ease is used
1088 if (!utility.isEmpty(this.transitionEase)) {
1089 elems.css('transitionTimingFunction', this.transitionEase);
1090 }
1091
1092 }
1093
1094 return this;
1095
1096 },
1097
1098 /**
1099 * Shift the time table
1100 * @param {Object?} e The jQuery Event object if available
1101 * @param {Array?} nav The x and y directions to shift '<' or '>', '^' or 'v'
1102 * @param {bool?} finished If the shift is finished
1103 * @param {bool?} toss If the time table should be tossed on quick movement
1104 * @return {Object} The Plugin instance
1105 */
1106 timeShift: function (e, nav, finished, toss) {
1107
1108 if (e === null) {
1109 e = { pageX: 0, pageY: 0 };
1110 }
1111
1112 const opts = this.options,
1113 canShiftX = this.shiftXEnabled,
1114 canShiftY = this.shiftYEnabled;
1115
1116 if (!canShiftX && !canShiftY) { return this; }
1117
1118 let touch = (!finished) ? utility.getTouchCoords(e) : null,
1119 x = (touch) ? touch.x : e.pageX,
1120 y = (touch) ? touch.y : e.pageY;
1121
1122 if (Array.isArray(nav)) {
1123
1124 if (nav[0]) {
1125
1126 this.shiftDirX = nav[0];
1127 this.shiftPosX = (nav[0] === '<') ? this.getTablePosition() - opts.navigateAmount
1128 : this.getTablePosition() + opts.navigateAmount;
1129
1130 }
1131 if (nav[1]) {
1132
1133 this.shiftDirY = nav[1];
1134 this.shiftPosY = (nav[1] === '<') ? this.getTableBodyPosition() - opts.navigateAmount
1135 : this.getTableBodyPosition() + opts.navigateAmount;
1136
1137 }
1138
1139 }
1140 if (canShiftX) {
1141
1142 this.timeShiftPos('X', toss)
1143 .updateCurWideHeading(
1144 (this.shiftPosX !== null) ? parseInt(this.timeTable.css('left')) - this.shiftPosX : 0
1145 )
1146 .timeShiftCache('X', x, finished);
1147
1148 }
1149 if (canShiftY) {
1150
1151 this.timeShiftPos('Y', toss)
1152 .timeShiftCache('Y', y, finished);
1153
1154 }
1155
1156 return this;
1157
1158 },
1159
1160 /**
1161 * Apply the new position to the time table
1162 * @param {string} plane 'X' or 'Y'
1163 * @param {bool} toss If the time table should be tossed on quick movement
1164 * @return {Object} The Plugin instance
1165 */
1166 timeShiftPos: function (plane, toss) {
1167
1168 if (this['shiftPos' + plane] === null) { return this; }
1169
1170 const isX = plane === 'X',
1171 target = (isX) ? 'timeTable' : 'timeTableBody',
1172 shiftPos = 'shiftPos' + plane,
1173 shiftDir = 'shiftDir' + plane,
1174 shiftDiff = 'shiftDiff' + plane,
1175 tableOffset = 'tableOffset' + plane,
1176 css = (isX) ? 'left' : 'top';
1177
1178 // Add to the final shift position if tossing
1179 if (toss) { this[shiftPos] += this[shiftDiff] * 10; }
1180
1181 // Time table must be moved within bounds
1182 if ((this[shiftDir] === '<' && this[shiftPos] >= -this.viewData[tableOffset])
1183 || (this[shiftDir] === '>' && this[shiftPos] <= 0)) {
1184
1185 this[target].css(css, this[shiftPos] + 'px');
1186
1187 } else if (this[shiftDir] === '<' && this[shiftPos] < -this.viewData[tableOffset]) {
1188
1189 this[shiftPos] = -this.viewData[tableOffset];
1190 this[target].css(css, -this.viewData[tableOffset] + 'px');
1191
1192 } else if (this[shiftDir] === '>' && this[shiftPos] > 0) {
1193
1194 this[shiftPos] = 0;
1195 this[target].css(css, 0);
1196
1197 }
1198
1199 if (isX) { this.container.trigger('shiftX.jqTimespace'); }
1200
1201 return this;
1202
1203 },
1204
1205 /**
1206 * Cache new position for next mousemove event
1207 * @param {string} plane 'X' or 'Y'
1208 * @param {number} val The x or y value
1209 * @param {bool} finished If the time shift is finished
1210 * @return {Object} The Plugin instance
1211 */
1212 timeShiftCache: function (plane, val, finished) {
1213
1214 const isX = (plane === 'X'),
1215 lastMousePos = 'lastMousePos' + plane,
1216 shiftPos = 'shiftPos' + plane,
1217 shiftDir = 'shiftDir' + plane,
1218 shiftDiff = 'shiftDiff' + plane,
1219 dragMultiplier = `drag${plane}Multiplier`,
1220 posMethod = (isX) ? 'getTablePosition' : 'getTableBodyPosition';
1221
1222 let dir = 0;
1223
1224 if (val !== this[lastMousePos] && !finished) {
1225
1226 dir = val - this[lastMousePos];
1227 this[shiftDiff] = dir;
1228 this[shiftPos] = this[posMethod]() + (dir * this.options[dragMultiplier]);
1229 this[shiftDir] = (dir < 0) ? '<' : '>';
1230 this[lastMousePos] = val;
1231
1232 } else {
1233 this[shiftPos] = null;
1234 }
1235
1236 return this;
1237
1238 },
1239
1240 /**
1241 * Display a time event
1242 * @param {Object} elem The time event jQuery element
1243 * @param {bool} preventScroll If the height overhang scroll should be prevented
1244 * @return {Object} The Plugin instance
1245 */
1246 displayEvent: function (elem, preventScroll) {
1247
1248 let top = elem.offset().top;
1249
1250 this.curEvent = elem;
1251 this.timeEvents.removeClass(classes.eventSelected);
1252 this.display.show();
1253 this.displayTitle.html(elem.data('title'));
1254 this.displayBody.empty()
1255 .append(elem.data('description'));
1256 elem.addClass(classes.eventSelected);
1257
1258 if (!utility.isEmpty(elem.data('time'))) {
1259
1260 this.displayTime.text(elem.data('time'))
1261 .addClass(classes.timeframe);
1262
1263 } else {
1264 this.displayTime.removeClass(classes.timeframe);
1265 }
1266
1267 if (this.options.scrollToDisplayBox
1268 && !preventScroll
1269 && this.viewData.heightOverhang
1270 && elem.data('description').length > 0) {
1271
1272 // Scroll to the Event Display Box if it has a description
1273 $('html, body').animate({ scrollTop: this.display.offset().top });
1274
1275 }
1276
1277 if (this.options.shiftOnEventSelect) {
1278
1279 // Shift the time table to the selected event
1280 this.navigate([
1281 this.timeTableLine.position().left - elem.parents('div').position().left,
1282 top - (this.viewData.offsetY - this.viewData.halfY),
1283 ], -1, null, true);
1284
1285 }
1286
1287 elem.data('eventCallback')();
1288
1289 return this;
1290
1291 },
1292
1293 /**
1294 * Get the time table's left position
1295 * @param {bool} offset If the offset is needed
1296 * @return {number}
1297 */
1298 getTablePosition: function (offset) {
1299 return parseFloat((offset) ? this.timeTable.offset().left : this.timeTable.css('left'));
1300 },
1301
1302 /**
1303 * Get the time table body's top position
1304 * @return {number}
1305 */
1306 getTableBodyPosition: function () {
1307 return parseFloat(this.timeTableBody.css('top'));
1308 },
1309
1310 /**
1311 * Get a time string appropriate for displaying
1312 * @param {number} time The time integer
1313 * @return {string|null}
1314 */
1315 getDisplayTime: function (time) {
1316
1317 if (!utility.isEmpty(time)) {
1318
1319 return this.getTime(time)
1320 + this.getMinutes(time)
1321 + this.getTimeSuffix(time);
1322
1323 }
1324
1325 return time;
1326
1327 },
1328
1329 /**
1330 * Get the hours of a time, or the date
1331 * @param {number} time
1332 * @return {string|any}
1333 */
1334 getTime: function (time) {
1335
1336 if (this.options.timeType === 'hour') {
1337 return utility.getHours(time, !this.options.use12HourTime);
1338 } else if (this.options.timeType === 'date') {
1339 // Correct if time is 0 AD
1340 return (time === 0) ? 1 : Math.abs(time);
1341 }
1342
1343 return time;
1344
1345 },
1346
1347 /**
1348 * Get the minutes of a time, or an empty string if not using hour type
1349 * @param {number} time
1350 * @return {string}
1351 */
1352 getMinutes: function (time) {
1353
1354 if (this.options.timeType === 'hour') {
1355 return ':' + utility.getMinutes(time);
1356 }
1357
1358 return '';
1359
1360 },
1361
1362 /**
1363 * Get the time suffix for the time
1364 * @param {number} time
1365 * @return {string}
1366 */
1367 getTimeSuffix: function (time) {
1368
1369 const opts = this.options;
1370
1371 if (opts.useTimeSuffix) {
1372
1373 if (opts.timeType === 'hour') {
1374 if (opts.use12HourTime) {
1375 return (time < 12) ? opts.timeSuffixFunction('am')
1376 : opts.timeSuffixFunction('pm');
1377 }
1378 } else if (opts.timeType === 'date') {
1379 return (time < 0) ? opts.timeSuffixFunction('bc')
1380 : opts.timeSuffixFunction('ad');
1381 }
1382 }
1383
1384 return '';
1385
1386 },
1387
1388 /**
1389 * Get the full start and end date string
1390 * @param {number} start The start date with the suffix
1391 * @param {number} end The end date with the suffix
1392 * @return {string}
1393 */
1394 getFullDate: function (start, end) {
1395
1396 let time = (!utility.isEmpty(start)) ? this.getDisplayTime(start) : '';
1397 time += (!utility.isEmpty(end) && end !== start) ? ` – ${this.getDisplayTime(end)}` : '';
1398
1399 return time;
1400
1401 },
1402
1403 /**
1404 * Update the static container data
1405 * @return {Object} The Plugin instance
1406 */
1407 updateStaticData: function () {
1408
1409 this.viewData.height = Math.ceil(this.dataContainer.innerHeight());
1410 this.viewData.halfY = Math.ceil(this.viewData.height / 2);
1411 this.viewData.tableOffsetY = this.timeTable.outerHeight() - this.dataContainer.outerHeight();
1412
1413 return this;
1414
1415 },
1416
1417 /**
1418 * Update the dynamic container and time table data
1419 * @return {Object} The Plugin instance
1420 */
1421 updateDynamicData: function () {
1422
1423 this.viewData.left = Math.ceil(this.dataContainer.offset().left);
1424 this.viewData.offsetY = this.viewData.top + this.viewData.height;
1425 this.viewData.top = Math.ceil(this.dataContainer.offset().top);
1426 this.viewData.width = Math.ceil(this.dataContainer.innerWidth());
1427 this.viewData.halfX = Math.ceil(this.viewData.width / 2);
1428 this.viewData.heightOverhang = (this.dataContainer.outerHeight() > $(global).height() * 0.8);
1429 this.viewData.offsetX = this.viewData.left + this.viewData.width;
1430 this.viewData.shiftOriginX = this.getTablePosition();
1431 this.viewData.shiftOriginY = this.getTableBodyPosition();
1432 this.viewData.tableOffsetX = this.timeTable.outerWidth() - this.dataContainer.outerWidth();
1433
1434 // Check if time table is too small to shift
1435 if (this.viewData.tableOffsetX < 0) {
1436
1437 this.shiftXEnabled = false;
1438 this.timeTable.css('margin', '0 auto');
1439 this.timeTableLine.hide();
1440 this.navLeft.hide();
1441 this.navRight.hide();
1442
1443 } else {
1444
1445 this.shiftXEnabled = true;
1446 this.timeTable.css('margin', 0);
1447 this.timeTableLine.show();
1448
1449 if (this.options.navigateAmount > 0) {
1450
1451 this.navLeft.show();
1452 this.navRight.show();
1453
1454 }
1455
1456 }
1457 if (this.viewData.tableOffsetY < 0) {
1458 this.shiftYEnabled = false;
1459 }
1460
1461 this.updateCurWideHeading();
1462
1463 return this;
1464
1465 },
1466
1467 /**
1468 * Update the currently visible wide heading
1469 * @param {number} xDiff The shift x difference if time table is shifting
1470 * @return {Object} The Plugin instance
1471 */
1472 updateCurWideHeading: function (xDiff) {
1473
1474 if (!this.checkCurWideHeading(null, xDiff) && this.wideHeadings.length > 0) {
1475 this.wideHeadings.each((i, elem) => {
1476 this.setCurWideHeading($(elem), xDiff);
1477 });
1478 }
1479
1480 return this;
1481
1482 },
1483
1484 /**
1485 * Check if the current wide heading is still in visible bounds
1486 * @param {Object?} elem The optional jQuery heading element
1487 * @param {number} xDiff The shift x difference if time table is shifting
1488 * @return {bool}
1489 */
1490 checkCurWideHeading: function (elem, xDiff) {
1491
1492 const e = elem || this.curWideHeading;
1493
1494 if (!e || e.length < 1) { return false; }
1495
1496 const left = e.offset().left - (xDiff || 0),
1497 textSpan = e.data('textSpan');
1498
1499 return ((left + e.data('span') - textSpan - this.viewData.halfX) > this.viewData.left
1500 && (left + textSpan + this.viewData.halfX) < this.viewData.offsetX);
1501
1502 },
1503
1504 /**
1505 * Set the currently visible wide heading
1506 * @param {Object} elem The jQuery heading element
1507 * @param {number} xDiff The shift x difference if time table is shifting
1508 * @return {Object} The Plugin instance
1509 */
1510 setCurWideHeading: function (elem, xDiff) {
1511
1512 const span = elem.children('span');
1513
1514 if (this.checkCurWideHeading(elem, xDiff)) {
1515
1516 // Remove current title clamp if exists
1517 if (this.curWideHeading) {
1518 this.curWideHeading.children('span').css('opacity', 1);
1519 }
1520
1521 // Set up new clone title for heading clamp
1522 this.curWideHeading = elem;
1523 span.css('opacity', 0);
1524 this.titleClamp.text(span.text())
1525 .stop()
1526 .animate({ opacity: 1 }, 250);
1527
1528 } else if (this.curWideHeading
1529 && this.curWideHeading[0] === elem[0]) {
1530
1531 // Current wide heading is no longer within view range
1532 this.curWideHeading.children('span').css('opacity', 1);
1533 this.curWideHeading = null;
1534 this.titleClamp.stop().animate({ 'opacity' : 0 }, 250);
1535
1536 }
1537
1538 return this;
1539
1540 },
1541
1542 /**
1543 * Update the position of a wide event's title
1544 * @param {number} eventOffset The event container's left offset from time table
1545 * @param {number} elemWidth The event element's width
1546 * @param {Object} span The span element to position
1547 * @param {number} spanWidth The event element's span width
1548 * @return {Object} The Plugin instance
1549 */
1550 updateWideEvent: function (eventOffset, elemWidth, span, spanWidth) {
1551
1552 const leftPos = eventOffset + this.viewData.left + this.shiftPosX,
1553 newPos = this.viewData.left - leftPos;
1554
1555 if (leftPos < this.viewData.left
1556 && spanWidth <= this.viewData.width) {
1557
1558 if (newPos > elemWidth - spanWidth) {
1559 span.css('left', elemWidth - spanWidth);
1560 } else {
1561 span.css('left', newPos);
1562 }
1563
1564 } else {
1565 span.css('left', 0);
1566 }
1567
1568 return this;
1569
1570 },
1571
1572 /**
1573 * Update an event's position if overlapping other events
1574 * @param {number} i The index of the current event
1575 * @param {Object} rowData {rows, curRow, marginOrigin, marginTop, event, eventElem}
1576 * @param {number} eventOffset The event's left offset
1577 * @return {Object} The Plugin instance
1578 */
1579 updateEventOverlap: function (i, rowData, eventOffset) {
1580
1581 // Check if a jqTimespaceEvent div already exists in the time marker
1582 const sharingWith = (rowData.event.siblings(`.${classes.event}`).length > 0)
1583 ? rowData.event.siblings(`.${classes.event}`) : null,
1584 span = eventOffset + Math.floor(rowData.event.outerWidth());
1585
1586 let sharedSpace = 0;
1587
1588 if (i === 0) {
1589
1590 rowData.rows.push(span);
1591 rowData.marginOrigin = parseInt(rowData.event.css('marginTop'));
1592 rowData.marginTop = Math.floor(rowData.marginOrigin + rowData.eventElem.outerHeight());
1593
1594 } else {
1595
1596 if (sharingWith) {
1597
1598 // Event is sharing the same td with another event
1599 // Start on the next row of the shared element
1600 // And start with the basic padding
1601 sharedSpace = rowData.marginOrigin;
1602 rowData.curRow += 1;
1603
1604 // Check if rows array needs expanding
1605 if (rowData.rows.length === rowData.curRow) {
1606 rowData.rows[rowData.curRow] = 0;
1607 }
1608
1609 }
1610
1611 for (let row = (sharingWith) ? rowData.curRow : 0; row < rowData.rows.length; row += 1) {
1612
1613 if (rowData.rows[row] <= eventOffset) {
1614
1615 // Row is clear / Cache the new span width and switch to this row space
1616 rowData.rows[row] = span;
1617 rowData.curRow = row;
1618
1619 // If first row, the normal marginTop will be used
1620 // Otherwise, calculate the padding for the current row
1621 if (row > 0) {
1622 if (sharingWith) {
1623 rowData.event.css('marginTop', sharedSpace);
1624 } else {
1625 rowData.event.css('marginTop', row * rowData.marginTop + rowData.marginOrigin);
1626 }
1627 }
1628
1629 break;
1630
1631 } else {
1632
1633 // Push the event down to the next row space
1634 if (sharingWith) {
1635
1636 // Cache the amount of padding for next row check
1637 sharedSpace += rowData.marginTop;
1638 rowData.event.css('marginTop', sharedSpace);
1639
1640 } else {
1641 rowData.event.css('marginTop', (row + 1) * rowData.marginTop + rowData.marginOrigin);
1642 }
1643
1644 // If on last cached row, settle with the next row space
1645 if (row === rowData.rows.length - 1) {
1646
1647 rowData.rows[row + 1] = span;
1648 rowData.curRow = row + 1;
1649
1650 break;
1651
1652 }
1653
1654 }
1655
1656 }
1657
1658 }
1659
1660 return this;
1661
1662 },
1663
1664 /**
1665 * Update the Display element height for MutationObserver
1666 * @return {Object} The Plugin instance
1667 */
1668 updateDisplayHeight: function () {
1669
1670 this.display.css('height', this.displayWrapper.outerHeight(true));
1671
1672 },
1673
1674 };
1675
1676 /*******/
1677 /* API */
1678 /*******/
1679
1680 API = {
1681
1682 // The ID used for the isnt array to target the correct instance
1683 id: 0,
1684
1685 // Element Getters
1686 get container () {
1687
1688 const me = inst[this.id];
1689
1690 if (!utility.checkInstance(me)) { return this; }
1691 return me.container;
1692
1693 },
1694 get event () {
1695
1696 const me = inst[this.id];
1697
1698 if (!utility.checkInstance(me)) { return this; }
1699 return (me.curEvent) ? me.curEvent.parent('div') : null;
1700
1701 },
1702
1703 // Option Setters
1704 set shiftOnEventSelect (v) {
1705
1706 const me = inst[this.id];
1707
1708 if (!utility.checkInstance(me)) { return this; }
1709 me.options.shiftOnEventSelect = v;
1710
1711 },
1712 set navigateAmount (v) {
1713
1714 const me = inst[this.id];
1715
1716 if (!utility.checkInstance(me)) { return this; }
1717 me.options.navigateAmount = v;
1718
1719 },
1720
1721 /**
1722 * Navigate the time table in a direction or by a specified amount
1723 * @param {Array} direction An [x, y] Array with x = 'left' or 'right', y = 'up' or 'down', or positive or negative numbers
1724 * @param {number} duration The amount of seconds to complete the navigation animation
1725 * @return {Object} The API
1726 */
1727 navigateTime: function (direction, duration) {
1728
1729 const me = inst[this.id];
1730
1731 if (!utility.checkInstance(me)) { return this; }
1732
1733 duration = parseFloat(duration) || -1;
1734 me.navigate(direction, duration);
1735
1736 return this;
1737
1738 },
1739
1740 };
1741 APILoader = function (id) { this.id = id; };
1742 APILoader.prototype = API;
1743 APILoader.prototype.constructor = APILoader;
1744
1745 /***********/
1746 /* Utility */
1747 /***********/
1748
1749 utility = {
1750
1751 /**
1752 * Round time up or down to the increment
1753 * @param {string} fn The Math function to use
1754 * @param {number} increment The time marker increment
1755 * @param {number} number The number to round
1756 * @return {Array} The rounded number
1757 */
1758 roundToIncrement: function (fn, increment, number) {
1759
1760 return Math[fn](number / increment) * increment;
1761
1762 },
1763
1764 /**
1765 * Get the amount of span width for a start and end time
1766 * @param {number} start The start time
1767 * @param {number} end The end time
1768 * @param {number} increment The time marker increment
1769 * @param {number} width The time marker width
1770 * @return {number|NaN}
1771 */
1772 getTimeSpan: function (start, end, increment, width) {
1773
1774 start = this.roundToIncrement('round', increment, start);
1775 end = this.roundToIncrement('round', increment, end);
1776
1777 return Math.abs(Math.floor((end - start) / increment)) * width;
1778
1779 },
1780
1781 /**
1782 * Compare two time numbers for less than, equal to, or greater than
1783 * @param {number} time1 The first time to compare
1784 * @param {number} time2 The second time to compare
1785 * @param {number} increment The time marker increment
1786 * @return {number|NaN} -1 if time1 is less than time2, 0 if equal, and 1 if greater than
1787 */
1788 compareTime: function (time1, time2, increment) {
1789
1790 time1 = this.roundToIncrement('round', increment, time1);
1791 time2 = this.roundToIncrement('round', increment, time2);
1792
1793 if (time1 < time2) { return -1; }
1794 if (time1 > time2) { return 1; }
1795
1796 return 0;
1797
1798 },
1799
1800 /**
1801 * Get the hours string from a time value
1802 * @param {number} time
1803 * @return {string}
1804 */
1805 getHours: function (time, military) {
1806
1807 time = parseInt(time);
1808
1809 if (isNaN(time)) {
1810 time = '';
1811 } else {
1812 if (military && time < 10) {
1813 // Pad 0 for military time
1814 time = '0' + time;
1815 } else if (!military && time < 1) {
1816 // Use 12 for 12AM
1817 time = 12;
1818 } else if (!military && time >= 13) {
1819 // Convert to 12 Hour Time
1820 time -= 12;
1821 }
1822 }
1823
1824 return time;
1825
1826 },
1827
1828 /**
1829 * Get the minutes string from a time value
1830 * @param {number} time
1831 * @return {string}
1832 */
1833 getMinutes: function (time) {
1834
1835 time = parseFloat(time) || 0;
1836 let minutes = Math.round((time % 1) * 60);
1837
1838 if (minutes < 10) { minutes = '0' + minutes; }
1839
1840 return minutes + '';
1841
1842 },
1843
1844 /**
1845 * Check if a variable is empty
1846 * @param {any} v The variable to check
1847 * @return {bool}
1848 */
1849 isEmpty: (v) => (v === null || v === undefined || v === ''),
1850
1851 /**
1852 * Sanitize a string for DOM insertion
1853 * @param {string} text The text to sanitize
1854 * @return {string}
1855 */
1856 sanitize: (text) => $('<div />').text(text).html(),
1857
1858 /**
1859 * Get the touch events coordinates if supported
1860 * @return {Object} x and y values
1861 */
1862 getTouchCoords: function (e) {
1863
1864 let origin = e.originalEvent,
1865 evt = (origin.touches && origin.touches.length === 1)
1866 ? origin.touches[0] : null,
1867 touch = {
1868 x: (evt) ? evt.pageX : 0,
1869 y: (evt) ? evt.pageY : 0,
1870 };
1871
1872 return (evt) ? touch : null;
1873
1874 },
1875
1876 /**
1877 * Check if the plugin instance is valid
1878 * @return {bool}
1879 */
1880 checkInstance: function (instance) {
1881
1882 if (!instance || !instance.API) {
1883
1884 errHandler(new Error(errors.INV_INSTANCE.msg), 'INV_INSTANCE');
1885 return false;
1886
1887 }
1888
1889 return true;
1890
1891 },
1892
1893 };
1894
1895})(jQuery, window);