Browse Source

Started reworking component/panel architecture

css_transitions
josdejong 10 years ago
parent
commit
27f9b20a28
14 changed files with 271 additions and 559 deletions
  1. +0
    -1
      Jakefile.js
  2. +0
    -1
      src/module/exports.js
  3. +0
    -184
      src/timeline/Controller.js
  4. +69
    -92
      src/timeline/Range.js
  5. +89
    -78
      src/timeline/Timeline.js
  6. +2
    -58
      src/timeline/component/Component.js
  7. +3
    -12
      src/timeline/component/CurrentTime.js
  8. +2
    -11
      src/timeline/component/CustomTime.js
  9. +0
    -5
      src/timeline/component/GroupSet.js
  10. +0
    -4
      src/timeline/component/ItemSet.js
  11. +54
    -8
      src/timeline/component/Panel.js
  12. +49
    -99
      src/timeline/component/RootPanel.js
  13. +1
    -6
      src/timeline/component/TimeAxis.js
  14. +2
    -0
      test/timeline.html

+ 0
- 1
Jakefile.js View File

@ -67,7 +67,6 @@ task('build', {async: true}, function () {
'./src/timeline/TimeStep.js',
'./src/timeline/Stack.js',
'./src/timeline/Range.js',
'./src/timeline/Controller.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/Panel.js',
'./src/timeline/component/RootPanel.js',

+ 0
- 1
src/module/exports.js View File

@ -4,7 +4,6 @@
var vis = {
util: util,
Controller: Controller,
DataSet: DataSet,
DataView: DataView,
Range: Range,

+ 0
- 184
src/timeline/Controller.js View File

@ -1,184 +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 = requestAnimationFrame(function () {
reflowTimer = null;
me.reflow();
});
}
}
});
/**
* 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 = requestAnimationFrame(function () {
repaintTimer = null;
me.repaint();
});
}
}
});
}
// 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) {
if (true) { // TODO: fix this loop
this.repaint();
}
// TODO: limit the number of nested reflows/repaints, prevent loop
};

+ 69
- 92
src/timeline/Range.js View File

@ -3,22 +3,58 @@
* A Range controls a numeric range with a start and end value.
* The Range adjusts the range based on mouse events or programmatic changes,
* and triggers events when the range is changing or has been changed.
* @param {Component} parent
* @param {Emitter} emitter
* @param {Object} [options] See description at Range.setOptions
* @extends Controller
*/
function Range(options) {
function Range(parent, emitter, options) {
this.id = util.randomUUID();
this.start = null; // Number
this.end = null; // Number
this.parent = parent;
this.emitter = emitter;
this.options = options || {};
// drag start listener
var me = this;
emitter.on('dragstart', function (event) {
me._onDragStart(event, parent);
});
// drag listener
emitter.on('drag', function (event) {
me._onDrag(event, parent);
});
// drag end listener
emitter.on('dragend', function (event) {
me._onDragEnd(event, parent);
});
// ignore dragging when holding
emitter.on('hold', function (event) {
me._onHold();
});
// mouse wheel
function mousewheel (event) {
me._onMouseWheel(event, parent);
}
emitter.on('mousewheel', mousewheel);
emitter.on('DOMMouseScroll', mousewheel); // For FF
// pinch
emitter.on('touch', function (event) {
me._onTouch(event);
});
emitter.on('pinch', function (event) {
me._onPinch(event, parent);
});
this.setOptions(options);
}
// extend the Range prototype with an event emitter mixin
Emitter(Range.prototype);
/**
* Set options for the range controller
* @param {Object} options Available options:
@ -49,59 +85,6 @@ function validateDirection (direction) {
}
}
/**
* Add listeners for mouse and touch events to the component
* @param {Controller} controller
* @param {Component} component Should be a rootpanel
* @param {String} event Available events: 'move', 'zoom'
* @param {String} direction Available directions: 'horizontal', 'vertical'
*/
Range.prototype.subscribe = function (controller, component, event, direction) {
var me = this;
if (event == 'move') {
// drag start listener
controller.on('dragstart', function (event) {
me._onDragStart(event, component);
});
// drag listener
controller.on('drag', function (event) {
me._onDrag(event, component, direction);
});
// drag end listener
controller.on('dragend', function (event) {
me._onDragEnd(event, component);
});
// ignore dragging when holding
controller.on('hold', function (event) {
me._onHold();
});
}
else if (event == 'zoom') {
// mouse wheel
function mousewheel (event) {
me._onMouseWheel(event, component, direction);
}
controller.on('mousewheel', mousewheel);
controller.on('DOMMouseScroll', mousewheel); // For FF
// pinch
controller.on('touch', function (event) {
me._onTouch(event);
});
controller.on('pinch', function (event) {
me._onPinch(event, component, direction);
});
}
else {
throw new TypeError('Unknown event "' + event + '". ' +
'Choose "move" or "zoom".');
}
};
/**
* Set a new start and end range
* @param {Number} [start]
@ -114,8 +97,8 @@ Range.prototype.setRange = function(start, end) {
start: this.start,
end: this.end
};
this.emit('rangechange', params);
this.emit('rangechanged', params);
this.emitter.emit('rangechange', params);
this.emitter.emit('rangechanged', params);
}
};
@ -280,10 +263,9 @@ var touchParams = {};
/**
* Start dragging horizontally or vertically
* @param {Event} event
* @param {Object} component
* @private
*/
Range.prototype._onDragStart = function(event, component) {
Range.prototype._onDragStart = function(event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
@ -293,7 +275,7 @@ Range.prototype._onDragStart = function(event, component) {
touchParams.start = this.start;
touchParams.end = this.end;
var frame = component.frame;
var frame = this.parent.frame;
if (frame) {
frame.style.cursor = 'move';
}
@ -302,11 +284,10 @@ Range.prototype._onDragStart = function(event, component) {
/**
* Perform dragging operating.
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onDrag = function (event, component, direction) {
Range.prototype._onDrag = function (event) {
var direction = this.options.direction;
validateDirection(direction);
// TODO: reckon with option movable
@ -318,12 +299,12 @@ Range.prototype._onDrag = function (event, component, direction) {
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start),
width = (direction == 'horizontal') ? component.width : component.height,
width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
diffRange = -delta / width * interval;
this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
this.emit('rangechange', {
this.emitter.emit('rangechange', {
start: this.start,
end: this.end
});
@ -332,22 +313,21 @@ Range.prototype._onDrag = function (event, component, direction) {
/**
* Stop dragging operating.
* @param {event} event
* @param {Component} component
* @private
*/
Range.prototype._onDragEnd = function (event, component) {
Range.prototype._onDragEnd = function (event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
// TODO: reckon with option movable
if (component.frame) {
component.frame.style.cursor = 'auto';
if (this.parent.frame) {
this.parent.frame.style.cursor = 'auto';
}
// fire a rangechanged event
this.emit('rangechanged', {
this.emitter.emit('rangechanged', {
start: this.start,
end: this.end
});
@ -357,13 +337,9 @@ Range.prototype._onDragEnd = function (event, component) {
* Event handler for mouse wheel event, used to zoom
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onMouseWheel = function(event, component, direction) {
validateDirection(direction);
Range.prototype._onMouseWheel = function(event) {
// TODO: reckon with option zoomable
// retrieve delta
@ -394,8 +370,8 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
pointer = getPointer(gesture.center, component.frame),
pointerDate = this._pointerToDate(component, direction, pointer);
pointer = getPointer(gesture.center, this.parent.frame),
pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
}
@ -434,24 +410,23 @@ Range.prototype._onHold = function () {
/**
* Handle pinch event
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onPinch = function (event, component, direction) {
Range.prototype._onPinch = function (event) {
var direction = this.options.direction;
touchParams.ignore = true;
// TODO: reckon with option zoomable
if (event.gesture.touches.length > 1) {
if (!touchParams.center) {
touchParams.center = getPointer(event.gesture.center, component.frame);
touchParams.center = getPointer(event.gesture.center, this.parent.frame);
}
var scale = 1 / event.gesture.scale,
initDate = this._pointerToDate(component, direction, touchParams.center),
center = getPointer(event.gesture.center, component.frame),
date = this._pointerToDate(component, direction, center),
initDate = this._pointerToDate(touchParams.center),
center = getPointer(event.gesture.center, this.parent.frame),
date = this._pointerToDate(this.parent, center),
delta = date - initDate; // TODO: utilize delta
// calculate new start and end
@ -465,21 +440,23 @@ Range.prototype._onPinch = function (event, component, direction) {
/**
* Helper function to calculate the center date for zooming
* @param {Component} component
* @param {{x: Number, y: Number}} pointer
* @param {String} direction 'horizontal' or 'vertical'
* @return {number} date
* @private
*/
Range.prototype._pointerToDate = function (component, direction, pointer) {
Range.prototype._pointerToDate = function (pointer) {
var conversion;
var direction = this.options.direction;
validateDirection(direction);
if (direction == 'horizontal') {
var width = component.width;
var width = this.parent.width;
conversion = this.conversion(width);
return pointer.x / conversion.scale + conversion.offset;
}
else {
var height = component.height;
var height = this.parent.height;
conversion = this.conversion(height);
return pointer.y / conversion.scale + conversion.offset;
}

+ 89
- 78
src/timeline/Timeline.js View File

@ -6,10 +6,14 @@
* @constructor
*/
function Timeline (container, items, options) {
// validate arguments
if (!container) throw new Error('No container element provided');
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.options = {
orientation: 'bottom',
direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
editable: false,
selectable: true,
@ -29,7 +33,6 @@ function Timeline (container, items, options) {
type: 'box',
align: 'center',
orientation: 'bottom',
margin: {
axis: 20,
item: 10
@ -50,106 +53,110 @@ function Timeline (container, items, options) {
}
};
// controller
this.controller = new Controller();
// event bus
this.emitter = new Emitter();
// root panel
if (!container) {
throw new Error('No container element provided');
}
var rootOptions = Object.create(this.options);
rootOptions.height = function () {
// TODO: change to height
if (me.options.height) {
// fixed height
return me.options.height;
}
else {
// auto height
return (me.timeaxis.height + me.content.height) + 'px';
// return (me.timeaxis.height + me.content.height) + 'px';
// TODO: return the sum of the height of the childs
}
};
this.rootPanel = new RootPanel(container, rootOptions);
this.controller.add(this.rootPanel);
this.rootPanel = new RootPanel(container, this.emitter, rootOptions);
// single select (or unselect) when tapping an item
this.controller.on('tap', this._onSelectItem.bind(this));
this.emitter.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.controller.on('hold', this._onMultiSelectItem.bind(this));
this.emitter.on('hold', this._onMultiSelectItem.bind(this));
// add item on doubletap
this.controller.on('doubletap', this._onAddItem.bind(this));
this.emitter.on('doubletap', this._onAddItem.bind(this));
// item panel
var itemOptions = Object.create(this.options);
itemOptions.left = function () {
return me.labelPanel.width;
};
itemOptions.width = function () {
return me.rootPanel.width - me.labelPanel.width;
};
itemOptions.top = null;
itemOptions.height = null;
this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
this.controller.add(this.itemPanel);
// range
// TODO: move range inside rootPanel?
var rangeOptions = Object.create(this.options);
this.range = new Range(this.rootPanel, this.emitter, rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
// label panel
var labelOptions = Object.create(this.options);
labelOptions.top = null;
labelOptions.left = null;
labelOptions.height = null;
labelOptions.top = '0';
labelOptions.bottom = null;
labelOptions.left = '0';
labelOptions.right = null;
labelOptions.height = '100%';
labelOptions.width = function () {
/* TODO: dynamically determine the width of the label panel
if (me.content && typeof me.content.getLabelsWidth === 'function') {
return me.content.getLabelsWidth();
}
else {
return 0;
}
*/
return 200;
};
this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
this.controller.add(this.labelPanel);
// range
var rangeOptions = Object.create(this.options);
this.range = new Range(rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) {
var force = true;
me.controller.emit('rangechange', properties);
me.controller.emit('request-reflow', force);
});
this.range.on('rangechanged', function (properties) {
var force = true;
me.controller.emit('rangechanged', properties);
me.controller.emit('request-reflow', force);
});
// time axis
var timeaxisOptions = Object.create(rootOptions);
timeaxisOptions.range = this.range;
timeaxisOptions.left = null;
timeaxisOptions.top = null;
timeaxisOptions.width = '100%';
timeaxisOptions.height = null;
this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
this.timeaxis.setRange(this.range);
this.controller.add(this.timeaxis);
this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
this.labelPanel = new Panel(labelOptions);
this.rootPanel.appendChild(this.labelPanel);
// main panel (contains time axis and itemsets)
var mainOptions = Object.create(this.options);
mainOptions.top = '0';
mainOptions.bottom = null;
mainOptions.left = null;
mainOptions.right = '0';
mainOptions.height = '100%';
mainOptions.width = function () {
return me.rootPanel.width - me.labelPanel.width;
};
this.mainPanel = new Panel(mainOptions);
this.rootPanel.appendChild(this.mainPanel);
// content panel (contains itemset(s))
var contentOptions = Object.create(this.options);
contentOptions.top = '0';
contentOptions.bottom = null;
contentOptions.left = '0';
contentOptions.right = null;
contentOptions.height = function () {
return me.mainPanel.height - me.timeAxis.height;
};
contentOptions.width = null;
this.contentPanel = new Panel(contentOptions);
this.mainPanel.appendChild(this.contentPanel);
// panel with time axis
var timeAxisOptions = Object.create(rootOptions);
timeAxisOptions.range = this.range;
timeAxisOptions.left = null;
timeAxisOptions.top = null;
timeAxisOptions.width = null;
timeAxisOptions.height = null; // height is determined by the
this.timeAxis = new TimeAxis(timeAxisOptions);
this.timeAxis.setRange(this.range);
this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
this.mainPanel.appendChild(this.timeAxis);
/* TODO
// current time bar
this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
this.controller.add(this.currenttime);
this.currenttime = new CurrentTime(rootOptions);
this.mainPanel.appendChild(this.currenttime);
// custom time bar
this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
this.controller.add(this.customtime);
this.customtime = new CustomTime(rootOptions);
this.mainPanel.appendChild(this.customtime);
*/
// create groupset
this.setGroups(null);
@ -175,7 +182,7 @@ function Timeline (container, items, options) {
* @param {function} callback
*/
Timeline.prototype.on = function on (event, callback) {
this.controller.on(event, callback);
this.emitter.on(event, callback);
};
/**
@ -184,7 +191,7 @@ Timeline.prototype.on = function on (event, callback) {
* @param {function} callback
*/
Timeline.prototype.off = function off (event, callback) {
this.controller.off(event, callback);
this.emitter.off(event, callback);
};
/**
@ -217,8 +224,8 @@ Timeline.prototype.setOptions = function (options) {
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
this.controller.reflow();
this.controller.repaint();
//this.controller.reflow(); // TODO: remove
this.rootPanel.repaint();
};
/**
@ -272,7 +279,9 @@ Timeline.prototype.setItems = function(items) {
// set items
this.itemsData = newDataSet;
/* TODO
this.content.setItems(newDataSet);
*/
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
// apply the data range as range
@ -326,7 +335,7 @@ Timeline.prototype.setGroups = function(groups) {
if (this.content.setGroups) {
this.content.setGroups(); // disconnect from groups
}
this.controller.remove(this.content);
//this.controller.remove(this.content); // TODO: cleanup
}
// create new content set
@ -367,6 +376,7 @@ Timeline.prototype.setGroups = function(groups) {
}
});
/* TODO
this.content = new Type(this.itemPanel, [this.timeaxis], options);
if (this.content.setRange) {
this.content.setRange(this.range);
@ -377,7 +387,7 @@ Timeline.prototype.setGroups = function(groups) {
if (this.content.setGroups) {
this.content.setGroups(this.groupsData);
}
this.controller.add(this.content);
*/
}
};
@ -482,7 +492,7 @@ Timeline.prototype._onSelectItem = function (event) {
var selection = item ? [item.id] : [];
this.setSelection(selection);
this.controller.emit('select', {
this.emitter.emit('select', {
items: this.getSelection()
});
@ -535,10 +545,11 @@ Timeline.prototype._onAddItem = function (event) {
me.itemsData.add(newItem);
// select the created item after it is repainted
me.controller.once('repaint', function () {
// FIXME: just repaint the whole thing, not via an emitted event
me.emitter.once('repaint', function () {
me.setSelection([id]);
me.controller.emit('select', {
me.emitter.emit('select', {
items: me.getSelection()
});
}.bind(me));
@ -573,7 +584,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
}
this.setSelection(selection);
this.controller.emit('select', {
this.emitter.emit('select', {
items: this.getSelection()
});

+ 2
- 58
src/timeline/component/Component.js View File

@ -4,8 +4,7 @@
function Component () {
this.id = null;
this.parent = null;
this.depends = null;
this.controller = null;
this.childs = null;
this.options = null;
this.frame = null; // main DOM element
@ -29,10 +28,7 @@ Component.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
if (this.controller) {
this.requestRepaint();
this.requestReflow();
}
this.repaint();
}
};
@ -54,23 +50,6 @@ Component.prototype.getOption = function getOption(name) {
return value;
};
/**
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/
Component.prototype.setController = function setController (controller) {
this.controller = controller || null;
};
/**
* Get controller of this component
* @return {Controller} controller
*/
Component.prototype.getController = function getController () {
return this.controller;
};
/**
* 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
@ -98,15 +77,6 @@ Component.prototype.repaint = function repaint() {
// should be implemented by the component
};
/**
* Reflow the component
* @return {Boolean} resized
*/
Component.prototype.reflow = function reflow() {
// should be implemented by the component
return false;
};
/**
* Hide the component from the DOM
* @return {Boolean} changed
@ -134,29 +104,3 @@ Component.prototype.show = function show() {
return false;
}
};
/**
* Request a repaint. The controller will schedule a repaint
*/
Component.prototype.requestRepaint = function requestRepaint() {
if (this.controller) {
this.controller.emit('request-repaint');
}
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 requestReflow() {
if (this.controller) {
this.controller.emit('request-reflow');
}
else {
throw new Error('Cannot request a reflow: no controller configured');
// TODO: just do a reflow when no parent is configured?
}
};

+ 3
- 12
src/timeline/component/CurrentTime.js View File

@ -1,18 +1,13 @@
/**
* A current time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {Boolean} [showCurrentTime]
* @constructor CurrentTime
* @extends Component
*/
function CurrentTime (parent, depends, options) {
function CurrentTime (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
this.defaultOptions = {
@ -39,13 +34,13 @@ CurrentTime.prototype.getContainer = function () {
*/
CurrentTime.prototype.repaint = function () {
var bar = this.frame,
parent = this.parent,
parentContainer = parent.parent.getContainer();
parent = this.parent;
if (!parent) {
throw new Error('Cannot repaint bar: no parent attached');
}
var parentContainer = parent.parent.getContainer(); // FIXME: this is weird
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
@ -70,10 +65,6 @@ CurrentTime.prototype.repaint = function () {
this.frame = bar;
}
if (!parent.conversion) {
parent._updateConversion();
}
var now = new Date();
var x = parent.toScreen(now);

+ 2
- 11
src/timeline/component/CustomTime.js View File

@ -1,18 +1,13 @@
/**
* A custom time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
function CustomTime (parent, depends, options) {
function CustomTime (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
this.defaultOptions = {
@ -50,7 +45,7 @@ CustomTime.prototype.repaint = function () {
throw new Error('Cannot repaint bar: no parent attached');
}
var parentContainer = parent.parent.getContainer();
var parentContainer = parent.parent.getContainer(); // FIXME: this is weird
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
@ -92,10 +87,6 @@ CustomTime.prototype.repaint = function () {
this.hammer.on('dragend', this._onDragEnd.bind(this));
}
if (!parent.conversion) {
parent._updateConversion();
}
var x = parent.toScreen(this.customTime);
bar.style.left = x + 'px';

+ 0
- 5
src/timeline/component/GroupSet.js View File

@ -500,11 +500,6 @@ GroupSet.prototype._toQueue = function _toQueue(ids, action) {
ids.forEach(function (id) {
queue[id] = action;
});
if (this.controller) {
//this.requestReflow();
this.requestRepaint();
}
};
/**

+ 0
- 4
src/timeline/component/ItemSet.js View File

@ -177,10 +177,6 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
item.select();
}
}
if (this.controller) {
this.requestRepaint();
}
}
};

+ 54
- 8
src/timeline/component/Panel.js View File

@ -1,8 +1,5 @@
/**
* A panel can contain components
* @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]
@ -12,10 +9,10 @@
* @constructor Panel
* @extends Component
*/
function Panel(parent, depends, options) {
function Panel(options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.parent = null;
this.childs = [];
this.options = options || {};
}
@ -42,6 +39,40 @@ Panel.prototype.getContainer = function () {
return this.frame;
};
/**
* Append a child to the panel
* @param {Component} child
*/
Panel.prototype.appendChild = function (child) {
this.childs.push(child);
child.parent = this;
};
/**
* Insert a child to the panel
* @param {Component} child
* @param {Component} beforeChild
*/
Panel.prototype.insertBefore = function (child, beforeChild) {
var index = this.childs.indexOf(beforeChild);
if (index != -1) {
this.childs.splice(index, 0, child);
child.parent = this;
}
};
/**
* Remove a child from the panel
* @param {Component} child
*/
Panel.prototype.removeChild = function (child) {
var index = this.childs.indexOf(child);
if (index != -1) {
this.childs.splice(index, 1);
child.parent = null;
}
};
/**
* Repaint the component
*/
@ -65,18 +96,33 @@ Panel.prototype.repaint = function () {
// update className
frame.className = 'vpanel' + (options.className ? (' ' + asSize(options.className)) : '');
// repaint the child components
this._repaintChilds();
// update frame size
this._updateSize();
};
/**
* Repaint all childs of the panel
* @private
*/
Panel.prototype._repaintChilds = function () {
for (var i = 0, ii = this.childs.length; i < ii; i++) {
this.childs[i].repaint();
}
};
/**
* Apply the size from options to the panel, and recalculate it's actual size.
* @private
*/
Panel.prototype._updateSize = function () {
// apply size
this.frame.style.top = util.option.asSize(this.options.top, '0px');
this.frame.style.left = util.option.asSize(this.options.left, '0px');
this.frame.style.top = util.option.asSize(this.options.top, null);
this.frame.style.bottom = util.option.asSize(this.options.bottom, null);
this.frame.style.left = util.option.asSize(this.options.left, null);
this.frame.style.right = util.option.asSize(this.options.right, null);
this.frame.style.width = util.option.asSize(this.options.width, '100%');
this.frame.style.height = util.option.asSize(this.options.height, '100%');

+ 49
- 99
src/timeline/component/RootPanel.js View File

@ -2,32 +2,15 @@
* A root panel can hold components. The root panel must be initialized with
* a DOM element as container.
* @param {HTMLElement} container
* @param {Emitter} emitter
* @param {Object} [options] Available parameters: see RootPanel.setOptions.
* @constructor RootPanel
* @extends Panel
*/
function RootPanel(container, options) {
function RootPanel(container, emitter, options) {
this.id = util.randomUUID();
this.container = container;
// create functions to be used as DOM event listeners
var me = this;
this.hammer = null;
// create listeners for all interesting events, these events will be emitted
// via the controller
var events = [
'touch', 'pinch', 'tap', 'doubletap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
];
this.listeners = {};
events.forEach(function (event) {
me.listeners[event] = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
me.controller.emit.apply(me.controller, args);
};
});
this.emitter = emitter;
this.options = options || {};
this.defaultOptions = {
@ -47,50 +30,66 @@ RootPanel.prototype = new Panel();
* {String | Number | function} [height]
* {Boolean | function} [autoResize]
*/
RootPanel.prototype.setOptions = Component.prototype.setOptions;
RootPanel.prototype.setOptions = function (options) {
if (options) {
util.extend(this.options, options);
this.repaint();
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
}
else {
this._unwatch();
}
}
};
/**
* Repaint the component
*/
RootPanel.prototype.repaint = function () {
var asSize = util.option.asSize,
options = this.options,
frame = this.frame;
// create frame
if (!frame) {
frame = document.createElement('div');
this.frame = frame;
if (!this.frame) {
if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
this.container.appendChild(frame);
this.frame = document.createElement('div');
this.container.appendChild(this.frame);
this._registerListeners();
// create event listeners for all interesting events, these events will be
// emitted via emitter
this.hammer = Hammer(this.frame, {
prevent_default: true
});
this.listeners = {};
var me = this;
var events = [
'touch', 'pinch', 'tap', 'doubletap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
];
events.forEach(function (event) {
var listener = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
me.emitter.emit.apply(me.emitter, args);
};
me.hammer.on(event, listener);
me.listeners[event] = listener;
});
}
// update class name
var options = this.options;
var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
if (options.className) className += ' ' + util.option.asString(className);
frame.className = className;
this.frame.className = className;
// repaint the child components
this._repaintChilds();
// update frame size
this._updateSize();
this._updateWatch();
};
/**
* Update watching for resize, depending on the current option
* @private
*/
RootPanel.prototype._updateWatch = function () {
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
}
else {
this._unwatch();
}
};
/**
@ -117,7 +116,8 @@ RootPanel.prototype._watch = function () {
(me.frame.clientHeight != me.lastHeight)) {
me.lastWidth = me.frame.clientWidth;
me.lastHeight = me.frame.clientHeight;
me.requestRepaint();
me.repaint();
// TODO: emit a resize event
}
}
};
@ -140,53 +140,3 @@ RootPanel.prototype._unwatch = function () {
// TODO: remove event listener on window.resize
};
/**
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/
RootPanel.prototype.setController = function setController (controller) {
this.controller = controller || null;
if (this.controller) {
this._registerListeners();
}
else {
this._unregisterListeners();
}
};
/**
* Register event emitters emitted by the rootpanel
* @private
*/
RootPanel.prototype._registerListeners = function () {
if (this.frame && this.controller && !this.hammer) {
this.hammer = Hammer(this.frame, {
prevent_default: true
});
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.on(event, this.listeners[event]);
}
}
}
};
/**
* Unregister event emitters from the rootpanel
* @private
*/
RootPanel.prototype._unregisterListeners = function () {
if (this.hammer) {
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.off(event, this.listeners[event]);
}
}
this.hammer = null;
}
};

+ 1
- 6
src/timeline/component/TimeAxis.js View File

@ -1,17 +1,12 @@
/**
* A horizontal time axis
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] See TimeAxis.setOptions for the available
* options.
* @constructor TimeAxis
* @extends Component
*/
function TimeAxis (parent, depends, options) {
function TimeAxis (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.dom = {
majorLines: [],

+ 2
- 0
test/timeline.html View File

@ -68,6 +68,8 @@
end: now.clone().add('days', 7),
//maxHeight: 200,
height: 200,
showCurrentTime: true,
showCustomTime: true,
//start: moment('2013-01-01'),
//end: moment('2013-12-31'),
//min: moment('2013-01-01'),

Loading…
Cancel
Save