├── .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 | Generate
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