├── .gitignore ├── .gitattributes ├── css ├── jquery.timespace.dark.css ├── jquery.timespace.light.css ├── _timespace.dark.css ├── _timespace.light.css └── _timespace.css ├── package.json ├── LICENSE ├── example.html ├── README.md └── jquery.timespace.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /css/jquery.timespace.dark.css: -------------------------------------------------------------------------------- 1 | @import '_timespace.dark.css'; 2 | @import '_timespace.css'; 3 | -------------------------------------------------------------------------------- /css/jquery.timespace.light.css: -------------------------------------------------------------------------------- 1 | @import '_timespace.light.css'; 2 | @import '_timespace.css'; 3 | -------------------------------------------------------------------------------- /css/_timespace.dark.css: -------------------------------------------------------------------------------- 1 | .jqTimepsaceContainer { 2 | --shadow: #000000; 3 | --border-primary: #cccccc; 4 | --border-secondary: #5f5f5f; 5 | --border-dull: #4e515c; 6 | --border-select: #818181; 7 | --border-transparent: rgba(0, 0, 0, 0); 8 | --bg-gradient: linear-gradient(to top, #ffffff 0%, #cacaca 20%, #474747 100%); 9 | --bg-primary: #555a64; 10 | --bg-secondary: #494e58; 11 | --bg-dull: #282c34; 12 | --bg-dull-tsp: rgba(50, 57, 68, 0.45); 13 | --bg-disabled: #22252c; 14 | --bg-error: #250000; 15 | --text-error: #ff6347; 16 | --text-primary: #ffffff; 17 | --text-secondary: #a0a9bd; 18 | --text-time: #bd4c3d; 19 | --text-nav: #ff7c6b; 20 | } 21 | -------------------------------------------------------------------------------- /css/_timespace.light.css: -------------------------------------------------------------------------------- 1 | .jqTimepsaceContainer { 2 | --shadow: #c4c4c4; 3 | --border-primary: #cccccc; 4 | --border-secondary: #d8d8d8; 5 | --border-dull: #b4b4b4; 6 | --border-select: #9e9e9e; 7 | --border-transparent: rgba(255, 255, 255, 0); 8 | --bg-gradient: linear-gradient(to top, #444444 0%, #afafaf 15%, #ffffff 100%); 9 | --bg-primary: #fff9f5; 10 | --bg-secondary: #fffaf2; 11 | --bg-dull: #ffffff; 12 | --bg-dull-transparent: rgba(255, 255, 255, 0.45); 13 | --bg-disabled: #eeeeee; 14 | --bg-error: #ffeae1; 15 | --text-error: #ac0000; 16 | --text-primary: #000000; 17 | --text-secondary: #573e29; 18 | --text-time: #cf3b00; 19 | --text-nav: #ff7c6b; 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqtimespace", 3 | "version": "1.3.1", 4 | "description": "The jQuery Timespace plugin provides a way to handle the display of event data in a horizontal table that can be dragged in all directions.", 5 | "main": "jquery.timespace.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/AdventCoding/Timespace.git" 12 | }, 13 | "keywords": [ 14 | "chart", 15 | "data", 16 | "date", 17 | "events", 18 | "ecosystem:jquery", 19 | "jquery-plugin", 20 | "schedule", 21 | "timeline", 22 | "timetable" 23 | ], 24 | "author": "Michael (https://github.com/AdventCoding/)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/AdventCoding/Timespace/issues" 28 | }, 29 | "homepage": "https://github.com/AdventCoding/Timespace#readme", 30 | "dependencies": { 31 | "jquery": ">=3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Howard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |element 66 | callback: Function, // The optional callback to run on event selection. 67 | // The callback cannot be an arrow function if calling any API methods within the callback 68 | } 69 | ] 70 | }; 71 | 72 | let controlText = { 73 | navLeft: 'Move Left', 74 | navRight: 'Move Right', 75 | drag: 'Drag', 76 | eventLeft: 'Previous Event', 77 | eventRight: 'Next Event', 78 | }; 79 | ``` 80 | 81 | ### Methods & Properties 82 | 83 | These methods and properties can only be accessed from within a callback function using the this keyword. 84 | 85 | ### .shiftOnEventSelect 86 | 87 | To set the shiftOnEventSelect option from within a callback function: 88 | ```js 89 | this.shiftOnEventSelect = true; 90 | ``` 91 | 92 | ### .navigateAmount 93 | 94 | To set the navigateAmount option from within a callback function: 95 | ```js 96 | this.navigateAmount = true; 97 | ``` 98 | 99 | ### .container 100 | 101 | To get the jQuery container element of the Timespace instance from within a callback function: 102 | ```js 103 | let myContainer = this.container; 104 | ``` 105 | 106 | ### .event 107 | 108 | To get the currently selected event's jQuery div element of the Timespace instance from within a callback function: 109 | Note: Returns null if no event is currently selected. 110 | ```js 111 | let eventBox = this.event; 112 | ``` 113 | 114 | ### .navigateTime(direction, duration) 115 | 116 | Navigate the time table in a direction or by a specified amount. 117 | - direction : An [x, y] Array with x = 'left' or 'right', y = 'up' or 'down', or positive or negative numbers 118 | - duration : The amount of seconds for the time table to animate its position 119 | 120 | ## License 121 | 122 | [MIT](https://github.com/AdventCoding/Timespace/blob/master/LICENSE) 123 | -------------------------------------------------------------------------------- /css/_timespace.css: -------------------------------------------------------------------------------- 1 | /* CSS Transition */ 2 | .jqTimespaceAnimated { 3 | transition: all 1.2s cubic-bezier(0.29, 0.575, 0.465, 1); 4 | } 5 | 6 | /* Error Display */ 7 | .jqTimespaceError { 8 | padding: 1rem; 9 | text-align: center; 10 | color: var(--text-error); 11 | background: var(--bg-error); 12 | } 13 | 14 | /* Containers */ 15 | .jqTimepsaceContainer, 16 | .jqTimepsaceContainer .jqTimespaceDataContainer { 17 | position: relative; 18 | overflow: hidden; 19 | margin: 0 auto; 20 | padding: 0; 21 | color: var(--text-primary); 22 | } 23 | .jqTimepsaceContainer .jqTimespaceDataContainer { 24 | box-shadow: 0 6px 5px -5px var(--shadow); 25 | margin: 1rem auto; 26 | border: 1px solid var(--border-secondary); 27 | -moz-user-select: none; 28 | user-select: none; 29 | background: var(--bg-dull); 30 | } 31 | 32 | /* Navigation and Line Guide */ 33 | .jqTimepsaceContainer .jqTimespaceLeft, 34 | .jqTimepsaceContainer .jqTimespaceRight { 35 | z-index: 3; 36 | position: absolute; 37 | bottom: 30%; 38 | margin: 0; 39 | border: 1px solid var(--border-dull); 40 | padding: 0.1rem 0.5rem; 41 | color: var(--text-secondary); 42 | font-size: 1.6rem; 43 | line-height: 2rem; 44 | text-align: center; 45 | cursor: pointer; 46 | background: var(--bg-dull-transparent); 47 | opacity: 0.7; 48 | } 49 | .jqTimepsaceContainer .jqTimespaceLeft { 50 | left: 0; 51 | } 52 | .jqTimepsaceContainer .jqTimespaceRight { 53 | right: 0; 54 | } 55 | .jqTimepsaceContainer .jqTimespaceLeft:hover, 56 | .jqTimepsaceContainer .jqTimespaceRight:hover { 57 | background: var(--bg-primary); 58 | opacity: 1; 59 | } 60 | .jqTimepsaceContainer .jqTimespaceLine { 61 | z-index: 3; 62 | position: absolute; 63 | box-sizing: border-box; 64 | top: 0; 65 | left: 50%; 66 | margin: 0; 67 | padding: 0; 68 | width: 3px; 69 | height: 100%; 70 | background: var(--bg-gradient); 71 | opacity: 0.15; 72 | cursor: move; 73 | } 74 | 75 | /* Timeline */ 76 | .jqTimepsaceContainer aside { 77 | position: relative; 78 | left: 0; 79 | margin: 0; 80 | padding: 0; 81 | line-break: strict; 82 | cursor: move; 83 | } 84 | .jqTimepsaceContainer header { 85 | z-index: 4; 86 | position: relative; 87 | background: var(--bg-dull); 88 | } 89 | .jqTimepsaceContainer .jqTimespaceTitleClamp { 90 | z-index: 5; 91 | display: block; 92 | position: absolute; 93 | left: 50%; 94 | transform: translateX(-50%) translateY(-50%); 95 | opacity: 0; 96 | cursor: move; 97 | } 98 | .jqTimepsaceContainer header > div { 99 | display: flex; 100 | position: relative; 101 | box-sizing: border-box; 102 | margin: 0; 103 | border-bottom: 1px solid var(--border-dull); 104 | text-align: center; 105 | } 106 | .jqTimepsaceContainer header > div:last-child { 107 | border-bottom: 1px solid var(--border-dull); 108 | text-align: left; 109 | } 110 | .jqTimepsaceContainer header h1, 111 | .jqTimepsaceContainer header time, 112 | .jqTimepsaceContainer header .jqTimespaceDummySpan { 113 | display: block; 114 | box-sizing: border-box; 115 | margin: 0; 116 | padding: 0.5rem 0; 117 | } 118 | .jqTimepsaceContainer header time { 119 | padding: 0.2rem 0 0.2rem 0.1rem; 120 | } 121 | .jqTimepsaceContainer header h1 { 122 | font-size: 1.2rem; 123 | } 124 | .jqTimepsaceContainer header h1:not(:first-child), 125 | .jqTimepsaceContainer header .jqTimespaceDummySpan:not(:first-child) { 126 | border-left: 1px solid var(--border-dull); 127 | } 128 | .jqTimepsaceContainer header .jqTimespaceDummySpan { 129 | background: var(--bg-disabled); 130 | opacity: 0.4; 131 | } 132 | .jqTimepsaceContainer header time:not(:first-child) { 133 | border-left: 1px dashed var(--border-dull); 134 | } 135 | .jqTimepsaceContainer aside section { 136 | z-index: 1; 137 | display: flex; 138 | position: relative; 139 | top: 0; 140 | min-height: 100%; 141 | } 142 | .jqTimepsaceContainer aside .jqTimespaceColumn { 143 | box-sizing: border-box; 144 | padding-bottom: 20px; 145 | } 146 | .jqTimepsaceContainer aside .jqTimespaceColumn:not(:first-child) { 147 | border-left: 1px dashed var(--border-secondary); 148 | } 149 | 150 | /* Event Boxes */ 151 | .jqTimepsaceContainer .jqTimespaceEvent { 152 | z-index: 2; 153 | position: relative; 154 | box-sizing: border-box; 155 | margin: 1rem 0 0 0; 156 | padding: 0; 157 | text-align: left; 158 | } 159 | .jqTimepsaceContainer .jqTimespaceEventRev { 160 | text-align: right; 161 | } 162 | .jqTimepsaceContainer .jqTimespaceEventBorder { 163 | z-index: 1; 164 | position: absolute; 165 | top: 0; 166 | width: 1px; 167 | height: 100%; 168 | border-left: 1px solid var(--border-transparent); 169 | } 170 | .jqTimespaceEvent:hover + .jqTimespaceEventBorder { 171 | border-left: 1px dashed var(--border-select); 172 | } 173 | .jqTimespaceEvent p { 174 | overflow: hidden; 175 | margin: 0; 176 | border: 1px solid var(--border-secondary); 177 | border-radius: 0 0.6rem 0.6rem 0; 178 | padding: 0.3rem 0.5rem; 179 | line-height: 1.6rem; 180 | font-size: 1rem; 181 | white-space: nowrap; 182 | cursor: pointer; 183 | background: var(--bg-dull); 184 | } 185 | .jqTimespaceEvent p span { 186 | position: relative; 187 | } 188 | .jqTimespaceEventRev p { 189 | border-radius: 0.6rem 0 0 0.6rem; 190 | } 191 | .jqTimespaceEvent p:hover, 192 | .jqTimespaceEvent .jqTimespaceEventSelected { 193 | border: 1px solid var(--border-select); 194 | background: var(--bg-secondary); 195 | } 196 | .jqTimespaceNoDisplay p { 197 | cursor: move; 198 | background: var(--bg-disabled); 199 | opacity: 0.6; 200 | } 201 | .jqTimespaceNoDisplay p:hover { 202 | border: 1px solid var(--border-secondary); 203 | background: var(--bg-disabled); 204 | } 205 | 206 | /* Display Box */ 207 | .jqTimespaceDisplay { 208 | box-sizing: border-box; 209 | box-shadow: 0 6px 5px -5px var(--shadow); 210 | margin: 1rem auto; 211 | border: 1px solid var(--border-secondary); 212 | padding: 0; 213 | height: 0; 214 | background: var(--bg-dull); 215 | overflow: hidden; 216 | transition: height 0.4s linear; 217 | } 218 | .jqTimespaceDisplay > div { 219 | margin: 0; 220 | padding: 1.5rem; 221 | overflow: auto; 222 | } 223 | .jqTimespaceDisplay h1 { 224 | margin: 0; 225 | padding: 0.5rem; 226 | text-align: left; 227 | font-size: 1.4rem; 228 | } 229 | .jqTimespaceDisplay .jqTimespaceDisplayTime { 230 | display: flex; 231 | position: relative; 232 | padding-bottom: 0.5rem; 233 | align-items: center; 234 | font-size: 1.1rem; 235 | } 236 | .jqTimespaceDisplay .jqTimespaceDisplayLeft, 237 | .jqTimespaceDisplay .jqTimespaceDisplayRight { 238 | width: 0.9rem; 239 | text-align: center; 240 | font-weight: bold; 241 | cursor: pointer; 242 | } 243 | .jqTimespaceDisplay .jqTimespaceDisplayLeft { 244 | margin-left: 0.5rem; 245 | } 246 | .jqTimespaceDisplay .jqTimespaceDisplayRight { 247 | margin-left: 1.2rem; 248 | } 249 | .jqTimespaceDisplay .jqTimespaceDisplayLeft:hover, 250 | .jqTimespaceDisplay .jqTimespaceDisplayRight:hover { 251 | color: var(--text-nav); 252 | } 253 | .jqTimespaceDisplay .jqTimespaceTimeframe { 254 | display: inline-block; 255 | margin: 0.2rem 0 0.2rem 1.2rem; 256 | padding: 0 0.4rem; 257 | border-left: 1px solid var(--border-primary); 258 | border-right: 1px solid var(--border-primary); 259 | color: var(--text-time); 260 | } 261 | .jqTimespaceDisplay section { 262 | margin: 0; 263 | padding: 0; 264 | max-width: 70%; 265 | } 266 | .jqTimespaceDisplay section:not(:empty) { 267 | padding: 0.8rem 0.5rem 0.5rem 0.5rem; 268 | } 269 | @media (max-width: 768px) { 270 | .jqTimespaceDisplay section { 271 | max-width: 100% 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /jquery.timespace.js: -------------------------------------------------------------------------------- 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
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
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 = $(`
${msg}
`), 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: '', 265 | error: '', 266 | titleClamp: '', 267 | timeTableLine: '', 268 | navLeft: '${utility.sanitize(v.description)}
`) 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 = $(``); 622 | rowData.eventElem = $(`${title}
`).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 | $(``) 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) => $('').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); 1896 | --------------------------------------------------------------------------------