diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..bb81dda8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +## Contributing + +Contributions to the vis.js library are very welcome! We can't do this alone. +You can contribute in different ways: spread the word, report bugs, come up with +ideas and suggestions, and contribute to the code. + +There are a few preferences regarding code contributions: + +- vis.js follows the node.js code style as described + [here](http://nodeguide.com/style.html). +- When implementing new features, please update the documentation accordingly. +- Send pull requests to the `develop` branch, not the `master` branch. +- Only commit changes done in the source files under `lib`, not to the builds + which are located in the folder `dist`. + +Thanks! diff --git a/HISTORY.md b/HISTORY.md index 3d99c305..f000c549 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -27,29 +27,82 @@ http://visjs.org - Rebuilt the cluster system +## not yet released, version 3.12.1-SNAPSHOT -## not yet released, version 3.10.1-SNAPSHOT +### Timeline + +- Fixed #761: Timeline and Graph2d throwing an error when locale is not found. + Gives a warning message instead. +- Fixed #782: Contents of items created from a template being unnecessary + recreated on click, causing mouse events to get lost. + +### Network + +- Fixed titles not working when any of the nodes has id `0`. + + +## 2015-04-07, version 3.12.0 + +### Network + +- Fixed support for DataSet with custom id fields (option `fieldId`). + +### Timeline + +- Orientation can now be configured separately for axis and items. +- The event handlers `onMove` and `onMoving` are now invoked with all item + properties as argument, and can be used to update all properties (like + content, className, etc) and add new properties as well. +- Fixed #654: removed unnecessary minimum height for groups, takes the + height of the group label as minimum height now. +- Fixed #708: detecting wrong group when page is scrolled. +- Fixed #733: background items being selected on shift+click. + + +## 2015-03-05, version 3.11.0 ### Network - (added gradient coloring for lines, but set for release in 4.0 due to required refactoring of options) - Fixed bug where a network that has frozen physics would resume redrawing after setData, setOptions etc. -- (add docs) Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default. -- (add docs) Added two new static smooth curves modes: curveCW and curve CCW. -- Added request redraw for certain internal processes to reduce number of draw calls. +- Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default. +- Added two new static smooth curves modes: curveCW and curve CCW. +- Added request redraw for certain internal processes to reduce number of draw calls (performance improvements!). - Added pull request for usage of Icons. Thanks @Dude9177! - Allow hierarchical view to be set in setOptions. - Fixed manipulation bar for mobile. - -### Graph2d - - +- Fixed #670: Bug when updating data in a DataSet, when Network is connected to the DataSet via a DataView. +- Fixed #688: Added a css class to be able to distinguish buttons "Edit node" + and "Edit edge". ### Timeline +- Implemented orientation option `'both'`, displaying a time axis both on top + and bottom (#665). +- Implemented creating new range items by dragging in an empty space with the + ctrl key down. +- Implemented configuration option `order: function` to define a custom ordering + for the items (see #538, #234). +- Implemented events `click`, `doubleClick`, and `contextMenu`. +- Implemented method `getEventProperties(event)`. - Fixed not property initializing with a DataView for groups. - Merged add custom timebar functionality, thanks @aytech! - Fixed #664: end of item not restored when canceling a move event. +- Fixed #609: reduce the left/right dragarea when an item range is very small, + so you can still move it as a whole. +- Fixed #676: misalignment of background items when using subgroups and the + group label's height is larger than the contents. + +### Graph2d + +- Implemented events `click`, `doubleClick`, and `contextMenu`. +- Implemented method `getEventProperties(event)`. + +### DataSet/DataView + +- Implemented support for mapping field names. Thanks @spatialillusions. +- Fixed #670: DataView not passing a data property on update events (see #670) + ## 2015-02-11, version 3.10.0 @@ -91,11 +144,6 @@ http://visjs.org - Fixed a bug in the `DataSet` returning an empty object instead of `null` when no item was found when using both a filter and specifying fields. -### Timeline - -- Implemented option `timeAxis: {scale: string, step: number}` to set a - fixed scale. - ## 2015-01-16, version 3.9.1 diff --git a/dist/vis.css b/dist/vis.css index b00cb06b..c05381a2 100644 --- a/dist/vis.css +++ b/dist/vis.css @@ -262,6 +262,7 @@ .vis-item.vis-range .vis-drag-left { position: absolute; width: 24px; + max-width: 20%; height: 100%; top: 0; left: -4px; @@ -272,6 +273,7 @@ .vis-item.vis-range .vis-drag-right { position: absolute; width: 24px; + max-width: 20%; height: 100%; top: 0; right: -4px; @@ -1221,4 +1223,4 @@ div.vis-color-picker input.vis-range-brightness { div.vis-color-picker input.vis-saturation-range { width: 289px !important; -}*/ \ No newline at end of file +}*/ diff --git a/docs/dataset.html b/docs/dataset.html index add1bc04..81152644 100644 --- a/docs/dataset.html +++ b/docs/dataset.html @@ -740,9 +740,10 @@ DataSet.map(callback [, options]); fields - String[ ] + String[ ] | Object.<String, String> - An array with field names. + An array with field names, or an object with current field name and + new field name that the field is returned as. By default, all properties of the items are emitted. When fields is defined, only the properties whose name is specified in fields will be included diff --git a/docs/dataview.html b/docs/dataview.html index 93b13042..28e3ec6c 100644 --- a/docs/dataview.html +++ b/docs/dataview.html @@ -129,9 +129,10 @@ var data = new vis.DataView(dataset, options) fields - String[ ] + String[ ] | Object.<String, String> - An array with field names. + An array with field names, or an object with current field name and + new field name that the field is returned as. By default, all properties of the items are emitted. When fields is defined, only the properties whose name is specified in fields will be included diff --git a/docs/graph2d.html b/docs/graph2d.html index aa3dce7b..d82db4eb 100644 --- a/docs/graph2d.html +++ b/docs/graph2d.html @@ -678,7 +678,7 @@ The options colored in green can also be used as options for the groups. All opt orientation String 'bottom' - Orientation of the timeline: 'top' or 'bottom' (default). If orientation is 'bottom', the time axis is drawn at the bottom, and if 'top', the axis is drawn on top. + Orientation of the timeline: 'top', 'bottom' (default), or 'both'. If orientation is 'bottom', the time axis is drawn at the bottom. When 'top', the axis is drawn on top. When 'both', two axes are drawn, both on top and at the bottom. @@ -813,6 +813,29 @@ Graph2d.clear({options: true}); // clear options only + + click + Fired when clicked inside the Graph2d. + + + Passes a properties object as returned by the method Graph2d.getEventProperties(event). + + + + + contextmenu + Fired when right-clicked inside the Graph2d. Note that in order to prevent the context menu from showing up, default behavior of the event must be stopped: +
graph2d.on('contextmenu', function (props) {
+  alert('Right click!');
+  props.event.preventDefault();
+});
+
+ + + Passes a properties object as returned by the method Graph2d.getEventProperties(event). + + + destroy() none @@ -820,6 +843,22 @@ Graph2d.clear({options: true}); // clear options only + + doubleClick + Fired when double clicked inside the Graph2d. + + + Passes a properties object as returned by the method Graph2d.getEventProperties(event). + + + + + fit() + none + Adjust the visible window such that it fits all items. + + + getCurrentTime() Date @@ -834,6 +873,24 @@ Graph2d.clear({options: true}); // clear options only + + getEventProperties(event) + Object + + Returns an Object with relevant properties from an event: + + + + getLegend(groupId, iconWidth, iconHeight) SVGelement, String, String @@ -854,9 +911,13 @@ Graph2d.clear({options: true}); // clear options only - fit() + hiddenDates + Object none - Adjust the visible window such that it fits all items. + This option allows you to hide specific timespans from the time axis. The dates can be supplied as an object: + {start: '2014-03-21 00:00:00', end: '2014-03-28 00:00:00', [repeat:'daily']} or as an Array of these objects. The repeat argument is optional. + The possible values are (case-sensitive): daily, weekly, monthly, yearly. To hide a weekend, pick any Saturday as start and the following Monday as end + and set repeat to weekly. diff --git a/docs/network.html b/docs/network.html index a730faa7..011e4ddb 100644 --- a/docs/network.html +++ b/docs/network.html @@ -165,14 +165,14 @@ The constructor accepts three parameters: edges, which both contain an array with objects. Optionally, data may contain an options object. The parameter data is optional, data can also be set using - the method setData. Section Data Format + the method setData. Section Data Format describes the data object.
  • options is an optional Object containing a name-value map with options. Options can also be set using the method setOptions. - Section Configuration Options + Section Configuration Options describes the available options.
  • @@ -214,7 +214,7 @@ var data = { A property options, containing an object with global options. Options can be provided as third parameter in the network constructor - as well. Section Configuration Options + as well. Section Configuration Options describes the available options. @@ -280,13 +280,13 @@ When using a DataSet, the network is automatically updating to changes in the Da allowedToMoveX Boolean no - If allowedToMoveX is false, then the node will not move in the X direction from its position. + If allowedToMoveX is false, then the node will not move in the X direction from its position. This does not do anything in hierarchical views. allowedToMoveY Boolean no - If allowedToMoveY is false, then the node will not move in the Y direction from its position. + If allowedToMoveY is false, then the node will not move in the Y direction from its position. This does not do anything in hierarchical views. @@ -599,7 +599,14 @@ var options = { When a Network is configured to be clickToUse, it will react to mouse, touch, and keyboard events only when active. When active, a blue shadow border is displayed around the Network. The Network is set active by clicking on it, and is changed to inactive again by clicking outside the Network or by pressing the ESC key. - + + useDefaultGroups + boolean + true + If true, the default groups are used when groups are used. If you have defined your own groups those will be used. If you have an item with a group that is NOT in your own group list, + setting useDefaultGroups true will iterate over the default groups for unknown groups. If it is set to false, it will iterate over your own groups for unknown groups. + + physics Object @@ -647,7 +654,7 @@ var options = { - freezeForStabilization + freezeForStabilization Boolean false @@ -758,7 +765,7 @@ var options = { smoothCurves.type String "continuous" - This option only affects NONdynamic smooth curves. The supported types are: continuous, discrete, diagonalCross, straightCross, horizontal, vertical. The effects of these types + This option only affects NONdynamic smooth curves. The supported types are: continuous, discrete, diagonalCross, straightCross, horizontal, vertical, curvedCW, curvedCCW. The effects of these types are shown in examples 26 and 27 @@ -972,7 +979,7 @@ mySize = minSize + diff * scale; When using values, you can let the font scale with the size of the nodes if you enable the scaleFontWithValue option. This is the minimum value of the fontSize. - fontSizeMax + fontSizeMax Number 30 When using values, you can let the font scale with the size of the nodes if you enable the scaleFontWithValue option. This is the maximum value of the fontSize. @@ -1483,7 +1490,7 @@ To unify the physics system, the damping, repulsion distance and edge length hav If no options for the physics system are supplied, the Barnes-Hut method will be used with the default parameters. If you want to customize the physics system easily, you can use the configurePhysics option.
    When using the hierarchical display option, hierarchicalRepulsion is automatically used as the physics solver. Similarly, if you use the hierarchicalRepulsion physics option, hierarchical display is automatically turned on with default settings. -

    Note: if the behaviour of your network is not the way you want it, use configurePhysics as described below or by example 25.

    +

    Note: if the behaviour of your network is not the way you want it, use configurePhysics as described below or by example 25.

     // These variables must be defined in an options object named physics.
    @@ -2648,6 +2655,13 @@ network.off('select', onSelect);
             none
             
         
    +    
    +        stabilizationIterationsDone
    +        Fired once when the network finished the initial stabilization run. This is fired REGARDLESS if the network has stabilized. It only means that the amount of configured stabilizationIterations have been completed.
    +        
    +        none
    +        
    +    
         
             stabilized
             Fired every time the network has been stabilized. This event can be used to trigger the .storePositions() function after stabilization. Fired with an object having the following properties:
    diff --git a/docs/timeline.html b/docs/timeline.html
    index e84eb2b1..9c4a3089 100644
    --- a/docs/timeline.html
    +++ b/docs/timeline.html
    @@ -205,7 +205,7 @@ var items = [
       
       
         end
    -    Date
    +    Date | number | string | Moment
         no
         The end date of the item. The end date is optional, and can be left null.
           If end date is provided, the item is displayed as a range.
    @@ -232,7 +232,7 @@ var items = [
       
       
         start
    -    Date
    +    Date | number | string | Moment
         yes
         The start date of the item, for example new Date(2010,9,23).
       
    @@ -468,7 +468,7 @@ var options = {
     
     
         end
    -    Date | Number | String
    +    Date | Number | String | Moment
         none
         The initial end date for the axis of the timeline.
             If not provided, the latest date present in the items set is taken as
    @@ -554,6 +554,13 @@ var options = {
         A map with i18n locales. See section Localization for more information.
     
     
    +
    +    margin
    +    Number | Object
    +    Object
    +    When a number, applies the margin to margin.axis, margin.item.horizontal, and margin.item.vertical.
    +
    +
     
         margin.axis
         Number
    @@ -584,7 +591,7 @@ var options = {
     
     
         max
    -    Date | Number | String
    +    Date | Number | String | Moment
         none
         Set a maximum Date for the visible range.
             It will not be possible to move beyond this maximum.
    @@ -600,7 +607,7 @@ var options = {
     
     
         min
    -    Date | Number | String
    +    Date | Number | String | Moment
         none
         Set a minimum Date for the visible range.
             It will not be possible to move beyond this minimum.
    @@ -664,24 +671,39 @@ var options = {
         
     
     
    -
     
     
         orientation
    +    String | Object
    +    'bottom'
    +    Orientation of the timelines axis and items. When orientation is a string, the value is applied to both items and axis. Can be 'top', 'bottom' (default), or 'both'.
    +
    +
    +
    +    orientation.axis
    +    String
    +    'bottom'
    +    Orientation of the timeline axis: 'top', 'bottom' (default), or 'both'. If orientation is 'bottom', the time axis is drawn at the bottom. When 'top', the axis is drawn on top. When 'both', two axes are drawn, both on top and at the bottom.
    +
    +
    +
    +    orientation.item
         String
         'bottom'
    -    Orientation of the timeline: 'top' or 'bottom' (default). If orientation is 'bottom', the time axis is drawn at the bottom, and if 'top', the axis is drawn on top.
    +    Orientation of the timeline items: 'top' or 'bottom' (default). Determines whether items are aligned to the top or bottom of the Timeline.
     
     
     
    @@ -762,7 +784,7 @@ var options = {
     
     
         start
    -    Date | Number | String
    +    Date | Number | String | Moment
         none
         The initial start date for the axis of the timeline.
             If not provided, the earliest date present in the events is taken as start date.
    @@ -918,6 +940,26 @@ timeline.clear({options: true}); // clear options only
         
       
     
    +  
    +    getEventProperties(event)
    +    Object
    +    
    +      Returns an Object with relevant properties from an event:
    +      
    +    
    +  
    +
       
         getSelection()
         Number[]
    @@ -1092,16 +1134,49 @@ timeline.off('select', onSelect);
         Description
         Properties
       
    -    
    -        finishedRedraw
    -        Fired after a redraw is complete. When moving the timeline around, this could be fired frequently.
    -        
    -        
    -            none.
    -        
    -    
    -
    -    
    +
    +  
    +    click
    +    Fired when clicked inside the Timeline.
    +    
    +    
    +      Passes a properties object as returned by the method Timeline.getEventProperties(event).
    +    
    +  
    +
    +  
    +    contextmenu
    +    Fired when right-clicked inside the Timeline. Note that in order to prevent the context menu from showing up, default behavior of the event must be stopped:
    +
    timeline.on('contextmenu', function (props) {
    +  alert('Right click!');
    +  props.event.preventDefault();
    +});
    +
    + + + Passes a properties object as returned by the method Timeline.getEventProperties(event). + + + + + doubleClick + Fired when double clicked inside the Timeline. + + + Passes a properties object as returned by the method Timeline.getEventProperties(event). + + + + + finishedRedraw + Fired after a redraw is complete. When moving the timeline around, this could be fired frequently. + + + none. + + + + rangechange Fired repeatedly when the timeline window is being changed. @@ -1170,8 +1245,16 @@ timeline.off('select', onSelect);

    Editing Items

    - When the Timeline is configured to be editable (both options selectable and editable are true), the user can move items by dragging them, can create a new item by double tapping on an empty space, can update an item by double tapping it, and can delete a selected item by clicking the delete button on the top right. + When the Timeline is configured to be editable (both options selectable and editable are true), the user can:

    +

    Option editable accepts a boolean or an object. When editable is a boolean, all manipulation actions will be either enabled or disabled. When editable is an object, one can enable individual manipulation actions:

    diff --git a/examples/graph2d/01_basic.html b/examples/graph2d/01_basic.html index d012536b..68ec7cbd 100644 --- a/examples/graph2d/01_basic.html +++ b/examples/graph2d/01_basic.html @@ -44,7 +44,7 @@ var dataset = new vis.DataSet(items); var options = { start: '2014-06-10', - end: '2014-06-18', + end: '2014-06-18' }; var graph2d = new vis.Graph2d(container, dataset, options); diff --git a/examples/graph2d/13_localization.html b/examples/graph2d/13_localization.html index feb2ea1c..5410b05b 100644 --- a/examples/graph2d/13_localization.html +++ b/examples/graph2d/13_localization.html @@ -13,7 +13,7 @@ } - + diff --git a/examples/graph2d/index.html b/examples/graph2d/index.html index 5d82b09e..bbbc6646 100644 --- a/examples/graph2d/index.html +++ b/examples/graph2d/index.html @@ -25,6 +25,7 @@

    16_bothAxis_titles.html

    17_dynamicStyling.html

    18_scatterplot.html

    +

    19_labels.html

    diff --git a/examples/timeline/08_edit_items.html b/examples/timeline/08_edit_items.html index 2a79ef0e..2ffbb91f 100644 --- a/examples/timeline/08_edit_items.html +++ b/examples/timeline/08_edit_items.html @@ -64,6 +64,7 @@ onMoving: function (item, callback) { if (item.start < min) item.start = min; if (item.start > max) item.start = max; + if (item.end > max) item.end = max; callback(item); // send back the (possibly) changed item }, diff --git a/examples/timeline/35_item_ordering.html b/examples/timeline/35_item_ordering.html new file mode 100644 index 00000000..a1b560b6 --- /dev/null +++ b/examples/timeline/35_item_ordering.html @@ -0,0 +1,80 @@ + + + + Timeline | Item ordering + + + + + + + +

    Item ordering

    +

    + By default, the items displayed on the Timeline are unordered. They are + stacked in the order that they where loaded. This means that way items are + stacked can change while moving and zooming the Timeline. +

    +

    + To display and stack the items in a controlled order, you can provide a + custom sorting function via the configuration option order. +

    +

    + WARNING: Custom ordering is only suitable for small amounts of items (up to a few + hundred), as the Timeline has to render all items once on load to + determine their width and height. +

    +

    + +

    + +
    + + + + \ No newline at end of file diff --git a/examples/timeline/index.html b/examples/timeline/index.html index 37edd650..ece375ed 100644 --- a/examples/timeline/index.html +++ b/examples/timeline/index.html @@ -45,6 +45,7 @@

    32_grid_styling.html

    33_custom_snapping.html

    34_add_custom_timebar.html

    +

    35_item_ordering.html

    requirejs_example.html

    diff --git a/lib/DataSet.js b/lib/DataSet.js index fff1b989..5675beff 100644 --- a/lib/DataSet.js +++ b/lib/DataSet.js @@ -661,9 +661,17 @@ DataSet.prototype._filterFields = function (item, fields) { var filteredItem = {}; - for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { - filteredItem[field] = item[field]; + if(Array.isArray(fields)){ + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; + } + } + }else{ + for (var field in item) { + if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { + filteredItem[fields[field]] = item[field]; + } } } diff --git a/lib/DataView.js b/lib/DataView.js index cc4e04bd..158a20e3 100644 --- a/lib/DataView.js +++ b/lib/DataView.js @@ -258,12 +258,13 @@ DataView.prototype.getDataSet = function () { * @private */ DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this._data, - added = [], - updated = [], - removed = []; + var i, len, id, item; + var ids = params && params.items; + var data = this._data; + var updatedData = []; + var added = []; + var updated = []; + var removed = []; if (ids && data) { switch (event) { @@ -290,6 +291,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { if (item) { if (this._ids[id]) { updated.push(id); + updatedData.push(params.data[i]); } else { this._ids[id] = true; @@ -328,7 +330,7 @@ DataView.prototype._onEvent = function (event, params, senderId) { this._trigger('add', {items: added}, senderId); } if (updated.length) { - this._trigger('update', {items: updated}, senderId); + this._trigger('update', {items: updated, data: updatedData}, senderId); } if (removed.length) { this._trigger('remove', {items: removed}, senderId); diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index ddd96f1e..63ac132a 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -6,6 +6,7 @@ var DataSet = require('../DataSet'); var DataView = require('../DataView'); var Range = require('./Range'); var ItemSet = require('./component/ItemSet'); +var TimeAxis = require('./component/TimeAxis'); var Activator = require('../shared/Activator'); var DateUtil = require('./DateUtil'); var CustomTime = require('./component/CustomTime'); @@ -27,7 +28,7 @@ Emitter(Core.prototype); * top, bottom, content, and background panel. * @param {Element} container The container element where the Core will * be attached. - * @private + * @protected */ Core.prototype._create = function (container) { this.dom = {}; @@ -205,9 +206,40 @@ Core.prototype._create = function (container) { Core.prototype.setOptions = function (options) { if (options) { // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse', 'dataAttributes', 'hiddenDates']; + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; util.selectiveExtend(fields, this.options, options); + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation = options.orientation; + } + else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { + this.options.orientation = options.orientation.axis; + } + } + + if (this.options.orientation === 'both') { + if (!this.timeAxis2) { + var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); + timeAxis2.setOptions = function (options) { + var _options = options ? util.extend({}, options) : {}; + _options.orientation = 'top'; // override the orientation option, always top + TimeAxis.prototype.setOptions.call(timeAxis2, _options); + }; + this.components.push(timeAxis2); + } + } + else { + if (this.timeAxis2) { + var index = this.components.indexOf(this.timeAxis2); + if (index !== -1) { + this.components.splice(index, 1); + } + this.timeAxis2.destroy(); + this.timeAxis2 = null; + } + } + if ('hiddenDates' in this.options) { DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); } @@ -233,11 +265,6 @@ Core.prototype.setOptions = function (options) { // propagate options to all components this.components.forEach(component => component.setOptions(options)); - // TODO: remove deprecation error one day (deprecated since version 0.8.0) - if (options && options.order) { - throw new Error('Option order is deprecated. There is no replacement for this feature.'); - } - // redraw everything this._redraw(); }; @@ -779,7 +806,7 @@ Core.prototype.getCurrentTime = function() { * Convert a position on screen (pixels) to a datetime * @param {int} x Position on the screen in pixels * @return {Date} time The datetime the corresponds with given position x - * @private + * @protected */ // TODO: move this function to Range Core.prototype._toTime = function(x) { @@ -790,7 +817,7 @@ Core.prototype._toTime = function(x) { * Convert a position on the global screen (pixels) to a datetime * @param {int} x Position on the screen in pixels * @return {Date} time The datetime the corresponds with given position x - * @private + * @protected */ // TODO: move this function to Range Core.prototype._toGlobalTime = function(x) { @@ -804,7 +831,7 @@ Core.prototype._toGlobalTime = function(x) { * @param {Date} time A date * @return {int} x The position on the screen in pixels which corresponds * with the given date. - * @private + * @protected */ // TODO: move this function to Range Core.prototype._toScreen = function(time) { @@ -819,7 +846,7 @@ Core.prototype._toScreen = function(time) { * @param {Date} time A date * @return {int} x The position on root in pixels which corresponds * with the given date. - * @private + * @protected */ // TODO: move this function to Range Core.prototype._toGlobalScreen = function(time) { diff --git a/lib/timeline/Graph2d.js b/lib/timeline/Graph2d.js index 07fe656d..3f8fd7cf 100644 --- a/lib/timeline/Graph2d.js +++ b/lib/timeline/Graph2d.js @@ -90,6 +90,16 @@ function Graph2d (container, items, groups, options) { this.itemsData = null; // DataSet this.groupsData = null; // DataSet + this.on('tap', function (event) { + me.emit('click', me.getEventProperties(event)) + }); + this.on('doubletap', function (event) { + me.emit('doubleClick', me.getEventProperties(event)) + }); + this.dom.root.oncontextmenu = function (event) { + me.emit('contextmenu', me.getEventProperties(event)) + }; + // apply options if (options) { this.setOptions(options); @@ -191,7 +201,7 @@ Graph2d.prototype.getLegend = function(groupId, width, height) { else { return "cannot find group:" + groupId; } -} +}; /** * This checks if the visible option of the supplied group (by ID) is true or false. @@ -205,7 +215,7 @@ Graph2d.prototype.isGroupVisible = function(groupId) { else { return false; } -} +}; /** @@ -239,5 +249,52 @@ Graph2d.prototype.getItemRange = function() { }; +/** + * Generate Timeline related information from an event + * @param {Event} event + * @return {Object} An object with related information, like on which area + * The event happened, whether clicked on an item, etc. + */ +Graph2d.prototype.getEventProperties = function (event) { + var pageX = event.gesture ? event.gesture.center.pageX : event.pageX; + var pageY = event.gesture ? event.gesture.center.pageY : event.pageY; + var x = pageX - util.getAbsoluteLeft(this.dom.centerContainer); + var y = pageY - util.getAbsoluteTop(this.dom.centerContainer); + var time = this._toTime(x); + + var element = util.getTarget(event); + var what = null; + if (util.hasParent(element, this.timeAxis.dom.foreground)) {what = 'axis';} + else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {what = 'axis';} + else if (util.hasParent(element, this.linegraph.yAxisLeft.dom.frame)) {what = 'data-axis';} + else if (util.hasParent(element, this.linegraph.yAxisRight.dom.frame)) {what = 'data-axis';} + else if (util.hasParent(element, this.linegraph.legendLeft.dom.frame)) {what = 'legend';} + else if (util.hasParent(element, this.linegraph.legendRight.dom.frame)) {what = 'legend';} + else if (util.hasParent(element, this.customTime.bar)) {what = 'custom-time';} // TODO: fix for multiple custom time bars + else if (util.hasParent(element, this.currentTime.bar)) {what = 'current-time';} + else if (util.hasParent(element, this.dom.center)) {what = 'background';} + + var value = []; + var yAxisLeft = this.linegraph.yAxisLeft; + var yAxisRight = this.linegraph.yAxisRight; + if (!yAxisLeft.hidden) { + value.push(yAxisLeft.screenToValue(y)); + } + if (!yAxisRight.hidden) { + value.push(yAxisRight.screenToValue(y)); + } + + return { + event: event, + what: what, + pageX: pageX, + pageY: pageY, + x: x, + y: y, + time: time, + value: value + } +}; + module.exports = Graph2d; diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 2a61d12e..7843710d 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -38,7 +38,7 @@ function Timeline (container, items, groups, options) { autoResize: true, - orientation: 'bottom', + orientation: 'bottom', // axis orientation: 'bottom', 'top', or 'both' width: null, height: null, maxHeight: null, @@ -83,6 +83,7 @@ function Timeline (container, items, groups, options) { // time axis this.timeAxis = new TimeAxis(this.body); + this.timeAxis2 = null; // used in case of orientation option 'both' this.components.push(this.timeAxis); // current time bar @@ -101,6 +102,16 @@ function Timeline (container, items, groups, options) { this.itemsData = null; // DataSet this.groupsData = null; // DataSet + this.on('tap', function (event) { + me.emit('click', me.getEventProperties(event)) + }); + this.on('doubletap', function (event) { + me.emit('doubleClick', me.getEventProperties(event)) + }); + this.dom.root.oncontextmenu = function (event) { + me.emit('contextmenu', me.getEventProperties(event)) + }; + // apply options if (options) { this.setOptions(options); @@ -325,5 +336,48 @@ Timeline.prototype.getItemRange = function() { }; }; +/** + * Generate Timeline related information from an event + * @param {Event} event + * @return {Object} An object with related information, like on which area + * The event happened, whether clicked on an item, etc. + */ +Timeline.prototype.getEventProperties = function (event) { + var item = this.itemSet.itemFromTarget(event); + var group = this.itemSet.groupFromTarget(event); + var pageX = event.gesture ? event.gesture.center.pageX : event.pageX; + var pageY = event.gesture ? event.gesture.center.pageY : event.pageY; + var x = pageX - util.getAbsoluteLeft(this.dom.centerContainer); + var y = pageY - util.getAbsoluteTop(this.dom.centerContainer); + + var snap = this.itemSet.options.snap || null; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var time = this._toTime(x); + var snappedTime = snap ? snap(time, scale, step) : time; + + var element = util.getTarget(event); + var what = null; + if (item != null) {what = 'item';} + else if (util.hasParent(element, this.timeAxis.dom.foreground)) {what = 'axis';} + else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {what = 'axis';} + else if (util.hasParent(element, this.itemSet.dom.labelSet)) {what = 'group-label';} + else if (util.hasParent(element, this.customTime.bar)) {what = 'custom-time';} // TODO: fix for multiple custom time bars + else if (util.hasParent(element, this.currentTime.bar)) {what = 'current-time';} + else if (util.hasParent(element, this.dom.center)) {what = 'background';} + + return { + event: event, + item: item ? item.id : null, + group: group ? group.groupId : null, + what: what, + pageX: pageX, + pageY: pageY, + x: x, + y: y, + time: time, + snappedTime: snappedTime + } +}; module.exports = Timeline; diff --git a/lib/timeline/component/CurrentTime.js b/lib/timeline/component/CurrentTime.js index 86fb7e7a..e0751462 100644 --- a/lib/timeline/component/CurrentTime.js +++ b/lib/timeline/component/CurrentTime.js @@ -88,6 +88,13 @@ CurrentTime.prototype.redraw = function() { var x = this.body.util.toScreen(now); var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; + } + locale = this.options.locales['en']; // fall back on english when not available + } var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); title = title.charAt(0).toUpperCase() + title.substring(1); diff --git a/lib/timeline/component/CustomTime.js b/lib/timeline/component/CustomTime.js index 4a331bce..722d594f 100644 --- a/lib/timeline/component/CustomTime.js +++ b/lib/timeline/component/CustomTime.js @@ -120,6 +120,13 @@ CustomTime.prototype.redraw = function () { var x = this.body.util.toScreen(this.customTime); var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; + } + locale = this.options.locales['en']; // fall back on english when not available + } var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); title = title.charAt(0).toUpperCase() + title.substring(1); diff --git a/lib/timeline/component/DataAxis.js b/lib/timeline/component/DataAxis.js index 6782eb1e..7a8299ba 100644 --- a/lib/timeline/component/DataAxis.js +++ b/lib/timeline/component/DataAxis.js @@ -484,6 +484,10 @@ DataAxis.prototype.convertValue = function (value) { return convertedValue; }; +DataAxis.prototype.screenToValue = function (x) { + return this.valueAtZero - (x / this.conversionFactor); +}; + /** * Create a label for the axis at position x * @private diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 98a0a963..226ebab6 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -148,8 +148,6 @@ Group.prototype.getLabelWidth = function() { Group.prototype.redraw = function(range, margin, restack) { var resized = false; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // force recalculation of the height of the items when the marker height changed // (due to the Timeline being attached to the DOM or changed from display:none to visible) var markerHeight = this.dom.marker.clientHeight; @@ -165,11 +163,42 @@ Group.prototype.redraw = function(range, margin, restack) { } // reposition visible items vertically - if (this.itemSet.options.stack) { // TODO: ugly way to access options... - stack.stack(this.visibleItems, margin, restack); + if (typeof this.itemSet.options.order === 'function') { + // a custom order function + + if (restack) { + // brute force restack of all items + + // show all items + var me = this; + var limitSize = false; + util.forEach(this.items, function (item) { + if (!item.displayed) { + item.redraw(); + me.visibleItems.push(item); + } + item.repositionX(limitSize); + }); + + // order all items and force a restacking + var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { + return me.itemSet.options.order(a.data, b.data); + }); + stack.stack(customOrderedItems, margin, true /* restack=true */); + } + + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); } - else { // no stacking - stack.nostack(this.visibleItems, margin, this.subgroups); + else { + // no custom order function, lazy stacking + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + if (this.itemSet.options.stack) { // TODO: ugly way to access options... + stack.stack(this.visibleItems, margin, restack); + } + else { // no stacking + stack.nostack(this.visibleItems, margin, this.subgroups); + } } // recalculate the height of the group @@ -236,7 +265,7 @@ Group.prototype._calculateHeight = function (margin) { height = max + margin.item.vertical / 2; } else { - height = margin.axis + margin.item.vertical; + height = 0; } height = Math.max(height, this.props.label.height); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 104c8057..96bddac5 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -29,7 +29,7 @@ function ItemSet(body, options) { this.defaultOptions = { type: null, // 'box', 'point', 'range', 'background' - orientation: 'bottom', // 'top' or 'bottom' + orientation: 'bottom', // item orientation: 'top' or 'bottom' align: 'auto', // alignment of box items stack: true, groupOrder: null, @@ -276,9 +276,18 @@ ItemSet.prototype._create = function(){ ItemSet.prototype.setOptions = function(options) { if (options) { // copy all options that we know - var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap']; + var fields = ['type', 'align', 'order', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap']; util.selectiveExtend(fields, this.options, options); + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation = options.orientation; + } + else if (typeof options.orientation === 'object' && 'item' in options.orientation) { + this.options.orientation = options.orientation.item; + } + } + if ('margin' in options) { if (typeof options.margin === 'number') { this.options.margin.axis = options.margin; @@ -1020,12 +1029,13 @@ ItemSet.prototype._addItem = function(item) { */ ItemSet.prototype._updateItem = function(item, itemData) { var oldGroupId = item.data.group; + var oldSubGroupId = item.data.subgroup; // update the items data (will redraw the item when displayed) item.setData(itemData); // update group - if (oldGroupId != item.data.group) { + if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { var oldGroup = this.groups[oldGroupId]; if (oldGroup) oldGroup.remove(item); @@ -1112,31 +1122,21 @@ ItemSet.prototype._onDragStart = function (event) { if (dragLeftItem) { props = { item: dragLeftItem, - initialX: event.center.x + initialX: event.center.x, + dragLeft: true, + data: util.extend({}, item.data) // clone the items data }; - if (me.options.editable.updateTime) { - props.start = item.data.start.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - this.touchParams.itemProps = [props]; } else if (dragRightItem) { props = { item: dragRightItem, - initialX: event.center.x + initialX: event.center.x, + dragRight: true, + data: util.extend({}, item.data) // clone the items data }; - if (me.options.editable.updateTime) { - props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - this.touchParams.itemProps = [props]; } else { @@ -1144,30 +1144,66 @@ ItemSet.prototype._onDragStart = function (event) { var item = me.items[id]; var props = { item: item, - initialX: event.center.x + initialX: event.center.x, + data: util.extend({}, item.data) // clone the items data }; - if (me.options.editable.updateTime) { - if ('start' in item.data) { - props.start = item.data.start.valueOf(); - - if ('end' in item.data) { - // we store a duration here in order not to change the width - // of the item when moving it. - props.duration = item.data.end.valueOf() - props.start; - } - } - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - return props; }); } event.stopPropagation(); } + else if (this.options.editable.add && event.gesture.srcEvent.ctrlKey) { + // create a new range item when dragging with ctrl key down + this._onDragStartAddItem(event); + } +}; + +/** + * Start creating a new range item by dragging. + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragStartAddItem = function (event) { + var snap = this.options.snap || null; + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.gesture.center.pageX - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px + var time = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var start = snap ? snap(time, scale, step) : start; + var end = start; + + var itemData = { + type: 'range', + start: start, + end: end, + content: 'new item' + }; + + var id = util.randomUUID(); + itemData[this.itemsData._fieldId] = id; + + var group = this.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } + + var newItem = new RangeItem(itemData, this.conversion, this.options); + newItem.id = id; // TODO: not so nice setting id afterwards + newItem.data = itemData; + this._addItem(newItem); + + var props = { + item: newItem, + dragRight: true, + initialX: event.gesture.center.pageX, + data: util.extend({}, itemData) + }; + this.touchParams.itemProps = [props]; + + event.stopPropagation(); }; /** @@ -1177,6 +1213,8 @@ ItemSet.prototype._onDragStart = function (event) { */ ItemSet.prototype._onDrag = function (event) { if (this.touchParams.itemProps) { + event.stopPropagation(); + var me = this; var snap = this.options.snap || null; var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; @@ -1190,60 +1228,65 @@ ItemSet.prototype._onDrag = function (event) { var initial = me.body.util.toTime(props.initialX - xOffset); var offset = current - initial; - if ('start' in props) { - var start = new Date(props.start + offset); - newProps.start = snap ? snap(start, scale, step) : start; - } + var itemData = util.extend({}, props.item.data); // clone the data - if ('end' in props) { - var end = new Date(props.end + offset); - newProps.end = snap ? snap(end, scale, step) : end; - } - else if ('duration' in props) { - newProps.end = new Date(newProps.start.valueOf() + props.duration); + if (me.options.editable.updateTime) { + if (props.dragLeft) { + // drag left side of a range item + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date'); + var start = new Date(initialStart.valueOf() + offset); + itemData.start = snap ? snap(start, scale, step) : start; + } + } + else if (props.dragRight) { + // drag right side of a range item + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var end = new Date(initialEnd.valueOf() + offset); + itemData.end = snap ? snap(end, scale, step) : end; + } + } + else { + // drag both start and end + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date').valueOf(); + var start = new Date(initialStart + offset); + + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var duration = initialEnd.valueOf() - initialStart.valueOf(); + + itemData.start = snap ? snap(start, scale, step) : start; + itemData.end = new Date(itemData.start.valueOf() + duration); + } + else { + itemData.start = snap ? snap(start, scale, step) : start; + } + } + } } - if ('group' in props) { - // drag from one group to another - var group = me.groupFromTarget(event); - newProps.group = group && group.groupId; + if (me.options.editable.updateGroup && (!props.dragLeft && !props.dragRight)) { + if (itemData.group != undefined) { + // drag from one group to another + var group = me.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } + } } // confirm moving the item - var itemData = util.extend({}, props.item.data, newProps); me.options.onMoving(itemData, function (itemData) { if (itemData) { - me._updateItemProps(props.item, itemData); + props.item.setData(itemData); } }); }); this.stackDirty = true; // force re-stacking of all items next redraw this.body.emitter.emit('change'); - - event.stopPropagation(); - } -}; - -/** - * Update an items properties - * @param {Item} item - * @param {Object} props Can contain properties start, end, and group. - * @private - */ -ItemSet.prototype._updateItemProps = function(item, props) { - // TODO: copy all properties from props to item? (also new ones) - if ('start' in props) { - item.data.start = props.start; - } - if ('end' in props) { - item.data.end = props.end; - } - else if ('duration' in props) { - item.data.end = new Date(props.start.valueOf() + props.duration); - } - if ('group' in props && item.data.group != props.group) { - this._moveToGroup(item, props.group) } }; @@ -1273,35 +1316,35 @@ ItemSet.prototype._moveToGroup = function(item, groupId) { */ ItemSet.prototype._onDragEnd = function (event) { if (this.touchParams.itemProps) { + event.stopPropagation(); + // prepare a change set for the changed items - var changes = [], - me = this, - dataset = this.itemsData.getDataSet(); + var changes = []; + var me = this; + var dataset = this.itemsData.getDataSet(); var itemProps = this.touchParams.itemProps ; this.touchParams.itemProps = null; itemProps.forEach(function (props) { - var id = props.item.id, - itemData = me.itemsData.get(id, me.itemOptions); - - var changed = false; - if ('start' in props.item.data) { - changed = (props.start != props.item.data.start.valueOf()); - itemData.start = util.convert(props.item.data.start, - dataset._options.type && dataset._options.type.start || 'Date'); - } - if ('end' in props.item.data) { - changed = changed || (props.end != props.item.data.end.valueOf()); - itemData.end = util.convert(props.item.data.end, - dataset._options.type && dataset._options.type.end || 'Date'); - } - if ('group' in props.item.data) { - changed = changed || (props.group != props.item.data.group); - itemData.group = props.item.data.group; - } + var id = props.item.id; + var exists = me.itemsData.get(id, me.itemOptions) != null; - // only apply changes when start or end is actually changed - if (changed) { + if (!exists) { + // add a new item + me.options.onAdd(props.item.data, function (itemData) { + me._removeItem(props.item); // remove temporary item + if (itemData) { + me.itemsData.getDataSet().add(itemData); + } + + // force re-stacking of all items next redraw + me.stackDirty = true; + me.body.emitter.emit('change'); + }); + } + else { + // update existing item + var itemData = util.extend({}, props.item.data); // clone the data me.options.onMove(itemData, function (itemData) { if (itemData) { // apply changes @@ -1310,7 +1353,7 @@ ItemSet.prototype._onDragEnd = function (event) { } else { // restore original values - me._updateItemProps(props.item, props); + props.item.setData(props.data); me.stackDirty = true; // force re-stacking of all items next redraw me.body.emitter.emit('change'); @@ -1323,8 +1366,6 @@ ItemSet.prototype._onDragEnd = function (event) { if (changes.length) { dataset.update(changes); } - - event.stopPropagation(); } }; @@ -1345,7 +1386,7 @@ ItemSet.prototype._onSelectItem = function (event) { var oldSelection = this.getSelection(); - var item = ItemSet.itemFromTarget(event); + var item = this.itemFromTarget(event); var selection = item ? [item.id] : []; this.setSelection(selection); @@ -1371,7 +1412,7 @@ ItemSet.prototype._onAddItem = function (event) { var me = this, snap = this.options.snap || null, - item = ItemSet.itemFromTarget(event); + item = this.itemFromTarget(event); if (item) { // update item @@ -1429,7 +1470,7 @@ ItemSet.prototype._onMultiSelectItem = function (event) { if (!this.options.selectable) return; var selection, - item = ItemSet.itemFromTarget(event); + item = this.itemFromTarget(event); if (item) { // multi select items @@ -1451,7 +1492,9 @@ ItemSet.prototype._onMultiSelectItem = function (event) { var start = _item.data.start; var end = (_item.data.end !== undefined) ? _item.data.end : start; - if (start >= range.min && end <= range.max) { + if (start >= range.min && + end <= range.max && + !(_item instanceof BackgroundItem)) { selection.push(_item.id); // do not use id but item.id, id itself is stringified } } @@ -1517,7 +1560,7 @@ ItemSet._getItemRange = function(itemsData) { * @param {Event} event * @return {Item | null} item */ -ItemSet.itemFromTarget = function(event) { +ItemSet.prototype.itemFromTarget = function(event) { var target = event.target; while (target) { if (target.hasOwnProperty('timeline-item')) { @@ -1536,33 +1579,23 @@ ItemSet.itemFromTarget = function(event) { * @return {Group | null} group */ ItemSet.prototype.groupFromTarget = function(event) { - // TODO: cleanup when the new solution is stable (also on mobile) - //var target = event.target; - //while (target) { - // if (target.hasOwnProperty('timeline-group')) { - // return target['timeline-group']; - // } - // target = target.parentNode; - //} - // - - var clientY = event.center.clientY; + var pageY = event.gesture ? event.gesture.center.pageY : event.pageY; for (var i = 0; i < this.groupIds.length; i++) { var groupId = this.groupIds[i]; var group = this.groups[groupId]; var foreground = group.dom.foreground; var top = util.getAbsoluteTop(foreground); - if (clientY > top && clientY < top + foreground.offsetHeight) { + if (pageY > top && pageY < top + foreground.offsetHeight) { return group; } if (this.options.orientation === 'top') { - if (i === this.groupIds.length - 1 && clientY > top) { + if (i === this.groupIds.length - 1 && pageY > top) { return group; } } else { - if (i === 0 && clientY < top + foreground.offset) { + if (i === 0 && pageY < top + foreground.offset) { return group; } } diff --git a/lib/timeline/component/TimeAxis.js b/lib/timeline/component/TimeAxis.js index fba831ff..ad60c8ef 100644 --- a/lib/timeline/component/TimeAxis.js +++ b/lib/timeline/component/TimeAxis.js @@ -34,8 +34,7 @@ function TimeAxis (body, options) { }; this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' + orientation: 'bottom', // axis orientation: 'top' or 'bottom' showMinorLabels: true, showMajorLabels: true, format: null, @@ -65,7 +64,6 @@ TimeAxis.prototype.setOptions = function(options) { if (options) { // copy all options that we know util.selectiveExtend([ - 'orientation', 'showMinorLabels', 'showMajorLabels', 'hiddenDates', @@ -73,6 +71,15 @@ TimeAxis.prototype.setOptions = function(options) { 'timeAxis' ], this.options, options); + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation = options.orientation; + } + else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { + this.options.orientation = options.orientation.axis; + } + } + // apply locale to moment.js // TODO: not so nice, this is applied globally to moment.js if ('locale' in options) { @@ -131,9 +138,8 @@ TimeAxis.prototype.redraw = function () { this._calculateCharSize(); // TODO: recalculate sizes only needed when parent is resized or options is changed - var orientation = this.options.orientation, - showMinorLabels = this.options.showMinorLabels, - showMajorLabels = this.options.showMajorLabels; + var showMinorLabels = this.options.showMinorLabels; + var showMajorLabels = this.options.showMajorLabels; // determine the width and height of the elemens for the axis props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; diff --git a/lib/timeline/component/css/item.css b/lib/timeline/component/css/item.css index a7692afc..46f852da 100644 --- a/lib/timeline/component/css/item.css +++ b/lib/timeline/component/css/item.css @@ -97,6 +97,7 @@ .vis-item.vis-range .vis-drag-left { position: absolute; width: 24px; + max-width: 20%; height: 100%; top: 0; left: -4px; @@ -107,6 +108,7 @@ .vis-item.vis-range .vis-drag-right { position: absolute; width: 24px; + max-width: 20%; height: 100%; top: 0; right: -4px; diff --git a/lib/timeline/component/item/BackgroundItem.js b/lib/timeline/component/item/BackgroundItem.js index 41d820d5..6ab6e165 100644 --- a/lib/timeline/component/item/BackgroundItem.js +++ b/lib/timeline/component/item/BackgroundItem.js @@ -147,6 +147,8 @@ BackgroundItem.prototype.repositionY = function(margin) { // special positioning for subgroups if (this.data.subgroup !== undefined) { + // TODO: instead of calculating the top position of the subgroups here for every BackgroundItem, calculate the top of the subgroup once in Itemset + var itemSubgroup = this.data.subgroup; var subgroups = this.parent.subgroups; var subgroupIndex = subgroups[itemSubgroup].index; @@ -172,15 +174,20 @@ BackgroundItem.prototype.repositionY = function(margin) { // and when the orientation is bottom: else { var newTop = this.parent.top; + var totalHeight = 0; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index > subgroupIndex) { - newTop += subgroups[subgroup].height + margin.item.vertical; + if (subgroups[subgroup].visible == true) { + var newHeight = subgroups[subgroup].height + margin.item.vertical; + totalHeight += newHeight; + if (subgroups[subgroup].index > subgroupIndex) { + newTop += newHeight; + } } } } height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - this.dom.box.style.top = newTop + 'px'; + this.dom.box.style.top = (this.parent.height - totalHeight + newTop) + 'px'; this.dom.box.style.bottom = ''; } } diff --git a/lib/timeline/component/item/BoxItem.js b/lib/timeline/component/item/BoxItem.js index e96c8e81..171a573a 100644 --- a/lib/timeline/component/item/BoxItem.js +++ b/lib/timeline/component/item/BoxItem.js @@ -151,9 +151,6 @@ BoxItem.prototype.hide = function() { 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; } }; @@ -166,9 +163,6 @@ BoxItem.prototype.repositionX = function() { var start = this.conversion.toScreen(this.data.start); var align = this.options.align; var left; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; // calculate left position of the box if (align == 'right') { @@ -183,13 +177,13 @@ BoxItem.prototype.repositionX = function() { } // reposition box - box.style.left = this.left + 'px'; + this.dom.box.style.left = this.left + 'px'; // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + this.dom.line.style.left = (start - this.props.line.width / 2) + 'px'; // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + this.dom.dot.style.left = (start - this.props.dot.width / 2) + 'px'; }; /** diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index a642a3c0..7cc97d27 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -54,6 +54,11 @@ Item.prototype.unselect = function() { * @param {Object} data */ Item.prototype.setData = function(data) { + var groupChanged = data.group != undefined && this.data.group != data.group; + if (groupChanged) { + this.parent.itemSet._moveToGroup(this, data.group); + } + this.data = data; this.dirty = true; if (this.displayed) this.redraw(); @@ -170,7 +175,8 @@ Item.prototype._updateContents = function (element) { content = this.data.content; } - if(content !== this.content) { + var changed = this._contentToString(this.content) !== this._contentToString(content); + if (changed) { // only replace the content when changed if (content instanceof Element) { element.innerHTML = ''; @@ -255,4 +261,16 @@ Item.prototype._updateStyle = function(element) { } }; +/** + * Stringify the items contents + * @param {string | Element | undefined} content + * @returns {string | undefined} + * @private + */ +Item.prototype._contentToString = function (content) { + if (typeof content === 'string') return content; + if (content && 'outerHTML' in content) return content.outerHTML; + return content; +}; + module.exports = Item; diff --git a/lib/timeline/component/item/PointItem.js b/lib/timeline/component/item/PointItem.js index fbaf14cb..d70f6a3b 100644 --- a/lib/timeline/component/item/PointItem.js +++ b/lib/timeline/component/item/PointItem.js @@ -144,9 +144,6 @@ PointItem.prototype.hide = function() { this.dom.point.parentNode.removeChild(this.dom.point); } - this.top = null; - this.left = null; - this.displayed = false; } }; diff --git a/lib/timeline/component/item/RangeItem.js b/lib/timeline/component/item/RangeItem.js index 377a0993..625c805d 100644 --- a/lib/timeline/component/item/RangeItem.js +++ b/lib/timeline/component/item/RangeItem.js @@ -140,30 +140,34 @@ RangeItem.prototype.hide = function() { box.parentNode.removeChild(box); } - this.top = null; - this.left = null; - this.displayed = false; } }; /** * Reposition the item horizontally + * @param {boolean} [limitSize=true] If true (default), the width of the range + * item will be limited, as the browser cannot + * display very wide divs. This means though + * that the applied left and width may + * not correspond to the ranges start and end * @Override */ -RangeItem.prototype.repositionX = function() { +RangeItem.prototype.repositionX = function(limitSize) { var parentWidth = this.parent.width; var start = this.conversion.toScreen(this.data.start); var end = this.conversion.toScreen(this.data.end); var contentLeft; var contentWidth; - // 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; + // limit the width of the range, as browsers cannot draw very wide divs + if (limitSize === undefined || limitSize === true) { + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } } var boxWidth = Math.max(end - start, 1); diff --git a/lib/timeline/locales.js b/lib/timeline/locales.js index b736d042..436aaecc 100644 --- a/lib/timeline/locales.js +++ b/lib/timeline/locales.js @@ -8,7 +8,7 @@ exports['en_US'] = exports['en']; // Dutch exports['nl'] = { - custom: 'aangepaste', + current: 'aangepaste', time: 'tijd' }; exports['nl_NL'] = exports['nl']; diff --git a/lib/util.js b/lib/util.js index 0d527f6f..b7b0ea2b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -722,6 +722,24 @@ exports.getTarget = function(event) { return target; }; +/** + * Check if given element contains given parent somewhere in the DOM tree + * @param {Element} element + * @param {Element} parent + */ +exports.hasParent = function (element, parent) { + var e = element; + + while (e) { + if (e === parent) { + return true; + } + e = e.parentNode; + } + + return false; +}; + exports.option = {}; /** diff --git a/misc/how_to_publish.md b/misc/how_to_publish.md index 1ab46341..676556ae 100644 --- a/misc/how_to_publish.md +++ b/misc/how_to_publish.md @@ -56,7 +56,7 @@ This generates the vis.js library in the folder `./dist`. Verify if it installs the just released version, and verify if it works. - - Verify within an hour whether vis.js is updated on http://cdnjs.com/ + - Verify within a day or so whether vis.js is updated on http://cdnjs.com/ ## Update website @@ -66,7 +66,7 @@ This generates the vis.js library in the folder `./dist`. - Copy the `examples` folder from the `master` branch to the `github-pages` branch. - Create a packaged version of vis.js. Go to the `master` branch and run: - zip vis.zip dist docs examples README.md HISTORY.md LICENSE* NOTICE -r + zip vis.zip dist docs examples README.md HISTORY.md CONTRIBUTING.md LICENSE* NOTICE -r - Move the created zip file `vis.zip` to the `download` folder in the `github-pages` branch. TODO: this should be automated. diff --git a/test/DataView.test.js b/test/DataView.test.js index 17aa7680..8a267528 100644 --- a/test/DataView.test.js +++ b/test/DataView.test.js @@ -148,5 +148,49 @@ describe('DataView', function () { assert.deepEqual(added, [2, 3]); assert.deepEqual(updated, []); assert.deepEqual(removed, []); - }) + }); + + it('should pass data of changed items when updating a DataSet', function () { + var data = new DataSet([ + {id: 1, title: 'Item 1', group: 1}, + {id: 2, title: 'Item 2', group: 2}, + {id: 3, title: 'Item 3', group: 2} + ]); + var view = new DataView(data, { + filter: function (item) { + return item.group === 2; + } + }); + + var dataUpdates = []; + var viewUpdates = []; + + + data.on('update', function (event, properties, senderId) { + dataUpdates.push([event, properties]); + }); + + view.on('update', function (event, properties, senderId) { + viewUpdates.push([event, properties]); + }); + + // make a change not affecting the DataView + data.update({id: 1, title: 'Item 1 (changed)'}); + assert.deepEqual(dataUpdates, [ + ['update', {items: [1], data: [{id: 1, title: 'Item 1 (changed)'}]}] + ]); + assert.deepEqual(viewUpdates, []); + + // make a change affecting the DataView + data.update({id: 2, title: 'Item 2 (changed)'}); + assert.deepEqual(dataUpdates, [ + ['update', {items: [1], data: [{id: 1, title: 'Item 1 (changed)'}]}], + ['update', {items: [2], data: [{id: 2, title: 'Item 2 (changed)'}]}] + ]); + assert.deepEqual(viewUpdates, [ + ['update', {items: [2], data: [{id: 2, title: 'Item 2 (changed)'}]}] + ]); + + }); + }); diff --git a/test/timeline.html b/test/timeline.html index 401ea8f7..1d7315ae 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -41,14 +41,15 @@ } - +
    @@ -23,8 +36,9 @@