├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── demo ├── demo.png ├── index.html └── main.js ├── jquery.skeduler.css ├── jquery.skeduler.js ├── jquery.skeduler.min.css ├── jquery.skeduler.min.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Oleg Mishenkin 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery Skeduler Plugin 2 | ====================== 3 | ### By Oleg Mishenkin, 2016 4 | 5 | This is [jQuery](http://jquery.com) plugin which provider you simple 6 | scheduler with some items on OX and 24-hours timeline on OY. 7 | 8 | Demos 9 | ----- 10 | 11 | The demo live in demo/ directory. Open demo/index.html directly in your web browser. 12 | 13 | Install 14 | ------- 15 | 16 | Install by Bower: 17 | > bower install jquery-skeduler 18 | 19 | Documentation 20 | ------------- 21 | ### Basic using 22 | 23 | The .skeduler() method can be used to create skeduler instance. 24 | > $('#mySkeduler').skeduler(options); 25 | 26 | ### Options description 27 | Options contains follow fields: 28 | * headers: string[] - array of headers 29 | * tasks: Task[] - array of tasks 30 | * containerCssClass: string - css class of main container 31 | * headerContainerCssClass: string - css class of header container 32 | * schedulerContainerCssClass: string - css class of scheduler 33 | * lineHeight - height of one half-hour cell in grid 34 | * borderWidth - width of border of cell in grid 35 | * onClick - function (e, t) {} - where e - native event args and t is object of clicked card 36 | 37 | Roadmap 38 | ------- 39 | * [x] Initialize plugin 40 | * [x] Add click event 41 | * [ ] Make better documentation 42 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-skeduler", 3 | "version": "0.1.1", 4 | "description": "This is jQuery plugin which provider you simple scheduler with some items on OX and 24-hours timeline on OY.", 5 | "main": [ 6 | "jquery.skeduler.js", 7 | "jquery.skeduler.css" 8 | ], 9 | "authors": [ 10 | "Oleg Mishenkin" 11 | ], 12 | "license": "ISC", 13 | "keywords": [ 14 | "jQuery", 15 | "skeduler", 16 | "scheduler" 17 | ], 18 | "homepage": "", 19 | "dependencies": { 20 | "jquery": ">=2.2.4" 21 | }, 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decease/jquery-skeduler/17ff6b09c6a984c8f3713c6c06ead082dd34ab0d/demo/demo.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 20 | 21 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | function generate() { 2 | var tasks = []; 3 | for (var i = 0; i < 20; i++) { 4 | var startTime = -1; 5 | var duration = 0.5; 6 | for (var j = 0; j < 10; j++) { 7 | if (Math.random() * 10 > 5) { 8 | startTime += 0.5 + duration; 9 | } else { 10 | startTime += 1 + duration; 11 | } 12 | 13 | if (Math.random() * 10 > 5) { 14 | startTime -= duration; 15 | 16 | startTime = Math.max(0, startTime); 17 | } 18 | 19 | if (startTime > 23) { 20 | break; 21 | } 22 | 23 | duration = Math.ceil(Math.random() * 2) + (Math.random() * 10 > 5 ? 0 : 0.5); 24 | 25 | duration -= startTime + duration > 24 ? (startTime + duration) - 24 : 0; 26 | 27 | var task = { 28 | startTime: startTime, 29 | duration: duration, 30 | column: i, 31 | id: Math.ceil(Math.random() * 100000), 32 | title: 'Service ' + i + ' ' + j 33 | }; 34 | 35 | tasks.push(task); 36 | } 37 | } 38 | 39 | console.log("tasks count: " + tasks.length); 40 | 41 | console.log(JSON.stringify(tasks)); 42 | 43 | $("#skeduler-container").skeduler({ 44 | headers: ["Specialist 1", "Specialist 2", "Specialist 3", "Specialist 4", "Specialist 5", "Specialist 6", "Specialist 7", "Specialist 8", "Specialist 9", "Specialist 10"], 45 | tasks: tasks, 46 | cardTemplate: '
${id}
${title}
', 47 | onClick: function (e, t) { console.log(e, t); } 48 | }); 49 | } -------------------------------------------------------------------------------- /jquery.skeduler.css: -------------------------------------------------------------------------------- 1 | 2 | .skeduler-container { 3 | font-family: Helvetica, ​Arial, ​sans-serif; 4 | } 5 | 6 | .skeduler-container * { 7 | box-sizing: content-box; 8 | } 9 | 10 | .skeduler-headers { 11 | border-left: 1px solid #b0cee9; 12 | display: flex; 13 | padding-left: 60px; 14 | position: relative; 15 | } 16 | 17 | .skeduler-headers:before { 18 | border-top: 1px solid #b0cee9; 19 | content: ""; 20 | width: 60px; 21 | position: absolute; 22 | left: 0; 23 | } 24 | 25 | .skeduler-headers > div { 26 | flex: 0 0 200px; 27 | height: 30px; 28 | padding-top: 10px; 29 | background-color: #D3E0EF; 30 | border-left: 1px solid #B0CEE9; 31 | border-bottom: 1px solid #B0CEE9; 32 | border-top: 1px solid #b0cee9; 33 | text-align: center; 34 | } 35 | 36 | .skeduler-headers > div:last-child, 37 | .skeduler-main-body > div > div.skeduler-cell { 38 | border-right: 1px solid #B0CEE9; 39 | } 40 | 41 | .skeduler-main { 42 | display: flex; 43 | } 44 | 45 | .skeduler-main-timeline { 46 | margin-top: -1px; 47 | } 48 | 49 | .skeduler-main-timeline div { 50 | width: 50px; 51 | height: 27px; 52 | text-align: left; 53 | padding-left: 10px; 54 | padding-top: 3px; 55 | color: #333333; 56 | border-right: 1px solid #B0CEE9; 57 | border-left: 1px solid #B0CEE9; 58 | } 59 | 60 | .skeduler-main-timeline div:first-child { 61 | border-top: 1px solid #B0CEE9; 62 | } 63 | 64 | .skeduler-main-body { 65 | display: flex; 66 | } 67 | 68 | .skeduler-main-timeline div, 69 | .skeduler-main-body > div > div.skeduler-cell { 70 | background-color: #FFFFFF; 71 | } 72 | 73 | .skeduler-main-timeline div:nth-child(even), 74 | .skeduler-main-body > div > div.skeduler-cell:nth-child(odd) { 75 | border-top: 1px dotted #B0CEE9; 76 | border-bottom: 1px solid #B0CEE9; 77 | } 78 | 79 | .skeduler-main-body > div > div.skeduler-cell { 80 | width: 200px; 81 | height: 30px; 82 | } 83 | 84 | .skeduler-main-body > div > .skeduler-task-placeholder { 85 | height: 0; 86 | position: relative; 87 | } 88 | 89 | .skeduler-main-body > div > .skeduler-task-placeholder > div { 90 | position: absolute; 91 | overflow: hidden; 92 | background-color: #576D7C; 93 | padding: 10px; 94 | box-sizing: border-box; 95 | box-shadow: 0px .125em .25em rgba(0,0,0,.25); 96 | margin-top: 2px; 97 | cursor: pointer; 98 | color: #FFFFFF; 99 | word-wrap: break-word; 100 | min-width: 0; 101 | min-height: 0; 102 | transition: all .4s; 103 | } 104 | 105 | .skeduler-main-body > div > .skeduler-task-placeholder > div:hover { 106 | box-shadow: 0 .25em .5em rgba(0,0,0,.5); 107 | background-color: #3A9852; 108 | min-height: 150px; 109 | min-width: 200px; 110 | opacity: 0.8; 111 | z-index: 9999; 112 | } -------------------------------------------------------------------------------- /jquery.skeduler.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | var defaultSettings = { 3 | // Data attributes 4 | headers: [], // String[] - Array of column headers 5 | tasks: [], // Task[] - Array of tasks. Required fields: 6 | // id: number, startTime: number, duration: number, column: number 7 | 8 | // Card template - Inner content of task card. 9 | // You're able to use ${key} inside template, where key is any property from task. 10 | cardTemplate: '
${id}
', 11 | 12 | // OnClick event handler 13 | onClick: function (e, task) { }, 14 | 15 | // Css classes 16 | containerCssClass: 'skeduler-container', 17 | headerContainerCssClass: 'skeduler-headers', 18 | schedulerContainerCssClass: 'skeduler-main', 19 | taskPlaceholderCssClass: 'skeduler-task-placeholder', 20 | cellCssClass: 'skeduler-cell', 21 | 22 | lineHeight: 30, // height of one half-hour line in grid 23 | borderWidth: 1, // width of board of grid cell 24 | 25 | debug: false 26 | }; 27 | var settings = {}; 28 | 29 | /** 30 | * Convert double value of hours to zero-preposited string with 30 or 00 value of minutes 31 | */ 32 | function toTimeString(value) { 33 | return (value < 10 ? '0' : '') + Math.floor(value) + (Math.ceil(value) > Math.floor(value) ? ':30' : ':00'); 34 | } 35 | 36 | /** 37 | * Return height of task card based on duration of the task 38 | * duration - in hours 39 | */ 40 | function getCardHeight(duration) { 41 | return (settings.lineHeight + settings.borderWidth) * (duration * 2) - 1; 42 | } 43 | 44 | /** 45 | * Return top offset of task card based on start time of the task 46 | * startTime - in hours 47 | */ 48 | function getCardTopPosition(startTime) { 49 | return (settings.lineHeight + settings.borderWidth) * (startTime * 2); 50 | } 51 | 52 | /** 53 | * Render card template 54 | */ 55 | function renderInnerCardContent(task) { 56 | var result = settings.cardTemplate; 57 | for (var key in task) { 58 | if (task.hasOwnProperty(key)) { 59 | // TODO: replace all 60 | result = result.replace('${' + key + '}', task[key]); 61 | } 62 | } 63 | 64 | return $(result); 65 | } 66 | 67 | /** 68 | * Generate task cards 69 | */ 70 | function appendTasks(placeholder, tasks) { 71 | var findCoefficients = function () { 72 | var coefficients = []; 73 | for (var i = 0; i < tasks.length - 1; i++) { 74 | var k = 0; 75 | var j = i + 1; 76 | while (j < tasks.length && tasks[i].startTime < tasks[j].startTime 77 | && tasks[i].startTime + tasks[i].duration > tasks[j].startTime) { 78 | k++; 79 | j++; 80 | } 81 | 82 | coefficients.push(k); 83 | } 84 | 85 | coefficients.push(0); 86 | return coefficients; 87 | }; 88 | 89 | var normalize = function (args) { 90 | var indexes = {}; 91 | for (var i = 0; i < args.length; i++) { 92 | var start = i; 93 | var count = 0; 94 | while (args[i] != 0) { 95 | i++; 96 | count++; 97 | } 98 | var end = i; 99 | if (count) { 100 | count++; 101 | } 102 | 103 | var index = 0; 104 | for (var j = start; j <= end; j++) { 105 | args[j] = count; 106 | indexes[j] = index++; 107 | } 108 | } 109 | 110 | return {args: args, indexes: indexes}; 111 | }; 112 | 113 | var args = 114 | normalize( 115 | findCoefficients() 116 | ); 117 | 118 | for (var i = 0; i < args.args.length; i++) { 119 | var width = 194 / (args.args[i] || 1); 120 | 121 | tasks[i].width = width; 122 | tasks[i].left = (args.indexes[i] * width) || 4; 123 | } 124 | 125 | tasks.forEach(function (task, index) { 126 | var innerContent = renderInnerCardContent(task); 127 | var top = getCardTopPosition(task.startTime) + 2; 128 | var height = getCardHeight(task.duration); 129 | var width = task.width || 194; 130 | var left = task.left || 4; 131 | 132 | var card = $('
') 133 | .attr({ 134 | style: 'top: ' + top + 'px; height: ' + (height - 4) + 'px; ' + 'width: ' + (width - 8) + 'px; left: ' + left + 'px', 135 | title: toTimeString(task.startTime) + ' - ' + toTimeString(task.startTime + task.duration) 136 | }); 137 | card.on('click', function (e) { settings.onClick && settings.onClick(e, task) }); 138 | card.append(innerContent) 139 | .appendTo(placeholder); 140 | }, this); 141 | } 142 | 143 | /** 144 | * Generate scheduler grid with task cards 145 | * options: 146 | * - headers: string[] - array of headers 147 | * - tasks: Task[] - array of tasks 148 | * - containerCssClass: string - css class of main container 149 | * - headerContainerCssClass: string - css class of header container 150 | * - schedulerContainerCssClass: string - css class of scheduler 151 | * - lineHeight - height of one half-hour cell in grid 152 | * - borderWidth - width of border of cell in grid 153 | */ 154 | $.fn.skeduler = function (options) { 155 | settings = $.extend(defaultSettings, options); 156 | 157 | if (settings.debug) { 158 | console.time('skeduler'); 159 | } 160 | 161 | var skedulerEl = $(this); 162 | 163 | skedulerEl.empty(); 164 | skedulerEl.addClass(settings.containerCssClass); 165 | 166 | var div = $('
'); 167 | 168 | // Add headers 169 | var headerContainer = div.clone().addClass(settings.headerContainerCssClass); 170 | settings.headers.forEach(function (element) { 171 | div.clone().text(element).appendTo(headerContainer); 172 | }, this); 173 | skedulerEl.append(headerContainer); 174 | 175 | // Add schedule 176 | var scheduleEl = div.clone().addClass(settings.schedulerContainerCssClass); 177 | var scheduleTimelineEl = div.clone().addClass(settings.schedulerContainerCssClass + '-timeline'); 178 | var scheduleBodyEl = div.clone().addClass(settings.schedulerContainerCssClass + '-body'); 179 | 180 | var gridColumnElement = div.clone(); 181 | 182 | for (var i = 0; i < 24; i++) { 183 | // Populate timeline 184 | div.clone() 185 | .text(toTimeString(i)) 186 | .appendTo(scheduleTimelineEl); 187 | div.clone().appendTo(scheduleTimelineEl); 188 | 189 | gridColumnElement.append(div.clone().addClass(settings.cellCssClass)); 190 | gridColumnElement.append(div.clone().addClass(settings.cellCssClass)); 191 | } 192 | 193 | // Populate grid 194 | for (var j = 0; j < settings.headers.length; j++) { 195 | var el = gridColumnElement.clone(); 196 | 197 | var placeholder = div.clone().addClass(settings.taskPlaceholderCssClass); 198 | appendTasks(placeholder, settings.tasks.filter(function (t) { return t.column == j })); 199 | 200 | el.prepend(placeholder); 201 | el.appendTo(scheduleBodyEl); 202 | } 203 | 204 | scheduleEl.append(scheduleTimelineEl); 205 | scheduleEl.append(scheduleBodyEl); 206 | 207 | skedulerEl.append(scheduleEl); 208 | 209 | if (settings.debug) { 210 | console.timeEnd('skeduler'); 211 | } 212 | 213 | return skedulerEl; 214 | }; 215 | }(jQuery)); -------------------------------------------------------------------------------- /jquery.skeduler.min.css: -------------------------------------------------------------------------------- 1 | .skeduler-container{font-family:Helvetica,​Arial,​sans-serif}.skeduler-container *{box-sizing:content-box}.skeduler-headers{border-left:1px solid #b0cee9;display:flex;padding-left:60px;position:relative}.skeduler-headers:before{border-top:1px solid #b0cee9;content:"";width:60px;position:absolute;left:0}.skeduler-headers > div{flex:0 0 200px;height:30px;padding-top:10px;background-color:#D3E0EF;border-left:1px solid #B0CEE9;border-bottom:1px solid #B0CEE9;border-top:1px solid #b0cee9;text-align:center}.skeduler-headers > div:last-child,.skeduler-main-body > div > div.skeduler-cell{border-right:1px solid #B0CEE9}.skeduler-main{display:flex}.skeduler-main-timeline{margin-top:-1px}.skeduler-main-timeline div{width:50px;height:27px;text-align:left;padding-left:10px;padding-top:3px;color:#333;border-right:1px solid #B0CEE9;border-left:1px solid #B0CEE9}.skeduler-main-timeline div:first-child{border-top:1px solid #B0CEE9}.skeduler-main-body{display:flex}.skeduler-main-timeline div,.skeduler-main-body > div > div.skeduler-cell{background-color:#FFF}.skeduler-main-timeline div:nth-child(even),.skeduler-main-body > div > div.skeduler-cell:nth-child(odd){border-top:1px dotted #B0CEE9;border-bottom:1px solid #B0CEE9}.skeduler-main-body > div > div.skeduler-cell{width:200px;height:30px}.skeduler-main-body > div > .skeduler-task-placeholder{height:0;position:relative}.skeduler-main-body > div > .skeduler-task-placeholder > div{position:absolute;overflow:hidden;background-color:#576D7C;padding:10px;box-sizing:border-box;box-shadow:0 .125em .25em rgba(0,0,0,.25);margin-top:2px;cursor:pointer;color:#FFF;word-wrap:break-word;min-width:0;min-height:0;transition:all .4s}.skeduler-main-body > div > .skeduler-task-placeholder > div:hover{box-shadow:0 .25em .5em rgba(0,0,0,.5);background-color:#3A9852;min-height:150px;min-width:200px;opacity:0.8;z-index:9999} -------------------------------------------------------------------------------- /jquery.skeduler.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function s(e){return(e<10?"0":"")+Math.floor(e)+(Math.ceil(e)>Math.floor(e)?":30":":00")}function a(e){return(d.lineHeight+d.borderWidth)*(2*e)-1}function n(e){return(d.lineHeight+d.borderWidth)*(2*e)}function r(s){var a=d.cardTemplate 2 | for(var n in s)s.hasOwnProperty(n)&&(a=a.replace("${"+n+"}",s[n])) 3 | return e(a)}function t(t,l){for(var o=function(e){for(var s={},a=0;al[n].startTime;)a++,n++ 7 | e.push(a)}return e.push(0),e}()),i=0;i").attr({style:"top: "+c+"px; height: "+(u-4)+"px; width: "+(C-8)+"px; left: "+h+"px",title:s(l.startTime)+" - "+s(l.startTime+l.duration)}) 9 | p.on("click",function(e){d.onClick&&d.onClick(e,l)}),p.append(i).appendTo(t)},this)}var l={headers:[],tasks:[],cardTemplate:"
${id}
",onClick:function(e,s){},containerCssClass:"skeduler-container",headerContainerCssClass:"skeduler-headers",schedulerContainerCssClass:"skeduler-main",taskPlaceholderCssClass:"skeduler-task-placeholder",cellCssClass:"skeduler-cell",lineHeight:30,borderWidth:1,debug:!1},d={} 10 | e.fn.skeduler=function(a){d=e.extend(l,a),d.debug&&console.time("skeduler") 11 | var n=e(this) 12 | n.empty(),n.addClass(d.containerCssClass) 13 | var r=e("
"),o=r.clone().addClass(d.headerContainerCssClass) 14 | d.headers.forEach(function(e){r.clone().text(e).appendTo(o)},this),n.append(o) 15 | for(var i=r.clone().addClass(d.schedulerContainerCssClass),c=r.clone().addClass(d.schedulerContainerCssClass+"-timeline"),u=r.clone().addClass(d.schedulerContainerCssClass+"-body"),C=r.clone(),h=0;h<24;h++)r.clone().text(s(h)).appendTo(c),r.clone().appendTo(c),C.append(r.clone().addClass(d.cellCssClass)),C.append(r.clone().addClass(d.cellCssClass)) 16 | for(var p=0;p