From f672f7b583d703dcd771e99df52ca916f2eeb3ca Mon Sep 17 00:00:00 2001 From: jos Date: Wed, 11 Jun 2014 15:20:13 +0200 Subject: [PATCH] Reworked options and setOptions --- HISTORY.md | 11 +- docs/timeline.html | 2 +- src/timeline/Range.js | 28 +++- src/timeline/Timeline.js | 192 ++++++++------------------ src/timeline/component/Component.js | 31 +---- src/timeline/component/CurrentTime.js | 20 ++- src/timeline/component/CustomTime.js | 21 ++- src/timeline/component/ItemSet.js | 142 ++++++++++++++++--- src/timeline/component/TimeAxis.js | 31 +++-- src/timeline/component/css/item.css | 2 +- src/util.js | 29 +++- 11 files changed, 303 insertions(+), 206 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index cd335d69..5f65d033 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,11 +4,16 @@ http://visjs.org ## 2014-06-06, version 1.1.1 +### Timeline + +- Changed default value of option `showCurrentTime` to true. + ### Graph -- reduced the timestep a little for smoother animations. -- fixed dataManipulation.initiallyVisible functionality (thanks theGrue). -- forced typecast of fontSize to Number. +- Reduced the timestep a little for smoother animations. +- Fixed dataManipulation.initiallyVisible functionality (thanks theGrue). +- Forced typecast of fontSize to Number. + ## 2014-06-06, version 1.1.0 diff --git a/docs/timeline.html b/docs/timeline.html index 179d11a1..43abb226 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -533,7 +533,7 @@ var options = { showCurrentTime boolean - false + true Show a vertical bar at the current time. diff --git a/src/timeline/Range.js b/src/timeline/Range.js index d50e8d13..1047848e 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -11,7 +11,18 @@ function Range(body, options) { this.end = null; // Number this.body = body; - this.options = options || {}; + + // default options + this.defaultOptions = { + start: null, + end: null, + direction: 'horizontal', // 'horizontal' or 'vertical' + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + }; + this.options = util.extend({}, this.defaultOptions); // drag listeners for dragging this.body.emitter.on('dragstart', this._onDragStart.bind(this)); @@ -32,9 +43,13 @@ function Range(body, options) { this.setOptions(options); } +Range.prototype = new Component(); + /** * Set options for the range controller * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range * {Number} min Minimum value for start * {Number} max Maximum value for end * {Number} zoomMin Set a minimum value for @@ -43,11 +58,14 @@ function Range(body, options) { * (end - start). */ Range.prototype.setOptions = function (options) { - util.extend(this.options, options); + if (options) { + // copy the options that we know + util.selectiveExtend(['direction', 'min', 'max', 'zoomMin', 'zoomMax'], this.options, options); - // re-apply range with new limitations - if (this.start !== null && this.end !== null) { - this.setRange(this.start, this.end); + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } } }; diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 321257f3..fc33abe1 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -12,65 +12,16 @@ function Timeline (container, items, options) { var me = this; var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); this.defaultOptions = { - orientation: 'bottom', - direction: 'horizontal', // 'horizontal' or 'vertical' autoResize: true, - stack: true, - - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, - - selectable: true, - - start: null, - end: null, - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds - // moveable: true, // TODO: option moveable - // zoomable: true, // TODO: option zoomable - - showMinorLabels: true, - showMajorLabels: true, - showCurrentTime: false, - showCustomTime: false, - - groupOrder: null, width: null, height: null, maxHeight: null, - minHeight: null, - - type: 'box', - align: 'center', - margin: { - axis: 20, - item: 10 - }, - padding: 5, - - onAdd: function (item, callback) { - callback(item); - }, - onUpdate: function (item, callback) { - callback(item); - }, - onMove: function (item, callback) { - callback(item); - }, - onRemove: function (item, callback) { - callback(item); - } - }; + minHeight: null - this.options = {}; - util.deepExtend(this.options, this.defaultOptions); + // TODO: implement options moveable and zoomable + }; + this.options = util.deepExtend({}, this.defaultOptions); // Create the DOM, props, and emitter this._create(); @@ -94,7 +45,9 @@ function Timeline (container, items, options) { }; // range - this.range = new Range(this.body, this.options); + this.range = new Range(this.body); + this.components.push(this.range); + // TODO: use default start and en of range? this.range.setRange( now.clone().add('days', -3).valueOf(), now.clone().add('days', 4).valueOf() @@ -102,21 +55,21 @@ function Timeline (container, items, options) { this.body.range = this.range; // time axis - this.timeAxis = new TimeAxis(this.body, this.options); + this.timeAxis = new TimeAxis(this.body); this.components.push(this.timeAxis); this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); // current time bar - this.currentTime = new CurrentTime(this.body, this.options); + this.currentTime = new CurrentTime(this.body); this.components.push(this.currentTime); // custom time bar // Note: time bar will be attached in this.setOptions when selected - this.customTime = new CustomTime(this.body, this.options); + this.customTime = new CustomTime(this.body); this.components.push(this.customTime); // item set - this.itemSet = new ItemSet(this.body, this.options); + this.itemSet = new ItemSet(this.body); this.components.push(this.itemSet); this.on('change', this.redraw.bind(this)); @@ -132,6 +85,9 @@ function Timeline (container, items, options) { if (items) { this.setItems(items); } + else { + this.redraw(); + } } // turn Timeline into an event emitter @@ -225,79 +181,41 @@ Timeline.prototype._create = function () { }; /** - * Set options - * @param {Object} options TODO: describe the available options + * Set options. Options will be passed to all components loaded in the Timeline. + * @param {Object} [options] + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Timeline will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window */ Timeline.prototype.setOptions = function (options) { - util.deepExtend(this.options, options); - - if ('editable' in options) { - var isBoolean = typeof options.editable === 'boolean'; - - this.options.editable = { - updateTime: isBoolean ? options.editable : (options.editable.updateTime || false), - updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false), - add: isBoolean ? options.editable : (options.editable.add || false), - remove: isBoolean ? options.editable : (options.editable.remove || false) - }; - } - - // force update of range (apply new min/max etc.) - // both start and end are optional - this.range.setRange(options.start, options.end); - - if ('editable' in options || 'selectable' in options) { - if (this.options.selectable) { - // force update of selection - this.setSelection(this.getSelection()); - } - else { - // remove selection - this.setSelection([]); - } - } - - // force the itemSet to refresh: options like orientation and margins may be changed - this.itemSet.markDirty(); - - // validate the callback functions - var validateCallback = (function (fn) { - if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) { - throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)'); - } - }).bind(this); - ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback); - - /* TODO - // add/remove the current time bar - if (this.options.showCurrentTime) { - if (!this.mainPanel.hasChild(this.currentTime)) { - this.mainPanel.appendChild(this.currentTime); - this.currentTime.start(); - } - } - else { - if (this.mainPanel.hasChild(this.currentTime)) { - this.currentTime.stop(); - this.mainPanel.removeChild(this.currentTime); - } - } + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end']; + util.selectiveExtend(fields, this.options, options); - // add/remove the custom time bar - if (this.options.showCustomTime) { - if (!this.mainPanel.hasChild(this.customTime)) { - this.mainPanel.appendChild(this.customTime); - } + // enable/disable autoResize + this._initAutoResize(); } - else { - if (this.mainPanel.hasChild(this.customTime)) { - this.mainPanel.removeChild(this.customTime); - } - } -*/ - // enable/disable autoResize - this._initAutoResize(); + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); // TODO: remove deprecation error one day (deprecated since version 0.8.0) if (options && options.order) { @@ -361,11 +279,11 @@ Timeline.prototype.setItems = function(items) { this.itemsData = newDataSet; this.itemSet && this.itemSet.setItems(newDataSet); - if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) { + if (initialLoad && ('start' in this.options || 'end' in this.options)) { this.fit(); - var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null; - var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null; + var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; + var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; this.setWindow(start, end); } @@ -414,9 +332,13 @@ Timeline.prototype.clear = function(what) { this.setGroups(null); } - // clear options + // clear options of timeline and of each of the components if (!what || what.options) { - this.setOptions(this.defaultOptions); + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }) + + this.setOptions(this.defaultOptions); // this will also do a redraw } }; @@ -549,15 +471,15 @@ Timeline.prototype.redraw = function() { var resized = false, options = this.options, props = this.props, - dom = this.dom, - editable = options.editable.updateTime || options.editable.updateGroup; + dom = this.dom; // update class names - dom.root.className = 'vis timeline root ' + options.orientation + (editable ? ' editable' : ''); + dom.root.className = 'vis timeline root ' + options.orientation; - // update root height options + // update root width and height options dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); + dom.root.style.width = util.option.asSize(options.width, ''); // calculate border widths props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index 8e3a96e7..02e6368a 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -9,39 +9,14 @@ function Component (body, options) { } /** - * Set parameters for the frame. Parameters will be merged in current parameter - * set. - * @param {Object} options Available parameters: - * {String | function} [className] - * {String | Number | function} [left] - * {String | Number | function} [top] - * {String | Number | function} [width] - * {String | Number | function} [height] + * Set options for the component. The new options will be merged into the + * current options. + * @param {Object} options */ Component.prototype.setOptions = function(options) { if (options) { util.extend(this.options, options); - - this.redraw(); - } -}; - -/** - * Get an option value by name - * The function will first check this.options object, and else will check - * this.defaultOptions. - * @param {String} name - * @return {*} value - */ -Component.prototype.getOption = function(name) { - var value; - if (this.options) { - value = this.options[name]; - } - if (value === undefined && this.defaultOptions) { - value = this.defaultOptions[name]; } - return value; }; /** diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index 975ad45c..946ccf04 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -10,9 +10,15 @@ function CurrentTime (body, options) { this.body = body; - this.options = options || {}; + // default options + this.defaultOptions = { + showCurrentTime: true + }; + this.options = util.extend({}, this.defaultOptions); this._create(); + + this.setOptions(options); } CurrentTime.prototype = new Component(); @@ -31,6 +37,18 @@ CurrentTime.prototype._create = function() { this.bar = bar; }; +/** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] + */ +CurrentTime.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCurrentTime'], this.options, options); + } +}; + /** * Repaint the component * @return {boolean} Returns true if the component is resized diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js index e944ba5f..038d7be2 100644 --- a/src/timeline/component/CustomTime.js +++ b/src/timeline/component/CustomTime.js @@ -9,17 +9,36 @@ function CustomTime (body, options) { this.body = body; - this.options = options || {}; + + // default options + this.defaultOptions = { + showCustomTime: false + }; + this.options = util.extend({}, this.defaultOptions); this.customTime = new Date(); this.eventParams = {}; // stores state parameters while dragging the bar // create the DOM this._create(); + + this.setOptions(options); } CustomTime.prototype = new Component(); +/** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] + */ +CustomTime.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCustomTime'], this.options, options); + } +}; + /** * Create the DOM for the custom time * @private diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 678606bd..809aac9f 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -12,10 +12,45 @@ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items function ItemSet(body, options) { this.body = body; - // one options object is shared by this itemset and all its items - this.options = options || {}; - this.itemOptions = Object.create(this.options); - this.itemConversion = { + this.defaultOptions = { + type: 'box', + orientation: 'bottom', // 'top' or 'bottom' + align: 'center', // alignment of box items + stack: true, + groupOrder: null, + + selectable: true, + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, + + onAdd: function (item, callback) { + callback(item); + }, + onUpdate: function (item, callback) { + callback(item); + }, + onMove: function (item, callback) { + callback(item); + }, + onRemove: function (item, callback) { + callback(item); + }, + + margin: { + item: 10, + axis: 20 + }, + padding: 5 + }; + + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); + + this.conversion = { toScreen: body.util.toScreen, toTime: body.util.toTime }; @@ -64,6 +99,8 @@ function ItemSet(body, options) { // create the HTML DOM this._create(); + + this.setOptions(options); } ItemSet.prototype = new Component(); @@ -138,7 +175,7 @@ ItemSet.prototype._create = function(){ /** * Set options for the ItemSet. Existing options will be extended/overwritten. * @param {Object} [options] The following options are available: - * {String} [type] + * {String} type * Default type for the items. Choose from 'box' * (default), 'point', or 'range'. The default * Style can be overwritten by individual items. @@ -149,19 +186,92 @@ ItemSet.prototype._create = function(){ * {String} orientation * Orientation of the item set. Choose 'top' or * 'bottom' (default). + * {Function} groupOrder + * A sorting function for ordering groups + * {Boolean} stack + * If true (deafult), items will be stacked on + * top of each other. * {Number} margin.axis * Margin between the axis and the items in pixels. * Default is 20. * {Number} margin.item * Margin between items in pixels. Default is 10. + * {Number} margin + * Set margin for both axis and items in pixels. * {Number} padding * Padding of the contents of an item in pixels. * Must correspond with the items css. Default is 5. - * {Function} snap - * Function to let items snap to nice dates when - * dragging items. + * {Boolean} selectable + * If true (default), items can be selected. + * {Boolean} editable + * Set all editable options to true or false + * {Boolean} editable.updateTime + * Allow dragging an item to an other moment in time + * {Boolean} editable.updateGroup + * Allow dragging an item to an other group + * {Boolean} editable.add + * Allow creating new items on double tap + * {Boolean} editable.remove + * Allow removing items by clicking the delete button + * top right of a selected item. + * {Function(item: Item, callback: Function)} onAdd + * Callback function triggered when an item is about to be added: + * when the user double taps an empty space in the Timeline. + * {Function(item: Item, callback: Function)} onUpdate + * Callback function fired when an item is about to be updated. + * This function typically has to show a dialog where the user + * change the item. If not implemented, nothing happens. + * {Function(item: Item, callback: Function)} onMove + * Fired when an item has been moved. If not implemented, + * the move action will be accepted. + * {Function(item: Item, callback: Function)} onRemove + * Fired when an item is about to be deleted. + * If not implemented, the item will be always removed. */ -ItemSet.prototype.setOptions = Component.prototype.setOptions; +ItemSet.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder']; + util.selectiveExtend(fields, this.options, options); + + if ('margin' in options) { + if (typeof options.margin === 'number') { + this.options.margin.axis = options.margin; + this.options.margin.item = options.margin; + } + else if (typeof options.margin === 'object'){ + util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin); + } + } + + if ('editable' in options) { + if (typeof options.editable === 'boolean') { + this.options.editable.updateTime = options.editable; + this.options.editable.updateGroup = options.editable; + this.options.editable.add = options.editable; + this.options.editable.remove = options.editable; + } + else if (typeof options.editable === 'object') { + util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); + } + } + + // callback functions + var addCallback = (function (name) { + if (name in options) { + var fn = options[name]; + if (!(fn instanceof Function) || fn.length != 2) { + throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); + } + this.options[name] = fn; + } + }).bind(this); + ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback); + + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); + } +}; /** * Mark the ItemSet dirty so it will refresh everything with next redraw @@ -281,15 +391,11 @@ ItemSet.prototype.redraw = function() { options = this.options, orientation = options.orientation, resized = false, - frame = this.dom.frame; + frame = this.dom.frame, + editable = options.editable.updateTime || options.editable.updateGroup; - // TODO: document this feature to specify one margin for both item and axis distance - if (typeof margin === 'number') { - margin = { - item: margin, - axis: margin - }; - } + // update class name + frame.className = 'itemset' + (editable ? ' editable' : ''); // reorder the groups (if needed) resized = this._orderGroups() || resized; @@ -574,7 +680,7 @@ ItemSet.prototype._onUpdate = function(ids) { if (!item) { // create item if (constructor) { - item = new constructor(itemData, me.itemConversion, me.itemOptions); + item = new constructor(itemData, me.conversion, me.options); item.id = id; // TODO: not so nice setting id afterwards me._addItem(item); } diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index 3d1f268b..3bed910d 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -29,31 +29,38 @@ function TimeAxis (body, options) { lineTop: 0 }; - this.options = Object.create(options) || {}; this.defaultOptions = { orientation: 'bottom', // supported: 'top', 'bottom' // TODO: implement timeaxis orientations 'left' and 'right' showMinorLabels: true, showMajorLabels: true }; + this.options = util.extend({}, this.defaultOptions); this.body = body; // create the HTML DOM this._create(); + + this.setOptions(options); } TimeAxis.prototype = new Component(); /** - * Set parameters for the timeaxis. - * Parameters will be merged in current parameter set. - * @param {Object} options Available parameters: + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: * {string} [orientation] * {boolean} [showMinorLabels] * {boolean} [showMajorLabels] */ -TimeAxis.prototype.setOptions = Component.prototype.setOptions; +TimeAxis.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options); + } +}; /** * Create the HTML DOM for the TimeAxis @@ -84,9 +91,9 @@ TimeAxis.prototype.redraw = function () { this._calculateCharSize(); // TODO: recalculate sizes only needed when parent is resized or options is changed - var orientation = this.getOption('orientation'), - showMinorLabels = this.getOption('showMinorLabels'), - showMajorLabels = this.getOption('showMajorLabels'); + var orientation = this.options.orientation, + showMinorLabels = this.options.showMinorLabels, + showMajorLabels = this.options.showMajorLabels; // determine the width and height of the elemens for the axis props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; @@ -132,7 +139,7 @@ TimeAxis.prototype.redraw = function () { * @private */ TimeAxis.prototype._repaintLabels = function () { - var orientation = this.getOption('orientation'); + var orientation = this.options.orientation; // calculate range and step (step such that we have space for 7 characters per label) var start = util.convert(this.body.range.start, 'Number'), @@ -166,11 +173,11 @@ TimeAxis.prototype._repaintLabels = function () { // TODO: lines must have a width, such that we can create css backgrounds - if (this.getOption('showMinorLabels')) { + if (this.options.showMinorLabels) { this._repaintMinorText(x, step.getLabelMinor(), orientation); } - if (isMajor && this.getOption('showMajorLabels')) { + if (isMajor && this.options.showMajorLabels) { if (x > 0) { if (xFirstMajorLabel == undefined) { xFirstMajorLabel = x; @@ -187,7 +194,7 @@ TimeAxis.prototype._repaintLabels = function () { } // create a major label on the left when needed - if (this.getOption('showMajorLabels')) { + if (this.options.showMajorLabels) { var leftTime = this.body.util.toTime(0), leftText = step.getLabelMajor(leftTime), widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 2075b92b..413112cf 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -20,7 +20,7 @@ z-index: 999; } -.vis.timeline.editable .item.selected { +.vis.timeline .editable .item.selected { cursor: move; } diff --git a/src/util.js b/src/util.js index 48ba74de..a68c3455 100644 --- a/src/util.js +++ b/src/util.js @@ -88,7 +88,34 @@ util.extend = function (a, b) { for (var i = 1, len = arguments.length; i < len; i++) { var other = arguments[i]; for (var prop in other) { - if (other.hasOwnProperty(prop) && other[prop] !== undefined) { + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } + } + } + + return a; +}; + +/** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ +util.selectiveExtend = function (props, a, b) { + if (!Array.isArray(props)) { + throw new Error('Array with property names expected as first argument'); + } + + for (var i = 1, len = arguments.length; i < len; i++) { + var other = arguments[i]; + + for (var p = 0, pp = props.length; p < pp; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { a[prop] = other[prop]; } }