| @ -0,0 +1,69 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | a lot of data</title> | |||
| <style> | |||
| body, html { | |||
| font-family: arial, sans-serif; | |||
| font-size: 11pt; | |||
| } | |||
| </style> | |||
| <!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js --> | |||
| <script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <h1> | |||
| Test with a lot of data | |||
| </h1> | |||
| <p> | |||
| <label for="count">Number of items</label> | |||
| <input id="count" value="10000"> | |||
| <input id="draw" type="button" value="draw"> | |||
| </p> | |||
| <div id="visualization"></div> | |||
| <script> | |||
| // create a dataset with items | |||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||
| var items = new vis.DataSet({ | |||
| convert: { | |||
| start: 'Date', | |||
| end: 'Date' | |||
| } | |||
| }); | |||
| // create data | |||
| function createData() { | |||
| var count = parseInt(document.getElementById('count').value) || 100; | |||
| var newData = []; | |||
| for (var i = 0; i < count; i++) { | |||
| newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)}); | |||
| } | |||
| items.clear(); | |||
| items.add(newData); | |||
| } | |||
| createData(); | |||
| document.getElementById('draw').onclick = createData; | |||
| var container = document.getElementById('visualization'); | |||
| var options = { | |||
| editable: true, | |||
| start: now.clone().add('days', -3), | |||
| end: now.clone().add('days', 11), | |||
| zoomMin: 1000 * 60 * 60 * 24, // a day | |||
| zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months | |||
| //maxHeight: 300, | |||
| //height: '300px', | |||
| //orientation: 'top' | |||
| }; | |||
| var timeline = new vis.Timeline(container, items, options); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -1,68 +0,0 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | a lot of data</title> | |||
| <style> | |||
| body, html { | |||
| font-family: arial, sans-serif; | |||
| font-size: 11pt; | |||
| } | |||
| </style> | |||
| <!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js --> | |||
| <script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <h1> | |||
| Test with a lot of data | |||
| </h1> | |||
| <p> | |||
| <label for="count">Number of items</label> | |||
| <input id="count" value="100"> | |||
| <input id="draw" type="button" value="draw"> | |||
| </p> | |||
| <div id="visualization"></div> | |||
| <script> | |||
| // create a dataset with items | |||
| var now = moment().minutes(0).seconds(0).milliseconds(0); | |||
| var items = new vis.DataSet({ | |||
| convert: { | |||
| start: 'Date', | |||
| end: 'Date' | |||
| } | |||
| }); | |||
| // create data | |||
| function createData() { | |||
| var count = parseInt(document.getElementById('count').value) || 100; | |||
| var newData = []; | |||
| for (var i = 0; i < count; i++) { | |||
| newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)}); | |||
| } | |||
| items.clear(); | |||
| items.add(newData); | |||
| } | |||
| createData(); | |||
| document.getElementById('draw').onclick = createData; | |||
| var container = document.getElementById('visualization'); | |||
| var options = { | |||
| start: now.clone().add('days', -3), | |||
| end: now.clone().add('days', 11), | |||
| zoomMin: 1000 * 60 * 60 * 24, // a day | |||
| zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months | |||
| //maxHeight: 300, | |||
| //height: '300px', | |||
| //orientation: 'top' | |||
| }; | |||
| var timeline = new vis.Timeline(container, items, options); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,66 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Order groups</title> | |||
| <style> | |||
| body, html { | |||
| font-family: arial, sans-serif; | |||
| font-size: 11pt; | |||
| } | |||
| #visualization { | |||
| box-sizing: border-box; | |||
| width: 100%; | |||
| height: 300px; | |||
| } | |||
| </style> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <p> | |||
| This example demonstrate custom ordering of groups. | |||
| </p> | |||
| <div id="visualization"></div> | |||
| <script> | |||
| var groups = new vis.DataSet([ | |||
| {id: 0, content: 'First', value: 1}, | |||
| {id: 1, content: 'Third', value: 3}, | |||
| {id: 2, content: 'Second', value: 2} | |||
| ]); | |||
| // create a dataset with items | |||
| var items = new vis.DataSet([ | |||
| {id: 0, group: 0, content: 'item 0', start: new Date(2014, 3, 17), end: new Date(2014, 3, 21)}, | |||
| {id: 1, group: 0, content: 'item 1', start: new Date(2014, 3, 19), end: new Date(2014, 3, 20)}, | |||
| {id: 2, group: 1, content: 'item 2', start: new Date(2014, 3, 16), end: new Date(2014, 3, 24)}, | |||
| {id: 3, group: 1, content: 'item 3', start: new Date(2014, 3, 23), end: new Date(2014, 3, 24)}, | |||
| {id: 4, group: 1, content: 'item 4', start: new Date(2014, 3, 22), end: new Date(2014, 3, 26)}, | |||
| {id: 5, group: 2, content: 'item 5', start: new Date(2014, 3, 24), end: new Date(2014, 3, 27)} | |||
| ]); | |||
| // create visualization | |||
| var container = document.getElementById('visualization'); | |||
| var options = { | |||
| // option groupOrder can be a property name or a sort function | |||
| // the sort function must compare two groups and return a value | |||
| // > 0 when a > b | |||
| // < 0 when a < b | |||
| // 0 when a == b | |||
| groupOrder: function (a, b) { | |||
| return a.value - b.value; | |||
| }, | |||
| editable: true | |||
| }; | |||
| var timeline = new vis.Timeline(container); | |||
| timeline.setOptions(options); | |||
| timeline.setGroups(groups); | |||
| timeline.setItems(items); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,51 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Limit move and zoom</title> | |||
| <style> | |||
| body, html { | |||
| font-family: arial, sans-serif; | |||
| font-size: 11pt; | |||
| } | |||
| </style> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <p> | |||
| The visible range is limited in this demo: | |||
| </p> | |||
| <ul> | |||
| <li>minimum visible date is limited to 2012-01-01 using option <code>min</code></li> | |||
| <li>maximum visible date is limited to 2013-01-01 (excluded) using option <code>max</code></li> | |||
| <li>visible zoom interval is limited to a minimum of 24 hours using option <code>zoomMin</code></li> | |||
| <li>visible zoom interval is limited to a maximum of about 3 months using option <code>zoomMax</code></li> | |||
| </ul> | |||
| <div id="visualization"></div> | |||
| <script> | |||
| // create some items | |||
| var items = [ | |||
| {'start': new Date(2012, 4, 25), 'content': 'First'}, | |||
| {'start': new Date(2012, 4, 26), 'content': 'Last'} | |||
| ]; | |||
| // create visualization | |||
| var container = document.getElementById('visualization'); | |||
| var options = { | |||
| height: '300px', | |||
| min: new Date(2012, 0, 1), // lower limit of visible range | |||
| max: new Date(2013, 0, 1), // upper limit of visible range | |||
| zoomMin: 1000 * 60 * 60 * 24, // one day in milliseconds | |||
| zoomMax: 1000 * 60 * 60 * 24 * 31 * 3 // about three months in milliseconds | |||
| }; | |||
| // create the timeline | |||
| var timeline = new vis.Timeline(container); | |||
| timeline.setOptions(options); | |||
| timeline.setItems(items); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,57 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Points</title> | |||
| <style type="text/css"> | |||
| body { | |||
| font: 10pt arial; | |||
| } | |||
| </style> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <h1>World War II timeline</h1> | |||
| <p>Source: <a href="http://www.onwar.com/chrono/index.htm" target="_blank">http://www.onwar.com/chrono/index.htm</a></p> | |||
| <div id="mytimeline" style="background-color: #FAFAFA;"></div> | |||
| <div id="visualization"></div> | |||
| <script type="text/javascript"> | |||
| var container = document.getElementById('visualization'); | |||
| var items = [ | |||
| {start: new Date(1939,8,1), content: 'German Invasion of Poland'}, | |||
| {start: new Date(1940,4,10), content: 'Battle of France and the Low Countries'}, | |||
| {start: new Date(1940,7,13), content: 'Battle of Britain - RAF vs. Luftwaffe'}, | |||
| {start: new Date(1941,1,14), content: 'German Afrika Korps arrives in North Africa'}, | |||
| {start: new Date(1941,5,22), content: 'Third Reich Invades the USSR'}, | |||
| {start: new Date(1941,11,7), content: 'Japanese Attack Pearl Harbor'}, | |||
| {start: new Date(1942,5,4), content: 'Battle of Midway in the Pacific'}, | |||
| {start: new Date(1942,10,8), content: 'Americans open Second Front in North Africa'}, | |||
| {start: new Date(1942,10,19),content: 'Battle of Stalingrad in Russia'}, | |||
| {start: new Date(1943,6,5), content: 'Battle of Kursk - Last German Offensive on Eastern Front'}, | |||
| {start: new Date(1943,6,10), content: 'Anglo-American Landings in Sicily'}, | |||
| {start: new Date(1944,2,8), content: 'Japanese Attack British India'}, | |||
| {start: new Date(1944,5,6), content: 'D-Day - Allied Invasion of Normandy'}, | |||
| {start: new Date(1944,5,22), content: 'Destruction of Army Group Center in Byelorussia'}, | |||
| {start: new Date(1944,7,1), content: 'The Warsaw Uprising in Occupied Poland'}, | |||
| {start: new Date(1944,9,20), content: 'American Liberation of the Philippines'}, | |||
| {start: new Date(1944,11,16),content: 'Battle of the Bulge in the Ardennes'}, | |||
| {start: new Date(1944,1,19), content: 'American Landings on Iwo Jima'}, | |||
| {start: new Date(1945,3,1), content: 'US Invasion of Okinawa'}, | |||
| {start: new Date(1945,3,16), content: 'Battle of Berlin - End of the Third Reich'} | |||
| ]; | |||
| var options = { | |||
| // Set global item type. Type can also be specified for items individually | |||
| // Available types: 'box' (default), 'point', 'range', 'rangeoverflow' | |||
| type: 'point' | |||
| }; | |||
| var timeline = new vis.Timeline(container, items, options); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,88 @@ | |||
| <!DOCTYPE HTML> | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Custom styling</title> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| <style type="text/css"> | |||
| .vis.timeline.rootpanel { | |||
| border: 2px solid purple; | |||
| font-family: purisa, 'comic sans', cursive; | |||
| font-size: 12pt; | |||
| background: #ffecea; | |||
| } | |||
| .vis.timeline .item { | |||
| border-color: #F991A3; | |||
| background-color: pink; | |||
| font-size: 15pt; | |||
| color: purple; | |||
| box-shadow: 5px 5px 20px rgba(128,128,128, 0.5); | |||
| } | |||
| .vis.timeline .item, | |||
| .vis.timeline .item.line { | |||
| border-width: 3px; | |||
| } | |||
| .vis.timeline .item.dot { | |||
| border-width: 10px; | |||
| border-radius: 10px; | |||
| } | |||
| .vis.timeline .item.selected { | |||
| border-color: green; | |||
| background-color: lightgreen; | |||
| } | |||
| .vis.timeline .timeaxis .text { | |||
| color: purple; | |||
| padding-top: 10px; | |||
| padding-left: 10px; | |||
| } | |||
| .vis.timeline .timeaxis .text.major { | |||
| font-weight: bold; | |||
| } | |||
| .vis.timeline .timeaxis .grid.minor { | |||
| border-width: 2px; | |||
| border-color: pink; | |||
| } | |||
| .vis.timeline .timeaxis .grid.major { | |||
| border-width: 2px; | |||
| border-color: #F991A3; | |||
| } | |||
| </style> | |||
| </head> | |||
| <body> | |||
| <div id="visualization"></div> | |||
| <script type="text/javascript"> | |||
| var container = document.getElementById('visualization'); | |||
| var items = [ | |||
| {start: new Date(2010,7,23), content: '<div>Conversation</div><img src="img/community-users-icon.png" style="width:32px; height:32px;">', type: 'point'}, | |||
| {start: new Date(2010,7,23,23,0,0), content: '<div>Mail from boss</div><img src="img/mail-icon.png" style="width:32px; height:32px;">'}, | |||
| {start: new Date(2010,7,24,16,0,0), content: 'Report'}, | |||
| {start: new Date(2010,7,26), end: new Date(2010,8,2), content: 'Traject A'}, | |||
| {start: new Date(2010,7,28), content: '<div>Memo</div><img src="img/notes-edit-icon.png" style="width:48px; height:48px;">'}, | |||
| {start: new Date(2010,7,29), content: '<div>Phone call</div><img src="img/Hardware-Mobile-Phone-icon.png" style="width:32px; height:32px;">'}, | |||
| {start: new Date(2010,7,31), end: new Date(2010,8,3), content: 'Traject B'}, | |||
| {start: new Date(2010,8,4,12,0,0), content: '<div>Report</div><img src="img/attachment-icon.png" style="width:32px; height:32px;">'} | |||
| ]; | |||
| var options = { | |||
| editable: true, | |||
| margin: { | |||
| item: 20, | |||
| axis: 40 | |||
| } | |||
| }; | |||
| var timeline = new vis.Timeline(container, items, options); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,88 @@ | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Past and future</title> | |||
| <style type="text/css"> | |||
| body { | |||
| font: 11pt verdana; | |||
| } | |||
| .vis.timeline .item.past { | |||
| filter: alpha(opacity=50); | |||
| opacity: 0.5; | |||
| } | |||
| </style> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| </head> | |||
| <body> | |||
| <p style="width: 600px;"> | |||
| When the custom time bar is shown, the user can drag this bar to a specific | |||
| time. The Timeline sends an event that the custom time is changed, after | |||
| which the contents of the timeline can be changed according to the specified | |||
| time in past or future. | |||
| </p> | |||
| <div id="customTime"> </div> | |||
| <p></p> | |||
| <div id="mytimeline"></div> | |||
| <script> | |||
| // create a data set | |||
| var data = new vis.DataSet([ | |||
| { | |||
| id: 1, | |||
| start: new Date((new Date()).getTime() - 60 * 1000), | |||
| end: new Date(), | |||
| content: 'Dynamic event' | |||
| } | |||
| ]); | |||
| // specify options | |||
| var options = { | |||
| showCurrentTime: true, | |||
| showCustomTime: true | |||
| }; | |||
| // create a timeline | |||
| var container = document.getElementById('mytimeline'); | |||
| timeline = new vis.Timeline(container, data, options); | |||
| // add event listener | |||
| timeline.on('timechange', function (event) { | |||
| document.getElementById("customTime").innerHTML = "Custom Time: " + event.time; | |||
| var item = data.get(1); | |||
| if (event.time > item.start) { | |||
| item.end = new Date(event.time); | |||
| var now = new Date(); | |||
| if (event.time < now) { | |||
| item.content = "Dynamic event (past)"; | |||
| item.className = 'past'; | |||
| } | |||
| else if (event.time > now) { | |||
| item.content = "Dynamic event (future)"; | |||
| item.className = 'future'; | |||
| } | |||
| else { | |||
| item.content = "Dynamic event (now)"; | |||
| item.className = 'now'; | |||
| } | |||
| data.update(item); | |||
| } | |||
| }); | |||
| // set a custom range from -2 minute to +3 minutes current time | |||
| var start = new Date((new Date()).getTime() - 2 * 60 * 1000); | |||
| var end = new Date((new Date()).getTime() + 3 * 60 * 1000); | |||
| timeline.setWindow(start, end); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,109 @@ | |||
| <html> | |||
| <head> | |||
| <title>Timeline | A lot of grouped data</title> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| <style type="text/css"> | |||
| body { | |||
| color: #4D4D4D; | |||
| font: 10pt arial; | |||
| } | |||
| </style> | |||
| </head> | |||
| <body onresize="/*timeline.checkResize();*/"> | |||
| <h1>Timeline grouping performance</h1> | |||
| <p> | |||
| Choose a number of items: | |||
| <a href="?count=100">100</a>, | |||
| <a href="?count=1000">1000</a>, | |||
| <a href="?count=10000">10000</a>, | |||
| <a href="?count=10000">100000</a> | |||
| <p> | |||
| <p> | |||
| Current number of items: <span id='count'>100</span> | |||
| </p> | |||
| <div id="mytimeline"></div> | |||
| <script> | |||
| /** | |||
| * Get URL parameter | |||
| * http://www.netlobo.com/url_query_string_javascript.html | |||
| */ | |||
| function gup( name ) { | |||
| name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); | |||
| var regexS = "[\\?&]"+name+"=([^&#]*)"; | |||
| var regex = new RegExp( regexS ); | |||
| var results = regex.exec( window.location.href ); | |||
| if( results == null ) | |||
| return ""; | |||
| else | |||
| return results[1]; | |||
| } | |||
| // get selected item count from url parameter | |||
| var count = (Number(gup('count')) || 1000); | |||
| // create groups | |||
| var groups = new vis.DataSet([ | |||
| {id: 1, content: 'Truck 1'}, | |||
| {id: 2, content: 'Truck 2'}, | |||
| {id: 3, content: 'Truck 3'}, | |||
| {id: 4, content: 'Truck 4'} | |||
| ]); | |||
| // create items | |||
| var items = new vis.DataSet(); | |||
| var order = 1; | |||
| var truck = 1; | |||
| for (var j = 0; j < 4; j++) { | |||
| var date = new Date(); | |||
| for (var i = 0; i < count/4; i++) { | |||
| date.setHours(date.getHours() + 4 * (Math.random() < 0.2)); | |||
| var start = new Date(date); | |||
| date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4)); | |||
| var end = new Date(date); | |||
| items.add({ | |||
| id: order, | |||
| group: truck, | |||
| start: start, | |||
| end: end, | |||
| content: 'Order ' + order | |||
| }); | |||
| order++; | |||
| } | |||
| truck++; | |||
| } | |||
| // specify options | |||
| var options = { | |||
| stack: false, | |||
| start: new Date(), | |||
| end: new Date(1000*60*60*24 + (new Date()).valueOf()), | |||
| editable: true, | |||
| margin: { | |||
| item: 10, // minimal margin between items | |||
| axis: 5 // minimal margin between items and the axis | |||
| }, | |||
| orientation: 'top' | |||
| }; | |||
| // create a Timeline | |||
| var container = document.getElementById('mytimeline'); | |||
| timeline = new vis.Timeline(container, null, options); | |||
| timeline.setGroups(groups); | |||
| timeline.setItems(items); | |||
| document.getElementById('count').innerHTML = count; | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,115 @@ | |||
| <html> | |||
| <head> | |||
| <title>Timeline | Item class names</title> | |||
| <script src="../../dist/vis.js"></script> | |||
| <link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||
| <style type="text/css"> | |||
| body, input { | |||
| font: 12pt verdana; | |||
| } | |||
| /* custom styles for individual items, load this after vis.css */ | |||
| .vis.timeline .item.green { | |||
| background-color: greenyellow; | |||
| border-color: green; | |||
| } | |||
| /* create a custom sized dot at the bottom of the red item */ | |||
| .vis.timeline .item.red { | |||
| background-color: red; | |||
| border-color: darkred; | |||
| color: white; | |||
| font-family: monospace; | |||
| box-shadow: 0 0 10px gray; | |||
| } | |||
| .vis.timeline .item.dot.red { | |||
| border-radius: 10px; | |||
| border-width: 10px; | |||
| } | |||
| .vis.timeline .item.line.red { | |||
| border-width: 5px; | |||
| } | |||
| .vis.timeline .item.box.red { | |||
| border-radius: 0; | |||
| border-width: 2px; | |||
| font-size: 24pt; | |||
| font-weight: bold; | |||
| } | |||
| .vis.timeline .item.orange { | |||
| background-color: gold; | |||
| border-color: orange; | |||
| } | |||
| .vis.timeline .item.orange.selected { | |||
| /* custom colors for selected orange items */ | |||
| background-color: orange; | |||
| border-color: orangered; | |||
| } | |||
| .vis.timeline .item.magenta { | |||
| background-color: magenta; | |||
| border-color: purple; | |||
| color: white; | |||
| } | |||
| /* our custom classes overrule the styles for selected events, | |||
| so lets define a new style for the selected events */ | |||
| .vis.timeline .item.selected { | |||
| background-color: white; | |||
| border-color: black; | |||
| color: black; | |||
| box-shadow: 0 0 10px gray; | |||
| } | |||
| </style> | |||
| </head> | |||
| <body> | |||
| <p>This page demonstrates the Timeline with custom css classes for individual items.</p> | |||
| <div id="mytimeline"></div> | |||
| <script type="text/javascript"> | |||
| // create data | |||
| var data = [ | |||
| { | |||
| 'start': new Date(2012,7,19), | |||
| 'content': 'default' | |||
| }, | |||
| { | |||
| 'start': new Date(2012,7,23), | |||
| 'content': 'green', | |||
| 'className': 'green' | |||
| }, | |||
| { | |||
| 'start': new Date(2012,7,29), | |||
| 'content': 'red', | |||
| 'className': 'red' | |||
| }, | |||
| { | |||
| 'start': new Date(2012,7,27), | |||
| 'end': new Date(2012,8,1), | |||
| 'content': 'orange', | |||
| 'className': 'orange' | |||
| }, | |||
| { | |||
| 'start': new Date(2012,8,2), | |||
| 'content': 'magenta', | |||
| 'className': 'magenta' | |||
| } | |||
| ]; | |||
| // specify options | |||
| var options = { | |||
| editable: true | |||
| }; | |||
| // create the timeline | |||
| var container = document.getElementById('mytimeline'); | |||
| timeline = new vis.Timeline(container, data, options); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @ -1,183 +0,0 @@ | |||
| /** | |||
| * @constructor Controller | |||
| * | |||
| * A Controller controls the reflows and repaints of all components, | |||
| * and is used as an event bus for all components. | |||
| */ | |||
| function Controller () { | |||
| var me = this; | |||
| this.id = util.randomUUID(); | |||
| this.components = {}; | |||
| /** | |||
| * Listen for a 'request-reflow' event. The controller will schedule a reflow | |||
| * @param {Boolean} [force] If true, an immediate reflow is forced. Default | |||
| * is false. | |||
| */ | |||
| var reflowTimer = null; | |||
| this.on('request-reflow', function requestReflow(force) { | |||
| if (force) { | |||
| me.reflow(); | |||
| } | |||
| else { | |||
| if (!reflowTimer) { | |||
| reflowTimer = setTimeout(function () { | |||
| reflowTimer = null; | |||
| me.reflow(); | |||
| }, 0); | |||
| } | |||
| } | |||
| }); | |||
| /** | |||
| * Request a repaint. The controller will schedule a repaint | |||
| * @param {Boolean} [force] If true, an immediate repaint is forced. Default | |||
| * is false. | |||
| */ | |||
| var repaintTimer = null; | |||
| this.on('request-repaint', function requestRepaint(force) { | |||
| if (force) { | |||
| me.repaint(); | |||
| } | |||
| else { | |||
| if (!repaintTimer) { | |||
| repaintTimer = setTimeout(function () { | |||
| repaintTimer = null; | |||
| me.repaint(); | |||
| }, 0); | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| // Extend controller with Emitter mixin | |||
| Emitter(Controller.prototype); | |||
| /** | |||
| * Add a component to the controller | |||
| * @param {Component} component | |||
| */ | |||
| Controller.prototype.add = function add(component) { | |||
| // validate the component | |||
| if (component.id == undefined) { | |||
| throw new Error('Component has no field id'); | |||
| } | |||
| if (!(component instanceof Component) && !(component instanceof Controller)) { | |||
| throw new TypeError('Component must be an instance of ' + | |||
| 'prototype Component or Controller'); | |||
| } | |||
| // add the component | |||
| component.setController(this); | |||
| this.components[component.id] = component; | |||
| }; | |||
| /** | |||
| * Remove a component from the controller | |||
| * @param {Component | String} component | |||
| */ | |||
| Controller.prototype.remove = function remove(component) { | |||
| var id; | |||
| for (id in this.components) { | |||
| if (this.components.hasOwnProperty(id)) { | |||
| if (id == component || this.components[id] === component) { | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| if (id) { | |||
| // unregister the controller (gives the component the ability to unregister | |||
| // event listeners and clean up other stuff) | |||
| this.components[id].setController(null); | |||
| delete this.components[id]; | |||
| } | |||
| }; | |||
| /** | |||
| * Repaint all components | |||
| */ | |||
| Controller.prototype.repaint = function repaint() { | |||
| var changed = false; | |||
| // cancel any running repaint request | |||
| if (this.repaintTimer) { | |||
| clearTimeout(this.repaintTimer); | |||
| this.repaintTimer = undefined; | |||
| } | |||
| var done = {}; | |||
| function repaint(component, id) { | |||
| if (!(id in done)) { | |||
| // first repaint the components on which this component is dependent | |||
| if (component.depends) { | |||
| component.depends.forEach(function (dep) { | |||
| repaint(dep, dep.id); | |||
| }); | |||
| } | |||
| if (component.parent) { | |||
| repaint(component.parent, component.parent.id); | |||
| } | |||
| // repaint the component itself and mark as done | |||
| changed = component.repaint() || changed; | |||
| done[id] = true; | |||
| } | |||
| } | |||
| util.forEach(this.components, repaint); | |||
| this.emit('repaint'); | |||
| // immediately reflow when needed | |||
| if (changed) { | |||
| this.reflow(); | |||
| } | |||
| // TODO: limit the number of nested reflows/repaints, prevent loop | |||
| }; | |||
| /** | |||
| * Reflow all components | |||
| */ | |||
| Controller.prototype.reflow = function reflow() { | |||
| var resized = false; | |||
| // cancel any running repaint request | |||
| if (this.reflowTimer) { | |||
| clearTimeout(this.reflowTimer); | |||
| this.reflowTimer = undefined; | |||
| } | |||
| var done = {}; | |||
| function reflow(component, id) { | |||
| if (!(id in done)) { | |||
| // first reflow the components on which this component is dependent | |||
| if (component.depends) { | |||
| component.depends.forEach(function (dep) { | |||
| reflow(dep, dep.id); | |||
| }); | |||
| } | |||
| if (component.parent) { | |||
| reflow(component.parent, component.parent.id); | |||
| } | |||
| // reflow the component itself and mark as done | |||
| resized = component.reflow() || resized; | |||
| done[id] = true; | |||
| } | |||
| } | |||
| util.forEach(this.components, reflow); | |||
| this.emit('reflow'); | |||
| // immediately repaint when needed | |||
| if (resized) { | |||
| this.repaint(); | |||
| } | |||
| // TODO: limit the number of nested reflows/repaints, prevent loop | |||
| }; | |||
| @ -1,190 +0,0 @@ | |||
| /** | |||
| * @constructor Stack | |||
| * Stacks items on top of each other. | |||
| * @param {ItemSet} itemset | |||
| * @param {Object} [options] | |||
| */ | |||
| function Stack (itemset, options) { | |||
| this.itemset = itemset; | |||
| this.options = options || {}; | |||
| this.defaultOptions = { | |||
| order: function (a, b) { | |||
| //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup | |||
| // Order: ranges over non-ranges, ranged ordered by width, and | |||
| // lastly ordered by start. | |||
| if (a instanceof ItemRange) { | |||
| if (b instanceof ItemRange) { | |||
| var aInt = (a.data.end - a.data.start); | |||
| var bInt = (b.data.end - b.data.start); | |||
| return (aInt - bInt) || (a.data.start - b.data.start); | |||
| } | |||
| else { | |||
| return -1; | |||
| } | |||
| } | |||
| else { | |||
| if (b instanceof ItemRange) { | |||
| return 1; | |||
| } | |||
| else { | |||
| return (a.data.start - b.data.start); | |||
| } | |||
| } | |||
| }, | |||
| margin: { | |||
| item: 10 | |||
| } | |||
| }; | |||
| this.ordered = []; // ordered items | |||
| } | |||
| /** | |||
| * Set options for the stack | |||
| * @param {Object} options Available options: | |||
| * {ItemSet} itemset | |||
| * {Number} margin | |||
| * {function} order Stacking order | |||
| */ | |||
| Stack.prototype.setOptions = function setOptions (options) { | |||
| util.extend(this.options, options); | |||
| // TODO: register on data changes at the connected itemset, and update the changed part only and immediately | |||
| }; | |||
| /** | |||
| * Stack the items such that they don't overlap. The items will have a minimal | |||
| * distance equal to options.margin.item. | |||
| */ | |||
| Stack.prototype.update = function update() { | |||
| this._order(); | |||
| this._stack(); | |||
| }; | |||
| /** | |||
| * Order the items. If a custom order function has been provided via the options, | |||
| * then this will be used. | |||
| * @private | |||
| */ | |||
| Stack.prototype._order = function _order () { | |||
| var items = this.itemset.items; | |||
| if (!items) { | |||
| throw new Error('Cannot stack items: ItemSet does not contain items'); | |||
| } | |||
| // TODO: store the sorted items, to have less work later on | |||
| var ordered = []; | |||
| var index = 0; | |||
| // items is a map (no array) | |||
| util.forEach(items, function (item) { | |||
| if (item.visible) { | |||
| ordered[index] = item; | |||
| index++; | |||
| } | |||
| }); | |||
| //if a customer stack order function exists, use it. | |||
| var order = this.options.order || this.defaultOptions.order; | |||
| if (!(typeof order === 'function')) { | |||
| throw new Error('Option order must be a function'); | |||
| } | |||
| ordered.sort(order); | |||
| this.ordered = ordered; | |||
| }; | |||
| /** | |||
| * Adjust vertical positions of the events such that they don't overlap each | |||
| * other. | |||
| * @private | |||
| */ | |||
| Stack.prototype._stack = function _stack () { | |||
| var i, | |||
| iMax, | |||
| ordered = this.ordered, | |||
| options = this.options, | |||
| orientation = options.orientation || this.defaultOptions.orientation, | |||
| axisOnTop = (orientation == 'top'), | |||
| margin; | |||
| if (options.margin && options.margin.item !== undefined) { | |||
| margin = options.margin.item; | |||
| } | |||
| else { | |||
| margin = this.defaultOptions.margin.item | |||
| } | |||
| // calculate new, non-overlapping positions | |||
| for (i = 0, iMax = ordered.length; i < iMax; i++) { | |||
| var item = ordered[i]; | |||
| var collidingItem = null; | |||
| do { | |||
| // TODO: optimize checking for overlap. when there is a gap without items, | |||
| // you only need to check for items from the next item on, not from zero | |||
| collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); | |||
| if (collidingItem != null) { | |||
| // There is a collision. Reposition the event above the colliding element | |||
| if (axisOnTop) { | |||
| item.top = collidingItem.top + collidingItem.height + margin; | |||
| } | |||
| else { | |||
| item.top = collidingItem.top - item.height - margin; | |||
| } | |||
| } | |||
| } while (collidingItem); | |||
| } | |||
| }; | |||
| /** | |||
| * Check if the destiny position of given item overlaps with any | |||
| * of the other items from index itemStart to itemEnd. | |||
| * @param {Array} items Array with items | |||
| * @param {int} itemIndex Number of the item to be checked for overlap | |||
| * @param {int} itemStart First item to be checked. | |||
| * @param {int} itemEnd Last item to be checked. | |||
| * @return {Object | null} colliding item, or undefined when no collisions | |||
| * @param {Number} margin A minimum required margin. | |||
| * If margin is provided, the two items will be | |||
| * marked colliding when they overlap or | |||
| * when the margin between the two is smaller than | |||
| * the requested margin. | |||
| */ | |||
| Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, | |||
| itemStart, itemEnd, margin) { | |||
| var collision = this.collision; | |||
| // we loop from end to start, as we suppose that the chance of a | |||
| // collision is larger for items at the end, so check these first. | |||
| var a = items[itemIndex]; | |||
| for (var i = itemEnd; i >= itemStart; i--) { | |||
| var b = items[i]; | |||
| if (collision(a, b, margin)) { | |||
| if (i != itemIndex) { | |||
| return b; | |||
| } | |||
| } | |||
| } | |||
| return null; | |||
| }; | |||
| /** | |||
| * Test if the two provided items collide | |||
| * The items must have parameters left, width, top, and height. | |||
| * @param {Component} a The first item | |||
| * @param {Component} b The second item | |||
| * @param {Number} margin A minimum required margin. | |||
| * If margin is provided, the two items will be | |||
| * marked colliding when they overlap or | |||
| * when the margin between the two is smaller than | |||
| * the requested margin. | |||
| * @return {boolean} true if a and b collide, else false | |||
| */ | |||
| Stack.prototype.collision = function collision (a, b, margin) { | |||
| return ((a.left - margin) < (b.left + b.width) && | |||
| (a.left + a.width + margin) > b.left && | |||
| (a.top - margin) < (b.top + b.height) && | |||
| (a.top + a.height + margin) > b.top); | |||
| }; | |||
| @ -1,113 +0,0 @@ | |||
| /** | |||
| * A content panel can contain a groupset or an itemset, and can handle | |||
| * vertical scrolling | |||
| * @param {Component} [parent] | |||
| * @param {Component[]} [depends] Components on which this components depends | |||
| * (except for the parent) | |||
| * @param {Object} [options] Available parameters: | |||
| * {String | Number | function} [left] | |||
| * {String | Number | function} [top] | |||
| * {String | Number | function} [width] | |||
| * {String | Number | function} [height] | |||
| * {String | function} [className] | |||
| * @constructor ContentPanel | |||
| * @extends Panel | |||
| */ | |||
| function ContentPanel(parent, depends, options) { | |||
| this.id = util.randomUUID(); | |||
| this.parent = parent; | |||
| this.depends = depends; | |||
| this.options = options || {}; | |||
| } | |||
| ContentPanel.prototype = new Component(); | |||
| /** | |||
| * Set options. Will extend the current options. | |||
| * @param {Object} [options] Available parameters: | |||
| * {String | function} [className] | |||
| * {String | Number | function} [left] | |||
| * {String | Number | function} [top] | |||
| * {String | Number | function} [width] | |||
| * {String | Number | function} [height] | |||
| */ | |||
| ContentPanel.prototype.setOptions = Component.prototype.setOptions; | |||
| /** | |||
| * Get the container element of the panel, which can be used by a child to | |||
| * add its own widgets. | |||
| * @returns {HTMLElement} container | |||
| */ | |||
| ContentPanel.prototype.getContainer = function () { | |||
| return this.frame; | |||
| }; | |||
| /** | |||
| * Repaint the component | |||
| * @return {Boolean} changed | |||
| */ | |||
| ContentPanel.prototype.repaint = function () { | |||
| var changed = 0, | |||
| update = util.updateProperty, | |||
| asSize = util.option.asSize, | |||
| options = this.options, | |||
| frame = this.frame; | |||
| if (!frame) { | |||
| frame = document.createElement('div'); | |||
| frame.className = 'content-panel'; | |||
| var className = options.className; | |||
| if (className) { | |||
| if (typeof className == 'function') { | |||
| util.addClassName(frame, String(className())); | |||
| } | |||
| else { | |||
| util.addClassName(frame, String(className)); | |||
| } | |||
| } | |||
| this.frame = frame; | |||
| changed += 1; | |||
| } | |||
| if (!frame.parentNode) { | |||
| if (!this.parent) { | |||
| throw new Error('Cannot repaint panel: no parent attached'); | |||
| } | |||
| var parentContainer = this.parent.getContainer(); | |||
| if (!parentContainer) { | |||
| throw new Error('Cannot repaint panel: parent has no container element'); | |||
| } | |||
| parentContainer.appendChild(frame); | |||
| changed += 1; | |||
| } | |||
| changed += update(frame.style, 'top', asSize(options.top, '0px')); | |||
| changed += update(frame.style, 'left', asSize(options.left, '0px')); | |||
| changed += update(frame.style, 'width', asSize(options.width, '100%')); | |||
| changed += update(frame.style, 'height', asSize(options.height, '100%')); | |||
| return (changed > 0); | |||
| }; | |||
| /** | |||
| * Reflow the component | |||
| * @return {Boolean} resized | |||
| */ | |||
| ContentPanel.prototype.reflow = function () { | |||
| var changed = 0, | |||
| update = util.updateProperty, | |||
| frame = this.frame; | |||
| if (frame) { | |||
| changed += update(this, 'top', frame.offsetTop); | |||
| changed += update(this, 'left', frame.offsetLeft); | |||
| changed += update(this, 'width', frame.offsetWidth); | |||
| changed += update(this, 'height', frame.offsetHeight); | |||
| } | |||
| else { | |||
| changed += 1; | |||
| } | |||
| return (changed > 0); | |||
| }; | |||
| @ -1,580 +0,0 @@ | |||
| /** | |||
| * An GroupSet holds a set of groups | |||
| * @param {Component} parent | |||
| * @param {Component[]} [depends] Components on which this components depends | |||
| * (except for the parent) | |||
| * @param {Object} [options] See GroupSet.setOptions for the available | |||
| * options. | |||
| * @constructor GroupSet | |||
| * @extends Panel | |||
| */ | |||
| function GroupSet(parent, depends, options) { | |||
| this.id = util.randomUUID(); | |||
| this.parent = parent; | |||
| this.depends = depends; | |||
| this.options = options || {}; | |||
| this.range = null; // Range or Object {start: number, end: number} | |||
| this.itemsData = null; // DataSet with items | |||
| this.groupsData = null; // DataSet with groups | |||
| this.groups = {}; // map with groups | |||
| this.dom = {}; | |||
| this.props = { | |||
| labels: { | |||
| width: 0 | |||
| } | |||
| }; | |||
| // TODO: implement right orientation of the labels | |||
| // changes in groups are queued key/value map containing id/action | |||
| this.queue = {}; | |||
| var me = this; | |||
| this.listeners = { | |||
| 'add': function (event, params) { | |||
| me._onAdd(params.items); | |||
| }, | |||
| 'update': function (event, params) { | |||
| me._onUpdate(params.items); | |||
| }, | |||
| 'remove': function (event, params) { | |||
| me._onRemove(params.items); | |||
| } | |||
| }; | |||
| } | |||
| GroupSet.prototype = new Panel(); | |||
| /** | |||
| * Set options for the GroupSet. Existing options will be extended/overwritten. | |||
| * @param {Object} [options] The following options are available: | |||
| * {String | function} groupsOrder | |||
| * TODO: describe options | |||
| */ | |||
| GroupSet.prototype.setOptions = Component.prototype.setOptions; | |||
| GroupSet.prototype.setRange = function (range) { | |||
| // TODO: implement setRange | |||
| }; | |||
| /** | |||
| * Set items | |||
| * @param {vis.DataSet | null} items | |||
| */ | |||
| GroupSet.prototype.setItems = function setItems(items) { | |||
| this.itemsData = items; | |||
| for (var id in this.groups) { | |||
| if (this.groups.hasOwnProperty(id)) { | |||
| var group = this.groups[id]; | |||
| group.setItems(items); | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * Get items | |||
| * @return {vis.DataSet | null} items | |||
| */ | |||
| GroupSet.prototype.getItems = function getItems() { | |||
| return this.itemsData; | |||
| }; | |||
| /** | |||
| * Set range (start and end). | |||
| * @param {Range | Object} range A Range or an object containing start and end. | |||
| */ | |||
| GroupSet.prototype.setRange = function setRange(range) { | |||
| this.range = range; | |||
| }; | |||
| /** | |||
| * Set groups | |||
| * @param {vis.DataSet} groups | |||
| */ | |||
| GroupSet.prototype.setGroups = function setGroups(groups) { | |||
| var me = this, | |||
| ids; | |||
| // unsubscribe from current dataset | |||
| if (this.groupsData) { | |||
| util.forEach(this.listeners, function (callback, event) { | |||
| me.groupsData.unsubscribe(event, callback); | |||
| }); | |||
| // remove all drawn groups | |||
| ids = this.groupsData.getIds(); | |||
| this._onRemove(ids); | |||
| } | |||
| // replace the dataset | |||
| if (!groups) { | |||
| this.groupsData = null; | |||
| } | |||
| else if (groups instanceof DataSet) { | |||
| this.groupsData = groups; | |||
| } | |||
| else { | |||
| this.groupsData = new DataSet({ | |||
| convert: { | |||
| start: 'Date', | |||
| end: 'Date' | |||
| } | |||
| }); | |||
| this.groupsData.add(groups); | |||
| } | |||
| if (this.groupsData) { | |||
| // subscribe to new dataset | |||
| var id = this.id; | |||
| util.forEach(this.listeners, function (callback, event) { | |||
| me.groupsData.on(event, callback, id); | |||
| }); | |||
| // draw all new groups | |||
| ids = this.groupsData.getIds(); | |||
| this._onAdd(ids); | |||
| } | |||
| }; | |||
| /** | |||
| * Get groups | |||
| * @return {vis.DataSet | null} groups | |||
| */ | |||
| GroupSet.prototype.getGroups = function getGroups() { | |||
| return this.groupsData; | |||
| }; | |||
| /** | |||
| * Set selected items by their id. Replaces the current selection. | |||
| * Unknown id's are silently ignored. | |||
| * @param {Array} [ids] An array with zero or more id's of the items to be | |||
| * selected. If ids is an empty array, all items will be | |||
| * unselected. | |||
| */ | |||
| GroupSet.prototype.setSelection = function setSelection(ids) { | |||
| var selection = [], | |||
| groups = this.groups; | |||
| // iterate over each of the groups | |||
| for (var id in groups) { | |||
| if (groups.hasOwnProperty(id)) { | |||
| var group = groups[id]; | |||
| group.setSelection(ids); | |||
| } | |||
| } | |||
| return selection; | |||
| }; | |||
| /** | |||
| * Get the selected items by their id | |||
| * @return {Array} ids The ids of the selected items | |||
| */ | |||
| GroupSet.prototype.getSelection = function getSelection() { | |||
| var selection = [], | |||
| groups = this.groups; | |||
| // iterate over each of the groups | |||
| for (var id in groups) { | |||
| if (groups.hasOwnProperty(id)) { | |||
| var group = groups[id]; | |||
| selection = selection.concat(group.getSelection()); | |||
| } | |||
| } | |||
| return selection; | |||
| }; | |||
| /** | |||
| * Repaint the component | |||
| * @return {Boolean} changed | |||
| */ | |||
| GroupSet.prototype.repaint = function repaint() { | |||
| var changed = 0, | |||
| i, id, group, label, | |||
| update = util.updateProperty, | |||
| asSize = util.option.asSize, | |||
| asElement = util.option.asElement, | |||
| options = this.options, | |||
| frame = this.dom.frame, | |||
| labels = this.dom.labels, | |||
| labelSet = this.dom.labelSet; | |||
| // create frame | |||
| if (!this.parent) { | |||
| throw new Error('Cannot repaint groupset: no parent attached'); | |||
| } | |||
| var parentContainer = this.parent.getContainer(); | |||
| if (!parentContainer) { | |||
| throw new Error('Cannot repaint groupset: parent has no container element'); | |||
| } | |||
| if (!frame) { | |||
| frame = document.createElement('div'); | |||
| frame.className = 'groupset'; | |||
| frame['timeline-groupset'] = this; | |||
| this.dom.frame = frame; | |||
| var className = options.className; | |||
| if (className) { | |||
| util.addClassName(frame, util.option.asString(className)); | |||
| } | |||
| changed += 1; | |||
| } | |||
| if (!frame.parentNode) { | |||
| parentContainer.appendChild(frame); | |||
| changed += 1; | |||
| } | |||
| // create labels | |||
| var labelContainer = asElement(options.labelContainer); | |||
| if (!labelContainer) { | |||
| throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); | |||
| } | |||
| if (!labels) { | |||
| labels = document.createElement('div'); | |||
| labels.className = 'labels'; | |||
| this.dom.labels = labels; | |||
| } | |||
| if (!labelSet) { | |||
| labelSet = document.createElement('div'); | |||
| labelSet.className = 'label-set'; | |||
| labels.appendChild(labelSet); | |||
| this.dom.labelSet = labelSet; | |||
| } | |||
| if (!labels.parentNode || labels.parentNode != labelContainer) { | |||
| if (labels.parentNode) { | |||
| labels.parentNode.removeChild(labels.parentNode); | |||
| } | |||
| labelContainer.appendChild(labels); | |||
| } | |||
| // reposition frame | |||
| changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); | |||
| changed += update(frame.style, 'top', asSize(options.top, '0px')); | |||
| changed += update(frame.style, 'left', asSize(options.left, '0px')); | |||
| changed += update(frame.style, 'width', asSize(options.width, '100%')); | |||
| // reposition labels | |||
| changed += update(labelSet.style, 'top', asSize(options.top, '0px')); | |||
| changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); | |||
| var me = this, | |||
| queue = this.queue, | |||
| groups = this.groups, | |||
| groupsData = this.groupsData; | |||
| // show/hide added/changed/removed groups | |||
| var ids = Object.keys(queue); | |||
| if (ids.length) { | |||
| ids.forEach(function (id) { | |||
| var action = queue[id]; | |||
| var group = groups[id]; | |||
| //noinspection FallthroughInSwitchStatementJS | |||
| switch (action) { | |||
| case 'add': | |||
| case 'update': | |||
| if (!group) { | |||
| var groupOptions = Object.create(me.options); | |||
| util.extend(groupOptions, { | |||
| height: null, | |||
| maxHeight: null | |||
| }); | |||
| group = new Group(me, id, groupOptions); | |||
| group.setItems(me.itemsData); // attach items data | |||
| groups[id] = group; | |||
| me.controller.add(group); | |||
| } | |||
| // TODO: update group data | |||
| group.data = groupsData.get(id); | |||
| delete queue[id]; | |||
| break; | |||
| case 'remove': | |||
| if (group) { | |||
| group.setItems(); // detach items data | |||
| delete groups[id]; | |||
| me.controller.remove(group); | |||
| } | |||
| // update lists | |||
| delete queue[id]; | |||
| break; | |||
| default: | |||
| console.log('Error: unknown action "' + action + '"'); | |||
| } | |||
| }); | |||
| // the groupset depends on each of the groups | |||
| //this.depends = this.groups; // TODO: gives a circular reference through the parent | |||
| // TODO: apply dependencies of the groupset | |||
| // update the top positions of the groups in the correct order | |||
| var orderedGroups = this.groupsData.getIds({ | |||
| order: this.options.groupOrder | |||
| }); | |||
| for (i = 0; i < orderedGroups.length; i++) { | |||
| (function (group, prevGroup) { | |||
| var top = 0; | |||
| if (prevGroup) { | |||
| top = function () { | |||
| // TODO: top must reckon with options.maxHeight | |||
| return prevGroup.top + prevGroup.height; | |||
| } | |||
| } | |||
| group.setOptions({ | |||
| top: top | |||
| }); | |||
| })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); | |||
| } | |||
| // (re)create the labels | |||
| while (labelSet.firstChild) { | |||
| labelSet.removeChild(labelSet.firstChild); | |||
| } | |||
| for (i = 0; i < orderedGroups.length; i++) { | |||
| id = orderedGroups[i]; | |||
| label = this._createLabel(id); | |||
| labelSet.appendChild(label); | |||
| } | |||
| changed++; | |||
| } | |||
| // reposition the labels | |||
| // TODO: labels are not displayed correctly when orientation=='top' | |||
| // TODO: width of labelPanel is not immediately updated on a change in groups | |||
| for (id in groups) { | |||
| if (groups.hasOwnProperty(id)) { | |||
| group = groups[id]; | |||
| label = group.label; | |||
| if (label) { | |||
| label.style.top = group.top + 'px'; | |||
| label.style.height = group.height + 'px'; | |||
| } | |||
| } | |||
| } | |||
| return (changed > 0); | |||
| }; | |||
| /** | |||
| * Create a label for group with given id | |||
| * @param {Number} id | |||
| * @return {Element} label | |||
| * @private | |||
| */ | |||
| GroupSet.prototype._createLabel = function(id) { | |||
| var group = this.groups[id]; | |||
| var label = document.createElement('div'); | |||
| label.className = 'vlabel'; | |||
| var inner = document.createElement('div'); | |||
| inner.className = 'inner'; | |||
| label.appendChild(inner); | |||
| var content = group.data && group.data.content; | |||
| if (content instanceof Element) { | |||
| inner.appendChild(content); | |||
| } | |||
| else if (content != undefined) { | |||
| inner.innerHTML = content; | |||
| } | |||
| var className = group.data && group.data.className; | |||
| if (className) { | |||
| util.addClassName(label, className); | |||
| } | |||
| group.label = label; // TODO: not so nice, parking labels in the group this way!!! | |||
| return label; | |||
| }; | |||
| /** | |||
| * Get container element | |||
| * @return {HTMLElement} container | |||
| */ | |||
| GroupSet.prototype.getContainer = function getContainer() { | |||
| return this.dom.frame; | |||
| }; | |||
| /** | |||
| * Get the width of the group labels | |||
| * @return {Number} width | |||
| */ | |||
| GroupSet.prototype.getLabelsWidth = function getContainer() { | |||
| return this.props.labels.width; | |||
| }; | |||
| /** | |||
| * Reflow the component | |||
| * @return {Boolean} resized | |||
| */ | |||
| GroupSet.prototype.reflow = function reflow() { | |||
| var changed = 0, | |||
| id, group, | |||
| options = this.options, | |||
| update = util.updateProperty, | |||
| asNumber = util.option.asNumber, | |||
| asSize = util.option.asSize, | |||
| frame = this.dom.frame; | |||
| if (frame) { | |||
| var maxHeight = asNumber(options.maxHeight); | |||
| var fixedHeight = (asSize(options.height) != null); | |||
| var height; | |||
| if (fixedHeight) { | |||
| height = frame.offsetHeight; | |||
| } | |||
| else { | |||
| // height is not specified, calculate the sum of the height of all groups | |||
| height = 0; | |||
| for (id in this.groups) { | |||
| if (this.groups.hasOwnProperty(id)) { | |||
| group = this.groups[id]; | |||
| height += group.height; | |||
| } | |||
| } | |||
| } | |||
| if (maxHeight != null) { | |||
| height = Math.min(height, maxHeight); | |||
| } | |||
| changed += update(this, 'height', height); | |||
| changed += update(this, 'top', frame.offsetTop); | |||
| changed += update(this, 'left', frame.offsetLeft); | |||
| changed += update(this, 'width', frame.offsetWidth); | |||
| } | |||
| // calculate the maximum width of the labels | |||
| var width = 0; | |||
| for (id in this.groups) { | |||
| if (this.groups.hasOwnProperty(id)) { | |||
| group = this.groups[id]; | |||
| var labelWidth = group.props && group.props.label && group.props.label.width || 0; | |||
| width = Math.max(width, labelWidth); | |||
| } | |||
| } | |||
| changed += update(this.props.labels, 'width', width); | |||
| return (changed > 0); | |||
| }; | |||
| /** | |||
| * Hide the component from the DOM | |||
| * @return {Boolean} changed | |||
| */ | |||
| GroupSet.prototype.hide = function hide() { | |||
| if (this.dom.frame && this.dom.frame.parentNode) { | |||
| this.dom.frame.parentNode.removeChild(this.dom.frame); | |||
| return true; | |||
| } | |||
| else { | |||
| return false; | |||
| } | |||
| }; | |||
| /** | |||
| * Show the component in the DOM (when not already visible). | |||
| * A repaint will be executed when the component is not visible | |||
| * @return {Boolean} changed | |||
| */ | |||
| GroupSet.prototype.show = function show() { | |||
| if (!this.dom.frame || !this.dom.frame.parentNode) { | |||
| return this.repaint(); | |||
| } | |||
| else { | |||
| return false; | |||
| } | |||
| }; | |||
| /** | |||
| * Handle updated groups | |||
| * @param {Number[]} ids | |||
| * @private | |||
| */ | |||
| GroupSet.prototype._onUpdate = function _onUpdate(ids) { | |||
| this._toQueue(ids, 'update'); | |||
| }; | |||
| /** | |||
| * Handle changed groups | |||
| * @param {Number[]} ids | |||
| * @private | |||
| */ | |||
| GroupSet.prototype._onAdd = function _onAdd(ids) { | |||
| this._toQueue(ids, 'add'); | |||
| }; | |||
| /** | |||
| * Handle removed groups | |||
| * @param {Number[]} ids | |||
| * @private | |||
| */ | |||
| GroupSet.prototype._onRemove = function _onRemove(ids) { | |||
| this._toQueue(ids, 'remove'); | |||
| }; | |||
| /** | |||
| * Put groups in the queue to be added/updated/remove | |||
| * @param {Number[]} ids | |||
| * @param {String} action can be 'add', 'update', 'remove' | |||
| */ | |||
| GroupSet.prototype._toQueue = function _toQueue(ids, action) { | |||
| var queue = this.queue; | |||
| ids.forEach(function (id) { | |||
| queue[id] = action; | |||
| }); | |||
| if (this.controller) { | |||
| //this.requestReflow(); | |||
| this.requestRepaint(); | |||
| } | |||
| }; | |||
| /** | |||
| * Find the Group from an event target: | |||
| * searches for the attribute 'timeline-groupset' in the event target's element | |||
| * tree, then finds the right group in this groupset | |||
| * @param {Event} event | |||
| * @return {Group | null} group | |||
| */ | |||
| GroupSet.groupFromTarget = function groupFromTarget (event) { | |||
| var groupset, | |||
| target = event.target; | |||
| while (target) { | |||
| if (target.hasOwnProperty('timeline-groupset')) { | |||
| groupset = target['timeline-groupset']; | |||
| break; | |||
| } | |||
| target = target.parentNode; | |||
| } | |||
| if (groupset) { | |||
| for (var groupId in groupset.groups) { | |||
| if (groupset.groups.hasOwnProperty(groupId)) { | |||
| var group = groupset.groups[groupId]; | |||
| if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) { | |||
| return group; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return null; | |||
| }; | |||
| @ -1,59 +0,0 @@ | |||
| .vis.timeline .groupset { | |||
| position: absolute; | |||
| padding: 0; | |||
| margin: 0; | |||
| } | |||
| .vis.timeline .labels { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| padding: 0; | |||
| margin: 0; | |||
| border-right: 1px solid #bfbfbf; | |||
| -moz-box-sizing: border-box; | |||
| box-sizing: border-box; | |||
| } | |||
| .vis.timeline .labels .label-set { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| overflow: hidden; | |||
| border-top: none; | |||
| border-bottom: 1px solid #bfbfbf; | |||
| } | |||
| .vis.timeline .labels .label-set .vlabel { | |||
| position: absolute; | |||
| left: 0; | |||
| top: 0; | |||
| width: 100%; | |||
| color: #4d4d4d; | |||
| } | |||
| .vis.timeline.top .labels .label-set .vlabel, | |||
| .vis.timeline.top .groupset .itemset-axis { | |||
| border-top: 1px solid #bfbfbf; | |||
| border-bottom: none; | |||
| } | |||
| .vis.timeline.bottom .labels .label-set .vlabel, | |||
| .vis.timeline.bottom .groupset .itemset-axis { | |||
| border-top: none; | |||
| border-bottom: 1px solid #bfbfbf; | |||
| } | |||
| .vis.timeline .labels .label-set .vlabel .inner { | |||
| display: inline-block; | |||
| padding: 5px; | |||
| } | |||
| @ -0,0 +1,34 @@ | |||
| .vis.timeline .labelset { | |||
| position: relative; | |||
| width: 100%; | |||
| overflow: hidden; | |||
| box-sizing: border-box; | |||
| } | |||
| .vis.timeline .labelset .vlabel { | |||
| position: relative; | |||
| left: 0; | |||
| top: 0; | |||
| width: 100%; | |||
| color: #4d4d4d; | |||
| box-sizing: border-box; | |||
| } | |||
| .vis.timeline.top .labelset .vlabel { | |||
| border-top: 1px solid #bfbfbf; | |||
| border-bottom: none; | |||
| } | |||
| .vis.timeline.bottom .labelset .vlabel { | |||
| border-top: none; | |||
| border-bottom: 1px solid #bfbfbf; | |||
| } | |||
| .vis.timeline .labelset .vlabel .inner { | |||
| display: inline-block; | |||
| padding: 5px; | |||
| } | |||
| @ -1,304 +1,230 @@ | |||
| /** | |||
| * @constructor ItemBox | |||
| * @extends Item | |||
| * @param {ItemSet} parent | |||
| * @param {Object} data Object containing parameters start | |||
| * content, className. | |||
| * @param {Object} [options] Options to set initial property values | |||
| * @param {Object} [defaultOptions] default options | |||
| * // TODO: describe available options | |||
| */ | |||
| function ItemBox (parent, data, options, defaultOptions) { | |||
| function ItemBox (data, options, defaultOptions) { | |||
| this.props = { | |||
| dot: { | |||
| left: 0, | |||
| top: 0, | |||
| width: 0, | |||
| height: 0 | |||
| }, | |||
| line: { | |||
| top: 0, | |||
| left: 0, | |||
| width: 0, | |||
| height: 0 | |||
| } | |||
| }; | |||
| Item.call(this, parent, data, options, defaultOptions); | |||
| // validate data | |||
| if (data) { | |||
| if (data.start == undefined) { | |||
| throw new Error('Property "start" missing in item ' + data); | |||
| } | |||
| } | |||
| Item.call(this, data, options, defaultOptions); | |||
| } | |||
| ItemBox.prototype = new Item (null, null); | |||
| ItemBox.prototype = new Item (null); | |||
| /** | |||
| * Check whether this item is visible inside given range | |||
| * @returns {{start: Number, end: Number}} range with a timestamp for start and end | |||
| * @returns {boolean} True if visible | |||
| */ | |||
| ItemBox.prototype.isVisible = function isVisible (range) { | |||
| // determine visibility | |||
| // TODO: account for the real width of the item. Right now we just add 1/4 to the window | |||
| var interval = (range.end - range.start) / 4; | |||
| return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); | |||
| }; | |||
| /** | |||
| * Repaint the item | |||
| * @return {Boolean} changed | |||
| */ | |||
| ItemBox.prototype.repaint = function repaint() { | |||
| // TODO: make an efficient repaint | |||
| var changed = false; | |||
| var dom = this.dom; | |||
| if (!dom) { | |||
| this._create(); | |||
| // create DOM | |||
| this.dom = {}; | |||
| dom = this.dom; | |||
| changed = true; | |||
| // create main box | |||
| dom.box = document.createElement('DIV'); | |||
| // contents box (inside the background box). used for making margins | |||
| dom.content = document.createElement('DIV'); | |||
| dom.content.className = 'content'; | |||
| dom.box.appendChild(dom.content); | |||
| // line to axis | |||
| dom.line = document.createElement('DIV'); | |||
| dom.line.className = 'line'; | |||
| // dot on axis | |||
| dom.dot = document.createElement('DIV'); | |||
| dom.dot.className = 'dot'; | |||
| // attach this item as attribute | |||
| dom.box['timeline-item'] = this; | |||
| } | |||
| if (dom) { | |||
| if (!this.parent) { | |||
| throw new Error('Cannot repaint item: no parent attached'); | |||
| // append DOM to parent DOM | |||
| if (!this.parent) { | |||
| throw new Error('Cannot repaint item: no parent attached'); | |||
| } | |||
| if (!dom.box.parentNode) { | |||
| var foreground = this.parent.getForeground(); | |||
| if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element'); | |||
| foreground.appendChild(dom.box); | |||
| } | |||
| if (!dom.line.parentNode) { | |||
| var background = this.parent.getBackground(); | |||
| if (!background) throw new Error('Cannot repaint time axis: parent has no background container element'); | |||
| background.appendChild(dom.line); | |||
| } | |||
| if (!dom.dot.parentNode) { | |||
| var axis = this.parent.getAxis(); | |||
| if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element'); | |||
| axis.appendChild(dom.dot); | |||
| } | |||
| this.displayed = true; | |||
| // update contents | |||
| if (this.data.content != this.content) { | |||
| this.content = this.data.content; | |||
| if (this.content instanceof Element) { | |||
| dom.content.innerHTML = ''; | |||
| dom.content.appendChild(this.content); | |||
| } | |||
| if (!dom.box.parentNode) { | |||
| var foreground = this.parent.getForeground(); | |||
| if (!foreground) { | |||
| throw new Error('Cannot repaint time axis: ' + | |||
| 'parent has no foreground container element'); | |||
| } | |||
| foreground.appendChild(dom.box); | |||
| changed = true; | |||
| else if (this.data.content != undefined) { | |||
| dom.content.innerHTML = this.content; | |||
| } | |||
| if (!dom.line.parentNode) { | |||
| var background = this.parent.getBackground(); | |||
| if (!background) { | |||
| throw new Error('Cannot repaint time axis: ' + | |||
| 'parent has no background container element'); | |||
| } | |||
| background.appendChild(dom.line); | |||
| changed = true; | |||
| else { | |||
| throw new Error('Property "content" missing in item ' + this.data.id); | |||
| } | |||
| if (!dom.dot.parentNode) { | |||
| var axis = this.parent.getAxis(); | |||
| if (!background) { | |||
| throw new Error('Cannot repaint time axis: ' + | |||
| 'parent has no axis container element'); | |||
| } | |||
| axis.appendChild(dom.dot); | |||
| changed = true; | |||
| } | |||
| this.dirty = true; | |||
| } | |||
| this._repaintDeleteButton(dom.box); | |||
| // update contents | |||
| if (this.data.content != this.content) { | |||
| this.content = this.data.content; | |||
| if (this.content instanceof Element) { | |||
| dom.content.innerHTML = ''; | |||
| dom.content.appendChild(this.content); | |||
| } | |||
| else if (this.data.content != undefined) { | |||
| dom.content.innerHTML = this.content; | |||
| } | |||
| else { | |||
| throw new Error('Property "content" missing in item ' + this.data.id); | |||
| } | |||
| changed = true; | |||
| } | |||
| // update class | |||
| var className = (this.data.className? ' ' + this.data.className : '') + | |||
| (this.selected ? ' selected' : ''); | |||
| if (this.className != className) { | |||
| this.className = className; | |||
| dom.box.className = 'item box' + className; | |||
| dom.line.className = 'item line' + className; | |||
| dom.dot.className = 'item dot' + className; | |||
| // update class | |||
| var className = (this.data.className? ' ' + this.data.className : '') + | |||
| (this.selected ? ' selected' : ''); | |||
| if (this.className != className) { | |||
| this.className = className; | |||
| dom.box.className = 'item box' + className; | |||
| dom.line.className = 'item line' + className; | |||
| dom.dot.className = 'item dot' + className; | |||
| changed = true; | |||
| } | |||
| this.dirty = true; | |||
| } | |||
| return changed; | |||
| // recalculate size | |||
| if (this.dirty) { | |||
| this.props.dot.height = dom.dot.offsetHeight; | |||
| this.props.dot.width = dom.dot.offsetWidth; | |||
| this.props.line.width = dom.line.offsetWidth; | |||
| this.width = dom.box.offsetWidth; | |||
| this.height = dom.box.offsetHeight; | |||
| this.dirty = false; | |||
| } | |||
| this._repaintDeleteButton(dom.box); | |||
| }; | |||
| /** | |||
| * Show the item in the DOM (when not already visible). The items DOM will | |||
| * Show the item in the DOM (when not already displayed). The items DOM will | |||
| * be created when needed. | |||
| * @return {Boolean} changed | |||
| */ | |||
| ItemBox.prototype.show = function show() { | |||
| if (!this.dom || !this.dom.box.parentNode) { | |||
| return this.repaint(); | |||
| } | |||
| else { | |||
| return false; | |||
| if (!this.displayed) { | |||
| this.repaint(); | |||
| } | |||
| }; | |||
| /** | |||
| * Hide the item from the DOM (when visible) | |||
| * @return {Boolean} changed | |||
| */ | |||
| ItemBox.prototype.hide = function hide() { | |||
| var changed = false, | |||
| dom = this.dom; | |||
| if (dom) { | |||
| if (dom.box.parentNode) { | |||
| dom.box.parentNode.removeChild(dom.box); | |||
| changed = true; | |||
| } | |||
| if (dom.line.parentNode) { | |||
| dom.line.parentNode.removeChild(dom.line); | |||
| } | |||
| if (dom.dot.parentNode) { | |||
| dom.dot.parentNode.removeChild(dom.dot); | |||
| } | |||
| if (this.displayed) { | |||
| var dom = this.dom; | |||
| if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); | |||
| if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); | |||
| if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); | |||
| this.top = null; | |||
| this.left = null; | |||
| this.displayed = false; | |||
| } | |||
| return changed; | |||
| }; | |||
| /** | |||
| * Reflow the item: calculate its actual size and position from the DOM | |||
| * @return {boolean} resized returns true if the axis is resized | |||
| * @override | |||
| * Reposition the item horizontally | |||
| * @Override | |||
| */ | |||
| ItemBox.prototype.reflow = function reflow() { | |||
| var changed = 0, | |||
| update, | |||
| dom, | |||
| props, | |||
| options, | |||
| margin, | |||
| start, | |||
| align, | |||
| orientation, | |||
| top, | |||
| ItemBox.prototype.repositionX = function repositionX() { | |||
| var start = this.defaultOptions.toScreen(this.data.start), | |||
| align = this.options.align || this.defaultOptions.align, | |||
| left, | |||
| data, | |||
| range; | |||
| box = this.dom.box, | |||
| line = this.dom.line, | |||
| dot = this.dom.dot; | |||
| if (this.data.start == undefined) { | |||
| throw new Error('Property "start" missing in item ' + this.data.id); | |||
| // calculate left position of the box | |||
| if (align == 'right') { | |||
| this.left = start - this.width; | |||
| } | |||
| data = this.data; | |||
| range = this.parent && this.parent.range; | |||
| if (data && range) { | |||
| // TODO: account for the width of the item | |||
| var interval = (range.end - range.start); | |||
| this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); | |||
| else if (align == 'left') { | |||
| this.left = start; | |||
| } | |||
| else { | |||
| this.visible = false; | |||
| // default or 'center' | |||
| this.left = start - this.width / 2; | |||
| } | |||
| if (this.visible) { | |||
| dom = this.dom; | |||
| if (dom) { | |||
| update = util.updateProperty; | |||
| props = this.props; | |||
| options = this.options; | |||
| start = this.parent.toScreen(this.data.start) + this.offset; | |||
| align = options.align || this.defaultOptions.align; | |||
| margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; | |||
| orientation = options.orientation || this.defaultOptions.orientation; | |||
| changed += update(props.dot, 'height', dom.dot.offsetHeight); | |||
| changed += update(props.dot, 'width', dom.dot.offsetWidth); | |||
| changed += update(props.line, 'width', dom.line.offsetWidth); | |||
| changed += update(props.line, 'height', dom.line.offsetHeight); | |||
| changed += update(props.line, 'top', dom.line.offsetTop); | |||
| changed += update(this, 'width', dom.box.offsetWidth); | |||
| changed += update(this, 'height', dom.box.offsetHeight); | |||
| if (align == 'right') { | |||
| left = start - this.width; | |||
| } | |||
| else if (align == 'left') { | |||
| left = start; | |||
| } | |||
| else { | |||
| // default or 'center' | |||
| left = start - this.width / 2; | |||
| } | |||
| changed += update(this, 'left', left); | |||
| changed += update(props.line, 'left', start - props.line.width / 2); | |||
| changed += update(props.dot, 'left', start - props.dot.width / 2); | |||
| changed += update(props.dot, 'top', -props.dot.height / 2); | |||
| if (orientation == 'top') { | |||
| top = margin; | |||
| changed += update(this, 'top', top); | |||
| } | |||
| else { | |||
| // default or 'bottom' | |||
| var parentHeight = this.parent.height; | |||
| top = parentHeight - this.height - margin; | |||
| changed += update(this, 'top', top); | |||
| } | |||
| } | |||
| else { | |||
| changed += 1; | |||
| } | |||
| } | |||
| return (changed > 0); | |||
| }; | |||
| /** | |||
| * Create an items DOM | |||
| * @private | |||
| */ | |||
| ItemBox.prototype._create = function _create() { | |||
| var dom = this.dom; | |||
| if (!dom) { | |||
| this.dom = dom = {}; | |||
| // reposition box | |||
| box.style.left = this.left + 'px'; | |||
| // create the box | |||
| dom.box = document.createElement('DIV'); | |||
| // className is updated in repaint() | |||
| // reposition line | |||
| line.style.left = (start - this.props.line.width / 2) + 'px'; | |||
| // contents box (inside the background box). used for making margins | |||
| dom.content = document.createElement('DIV'); | |||
| dom.content.className = 'content'; | |||
| dom.box.appendChild(dom.content); | |||
| // line to axis | |||
| dom.line = document.createElement('DIV'); | |||
| dom.line.className = 'line'; | |||
| // dot on axis | |||
| dom.dot = document.createElement('DIV'); | |||
| dom.dot.className = 'dot'; | |||
| // attach this item as attribute | |||
| dom.box['timeline-item'] = this; | |||
| } | |||
| // reposition dot | |||
| dot.style.left = (start - this.props.dot.width / 2) + 'px'; | |||
| }; | |||
| /** | |||
| * Reposition the item, recalculate its left, top, and width, using the current | |||
| * range and size of the items itemset | |||
| * @override | |||
| * Reposition the item vertically | |||
| * @Override | |||
| */ | |||
| ItemBox.prototype.reposition = function reposition() { | |||
| var dom = this.dom, | |||
| props = this.props, | |||
| orientation = this.options.orientation || this.defaultOptions.orientation; | |||
| if (dom) { | |||
| var box = dom.box, | |||
| line = dom.line, | |||
| dot = dom.dot; | |||
| box.style.left = this.left + 'px'; | |||
| box.style.top = this.top + 'px'; | |||
| line.style.left = props.line.left + 'px'; | |||
| if (orientation == 'top') { | |||
| line.style.top = 0 + 'px'; | |||
| line.style.height = this.top + 'px'; | |||
| } | |||
| else { | |||
| // orientation 'bottom' | |||
| line.style.top = (this.top + this.height) + 'px'; | |||
| line.style.height = Math.max(this.parent.height - this.top - this.height + | |||
| this.props.dot.height / 2, 0) + 'px'; | |||
| } | |||
| ItemBox.prototype.repositionY = function repositionY () { | |||
| var orientation = this.options.orientation || this.defaultOptions.orientation, | |||
| box = this.dom.box, | |||
| line = this.dom.line, | |||
| dot = this.dom.dot; | |||
| if (orientation == 'top') { | |||
| box.style.top = (this.top || 0) + 'px'; | |||
| box.style.bottom = ''; | |||
| line.style.top = '0'; | |||
| line.style.bottom = ''; | |||
| line.style.height = (this.parent.top + this.top + 1) + 'px'; | |||
| } | |||
| else { // orientation 'bottom' | |||
| box.style.top = ''; | |||
| box.style.bottom = (this.top || 0) + 'px'; | |||
| dot.style.left = props.dot.left + 'px'; | |||
| dot.style.top = props.dot.top + 'px'; | |||
| line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px'; | |||
| line.style.bottom = '0'; | |||
| line.style.height = ''; | |||
| } | |||
| dot.style.top = (-this.props.dot.height / 2) + 'px'; | |||
| }; | |||
| @ -0,0 +1,112 @@ | |||
| /** | |||
| * Utility functions for ordering and stacking of items | |||
| */ | |||
| var stack = {}; | |||
| /** | |||
| * Order items by their start data | |||
| * @param {Item[]} items | |||
| */ | |||
| stack.orderByStart = function orderByStart(items) { | |||
| items.sort(function (a, b) { | |||
| return a.data.start - b.data.start; | |||
| }); | |||
| }; | |||
| /** | |||
| * Order items by their end date. If they have no end date, their start date | |||
| * is used. | |||
| * @param {Item[]} items | |||
| */ | |||
| stack.orderByEnd = function orderByEnd(items) { | |||
| items.sort(function (a, b) { | |||
| var aTime = ('end' in a.data) ? a.data.end : a.data.start, | |||
| bTime = ('end' in b.data) ? b.data.end : b.data.start; | |||
| return aTime - bTime; | |||
| }); | |||
| }; | |||
| /** | |||
| * Adjust vertical positions of the items such that they don't overlap each | |||
| * other. | |||
| * @param {Item[]} items | |||
| * All visible items | |||
| * @param {{item: number, axis: number}} margin | |||
| * Margins between items and between items and the axis. | |||
| * @param {boolean} [force=false] | |||
| * If true, all items will be repositioned. If false (default), only | |||
| * items having a top===null will be re-stacked | |||
| */ | |||
| stack.stack = function _stack (items, margin, force) { | |||
| var i, iMax; | |||
| if (force) { | |||
| // reset top position of all items | |||
| for (i = 0, iMax = items.length; i < iMax; i++) { | |||
| items[i].top = null; | |||
| } | |||
| } | |||
| // calculate new, non-overlapping positions | |||
| for (i = 0, iMax = items.length; i < iMax; i++) { | |||
| var item = items[i]; | |||
| if (item.top === null) { | |||
| // initialize top position | |||
| item.top = margin.axis; | |||
| do { | |||
| // TODO: optimize checking for overlap. when there is a gap without items, | |||
| // you only need to check for items from the next item on, not from zero | |||
| var collidingItem = null; | |||
| for (var j = 0, jj = items.length; j < jj; j++) { | |||
| var other = items[j]; | |||
| if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) { | |||
| collidingItem = other; | |||
| break; | |||
| } | |||
| } | |||
| if (collidingItem != null) { | |||
| // There is a collision. Reposition the items above the colliding element | |||
| item.top = collidingItem.top + collidingItem.height + margin.item; | |||
| } | |||
| } while (collidingItem); | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * Adjust vertical positions of the items without stacking them | |||
| * @param {Item[]} items | |||
| * All visible items | |||
| * @param {{item: number, axis: number}} margin | |||
| * Margins between items and between items and the axis. | |||
| */ | |||
| stack.nostack = function nostack (items, margin) { | |||
| var i, iMax; | |||
| // reset top position of all items | |||
| for (i = 0, iMax = items.length; i < iMax; i++) { | |||
| items[i].top = margin.axis; | |||
| } | |||
| }; | |||
| /** | |||
| * Test if the two provided items collide | |||
| * The items must have parameters left, width, top, and height. | |||
| * @param {Item} a The first item | |||
| * @param {Item} b The second item | |||
| * @param {Number} margin A minimum required margin. | |||
| * If margin is provided, the two items will be | |||
| * marked colliding when they overlap or | |||
| * when the margin between the two is smaller than | |||
| * the requested margin. | |||
| * @return {boolean} true if a and b collide, else false | |||
| */ | |||
| stack.collision = function collision (a, b, margin) { | |||
| return ((a.left - margin) < (b.left + b.width) && | |||
| (a.left + a.width + margin) > b.left && | |||
| (a.top - margin) < (b.top + b.height) && | |||
| (a.top + a.height + margin) > b.top); | |||
| }; | |||