vis-timeline
Version:
Create a fully customizable, interactive timeline with items and ranges.
1 lines • 987 kB
Source Map (JSON)
{"version":3,"file":"vis-timeline-graph2d.min.mjs","sources":["../../lib/module/moment.js","../../lib/util.js","../../lib/timeline/component/Component.js","../../lib/timeline/DateUtil.js","../../lib/timeline/Range.js","../../lib/module/hammer.js","../../lib/hammerUtil.js","../../lib/timeline/TimeStep.js","../../lib/timeline/component/TimeAxis.js","../../lib/shared/Activator.js","../../lib/timeline/locales.js","../../lib/timeline/component/CustomTime.js","../../lib/timeline/Core.js","../../lib/timeline/component/CurrentTime.js","../../lib/timeline/Stack.js","../../lib/timeline/component/Group.js","../../lib/timeline/component/BackgroundGroup.js","../../lib/timeline/component/item/Item.js","../../lib/timeline/component/item/BoxItem.js","../../lib/timeline/component/item/PointItem.js","../../lib/timeline/component/item/RangeItem.js","../../lib/timeline/component/item/BackgroundItem.js","../../lib/shared/Popup.js","../../lib/timeline/component/item/ClusterItem.js","../../lib/timeline/component/ClusterGenerator.js","../../lib/timeline/component/ItemSet.js","../../lib/shared/Validator.js","../../lib/timeline/optionsTimeline.js","../../lib/shared/ColorPicker.js","../../lib/shared/Configurator.js","../../lib/timeline/Timeline.js","../../lib/DOMutil.js","../../lib/timeline/component/DataScale.js","../../lib/timeline/component/DataAxis.js","../../lib/timeline/component/graph2d_types/points.js","../../lib/timeline/component/graph2d_types/bar.js","../../lib/timeline/component/graph2d_types/line.js","../../lib/timeline/component/GraphGroup.js","../../lib/timeline/component/Legend.js","../../lib/timeline/component/LineGraph.js","../../lib/timeline/optionsGraph2d.js","../../lib/timeline/Graph2d.js","../../lib/entry-esnext.js"],"sourcesContent":["import bundledMoment from 'moment';\n\n// Check if Moment.js is already loaded in the browser window, if so, use this\n// instance, else use bundled Moment.js.\nconst moment = ((typeof window !== 'undefined') && window['moment']) || bundledMoment;\n\nexport default moment;\n","// utility functions\n\nexport * from \"vis-util/esnext\";\nimport * as util from \"vis-util/esnext\";\nimport { getType, isNumber, isString } from \"vis-util/esnext\";\nimport { DataSet, createNewDataPipeFrom } from \"vis-data/esnext\";\nimport {isDataViewLike as isDataViewLikeUpstream} from \"vis-data/esnext\";\n\nimport moment from \"moment\";\nimport xssFilter from 'xss';\n\nexport { v4 as randomUUID } from \"uuid\";\n/**\n * Test if an object implements the DataView interface from vis-data.\n * Uses the idProp property instead of expecting a hardcoded id field \"id\".\n */\nexport function isDataViewLike(obj) {\n if(!obj) {\n return false;\n }\n let idProp = obj.idProp ?? obj._idProp;\n if(!idProp) {\n return false;\n }\n return isDataViewLikeUpstream(idProp, obj);\n}\n\n// parse ASP.Net Date pattern,\n// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'\n// code from http://momentjs.com/\nconst ASPDateRegex = /^\\/?Date\\((-?\\d+)/i;\nconst NumericRegex = /^\\d+$/;\n/**\n * Convert an object into another type\n *\n * @param object - Value of unknown type.\n * @param type - Name of the desired type.\n *\n * @returns Object in the desired type.\n * @throws Error\n */\nexport function convert(object, type) {\n let match;\n\n if (object === undefined) {\n return undefined;\n }\n if (object === null) {\n return null;\n }\n\n if (!type) {\n return object;\n }\n if (!(typeof type === \"string\") && !(type instanceof String)) {\n throw new Error(\"Type must be a string\");\n }\n\n //noinspection FallthroughInSwitchStatementJS\n switch (type) {\n case \"boolean\":\n case \"Boolean\":\n return Boolean(object);\n\n case \"number\":\n case \"Number\":\n if (isString(object) && !isNaN(Date.parse(object))) {\n return moment(object).valueOf();\n } else {\n // @TODO: I don't think that Number and String constructors are a good idea.\n // This could also fail if the object doesn't have valueOf method or if it's redefined.\n // For example: Object.create(null) or { valueOf: 7 }.\n return Number(object.valueOf());\n }\n case \"string\":\n case \"String\":\n return String(object);\n\n case \"Date\":\n try {\n return convert(object, \"Moment\").toDate();\n }\n catch(e){\n if (e instanceof TypeError) {\n throw new TypeError(\n \"Cannot convert object of type \" + getType(object) + \" to type \" + type\n );\n } else {\n throw e;\n }\n }\n\n case \"Moment\":\n if (isNumber(object)) {\n return moment(object);\n }\n if (object instanceof Date) {\n return moment(object.valueOf());\n } else if (moment.isMoment(object)) {\n return moment(object);\n }\n if (isString(object)) {\n match = ASPDateRegex.exec(object);\n if (match) {\n // object is an ASP date\n return moment(Number(match[1])); // parse number\n }\n match = NumericRegex.exec(object);\n\n if (match) {\n return moment(Number(object));\n }\n\n return moment(object); // parse string\n } else {\n throw new TypeError(\n \"Cannot convert object of type \" + getType(object) + \" to type \" + type\n );\n }\n\n case \"ISODate\":\n if (isNumber(object)) {\n return new Date(object);\n } else if (object instanceof Date) {\n return object.toISOString();\n } else if (moment.isMoment(object)) {\n return object.toDate().toISOString();\n } else if (isString(object)) {\n match = ASPDateRegex.exec(object);\n if (match) {\n // object is an ASP date\n return new Date(Number(match[1])).toISOString(); // parse number\n } else {\n return moment(object).format(); // ISO 8601\n }\n } else {\n throw new Error(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type ISODate\"\n );\n }\n\n case \"ASPDate\":\n if (isNumber(object)) {\n return \"/Date(\" + object + \")/\";\n } else if (object instanceof Date || moment.isMoment(object)) {\n return \"/Date(\" + object.valueOf() + \")/\";\n } else if (isString(object)) {\n match = ASPDateRegex.exec(object);\n let value;\n if (match) {\n // object is an ASP date\n value = new Date(Number(match[1])).valueOf(); // parse number\n } else {\n value = new Date(object).valueOf(); // parse string\n }\n return \"/Date(\" + value + \")/\";\n } else {\n throw new Error(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type ASPDate\"\n );\n }\n\n default:\n throw new Error(`Unknown type ${type}`);\n }\n}\n\n/**\n * Create a Data Set like wrapper to seamlessly coerce data types.\n *\n * @param rawDS - The Data Set with raw uncoerced data.\n * @param type - A record assigning a data type to property name.\n *\n * @remarks\n * The write operations (`add`, `remove`, `update` and `updateOnly`) write into\n * the raw (uncoerced) data set. These values are then picked up by a pipe\n * which coerces the values using the [[convert]] function and feeds them into\n * the coerced data set. When querying (`forEach`, `get`, `getIds`, `off` and\n * `on`) the values are then fetched from the coerced data set and already have\n * the required data types. The values are coerced only once when inserted and\n * then the same value is returned each time until it is updated or deleted.\n *\n * For example: `typeCoercedDataSet.add({ id: 7, start: \"2020-01-21\" })` would\n * result in `typeCoercedDataSet.get(7)` returning `{ id: 7, start: moment(new\n * Date(\"2020-01-21\")).toDate() }`.\n *\n * Use the dispose method prior to throwing a reference to this away. Otherwise\n * the pipe connecting the two Data Sets will keep the unaccessible coerced\n * Data Set alive and updated as long as the raw Data Set exists.\n *\n * @returns A Data Set like object that saves data into the raw Data Set and\n * retrieves them from the coerced Data Set.\n */\nexport function typeCoerceDataSet(\n rawDS,\n type = { start: \"Date\", end: \"Date\" }\n) {\n const idProp = rawDS._idProp;\n const coercedDS = new DataSet({ fieldId: idProp });\n\n const pipe = createNewDataPipeFrom(rawDS)\n .map(item =>\n Object.keys(item).reduce((acc, key) => {\n acc[key] = convert(item[key], type[key]);\n return acc;\n }, {})\n )\n .to(coercedDS);\n\n pipe.all().start();\n\n return {\n // Write only.\n add: (...args) => rawDS.getDataSet().add(...args),\n remove: (...args) => rawDS.getDataSet().remove(...args),\n update: (...args) => rawDS.getDataSet().update(...args),\n updateOnly: (...args) => rawDS.getDataSet().updateOnly(...args),\n clear : (...args) => rawDS.getDataSet().clear(...args),\n\n // Read only.\n forEach: coercedDS.forEach.bind(coercedDS),\n get: coercedDS.get.bind(coercedDS),\n getIds: coercedDS.getIds.bind(coercedDS),\n off: coercedDS.off.bind(coercedDS),\n on: coercedDS.on.bind(coercedDS),\n\n get length() {\n return coercedDS.length;\n },\n\n // Non standard.\n idProp,\n type,\n\n rawDS,\n coercedDS,\n dispose: () => pipe.stop()\n };\n}\n\n// Configure XSS protection\nconst setupXSSCleaner = (options) => {\n const customXSS = new xssFilter.FilterXSS(options);\n\n return (input) => {\n if (typeof input === \"string\") {\n return customXSS.process(input);\n }\n return input; // Leave other types unchanged\n };\n};\nconst setupNoOpCleaner = (string) => string;\n\n// when nothing else is configured: filter XSS with the lib's default options\nlet configuredXSSProtection = setupXSSCleaner();\n\nconst setupXSSProtection = (options) => {\n // No options? Do nothing.\n if (!options) {\n return;\n }\n\n // Disable XSS protection completely on request\n if (options.disabled === true) {\n configuredXSSProtection = setupNoOpCleaner;\n console.warn('You disabled XSS protection for vis-Timeline. I sure hope you know what you\\'re doing!');\n } else {\n // Configure XSS protection with some custom options.\n // For a list of valid options check the lib's documentation:\n // https://github.com/leizongmin/js-xss#custom-filter-rules\n if (options.filterOptions) {\n configuredXSSProtection = setupXSSCleaner(options.filterOptions);\n }\n }\n};\n\nconst availableUtils = {\n ...util,\n convert,\n setupXSSProtection\n};\n\nObject.defineProperty(availableUtils, 'xss', {\n get: function() {\n return configuredXSSProtection;\n }\n})\n\nexport default availableUtils;\n","import util from '../../util.js';\n\n/** Prototype for visual components */\nexport default class Component {\n /**\n * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]\n * @param {Object} [options]\n */\n constructor(body, options) { // eslint-disable-line no-unused-vars\n this.options = null;\n this.props = null;\n }\n\n /**\n * Set options for the component. The new options will be merged into the\n * current options.\n * @param {Object} options\n */\n setOptions(options) {\n if (options) {\n util.extend(this.options, options);\n }\n }\n\n /**\n * Repaint the component\n * @return {boolean} Returns true if the component is resized\n */\n redraw() {\n // should be implemented by the component\n return false;\n }\n\n /**\n * Destroy the component. Cleanup DOM and event listeners\n */\n destroy() {\n // should be implemented by the component\n }\n\n /**\n * Test whether the component is resized since the last time _isResized() was\n * called.\n * @return {Boolean} Returns true if the component is resized\n * @protected\n */\n _isResized() {\n const resized = (\n this.props._previousWidth !== this.props.width ||\n this.props._previousHeight !== this.props.height\n );\n\n this.props._previousWidth = this.props.width;\n this.props._previousHeight = this.props.height;\n\n return resized;\n }\n}\n","\n/**\n * used in Core to convert the options into a volatile variable\n * \n * @param {function} moment\n * @param {Object} body\n * @param {Array | Object} hiddenDates\n * @returns {number}\n */\nexport function convertHiddenOptions(moment, body, hiddenDates) {\n if (hiddenDates && !Array.isArray(hiddenDates)) {\n return convertHiddenOptions(moment, body, [hiddenDates])\n }\n\n body.hiddenDates = [];\n if (hiddenDates) {\n if (Array.isArray(hiddenDates) == true) {\n for (let i = 0; i < hiddenDates.length; i++) {\n if (hiddenDates[i].repeat === undefined) {\n const dateItem = {};\n dateItem.start = moment(hiddenDates[i].start).toDate().valueOf();\n dateItem.end = moment(hiddenDates[i].end).toDate().valueOf();\n body.hiddenDates.push(dateItem);\n }\n }\n body.hiddenDates.sort((a, b) => a.start - b.start); // sort by start time\n }\n }\n}\n\n/**\n * create new entrees for the repeating hidden dates\n *\n * @param {function} moment\n * @param {Object} body\n * @param {Array | Object} hiddenDates\n * @returns {null}\n */\nexport function updateHiddenDates(moment, body, hiddenDates) {\n if (hiddenDates && !Array.isArray(hiddenDates)) {\n return updateHiddenDates(moment, body, [hiddenDates])\n }\n\n if (hiddenDates && body.domProps.centerContainer.width !== undefined) {\n convertHiddenOptions(moment, body, hiddenDates);\n\n const start = moment(body.range.start);\n const end = moment(body.range.end);\n\n const totalRange = (body.range.end - body.range.start);\n const pixelTime = totalRange / body.domProps.centerContainer.width;\n\n for (let i = 0; i < hiddenDates.length; i++) {\n if (hiddenDates[i].repeat !== undefined) {\n let startDate = moment(hiddenDates[i].start);\n let endDate = moment(hiddenDates[i].end);\n\n if (startDate._d == \"Invalid Date\") {\n throw new Error(`Supplied start date is not valid: ${hiddenDates[i].start}`);\n }\n if (endDate._d == \"Invalid Date\") {\n throw new Error(`Supplied end date is not valid: ${hiddenDates[i].end}`);\n }\n\n const duration = endDate - startDate;\n if (duration >= 4 * pixelTime) {\n\n let offset = 0;\n let runUntil = end.clone();\n switch (hiddenDates[i].repeat) {\n case \"daily\": // case of time\n if (startDate.day() != endDate.day()) {\n offset = 1;\n }\n startDate = startDate.dayOfYear(start.dayOfYear())\n .year(start.year())\n .subtract(7,'days');\n\n endDate = endDate.dayOfYear(start.dayOfYear())\n .year(start.year())\n .subtract(7 - offset,'days');\n\n runUntil.add(1, 'weeks');\n break;\n case \"weekly\": {\n const dayOffset = endDate.diff(startDate,'days');\n const day = startDate.day();\n\n // set the start date to the range.start\n startDate = startDate.date(start.date())\n .month(start.month())\n .year(start.year());\n endDate = startDate.clone();\n\n // force\n startDate = startDate.day(day).subtract(1,'weeks');\n endDate = endDate.day(day)\n .add(dayOffset,'days')\n .subtract(1,'weeks');\n\n runUntil.add(1, 'weeks');\n break;\n }\n case \"monthly\":\n if (startDate.month() != endDate.month()) {\n offset = 1;\n }\n startDate = startDate.month(start.month())\n .year(start.year())\n .subtract(1,'months');\n\n endDate = endDate.month(start.month())\n .year(start.year())\n .subtract(1,'months')\n .add(offset,'months');\n\n runUntil.add(1, 'months');\n break;\n case \"yearly\":\n if (startDate.year() != endDate.year()) {\n offset = 1;\n }\n startDate = startDate.year(start.year())\n .subtract(1,'years');\n endDate = endDate.year(start.year())\n .subtract(1,'years')\n .add(offset,'years');\n\n runUntil.add(1, 'years');\n break;\n default:\n console.log(\"Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:\", hiddenDates[i].repeat);\n return;\n }\n while (startDate < runUntil) {\n body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()});\n switch (hiddenDates[i].repeat) {\n case \"daily\":\n startDate = startDate.add(1, 'days');\n endDate = endDate.add(1, 'days');\n break;\n case \"weekly\":\n startDate = startDate.add(1, 'weeks');\n endDate = endDate.add(1, 'weeks');\n break;\n case \"monthly\":\n startDate = startDate.add(1, 'months');\n endDate = endDate.add(1, 'months');\n break;\n case \"yearly\":\n startDate = startDate.add(1, 'y');\n endDate = endDate.add(1, 'y');\n break;\n default:\n console.log(\"Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:\", hiddenDates[i].repeat);\n return;\n }\n }\n body.hiddenDates.push({start: startDate.valueOf(), end: endDate.valueOf()});\n }\n }\n }\n // remove duplicates, merge where possible\n removeDuplicates(body);\n // ensure the new positions are not on hidden dates\n const startHidden = getIsHidden(body.range.start, body.hiddenDates);\n const endHidden = getIsHidden(body.range.end,body.hiddenDates);\n let rangeStart = body.range.start;\n let rangeEnd = body.range.end;\n if (startHidden.hidden == true) {rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1;}\n if (endHidden.hidden == true) {rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1;}\n if (startHidden.hidden == true || endHidden.hidden == true) {\n body.range._applyRange(rangeStart, rangeEnd);\n }\n }\n\n}\n\n/**\n * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up.\n * Scales with N^2\n *\n * @param {Object} body\n */\nexport function removeDuplicates(body) {\n const hiddenDates = body.hiddenDates;\n const safeDates = [];\n for (var i = 0; i < hiddenDates.length; i++) {\n for (let j = 0; j < hiddenDates.length; j++) {\n if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) {\n // j inside i\n if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) {\n hiddenDates[j].remove = true;\n }\n // j start inside i\n else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) {\n hiddenDates[i].end = hiddenDates[j].end;\n hiddenDates[j].remove = true;\n }\n // j end inside i\n else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) {\n hiddenDates[i].start = hiddenDates[j].start;\n hiddenDates[j].remove = true;\n }\n }\n }\n }\n\n for (i = 0; i < hiddenDates.length; i++) {\n if (hiddenDates[i].remove !== true) {\n safeDates.push(hiddenDates[i]);\n }\n }\n\n body.hiddenDates = safeDates;\n body.hiddenDates.sort((a, b) => a.start - b.start); // sort by start time\n}\n\n/**\n * Prints dates to console\n * @param {array} dates\n */\nexport function printDates(dates) {\n for (let i =0; i < dates.length; i++) {\n console.log(i, new Date(dates[i].start),new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove);\n }\n}\n\n/**\n * Used in TimeStep to avoid the hidden times.\n * @param {function} moment\n * @param {TimeStep} timeStep\n * @param {Date} previousTime\n */\nexport function stepOverHiddenDates(moment, timeStep, previousTime) {\n let stepInHidden = false;\n const currentValue = timeStep.current.valueOf();\n for (let i = 0; i < timeStep.hiddenDates.length; i++) {\n const startDate = timeStep.hiddenDates[i].start;\n var endDate = timeStep.hiddenDates[i].end;\n if (currentValue >= startDate && currentValue < endDate) {\n stepInHidden = true;\n break;\n }\n }\n\n if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) {\n const prevValue = moment(previousTime);\n const newValue = moment(endDate);\n //check if the next step should be major\n if (prevValue.year() != newValue.year()) {timeStep.switchedYear = true;}\n else if (prevValue.month() != newValue.month()) {timeStep.switchedMonth = true;}\n else if (prevValue.dayOfYear() != newValue.dayOfYear()) {timeStep.switchedDay = true;}\n\n timeStep.current = newValue;\n }\n}\n\n///**\n// * Used in TimeStep to avoid the hidden times.\n// * @param timeStep\n// * @param previousTime\n// */\n//checkFirstStep = function(timeStep) {\n// var stepInHidden = false;\n// var currentValue = timeStep.current.valueOf();\n// for (var i = 0; i < timeStep.hiddenDates.length; i++) {\n// var startDate = timeStep.hiddenDates[i].start;\n// var endDate = timeStep.hiddenDates[i].end;\n// if (currentValue >= startDate && currentValue < endDate) {\n// stepInHidden = true;\n// break;\n// }\n// }\n//\n// if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) {\n// var newValue = moment(endDate);\n// timeStep.current = newValue.toDate();\n// }\n//};\n\n/**\n * replaces the Core toScreen methods\n *\n * @param {timeline.Core} Core\n * @param {Date} time\n * @param {number} width\n * @returns {number}\n */\nexport function toScreen(Core, time, width) {\n let conversion;\n if (Core.body.hiddenDates.length == 0) {\n conversion = Core.range.conversion(width);\n return (time.valueOf() - conversion.offset) * conversion.scale;\n } else {\n const hidden = getIsHidden(time, Core.body.hiddenDates);\n if (hidden.hidden == true) {\n time = hidden.startDate;\n }\n\n const duration = getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end);\n if (time < Core.range.start) {\n conversion = Core.range.conversion(width, duration);\n const hiddenBeforeStart = getHiddenDurationBeforeStart(Core.body.hiddenDates, time, conversion.offset);\n time = Core.options.moment(time).toDate().valueOf();\n time = time + hiddenBeforeStart;\n return -(conversion.offset - time.valueOf()) * conversion.scale;\n \n } else if (time > Core.range.end) {\n const rangeAfterEnd = {start: Core.range.start, end: time};\n time = correctTimeForHidden(Core.options.moment, Core.body.hiddenDates, rangeAfterEnd, time);\n conversion = Core.range.conversion(width, duration);\n return (time.valueOf() - conversion.offset) * conversion.scale;\n\n } else {\n time = correctTimeForHidden(Core.options.moment, Core.body.hiddenDates, Core.range, time);\n conversion = Core.range.conversion(width, duration);\n return (time.valueOf() - conversion.offset) * conversion.scale;\n }\n }\n }\n\n/**\n * Replaces the core toTime methods\n *\n * @param {timeline.Core} Core\n * @param {number} x\n * @param {number} width\n * @returns {Date}\n */\nexport function toTime(Core, x, width) {\n if (Core.body.hiddenDates.length == 0) {\n const conversion = Core.range.conversion(width);\n return new Date(x / conversion.scale + conversion.offset);\n }\n else {\n const hiddenDuration = getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end);\n const totalDuration = Core.range.end - Core.range.start - hiddenDuration;\n const partialDuration = totalDuration * x / width;\n const accumulatedHiddenDuration = getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration);\n\n return new Date(accumulatedHiddenDuration + partialDuration + Core.range.start);\n }\n}\n\n/**\n * Support function\n *\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {number} start\n * @param {number} end\n * @returns {number}\n */\nexport function getHiddenDurationBetween(hiddenDates, start, end) {\n let duration = 0;\n for (let i = 0; i < hiddenDates.length; i++) {\n const startDate = hiddenDates[i].start;\n const endDate = hiddenDates[i].end;\n // if time after the cutout, and the\n if (startDate >= start && endDate < end) {\n duration += endDate - startDate;\n }\n }\n return duration;\n}\n\n/**\n * Support function\n *\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {number} start\n * @param {number} end\n * @returns {number}\n */\nexport function getHiddenDurationBeforeStart(hiddenDates, start, end) {\n let duration = 0;\n for (let i = 0; i < hiddenDates.length; i++) {\n const startDate = hiddenDates[i].start;\n const endDate = hiddenDates[i].end;\n\n if (startDate >= start && endDate <= end) {\n duration += endDate - startDate;\n }\n }\n return duration;\n}\n\n/**\n * Support function\n * @param {function} moment\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {{start: number, end: number}} range\n * @param {Date} time\n * @returns {number}\n */\nexport function correctTimeForHidden(moment, hiddenDates, range, time) {\n time = moment(time).toDate().valueOf();\n time -= getHiddenDurationBefore(moment, hiddenDates,range,time);\n return time;\n}\n\n/**\n * Support function\n * @param {function} moment\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {{start: number, end: number}} range\n * @param {Date} time\n * @returns {number}\n */\nexport function getHiddenDurationBefore(moment, hiddenDates, range, time) {\n let timeOffset = 0;\n time = moment(time).toDate().valueOf();\n\n for (let i = 0; i < hiddenDates.length; i++) {\n const startDate = hiddenDates[i].start;\n const endDate = hiddenDates[i].end;\n // if time after the cutout, and the\n if (startDate >= range.start && endDate < range.end) {\n if (time >= endDate) {\n timeOffset += (endDate - startDate);\n }\n }\n }\n return timeOffset;\n}\n\n/**\n * sum the duration from start to finish, including the hidden duration,\n * until the required amount has been reached, return the accumulated hidden duration\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {{start: number, end: number}} range\n * @param {number} [requiredDuration=0]\n * @returns {number}\n */\nexport function getAccumulatedHiddenDuration(hiddenDates, range, requiredDuration) {\n let hiddenDuration = 0;\n let duration = 0;\n let previousPoint = range.start;\n //printDates(hiddenDates)\n for (let i = 0; i < hiddenDates.length; i++) {\n const startDate = hiddenDates[i].start;\n const endDate = hiddenDates[i].end;\n // if time after the cutout, and the\n if (startDate >= range.start && endDate < range.end) {\n duration += startDate - previousPoint;\n previousPoint = endDate;\n if (duration >= requiredDuration) {\n break;\n }\n else {\n hiddenDuration += endDate - startDate;\n }\n }\n }\n\n return hiddenDuration;\n}\n\n/**\n * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @param {Date} time\n * @param {number} direction\n * @param {boolean} correctionEnabled\n * @returns {Date|number}\n */\nexport function snapAwayFromHidden(hiddenDates, time, direction, correctionEnabled) {\n const isHidden = getIsHidden(time, hiddenDates);\n if (isHidden.hidden == true) {\n if (direction < 0) {\n if (correctionEnabled == true) {\n return isHidden.startDate - (isHidden.endDate - time) - 1;\n }\n else {\n return isHidden.startDate - 1;\n }\n }\n else {\n if (correctionEnabled == true) {\n return isHidden.endDate + (time - isHidden.startDate) + 1;\n }\n else {\n return isHidden.endDate + 1;\n }\n }\n }\n else {\n return time;\n }\n\n}\n\n/**\n * Check if a time is hidden\n *\n * @param {Date} time\n * @param {Array.<{start: Window.start, end: *}>} hiddenDates\n * @returns {{hidden: boolean, startDate: Window.start, endDate: *}}\n */\nexport function getIsHidden(time, hiddenDates) {\n for (let i = 0; i < hiddenDates.length; i++) {\n var startDate = hiddenDates[i].start;\n var endDate = hiddenDates[i].end;\n\n if (time >= startDate && time < endDate) { // if the start is entering a hidden zone\n return {hidden: true, startDate, endDate};\n }\n }\n return {hidden: false, startDate, endDate};\n}\n","import util from '../util.js';\nimport moment from '../module/moment.js';\nimport Component from './component/Component.js';\nimport * as DateUtil from './DateUtil.js';\n\n/**\n * A Range controls a numeric range with a start and end value.\n * The Range adjusts the range based on mouse events or programmatic changes,\n * and triggers events when the range is changing or has been changed.\n */\nexport default class Range extends Component {\n /**\n * @param {{dom: Object, domProps: Object, emitter: Emitter}} body\n * @param {Object} [options] See description at Range.setOptions\n * @constructor Range\n * @extends Component\n */\n constructor(body, options) {\n super();\n const now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);\n const start = now.clone().add(-3, 'days').valueOf();\n const end = now.clone().add(3, 'days').valueOf(); \n this.millisecondsPerPixelCache = undefined;\n \n if(options === undefined) {\n this.start = start;\n this.end = end;\n } else {\n this.start = options.start || start;\n this.end = options.end || end\n }\n\n this.rolling = false;\n\n this.body = body;\n this.deltaDifference = 0;\n this.scaleOffset = 0;\n this.startToFront = false;\n this.endToFront = true;\n\n // default options\n this.defaultOptions = {\n rtl: false,\n start: null,\n end: null,\n moment,\n direction: 'horizontal', // 'horizontal' or 'vertical'\n moveable: true,\n zoomable: true,\n min: null,\n max: null,\n zoomMin: 10, // milliseconds\n zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds\n rollingMode: {\n follow: false,\n offset: 0.5\n }\n };\n this.options = util.extend({}, this.defaultOptions);\n this.props = {\n touch: {}\n };\n this.animationTimer = null;\n\n // drag listeners for dragging\n this.body.emitter.on('panstart', this._onDragStart.bind(this));\n this.body.emitter.on('panmove', this._onDrag.bind(this));\n this.body.emitter.on('panend', this._onDragEnd.bind(this));\n\n // mouse wheel for zooming\n this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));\n\n // pinch to zoom\n this.body.emitter.on('touch', this._onTouch.bind(this));\n this.body.emitter.on('pinch', this._onPinch.bind(this));\n\n // on click of rolling mode button\n this.body.dom.rollingModeBtn.addEventListener('click', this.startRolling.bind(this));\n\n this.setOptions(options);\n }\n\n /**\n * Set options for the range controller\n * @param {Object} options Available options:\n * {number | Date | String} start Start date for the range\n * {number | Date | String} end End date for the range\n * {number} min Minimum value for start\n * {number} max Maximum value for end\n * {number} zoomMin Set a minimum value for\n * (end - start).\n * {number} zoomMax Set a maximum value for\n * (end - start).\n * {boolean} moveable Enable moving of the range\n * by dragging. True by default\n * {boolean} zoomable Enable zooming of the range\n * by pinching/scrolling. True by default\n */\n setOptions(options) {\n if (options) {\n // copy the options that we know\n const fields = [\n 'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable',\n 'moment', 'activate', 'hiddenDates', 'zoomKey', 'zoomFriction', 'rtl', 'showCurrentTime', 'rollingMode', 'horizontalScroll'\n ];\n util.selectiveExtend(fields, this.options, options);\n\n if (options.rollingMode && options.rollingMode.follow) {\n this.startRolling();\n }\n if ('start' in options || 'end' in options) {\n // apply a new range. both start and end are optional\n this.setRange(options.start, options.end);\n }\n }\n }\n\n /**\n * Start auto refreshing the current time bar\n */\n startRolling() {\n const me = this;\n\n /**\n * Updates the current time.\n */\n function update () {\n me.stopRolling();\n me.rolling = true;\n\n\n let interval = me.end - me.start;\n const t = util.convert(new Date(), 'Date').valueOf();\n const rollingModeOffset = me.options.rollingMode && me.options.rollingMode.offset || 0.5\n\n const start = t - interval * (rollingModeOffset);\n const end = t + interval * (1 - rollingModeOffset);\n\n const options = {\n animation: false\n };\n me.setRange(start, end, options);\n\n // determine interval to refresh\n const scale = me.conversion(me.body.domProps.center.width).scale;\n interval = 1 / scale / 10;\n if (interval < 30) interval = 30;\n if (interval > 1000) interval = 1000;\n\n me.body.dom.rollingModeBtn.style.visibility = \"hidden\";\n // start a renderTimer to adjust for the new time\n me.currentTimeTimer = setTimeout(update, interval);\n }\n\n update();\n }\n\n /**\n * Stop auto refreshing the current time bar\n */\n stopRolling() {\n if (this.currentTimeTimer !== undefined) {\n clearTimeout(this.currentTimeTimer);\n this.rolling = false;\n this.body.dom.rollingModeBtn.style.visibility = \"visible\";\n }\n }\n\n /**\n * Set a new start and end range\n * @param {Date | number | string} start\n * @param {Date | number | string} end\n * @param {Object} options Available options:\n * {boolean | {duration: number, easingFunction: string}} [animation=false]\n * If true, the range is animated\n * smoothly to the new window. An object can be\n * provided to specify duration and easing function.\n * Default duration is 500 ms, and default easing\n * function is 'easeInOutQuad'.\n * {boolean} [byUser=false]\n * {Event} event Mouse event\n * @param {Function} callback a callback function to be executed at the end of this function \n * @param {Function} frameCallback a callback function executed each frame of the range animation.\n * The callback will be passed three parameters:\n * {number} easeCoefficient an easing coefficent\n * {boolean} willDraw If true the caller will redraw after the callback completes\n * {boolean} done If true then animation is ending after the current frame\n * @return {void}\n */\n setRange(start, end, options, callback, frameCallback) {\n if (!options) {\n options = {};\n }\n if (options.byUser !== true) {\n options.byUser = false;\n }\n const me = this;\n const finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null;\n const finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null;\n this._cancelAnimation();\n this.millisecondsPerPixelCache = undefined;\n\n if (options.animation) { // true or an Object\n const initStart = this.start;\n const initEnd = this.end;\n const duration = (typeof options.animation === 'object' && 'duration' in options.animation) ? options.animation.duration : 500;\n const easingName = (typeof options.animation === 'object' && 'easingFunction' in options.animation) ? options.animation.easingFunction : 'easeInOutQuad';\n const easingFunction = util.easingFunctions[easingName];\n if (!easingFunction) {\n throw new Error(`Unknown easing function ${JSON.stringify(easingName)}. Choose from: ${Object.keys(util.easingFunctions).join(', ')}`);\n }\n\n const initTime = Date.now();\n let anyChanged = false;\n\n const next = () => {\n if (!me.props.touch.dragging) {\n const now = Date.now();\n const time = now - initTime;\n const ease = easingFunction(time / duration);\n const done = time > duration;\n const s = (done || finalStart === null) ? finalStart : initStart + (finalStart - initStart) * ease;\n const e = (done || finalEnd === null) ? finalEnd : initEnd + (finalEnd - initEnd) * ease;\n\n changed = me._applyRange(s, e);\n DateUtil.updateHiddenDates(me.options.moment, me.body, me.options.hiddenDates);\n anyChanged = anyChanged || changed;\n\n const params = {\n start: new Date(me.start), \n end: new Date(me.end), \n byUser: options.byUser,\n event: options.event\n };\n\n if (frameCallback) { frameCallback(ease, changed, done); }\n\n if (changed) { \n me.body.emitter.emit('rangechange', params);\n }\n\n if (done) {\n if (anyChanged) {\n me.body.emitter.emit('rangechanged', params);\n if (callback) { return callback() }\n }\n }\n else {\n // animate with as high as possible frame rate, leave 20 ms in between\n // each to prevent the browser from blocking\n me.animationTimer = setTimeout(next, 20);\n }\n }\n };\n\n return next();\n }\n else {\n var changed = this._applyRange(finalStart, finalEnd);\n DateUtil.updateHiddenDates(this.options.moment, this.body, this.options.hiddenDates);\n if (changed) {\n const params = {\n start: new Date(this.start), \n end: new Date(this.end), \n byUser: options.byUser, \n event: options.event\n };\n\n this.body.emitter.emit('rangechange', params);\n clearTimeout( me.timeoutID );\n me.timeoutID = setTimeout( () => {\n me.body.emitter.emit('rangechanged', params);\n }, 200 );\n if (callback) { return callback() }\n }\n }\n }\n\n /**\n * Get the number of milliseconds per pixel.\n *\n * @returns {undefined|number}\n */\n getMillisecondsPerPixel() {\n if (this.millisecondsPerPixelCache === undefined) {\n this.millisecondsPerPixelCache = (this.end - this.start) / this.body.dom.center.clientWidth;\n }\n return this.millisecondsPerPixelCache;\n }\n\n /**\n * Stop an animation\n * @private\n */\n _cancelAnimation() {\n if (this.animationTimer) {\n clearTimeout(this.animationTimer);\n this.animationTimer = null;\n }\n }\n\n /**\n * Set a new start and end range. This method is the same as setRange, but\n * does not trigger a range change and range changed event, and it returns\n * true when the range is changed\n * @param {number} [start]\n * @param {number} [end]\n * @return {boolean} changed\n * @private\n */\n _applyRange(start, end) {\n let newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start;\n let newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end;\n const max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null;\n const min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null;\n let diff;\n\n // check for valid number\n if (isNaN(newStart) || newStart === null) {\n throw new Error(`Invalid start \"${start}\"`);\n }\n if (isNaN(newEnd) || newEnd === null) {\n throw new Error(`Invalid end \"${end}\"`);\n }\n\n // prevent end < start\n if (newEnd < newStart) {\n newEnd = newStart;\n }\n\n // prevent start < min\n if (min !== null) {\n if (newStart < min) {\n diff = (min - newStart);\n newStart += diff;\n newEnd += diff;\n\n // prevent end > max\n if (max != null) {\n if (newEnd > max) {\n newEnd = max;\n }\n }\n }\n }\n\n // prevent end > max\n if (max !== null) {\n if (newEnd > max) {\n diff = (newEnd - max);\n newStart -= diff;\n newEnd -= diff;\n\n // prevent start < min\n if (min != null) {\n if (newStart < min) {\n newStart = min;\n }\n }\n }\n }\n\n // prevent (end-start) < zoomMin\n if (this.options.zoomMin !== null) {\n let zoomMin = parseFloat(this.options.zoomMin);\n if (zoomMin < 0) {\n zoomMin = 0;\n }\n if ((newEnd - newStart) < zoomMin) {\n // compensate for a scale of 0.5 ms\n const compensation = 0.5;\n if ((this.end - this.start) === zoomMin && newStart >= this.start - compensation && newEnd <= this.end) {\n // ignore this action, we are already zoomed to the minimum\n newStart = this.start;\n newEnd = this.end;\n }\n else {\n // zoom to the minimum\n diff = (zoomMin - (newEnd - newStart));\n newStart -= diff / 2;\n newEnd += diff / 2;\n }\n }\n }\n\n // prevent (end-start) > zoomMax\n if (this.options.zoomMax !== null) {\n let zoomMax = parseFloat(this.options.zoomMax);\n if (zoomMax < 0) {\n zoomMax = 0;\n }\n\n if ((newEnd - newStart) > zoomMax) {\n if ((this.end - this.start) === zoomMax && newStart < this.start && newEnd > this.end) {\n // ignore this action, we are already zoomed to the maximum\n newStart = this.start;\n newEnd = this.end;\n }\n else {\n // zoom to the maximum\n diff = ((newEnd - newStart) - zoomMax);\n newStart += diff / 2;\n newEnd -= diff / 2;\n }\n }\n }\n\n const changed = (this.start != newStart || this.end != newEnd);\n\n // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range)\n if (!((newStart >= this.start && newStart <= this.end) || (newEnd >= this.start && newEnd <= this.end)) &&\n !((this.start >= newStart && this.start <= newEnd) || (this.end >= newStart && this.end <= newEnd) )) {\n this.body.emitter.emit('checkRangedItems');\n }\n\n this.start = newStart;\n this.end = newEnd;\n return changed;\n }\n\n /**\n * Retrieve the current range.\n * @return {Object} An object with start and end properties\n */\n getRange() {\n return {\n start: this.start,\n end: this.end\n };\n }\n\n /**\n * Calculate the conversion offset and scale for current range, based on\n * the provided width\n * @param {number} width\n * @param {number} [totalHidden=0]\n * @returns {{offset: number, scale: number}} conversion\n */\n conversion(width, totalHidden) {\n return Range.conversion(this.start, this.end, width, totalHidden);\n }\n\n /**\n * Static method to calculate the conversion offset and scale for a range,\n * based on the provided start, end, and width\n * @param {number} start\n * @param {number} end\n * @param {number} width\n * @param {number} [totalHidden=0]\n * @returns {{offset: number, scale: number}} conversion\n */\n static conversion(start, end, width, totalHidden) {\n if (totalHidden === undefined) {\n totalHidden = 0;\n }\n if (width != 0 && (end - start != 0)) {\n return {\n offset: start,\n scale: width / (end - start - totalHidden)\n }\n }\n else {\n return {\n offset: 0,\n scale: 1\n };\n }\n }\n\n /**\n * Start dragging horizontally or vertically\n * @param {Event} event\n * @private\n */\n _onDragStart(event) {\n this.deltaDifference = 0;\n this.previousDelta = 0;\n\n // only allow dragging when configured as movable\n if (!this.options.moveable) return;\n\n // only start dragging when the mouse is inside the current range\n if (!this._isInsideRange(event)) return;\n\n // refuse to drag when we where pinching to prevent the timeline make a jump\n // when releasing the fingers in opposite order from the touch screen\n if (!this.props.touch.allowDragging) return;\n\n this.stopRolling();\n\n this.props.touch.start = this.start;\n this.props.touch.end = this.end;\n this.props.touch.dragging = true;\n\n if (this.body.dom.root) {\n this.body.dom.root.style.cursor = 'move';\n }\n }\n\n /**\n * Perform dragging operation\n * @param {Event} event\n * @private\n */\n _onDrag(event) {\n if (!event) return;\n\n if (!this.props.touch.dragging) return;\n\n // only allow dragging when configured as movable\n if (!this.options.moveable) return;\n\n // TODO: this may be redundant in hammerjs2\n // refuse to drag when we where pinching to prevent the timeline make a jump\n // when releasing the fingers in opposite order from the touch screen\n if (!this.props.touch.allowDragging) return;\n\n const direction = this.options.direction;\n validateDirection(direction);\n let delta = (direction == 'horizontal') ? event.deltaX : event.deltaY;\n delta -= this.deltaDifference;\n let interval = (this.props.touch.end - this.props.touch.start);\n\n // normalize dragging speed if cutout is in between.\n const duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end);\n interval -= duration;\n\n const width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;\n let diffRange;\n if (this.options.rtl) {\n diffRange = delta / width * interval;\n } else {\n diffRange = -delta / width * interval;\n }\n\n const newStart = this.props.touch.start + diffRange;\n const newEnd = this.props.touch.end + diffRange;\n\n // snapping times away from hidden zones\n const safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta-delta, true);\n const safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta-delta, true);\n if (safeStart != newStart || safeEnd != newEnd) {\n this.deltaDifference += delta;\n this.props.touch.start = safeStart;\n this.props.touch.end = safeEnd;\n this._onDrag(event);\n return;\n }\n\n this.previousDelta = delta;\n this._applyRange(newStart, newEnd);\n\n\n const startDate = new Date(this.start);\n const endDate = new Date(this.end);\n\n // fire a rangechange event\n this.body.emitter.emit('rangechange', {\n start: startDate,\n end: endDate,\n byUser: true,\n event\n });\n\n // fire a panmove event\n this.body.emitter.emit('panmove');\n }\n\n /**\n * Stop dragging operation\n * @param {event} event\n * @private\n */\n _onDragEnd(event) {\n if (!this.props.touch.dragging) return;\n\n // only allow dragging when configured as movable\n if (!this.options.moveable) return;\n\n // TODO: this may be redundant in hammerjs2\n // refuse to drag when we where pinching to prevent the timeline make a jump\n // when releasing the fingers in opposite order from the touch screen\n if (!this.props.touch.allowDragging) return;\n\n this.props.touch.dragging = false;\n if (this.body.dom.root) {\n this.body.dom.root.style.cursor = 'auto';\n }\n\n // fire a rangechanged event\n this.body.emitter.emit('rangechanged', {\n start: new Date(this.start),\n end: new Date(this.end),\n byUser: true,\n event\n });\n }\n\n /**\n * Event h