@ -0,0 +1,2 @@ | |||
.idea | |||
node_modules |
@ -0,0 +1,114 @@ | |||
/** | |||
* Jake build script | |||
*/ | |||
var jake = require('jake'), | |||
fs = require('fs'), | |||
path = require('path'); | |||
require('jake-utils'); | |||
/** | |||
* default task | |||
*/ | |||
desc('Execute all tasks: build all libraries'); | |||
task('default', ['timeline'], function () { | |||
console.log('done'); | |||
}); | |||
/** | |||
* timeline | |||
*/ | |||
desc('Build the timeline visualization'); | |||
task('timeline', function () { | |||
var TIMELINE = './bin/timeline/timeline.js'; | |||
var TIMELINE_MIN = './bin/timeline/timeline.min.js'; | |||
var DIR = './bin/timeline'; | |||
jake.rmRf(DIR); | |||
jake.mkdirP(DIR); | |||
// concatenate the script files | |||
concat({ | |||
dest: TIMELINE, | |||
src: [ | |||
'./src/header.js', | |||
'./src/util.js', | |||
'./src/events.js', | |||
'./src/timestep.js', | |||
'./src/dataset.js', | |||
'./src/stack.js', | |||
'./src/range.js', | |||
'./src/controller.js', | |||
'./src/component/component.js', | |||
'./src/component/panel.js', | |||
'./src/component/rootpanel.js', | |||
'./src/component/timeaxis.js', | |||
'./src/component/itemset.js', | |||
'./src/component/item/*.js', | |||
'./src/visualization/timeline.js', | |||
'./lib/moment.js' | |||
], | |||
separator: '\n' | |||
}); | |||
// concatenate the css files | |||
concat({ | |||
dest: './bin/timeline/timeline.css', | |||
src: [ | |||
'./src/component/css/panel.css', | |||
'./src/component/css/item.css', | |||
'./src/component/css/timeaxis.css' | |||
], | |||
separator: '\n' | |||
}); | |||
// minify javascript | |||
minify({ | |||
src: TIMELINE, | |||
dest: TIMELINE_MIN, | |||
header: read('./src/header.js') | |||
}); | |||
// update version number and stuff in the javascript files | |||
[TIMELINE, TIMELINE_MIN].forEach(function (file) { | |||
replace({ | |||
replacements: [ | |||
{pattern: '@@name', replacement: 'timeline'}, | |||
{pattern: '@@date', replacement: today()}, | |||
{pattern: '@@version', replacement: version()} | |||
], | |||
src: file | |||
}); | |||
}); | |||
// copy examples | |||
jake.cpR('./src/examples/timeline', './bin/timeline/examples/'); | |||
console.log('created timeline library'); | |||
}); | |||
/** | |||
* Recursively remove a directory and its files | |||
* https://gist.github.com/tkihira/2367067 | |||
* @param {String} dir | |||
*/ | |||
var rmdir = function(dir) { | |||
var list = fs.readdirSync(dir); | |||
for(var i = 0; i < list.length; i++) { | |||
var filename = path.join(dir, list[i]); | |||
var stat = fs.statSync(filename); | |||
if(filename == "." || filename == "..") { | |||
// pass these files | |||
} else if(stat.isDirectory()) { | |||
// rmdir recursively | |||
rmdir(filename); | |||
} else { | |||
// rm fiilename | |||
fs.unlinkSync(filename); | |||
} | |||
} | |||
fs.rmdirSync(dir); | |||
}; |
@ -0,0 +1,176 @@ | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS |
@ -0,0 +1,14 @@ | |||
Vis.js | |||
Copyright 2010-2013 Almende B.V. | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at | |||
http://www.apache.org/licenses/LICENSE-2.0 | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@ -1,4 +1,10 @@ | |||
vis | |||
=== | |||
vis.js | |||
================== | |||
Dynamic, browser based visualization library | |||
Vis.js is a dynamic, browser based visualization library. | |||
The library is designed to be easy to use, to handle large amounts | |||
of dynamic data, and to enable manipulation of the data. | |||
The library consists of Timeline, LineChart, LineChart3d, Graph, and Treegrid. | |||
vis.js Library is part of [CHAP](http://chap.almende.com), | |||
the Common Hybrid Agent Platform, developed by [Almende B.V](http://almende.com). |
@ -0,0 +1,62 @@ | |||
<!DOCTYPE HTML> | |||
<html> | |||
<head> | |||
<title>Timeline demo</title> | |||
<script src="../timeline.js"></script> | |||
<link href="../timeline.css" rel="stylesheet" type="text/css" /> | |||
<style> | |||
body, html { | |||
width: 100%; | |||
height: 100%; | |||
padding: 0; | |||
margin: 0; | |||
font-family: arial, sans-serif; | |||
font-size: 12pt; | |||
} | |||
#visualization { | |||
box-sizing: border-box; | |||
padding: 10px; | |||
width: 100%; | |||
height: 300px; | |||
} | |||
#visualization .itemset { | |||
/*background: rgba(255, 255, 0, 0.5);*/ | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="visualization"></div> | |||
<script> | |||
// create a dataset with items | |||
var now = moment().minutes(0).seconds(0).milliseconds(0); | |||
var data = new DataSet({ | |||
fieldTypes: { | |||
start: 'Date', | |||
end: 'Date' | |||
} | |||
}); | |||
data.add([ | |||
{id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4).toDate()}, | |||
{id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, | |||
{id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, | |||
{id: 4, content: 'item 4', start: now.clone().add('days', 0).toDate(), end: now.clone().add('days', 3).toDate()}, | |||
{id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point'}, | |||
{id: 6, content: 'item 6', start: now.clone().add('days', 11).toDate()} | |||
]); | |||
var container = document.getElementById('visualization'); | |||
var options = { | |||
start: now.clone().add('days', -3).valueOf(), | |||
end: now.clone().add('days', 7).valueOf() | |||
}; | |||
var timeline = new Timeline(container, data, options); | |||
</script> | |||
</body> | |||
</html> |
@ -0,0 +1,142 @@ | |||
.graph { | |||
position: relative; | |||
border: 1px solid #bfbfbf; | |||
} | |||
.graph .panel { | |||
position: absolute; | |||
} | |||
.graph .itemset { | |||
position: absolute; | |||
} | |||
.graph .item { | |||
position: absolute; | |||
color: #1A1A1A; | |||
border-color: #97B0F8; | |||
background-color: #D5DDF6; | |||
display: inline-block; | |||
} | |||
.graph .item.selected { | |||
border-color: #FFC200; | |||
background-color: #FFF785; | |||
z-index: 999; | |||
} | |||
.graph .item.cluster { | |||
/* TODO: use another color or pattern? */ | |||
background: #97B0F8 url('img/cluster_bg.png'); | |||
color: white; | |||
} | |||
.graph .item.cluster.point { | |||
border-color: #D5DDF6; | |||
} | |||
.graph .item.box { | |||
text-align: center; | |||
border-style: solid; | |||
border-width: 1px; | |||
border-radius: 5px; | |||
-moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.point { | |||
background: none; | |||
} | |||
.graph .dot { | |||
border: 5px solid #97B0F8; | |||
position: absolute; | |||
border-radius: 5px; | |||
-moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.range { | |||
overflow: hidden; | |||
border-style: solid; | |||
border-width: 1px; | |||
border-radius: 2px; | |||
-moz-border-radius: 2px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.range .drag-left { | |||
cursor: w-resize; | |||
z-index: 1000; | |||
} | |||
.graph .item.range .drag-right { | |||
cursor: e-resize; | |||
z-index: 1000; | |||
} | |||
.graph .item.range .content { | |||
position: relative; | |||
display: inline-block; | |||
} | |||
.graph .item.line { | |||
position: absolute; | |||
width: 0; | |||
border-left-width: 1px; | |||
border-left-style: solid; | |||
z-index: -1; | |||
} | |||
.graph .item .content { | |||
margin: 5px; | |||
white-space: nowrap; | |||
overflow: hidden; | |||
} | |||
/* TODO: better css name, 'graph' is way to generic */ | |||
.graph { | |||
overflow: hidden; | |||
} | |||
.graph .axis { | |||
position: relative; | |||
} | |||
.graph .axis .text { | |||
position: absolute; | |||
color: #4d4d4d; | |||
padding: 3px; | |||
white-space: nowrap; | |||
} | |||
.graph .axis .text.measure { | |||
position: absolute; | |||
padding-left: 0; | |||
padding-right: 0; | |||
margin-left: 0; | |||
margin-right: 0; | |||
visibility: hidden; | |||
} | |||
.graph .axis .grid.vertical { | |||
position: absolute; | |||
width: 0; | |||
border-right: 1px solid; | |||
} | |||
.graph .axis .grid.horizontal { | |||
position: absolute; | |||
left: 0; | |||
width: 100%; | |||
height: 0; | |||
border-bottom: 1px solid; | |||
} | |||
.graph .axis .grid.minor { | |||
border-color: #e5e5e5; | |||
} | |||
.graph .axis .grid.major { | |||
border-color: #bfbfbf; | |||
} | |||
@ -0,0 +1,30 @@ | |||
{ | |||
"name": "vis", | |||
"version": "3.0.0-SNAPSHOT", | |||
"description": "A dynamic, browser-based visualization library.", | |||
"homepage": "https://github.com/almende/vis", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/almende/vis.git" | |||
}, | |||
"keywords": [ | |||
"visualization", | |||
"web based", | |||
"browser based", | |||
"chart", | |||
"linechart", | |||
"timeline", | |||
"graph", | |||
"network", | |||
"javascript", | |||
"browser" | |||
], | |||
"dependencies": {}, | |||
"devDependencies": { | |||
"jake": ">= 0.5.9", | |||
"jake-utils": ">= 0.0.18", | |||
"uglify-js": ">= 2.2.5", | |||
"nodeunit": ">= 0.7.4", | |||
"dateable": ">= 0.1.2" | |||
} | |||
} |
@ -0,0 +1,116 @@ | |||
/** | |||
* Prototype for visual components | |||
*/ | |||
function Component () { | |||
this.id = null; | |||
this.parent = null; | |||
this.depends = null; | |||
this.controller = null; | |||
this.options = null; | |||
this.frame = null; // main DOM element | |||
this.top = 0; | |||
this.left = 0; | |||
this.width = 0; | |||
this.height = 0; | |||
} | |||
/** | |||
* Set parameters for the frame. Parameters will be merged in current parameter | |||
* set. | |||
* @param {Object} options Available parameters: | |||
* {String | function} [className] | |||
* {String | Number | function} [left] | |||
* {String | Number | function} [top] | |||
* {String | Number | function} [width] | |||
* {String | Number | function} [height] | |||
*/ | |||
Component.prototype.setOptions = function(options) { | |||
if (options) { | |||
util.extend(this.options, options); | |||
} | |||
if (this.controller) { | |||
this.requestRepaint(); | |||
this.requestReflow(); | |||
} | |||
}; | |||
/** | |||
* Get the container element of the component, which can be used by a child to | |||
* add its own widgets. Not all components do have a container for childs, in | |||
* that case null is returned. | |||
* @returns {HTMLElement | null} container | |||
*/ | |||
Component.prototype.getContainer = function () { | |||
// should be implemented by the component | |||
return null; | |||
}; | |||
/** | |||
* Get the frame element of the component, the outer HTML DOM element. | |||
* @returns {HTMLElement | null} frame | |||
*/ | |||
Component.prototype.getFrame = function () { | |||
return this.frame; | |||
}; | |||
/** | |||
* Repaint the component | |||
* @return {Boolean} changed | |||
*/ | |||
Component.prototype.repaint = function () { | |||
// should be implemented by the component | |||
return false; | |||
}; | |||
/** | |||
* Reflow the component | |||
* @return {Boolean} resized | |||
*/ | |||
Component.prototype.reflow = function () { | |||
// should be implemented by the component | |||
return false; | |||
}; | |||
/** | |||
* Request a repaint. The controller will schedule a repaint | |||
*/ | |||
Component.prototype.requestRepaint = function () { | |||
if (this.controller) { | |||
this.controller.requestRepaint(); | |||
} | |||
else { | |||
throw new Error('Cannot request a repaint: no controller configured'); | |||
// TODO: just do a repaint when no parent is configured? | |||
} | |||
}; | |||
/** | |||
* Request a reflow. The controller will schedule a reflow | |||
*/ | |||
Component.prototype.requestReflow = function () { | |||
if (this.controller) { | |||
this.controller.requestReflow(); | |||
} | |||
else { | |||
throw new Error('Cannot request a reflow: no controller configured'); | |||
// TODO: just do a reflow when no parent is configured? | |||
} | |||
}; | |||
/** | |||
* Event handler | |||
* @param {String} event name of the event, for example 'click', 'mousemove' | |||
* @param {function} callback callback handler, invoked with the raw HTML Event | |||
* as parameter. | |||
*/ | |||
Component.prototype.on = function (event, callback) { | |||
// TODO: rethink the way of event delegation | |||
if (this.parent) { | |||
this.parent.on(event, callback); | |||
} | |||
else { | |||
throw new Error('Cannot attach event: no root panel found'); | |||
} | |||
}; |
@ -0,0 +1,84 @@ | |||
.graph .itemset { | |||
position: absolute; | |||
} | |||
.graph .item { | |||
position: absolute; | |||
color: #1A1A1A; | |||
border-color: #97B0F8; | |||
background-color: #D5DDF6; | |||
display: inline-block; | |||
} | |||
.graph .item.selected { | |||
border-color: #FFC200; | |||
background-color: #FFF785; | |||
z-index: 999; | |||
} | |||
.graph .item.cluster { | |||
/* TODO: use another color or pattern? */ | |||
background: #97B0F8 url('img/cluster_bg.png'); | |||
color: white; | |||
} | |||
.graph .item.cluster.point { | |||
border-color: #D5DDF6; | |||
} | |||
.graph .item.box { | |||
text-align: center; | |||
border-style: solid; | |||
border-width: 1px; | |||
border-radius: 5px; | |||
-moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.point { | |||
background: none; | |||
} | |||
.graph .dot { | |||
border: 5px solid #97B0F8; | |||
position: absolute; | |||
border-radius: 5px; | |||
-moz-border-radius: 5px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.range { | |||
overflow: hidden; | |||
border-style: solid; | |||
border-width: 1px; | |||
border-radius: 2px; | |||
-moz-border-radius: 2px; /* For Firefox 3.6 and older */ | |||
} | |||
.graph .item.range .drag-left { | |||
cursor: w-resize; | |||
z-index: 1000; | |||
} | |||
.graph .item.range .drag-right { | |||
cursor: e-resize; | |||
z-index: 1000; | |||
} | |||
.graph .item.range .content { | |||
position: relative; | |||
display: inline-block; | |||
} | |||
.graph .item.line { | |||
position: absolute; | |||
width: 0; | |||
border-left-width: 1px; | |||
border-left-style: solid; | |||
z-index: -1; | |||
} | |||
.graph .item .content { | |||
margin: 5px; | |||
white-space: nowrap; | |||
overflow: hidden; | |||
} |
@ -0,0 +1,9 @@ | |||
.graph { | |||
position: relative; | |||
border: 1px solid #bfbfbf; | |||
} | |||
.graph .panel { | |||
position: absolute; | |||
} |
@ -0,0 +1,47 @@ | |||
/* TODO: better css name, 'graph' is way to generic */ | |||
.graph { | |||
overflow: hidden; | |||
} | |||
.graph .axis { | |||
position: relative; | |||
} | |||
.graph .axis .text { | |||
position: absolute; | |||
color: #4d4d4d; | |||
padding: 3px; | |||
white-space: nowrap; | |||
} | |||
.graph .axis .text.measure { | |||
position: absolute; | |||
padding-left: 0; | |||
padding-right: 0; | |||
margin-left: 0; | |||
margin-right: 0; | |||
visibility: hidden; | |||
} | |||
.graph .axis .grid.vertical { | |||
position: absolute; | |||
width: 0; | |||
border-right: 1px solid; | |||
} | |||
.graph .axis .grid.horizontal { | |||
position: absolute; | |||
left: 0; | |||
width: 100%; | |||
height: 0; | |||
border-bottom: 1px solid; | |||
} | |||
.graph .axis .grid.minor { | |||
border-color: #e5e5e5; | |||
} | |||
.graph .axis .grid.major { | |||
border-color: #bfbfbf; | |||
} |
@ -0,0 +1,36 @@ | |||
/** | |||
* @constructor Item | |||
* @param {ItemSet} parent | |||
* @param {Object} data Object containing (optional) parameters type, | |||
* start, end, content, group, className. | |||
* @param {Object} [options] Options to set initial property values | |||
* // TODO: describe available options | |||
*/ | |||
function Item (parent, data, options) { | |||
this.parent = parent; | |||
this.data = data; | |||
this.selected = false; | |||
this.visible = true; | |||
this.dom = null; | |||
this.options = options; | |||
} | |||
Item.prototype = new Component(); | |||
/** | |||
* Select current item | |||
*/ | |||
Item.prototype.select = function () { | |||
this.selected = true; | |||
}; | |||
/** | |||
* Unselect current item | |||
*/ | |||
Item.prototype.unselect = function () { | |||
this.selected = false; | |||
}; | |||
// create a namespace for all item types | |||
var itemTypes = {}; |
@ -0,0 +1,270 @@ | |||
/** | |||
* @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 | |||
* // TODO: describe available options | |||
*/ | |||
function ItemBox (parent, data, options) { | |||
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); | |||
} | |||
ItemBox.prototype = new Item (null, null); | |||
// register the ItemBox in the item types | |||
itemTypes['box'] = ItemBox; | |||
/** | |||
* Select the item | |||
* @override | |||
*/ | |||
ItemBox.prototype.select = function () { | |||
this.selected = true; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Unselect the item | |||
* @override | |||
*/ | |||
ItemBox.prototype.unselect = function () { | |||
this.selected = false; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Repaint the item | |||
* @return {Boolean} changed | |||
*/ | |||
ItemBox.prototype.repaint = function () { | |||
// TODO: make an efficient repaint | |||
var changed = false; | |||
var dom = this.dom; | |||
if (this.visible) { | |||
if (!dom) { | |||
this._create(); | |||
changed = true; | |||
} | |||
dom = this.dom; | |||
if (dom) { | |||
if (!this.options && !this.parent) { | |||
throw new Error('Cannot repaint item: no parent attached'); | |||
} | |||
var parentContainer = this.parent.getContainer(); | |||
if (!parentContainer) { | |||
throw new Error('Cannot repaint time axis: parent has no container element'); | |||
} | |||
if (!dom.box.parentNode) { | |||
parentContainer.appendChild(dom.box); | |||
changed = true; | |||
} | |||
if (!dom.line.parentNode) { | |||
parentContainer.appendChild(dom.line); | |||
changed = true; | |||
} | |||
if (!dom.dot.parentNode) { | |||
parentContainer.appendChild(dom.dot); | |||
changed = 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); | |||
} | |||
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; | |||
changed = true; | |||
} | |||
} | |||
} | |||
else { | |||
// hide when visible | |||
if (dom) { | |||
if (dom.box.parentNode) { | |||
dom.box.parentNode.removeChild(dom.box); | |||
changed = true; | |||
} | |||
if (dom.line.parentNode) { | |||
dom.line.parentNode.removeChild(dom.line); | |||
changed = true; | |||
} | |||
if (dom.dot.parentNode) { | |||
dom.dot.parentNode.removeChild(dom.dot); | |||
changed = true; | |||
} | |||
} | |||
} | |||
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 | |||
*/ | |||
ItemBox.prototype.reflow = function () { | |||
if (this.data.start == undefined) { | |||
throw new Error('Property "start" missing in item ' + this.data.id); | |||
} | |||
var update = util.updateProperty, | |||
dom = this.dom, | |||
props = this.props, | |||
options = this.options, | |||
start = this.parent.toScreen(this.data.start), | |||
align = options && options.align, | |||
orientation = options.orientation, | |||
changed = 0, | |||
top, | |||
left; | |||
if (dom) { | |||
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, 'width', dom.line.offsetWidth); | |||
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); | |||
if (orientation == 'top') { | |||
top = options.margin.axis; | |||
changed += update(this, 'top', top); | |||
changed += update(props.line, 'top', 0); | |||
changed += update(props.line, 'height', top); | |||
changed += update(props.dot, 'top', -props.dot.height / 2); | |||
} | |||
else { | |||
// default or 'bottom' | |||
var parentHeight = this.parent.height; | |||
top = parentHeight - this.height - options.margin.axis; | |||
changed += update(this, 'top', top); | |||
changed += update(props.line, 'top', top + this.height); | |||
changed += update(props.line, 'height', Math.max(options.margin.axis, 0)); | |||
changed += update(props.dot, 'top', parentHeight - props.dot.height / 2); | |||
} | |||
} | |||
else { | |||
changed += 1; | |||
} | |||
return (changed > 0); | |||
}; | |||
/** | |||
* Create an items DOM | |||
* @private | |||
*/ | |||
ItemBox.prototype._create = function () { | |||
var dom = this.dom; | |||
if (!dom) { | |||
this.dom = dom = {}; | |||
// create the box | |||
dom.box = document.createElement('DIV'); | |||
// className is updated in repaint() | |||
// 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'; | |||
} | |||
}; | |||
/** | |||
* Reposition the item, recalculate its left, top, and width, using the current | |||
* range and size of the items itemset | |||
* @override | |||
*/ | |||
ItemBox.prototype.reposition = function () { | |||
var dom = this.dom, | |||
props = this.props, | |||
orientation = this.options.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 = props.line.top + 'px'; | |||
line.style.top = (this.top + this.height) + 'px'; | |||
line.style.height = (props.dot.top - this.top - this.height) + 'px'; | |||
} | |||
dot.style.left = props.dot.left + 'px'; | |||
dot.style.top = props.dot.top + 'px'; | |||
} | |||
}; |
@ -0,0 +1,209 @@ | |||
/** | |||
* @constructor ItemPoint | |||
* @extends Item | |||
* @param {ItemSet} parent | |||
* @param {Object} data Object containing parameters start | |||
* content, className. | |||
* @param {Object} [options] Options to set initial property values | |||
* // TODO: describe available options | |||
*/ | |||
function ItemPoint (parent, data, options) { | |||
this.props = { | |||
dot: { | |||
top: 0, | |||
width: 0, | |||
height: 0 | |||
}, | |||
content: { | |||
height: 0, | |||
marginLeft: 0 | |||
} | |||
}; | |||
Item.call(this, parent, data, options); | |||
} | |||
ItemPoint.prototype = new Item (null, null); | |||
// register the ItemPoint in the item types | |||
itemTypes['point'] = ItemPoint; | |||
/** | |||
* Select the item | |||
* @override | |||
*/ | |||
ItemPoint.prototype.select = function () { | |||
this.selected = true; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Unselect the item | |||
* @override | |||
*/ | |||
ItemPoint.prototype.unselect = function () { | |||
this.selected = false; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Repaint the item | |||
* @return {Boolean} changed | |||
*/ | |||
ItemPoint.prototype.repaint = function () { | |||
// TODO: make an efficient repaint | |||
var changed = false; | |||
var dom = this.dom; | |||
if (this.visible) { | |||
if (!dom) { | |||
this._create(); | |||
changed = true; | |||
} | |||
dom = this.dom; | |||
if (dom) { | |||
if (!this.options && !this.options.parent) { | |||
throw new Error('Cannot repaint item: no parent attached'); | |||
} | |||
var parentContainer = this.parent.getContainer(); | |||
if (!parentContainer) { | |||
throw new Error('Cannot repaint time axis: parent has no container element'); | |||
} | |||
if (!dom.point.parentNode) { | |||
parentContainer.appendChild(dom.point); | |||
changed = 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); | |||
} | |||
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.point.className = 'item point' + className; | |||
changed = true; | |||
} | |||
} | |||
} | |||
else { | |||
// hide when visible | |||
if (dom) { | |||
if (dom.point.parentNode) { | |||
dom.point.parentNode.removeChild(dom.point); | |||
changed = true; | |||
} | |||
} | |||
} | |||
return changed; | |||
}; | |||
/** | |||
* Reflow the item: calculate its actual size from the DOM | |||
* @return {boolean} resized returns true if the axis is resized | |||
* @override | |||
*/ | |||
ItemPoint.prototype.reflow = function () { | |||
if (this.data.start == undefined) { | |||
throw new Error('Property "start" missing in item ' + this.data.id); | |||
} | |||
var update = util.updateProperty, | |||
dom = this.dom, | |||
props = this.props, | |||
options = this.options, | |||
orientation = options.orientation, | |||
start = this.parent.toScreen(this.data.start), | |||
changed = 0, | |||
top; | |||
if (dom) { | |||
changed += update(this, 'width', dom.point.offsetWidth); | |||
changed += update(this, 'height', dom.point.offsetHeight); | |||
changed += update(props.dot, 'width', dom.dot.offsetWidth); | |||
changed += update(props.dot, 'height', dom.dot.offsetHeight); | |||
changed += update(props.content, 'height', dom.content.offsetHeight); | |||
if (orientation == 'top') { | |||
top = options.margin.axis; | |||
} | |||
else { | |||
// default or 'bottom' | |||
var parentHeight = this.parent.height; | |||
top = parentHeight - this.height - options.margin.axis; | |||
} | |||
changed += update(this, 'top', top); | |||
changed += update(this, 'left', start - props.dot.width / 2); | |||
changed += update(props.content, 'marginLeft', 1.5 * props.dot.width); | |||
//changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO | |||
changed += update(props.dot, 'top', (this.height - props.dot.height) / 2); | |||
} | |||
else { | |||
changed += 1; | |||
} | |||
return (changed > 0); | |||
}; | |||
/** | |||
* Create an items DOM | |||
* @private | |||
*/ | |||
ItemPoint.prototype._create = function () { | |||
var dom = this.dom; | |||
if (!dom) { | |||
this.dom = dom = {}; | |||
// background box | |||
dom.point = document.createElement('div'); | |||
// className is updated in repaint() | |||
// contents box, right from the dot | |||
dom.content = document.createElement('div'); | |||
dom.content.className = 'content'; | |||
dom.point.appendChild(dom.content); | |||
// dot at start | |||
dom.dot = document.createElement('div'); | |||
dom.dot.className = 'dot'; | |||
dom.point.appendChild(dom.dot); | |||
} | |||
}; | |||
/** | |||
* Reposition the item, recalculate its left, top, and width, using the current | |||
* range and size of the items itemset | |||
* @override | |||
*/ | |||
ItemPoint.prototype.reposition = function () { | |||
var dom = this.dom, | |||
props = this.props; | |||
if (dom) { | |||
dom.point.style.top = this.top + 'px'; | |||
dom.point.style.left = this.left + 'px'; | |||
dom.content.style.marginLeft = props.content.marginLeft + 'px'; | |||
//dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO | |||
dom.dot.style.top = props.dot.top + 'px'; | |||
} | |||
}; |
@ -0,0 +1,219 @@ | |||
/** | |||
* @constructor ItemRange | |||
* @extends Item | |||
* @param {ItemSet} parent | |||
* @param {Object} data Object containing parameters start, end | |||
* content, className. | |||
* @param {Object} [options] Options to set initial property values | |||
* // TODO: describe available options | |||
*/ | |||
function ItemRange (parent, data, options) { | |||
this.props = { | |||
content: { | |||
left: 0, | |||
width: 0 | |||
} | |||
}; | |||
Item.call(this, parent, data, options); | |||
} | |||
ItemRange.prototype = new Item (null, null); | |||
// register the ItemBox in the item types | |||
itemTypes['range'] = ItemRange; | |||
/** | |||
* Select the item | |||
* @override | |||
*/ | |||
ItemRange.prototype.select = function () { | |||
this.selected = true; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Unselect the item | |||
* @override | |||
*/ | |||
ItemRange.prototype.unselect = function () { | |||
this.selected = false; | |||
// TODO: select and unselect | |||
}; | |||
/** | |||
* Repaint the item | |||
* @return {Boolean} changed | |||
*/ | |||
ItemRange.prototype.repaint = function () { | |||
// TODO: make an efficient repaint | |||
var changed = false; | |||
var dom = this.dom; | |||
if (this.visible) { | |||
if (!dom) { | |||
this._create(); | |||
changed = true; | |||
} | |||
dom = this.dom; | |||
if (dom) { | |||
if (!this.options && !this.options.parent) { | |||
throw new Error('Cannot repaint item: no parent attached'); | |||
} | |||
var parentContainer = this.parent.getContainer(); | |||
if (!parentContainer) { | |||
throw new Error('Cannot repaint time axis: parent has no container element'); | |||
} | |||
if (!dom.box.parentNode) { | |||
parentContainer.appendChild(dom.box); | |||
changed = true; | |||
} | |||
// update content | |||
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) : ''; | |||
if (this.className != className) { | |||
this.className = className; | |||
dom.box.className = 'item range' + className; | |||
changed = true; | |||
} | |||
} | |||
} | |||
else { | |||
// hide when visible | |||
if (dom) { | |||
if (dom.box.parentNode) { | |||
dom.box.parentNode.removeChild(dom.box); | |||
changed = true; | |||
} | |||
} | |||
} | |||
return changed; | |||
}; | |||
/** | |||
* Reflow the item: calculate its actual size from the DOM | |||
* @return {boolean} resized returns true if the axis is resized | |||
* @override | |||
*/ | |||
ItemRange.prototype.reflow = function () { | |||
if (this.data.start == undefined) { | |||
throw new Error('Property "start" missing in item ' + this.data.id); | |||
} | |||
if (this.data.end == undefined) { | |||
throw new Error('Property "end" missing in item ' + this.data.id); | |||
} | |||
var dom = this.dom, | |||
props = this.props, | |||
options = this.options, | |||
parent = this.parent, | |||
start = parent.toScreen(this.data.start), | |||
end = parent.toScreen(this.data.end), | |||
changed = 0; | |||
if (dom) { | |||
var update = util.updateProperty, | |||
box = dom.box, | |||
parentWidth = parent.width, | |||
orientation = options.orientation, | |||
contentLeft, | |||
top; | |||
changed += update(props.content, 'width', dom.content.offsetWidth); | |||
changed += update(this, 'height', box.offsetHeight); | |||
// limit the width of the this, as browsers cannot draw very wide divs | |||
if (start < -parentWidth) { | |||
start = -parentWidth; | |||
} | |||
if (end > 2 * parentWidth) { | |||
end = 2 * parentWidth; | |||
} | |||
// when range exceeds left of the window, position the contents at the left of the visible area | |||
if (start < 0) { | |||
contentLeft = Math.min(-start, | |||
(end - start - props.content.width - 2 * options.padding)); | |||
// TODO: remove the need for options.padding. it's terrible. | |||
} | |||
else { | |||
contentLeft = 0; | |||
} | |||
changed += update(props.content, 'left', contentLeft); | |||
if (orientation == 'top') { | |||
top = options.margin.axis; | |||
changed += update(this, 'top', top); | |||
} | |||
else { | |||
// default or 'bottom' | |||
top = parent.height - this.height - options.margin.axis; | |||
changed += update(this, 'top', top); | |||
} | |||
changed += update(this, 'left', start); | |||
changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width; | |||
} | |||
else { | |||
changed += 1; | |||
} | |||
return (changed > 0); | |||
}; | |||
/** | |||
* Create an items DOM | |||
* @private | |||
*/ | |||
ItemRange.prototype._create = function () { | |||
var dom = this.dom; | |||
if (!dom) { | |||
this.dom = dom = {}; | |||
// background box | |||
dom.box = document.createElement('div'); | |||
// className is updated in repaint() | |||
// contents box | |||
dom.content = document.createElement('div'); | |||
dom.content.className = 'content'; | |||
dom.box.appendChild(dom.content); | |||
} | |||
}; | |||
/** | |||
* Reposition the item, recalculate its left, top, and width, using the current | |||
* range and size of the items itemset | |||
* @override | |||
*/ | |||
ItemRange.prototype.reposition = function () { | |||
var dom = this.dom, | |||
props = this.props; | |||
if (dom) { | |||
dom.box.style.top = this.top + 'px'; | |||
dom.box.style.left = this.left + 'px'; | |||
dom.box.style.width = this.width + 'px'; | |||
dom.content.style.left = props.content.left + 'px'; | |||
} | |||
}; |
@ -0,0 +1,416 @@ | |||
/** | |||
* An ItemSet holds a set of items and ranges which can be displayed in a | |||
* range. The width is determined by the parent of the ItemSet, and the height | |||
* is determined by the size of the items. | |||
* @param {Component} parent | |||
* @param {Component[]} [depends] Components on which this components depends | |||
* (except for the parent) | |||
* @param {Object} [options] See ItemSet.setOptions for the available | |||
* options. | |||
* @constructor ItemSet | |||
* @extends Panel | |||
*/ | |||
function ItemSet(parent, depends, options) { | |||
this.id = util.randomUUID(); | |||
this.parent = parent; | |||
this.depends = depends; | |||
// one options object is shared by this itemset and all its items | |||
this.options = { | |||
style: 'box', | |||
align: 'center', | |||
orientation: 'bottom', | |||
margin: { | |||
axis: 20, | |||
item: 10 | |||
}, | |||
padding: 5 | |||
}; | |||
var me = this; | |||
this.data = null; // DataSet | |||
this.range = null; // Range or Object {start: number, end: number} | |||
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); | |||
} | |||
}; | |||
this.items = {}; | |||
this.queue = {}; // queue with items to be added/updated/removed | |||
this.stack = new Stack(this); | |||
this.conversion = null; | |||
this.setOptions(options); | |||
} | |||
ItemSet.prototype = new Panel(); | |||
/** | |||
* Set options for the ItemSet. Existing options will be extended/overwritten. | |||
* @param {Object} [options] The following options are available: | |||
* {String | function} [className] | |||
* class name for the itemset | |||
* {String} [style] | |||
* Default style for the items. Choose from 'box' | |||
* (default), 'point', or 'range'. The default | |||
* Style can be overwritten by individual items. | |||
* {String} align | |||
* Alignment for the items, only applicable for | |||
* ItemBox. Choose 'center' (default), 'left', or | |||
* 'right'. | |||
* {String} orientation | |||
* Orientation of the item set. Choose 'top' or | |||
* 'bottom' (default). | |||
* {Number} margin.axis | |||
* Margin between the axis and the items in pixels. | |||
* Default is 20. | |||
* {Number} margin.item | |||
* Margin between items in pixels. Default is 10. | |||
* {Number} padding | |||
* Padding of the contents of an item in pixels. | |||
* Must correspond with the items css. Default is 5. | |||
*/ | |||
ItemSet.prototype.setOptions = function (options) { | |||
util.extend(this.options, options); | |||
// TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis | |||
this.stack.setOptions(this.options); | |||
}; | |||
/** | |||
* Set range (start and end). | |||
* @param {Range | Object} range A Range or an object containing start and end. | |||
*/ | |||
ItemSet.prototype.setRange = function (range) { | |||
if (!(range instanceof Range) && (!range || !range.start || !range.end)) { | |||
throw new TypeError('Range must be an instance of Range, ' + | |||
'or an object containing start and end.'); | |||
} | |||
this.range = range; | |||
}; | |||
/** | |||
* Repaint the component | |||
* @return {Boolean} changed | |||
*/ | |||
ItemSet.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 = 'itemset'; | |||
if (options.className) { | |||
util.addClassName(frame, util.option.asString(options.className)); | |||
} | |||
this.frame = frame; | |||
changed += 1; | |||
} | |||
if (!frame.parentNode) { | |||
if (!this.parent) { | |||