vis-timeline
Version:
Create a fully customizable, interactive timeline with items and ranges.
1 lines • 1.01 MB
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 =\n (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 * @param {Object} obj The object to test.\n * @returns {boolean} True if the object implements vis-data DataView interface otherwise false.\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} object - Value of unknown type.\n * @param {string} type - Name of the desired type.\n *\n * @returns {Object} 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 } catch (e) {\n if (e instanceof TypeError) {\n throw new TypeError(\n \"Cannot convert object of type \" +\n getType(object) +\n \" to type \" +\n 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 \" +\n getType(object) +\n \" to type \" +\n 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 {Object} rawDS - The Data Set with raw uncoerced data.\n * @param {Object} type - A record assigning a data type to property name.\n * @param {string} type.start - Data type name of property 'start'. Default: Date.\n * @param {string} type.end - Data type name of property 'end'. Default: Date.\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 {Object} 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(\n \"You disabled XSS protection for vis-Timeline. I sure hope you know what you're doing!\",\n );\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 */\n constructor() {\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 this.props._previousWidth = this.props.width;\n this.props._previousHeight = this.props.height;\n\n return resized;\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(\n `Supplied start date is not valid: ${hiddenDates[i].start}`,\n );\n }\n if (endDate._d == \"Invalid Date\") {\n throw new Error(\n `Supplied end date is not valid: ${hiddenDates[i].end}`,\n );\n }\n\n const duration = endDate - startDate;\n if (duration >= 4 * pixelTime) {\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\n .dayOfYear(start.dayOfYear())\n .year(start.year())\n .subtract(7, \"days\");\n\n endDate = endDate\n .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\n .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\n .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\n .month(start.month())\n .year(start.year())\n .subtract(1, \"months\");\n\n endDate = endDate\n .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()).subtract(1, \"years\");\n endDate = endDate\n .year(start.year())\n .subtract(1, \"years\")\n .add(offset, \"years\");\n\n runUntil.add(1, \"years\");\n break;\n default:\n console.log(\n \"Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:\",\n hiddenDates[i].repeat,\n );\n return;\n }\n while (startDate < runUntil) {\n body.hiddenDates.push({\n start: startDate.valueOf(),\n end: endDate.valueOf(),\n });\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(\n \"Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:\",\n hiddenDates[i].repeat,\n );\n return;\n }\n }\n body.hiddenDates.push({\n start: startDate.valueOf(),\n end: endDate.valueOf(),\n });\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) {\n rangeStart =\n body.range.startToFront == true\n ? startHidden.startDate - 1\n : startHidden.endDate + 1;\n }\n if (endHidden.hidden == true) {\n rangeEnd =\n body.range.endToFront == true\n ? endHidden.startDate - 1\n : endHidden.endDate + 1;\n }\n if (startHidden.hidden == true || endHidden.hidden == true) {\n body.range._applyRange(rangeStart, rangeEnd);\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 (\n i != j &&\n hiddenDates[j].remove != true &&\n hiddenDates[i].remove != true\n ) {\n // j inside i\n if (\n hiddenDates[j].start >= hiddenDates[i].start &&\n hiddenDates[j].end <= hiddenDates[i].end\n ) {\n hiddenDates[j].remove = true;\n }\n // j start inside i\n else if (\n hiddenDates[j].start >= hiddenDates[i].start &&\n hiddenDates[j].start <= hiddenDates[i].end\n ) {\n hiddenDates[i].end = hiddenDates[j].end;\n hiddenDates[j].remove = true;\n }\n // j end inside i\n else if (\n hiddenDates[j].end >= hiddenDates[i].start &&\n hiddenDates[j].end <= hiddenDates[i].end\n ) {\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(\n i,\n new Date(dates[i].start),\n new Date(dates[i].end),\n dates[i].start,\n dates[i].end,\n dates[i].remove,\n );\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 (\n stepInHidden == true &&\n currentValue < timeStep._end.valueOf() &&\n currentValue != previousTime\n ) {\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()) {\n timeStep.switchedYear = true;\n } else if (prevValue.month() != newValue.month()) {\n timeStep.switchedMonth = true;\n } else if (prevValue.dayOfYear() != newValue.dayOfYear()) {\n timeStep.switchedDay = true;\n }\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(\n Core.body.hiddenDates,\n Core.range.start,\n Core.range.end,\n );\n if (time < Core.range.start) {\n conversion = Core.range.conversion(width, duration);\n const hiddenBeforeStart = getHiddenDurationBeforeStart(\n Core.body.hiddenDates,\n time,\n conversion.offset,\n );\n time = Core.options.moment(time).toDate().valueOf();\n time = time + hiddenBeforeStart;\n return -(conversion.offset - time.valueOf()) * conversion.scale;\n } else if (time > Core.range.end) {\n const rangeAfterEnd = { start: Core.range.start, end: time };\n time = correctTimeForHidden(\n Core.options.moment,\n Core.body.hiddenDates,\n rangeAfterEnd,\n time,\n );\n conversion = Core.range.conversion(width, duration);\n return (time.valueOf() - conversion.offset) * conversion.scale;\n } else {\n time = correctTimeForHidden(\n Core.options.moment,\n Core.body.hiddenDates,\n Core.range,\n time,\n );\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 } else {\n const hiddenDuration = getHiddenDurationBetween(\n Core.body.hiddenDates,\n Core.range.start,\n Core.range.end,\n );\n const totalDuration = Core.range.end - Core.range.start - hiddenDuration;\n const partialDuration = (totalDuration * x) / width;\n const accumulatedHiddenDuration = getAccumulatedHiddenDuration(\n Core.body.hiddenDates,\n Core.range,\n partialDuration,\n );\n\n return new Date(\n accumulatedHiddenDuration + partialDuration + Core.range.start,\n );\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(\n hiddenDates,\n range,\n requiredDuration,\n) {\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 } 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(\n hiddenDates,\n time,\n direction,\n correctionEnabled,\n) {\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 } else {\n return isHidden.startDate - 1;\n }\n } else {\n if (correctionEnabled == true) {\n return isHidden.endDate + (time - isHidden.startDate) + 1;\n } else {\n return isHidden.endDate + 1;\n }\n }\n } else {\n return time;\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) {\n // 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(\n \"click\",\n this.startRolling.bind(this),\n );\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\",\n \"direction\",\n \"min\",\n \"max\",\n \"zoomMin\",\n \"zoomMax\",\n \"moveable\",\n \"zoomable\",\n \"moment\",\n \"activate\",\n \"hiddenDates\",\n \"zoomKey\",\n \"zoomFriction\",\n \"rtl\",\n \"showCurrentTime\",\n \"rollingMode\",\n \"horizontalScroll\",\n \"horizontalScrollKey\",\n \"horizontalScrollInvert\",\n \"verticalScroll\",\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 let interval = me.end - me.start;\n const t = util.convert(new Date(), \"Date\").valueOf();\n const rollingModeOffset =\n (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 =\n start != undefined ? util.convert(start, \"Date\").valueOf() : null;\n const finalEnd =\n end != undefined ? util.convert(end, \"Date\").valueOf() : null;\n this._cancelAnimation();\n this.millisecondsPerPixelCache = undefined;\n\n if (options.animation) {\n // true or an Object\n const initStart = this.start;\n const initEnd = this.end;\n const duration =\n typeof options.animation === \"object\" && \"duration\" in options.animation\n ? options.animation.duration\n : 500;\n const easingName =\n typeof options.animation === \"object\" &&\n \"easingFunction\" in options.animation\n ? options.animation.easingFunction\n : \"easeInOutQuad\";\n const easingFunction = util.easingFunctions[easingName];\n if (!easingFunction) {\n throw new Error(\n `Unknown easing function ${JSON.stringify(easingName)}. Choose from: ${Object.keys(util.easingFunctions).join(\", \")}`,\n );\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 =\n done || finalStart === null\n ? finalStart\n : initStart + (finalStart - initStart) * ease;\n const e =\n done || finalEnd === null\n ? finalEnd\n : initEnd + (finalEnd - initEnd) * ease;\n\n changed = me._applyRange(s, e);\n DateUtil.updateHiddenDates(\n me.options.moment,\n me.body,\n me.options.hiddenDates,\n );\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) {\n frameCallback(ease, changed, done);\n }\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) {\n 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 } else {\n var changed = this._applyRange(finalStart, finalEnd);\n DateUtil.updateHiddenDates(\n this.options.moment,\n this.body,\n this.options.hiddenDates,\n );\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) {\n return callback();\n }\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 =\n (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 =\n start != null ? util.convert(start, \"Date\").valueOf() : this.start;\n let newEnd = end != null ? util.convert(end, \"Date\").valueOf() : this.end;\n const max =\n this.options.max != null\n ? util.convert(this.options.max, \"Date\").valueOf()\n : null;\n const min =\n this.options.min != null\n ? util.convert(this.options.min, \"Date\").valueOf()\n : 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 (\n this.end - this.start === zoomMin &&\n newStart >= this.start - compensation &&\n newEnd <= this.end\n ) {\n // ignore this action, we are already zoomed to the minimum\n newStart = this.start;\n newEnd = this.end;\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 (\n this.end - this.start === zoomMax &&\n newStart < this.start &&\n newEnd > this.end\n ) {\n // ignore this action, we are already zoomed to the maximum\n newStart = this.start;\n newEnd = this.end;\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 (\n !(\n (newStart >= this.start && newStart <= this.end) ||\n (newEnd >= this.start && newEnd <= this.end)\n ) &&\n !(\n (this.start >= newStart && this.start <= newEnd) ||\n (this.end >= newStart && this.end <= newEnd)\n )\n ) {\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 } 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(\n this.body.hiddenDates,\n this.start,\n this.end,\n );\n interval -= duration;\n\n const width =\n direction == \"horizontal\"\n ? this.body.domProps.center.width\n : this.body.domProps.center.heig