├── .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 | jQuery Timespace Example 7 | 8 | 25 | 26 | 27 | 28 | 29 |

24-Hour Timeline

30 |
31 |

Event Timeline

32 |
33 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery Timespace 2 | 3 | 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. Each event in the time table can be selected to display more details about the event. 4 | >Important: This Plugin uses features that are not supported by any Internet Explorer version. 5 | 6 | ## Example 7 | 8 | See the example.html file for more details. 9 | >[Codepen Example](https://codepen.io/adventcoding/full/pLXGOO/) 10 | 11 | ## API 12 | 13 | We can call the Timespace plugin on an empty container and send in some data. See the example.html file for more information on the options argument and how to format the data. 14 | 15 | ```js 16 | $('#timeContainer').timespace(options, callback); 17 | ``` 18 | - options : The options object 19 | - callback : A callback function to execute on completion. If using a URL for the data option and it fails to load, the callback will receive the jqxhr object. 20 | 21 | ### Options 22 | 23 | | Key | Description | Default | 24 | | :---: | --- | :---: | 25 | | data | The data to use for the Timespace instance (See data variable below), or a URL for loading the data object with jQuery.get() | null | 26 | | startTime | The starting time of the time table | 0 | 27 | | endTime | The ending time of the time table | 24 | 28 | | markerAmount | The amount of time markers to use (0 to calculate from startTime, endTime, and markerIncrement) | 0 | 29 | | markerIncrement | The amount of time each marker spans | 1 | 30 | | markerWidth | The width of each time marker td element (0 to calculate from maxWidth and markerAmount) | 100 | 31 | | maxWidth | The maximum width for the time table container | 1000 | 32 | | maxHeight | The maximum height for the time table container | 280 | 33 | | navigateAmount | The amount of pixels to move the time table on navigation (0 to disable) | 400 | 34 | | dragXMultiplier | The multiplier to use with navigateAmount when dragging the time table horizontally | 1 | 35 | | dragYMultiplier | The multiplier to use with navigateAmount when dragging the time table vertically | 1 | 36 | | selectedEvent | The index number of the event to start on (0 for first event, -1 to disable) | 0 | 37 | | shiftOnEventSelect | If the time table should shift on event selection | true | 38 | | scrollToDisplayBox | If the window should scroll to the display box on event selection (only applies if the time table height is greater than the window height, and if the event has a description) | true | 39 | | customEventDisplay | The jQuery Object of the element to use for the display box | null | 40 | | timeType | Use 'hour' or 'date' for the type of time being used. Note: If using 'date', 0 AD will display as 1 AD | 'hour' | 41 | | use12HourTime | If using 12-Hour time (e.g. '2:00 PM' instead of '14:00') | true | 42 | | useTimeSuffix | If a suffix should be added to the displayed time (e.g. '12 AM' or '300 AD') - No time suffix is used if timeType is 'hour' and use12HourTime is false | true | 43 | | timeSuffixFunction | The function that receives the lowercase suffix string and returns a formatted string | s => ' ' + s[0].toUpperCase() + s[1].toUpperCase() | 44 | | controlText | The object of title texts for the various control elements | (See controlText variable below) | 45 | 46 | ```js 47 | let data = { 48 | headings: [ 49 | { 50 | // Important: The start and end times for the headings are rounded to the increment 51 | // e.g. If my increment is 10, a start time of 35 will round to 40, and 34 will round to 30. 52 | start: number, // The start time for the heading 53 | end: number, // The end time for the heading (only optional for the last heading) 54 | title: string, // The text for the heading 55 | } 56 | ], 57 | events: [ 58 | { 59 | start: number, // The start time for the event 60 | end: number, // The optional end time for the event 61 | title: string, // The text for the event title 62 | description: string||jQuery, // The optional text or jQuery Object for the event description 63 | width: number, // The optional width for the event box 64 | noDetails: bool, // If the time event should not have a display (If noDetails and a description exists, it will be used for the event's title attribute) 65 | class: string, // The optional CSS class to use for the event's

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: '
<
', 269 | navRight: '
>
', 270 | dataContainer: '
', 271 | timeTable: '', 272 | timeTableHead: '
', 273 | timeTableBody: '
', 274 | display: '
', 275 | displayWrapper: '
', 276 | displayTitle: '

', 277 | displayTimeDiv: '
', 278 | displayTime: '', 279 | displayBody: '
', 280 | displayLeft: '
<
', 281 | displayRight: '
>
', 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 = `

`, 455 | dummy = `
`, 456 | headings = $('
'), 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 = $('
'); 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 | $(`
`).width(opts.markerWidth) 571 | ); 572 | markers.append( 573 | $(``).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 | ? $(`

${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 | --------------------------------------------------------------------------------