Browse Source

Added timelineInteractive option. Added none interaction mode. Added nonInteractivePan mode. Added player 'play/pause' demo.

Ievgen Naida 2 years ago
parent
commit
89dd959c73

+ 70 - 3
README.md

@@ -149,7 +149,7 @@ Example on how to add a keyframe to existing model:
     timeline.setModel(existingModel);
 ```
 
-### Events
+### Events/Methods and options
 
 | Event name      | description                                                                                 |
 | --------------- | ------------------------------------------------------------------------------------------- |
@@ -172,6 +172,62 @@ this.timeline.onDragStarted((args: TimelineDragEvent) => {
 });
 ```
 
+### Methods
+
+| Method name      | description                                                                                                              |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
+| setTime         | set current active time. Returns bool to indicate whether time was set. Ex: cannot be changed when dragged. Also timeline interactions can be disabled. |
+| getTime         | get current position of the timeline.                                                       |
+| dispose     | Call to unsubscribe from all the events. Important when UI component is unmounted or page is closed. |
+| setOptions  | Set timeline properties                                                                                           |
+| getOptions        | Get current options of the timeline.                                     |
+| getAllKeyframes          | Get array of all keyframes from the current active model.                                              |
+
+### Options
+
+Options can be passed when timeline is created or by calling setOptions method.
+See all options in the TimelineOptions interface.
+
+Main options:
+| Property      | description                                                                                                              |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
+| groupsDraggable         |  keyframes group is draggable. Default: true |
+| keyframesDraggable         | keyframes group is draggable. Default: true                                                      |
+| timelineInteractive     | Timeline can be dragged or position can be changed by user interaction. Default True |
+
+### Keyboard shortcuts
+
+#### Selection Mode
+
+- Click - select single keyframe.
+- Ctrl + Click - add new keyframe, toggle existing keyframe.
+
+Keyframes can be marked as selectable = false to prevent interaction.
+
+#### Zoom Mode
+
+- Ctrl - reverse zoom in/zoom out.
+- Ctrl + Mouse wheel - zoom to the current active cursor. (Same logic for the pan mode)
+
+### Interaction Modes
+
+Selection - allow to select one or group of the keyframes.
+
+- 'selection'- Keyframe selection tool selecting single or group of keyframes.
+- 'singleSelection' - Keyframe selection tool without selector rectangle.
+- 'pan' - Pan tool with the possibility to select keyframes.
+- 'nonInteractivePan', Allow only pan without any keyframes interaction. Timeline still can be moved and controlled by option  'timelineInteractive'.
+- 'zoom - zoom tool
+- 'none' -  No iteraction, except moving a timeline. Timeline still can be moved and controlled by option 'timelineInteractive'.
+
+Example:
+
+```TypeScript
+  timeline.setInteractionMode('none');
+```
+
+For the TypeScript TimelineInteractionMode enum is used.
+
 ### Timeline units and position
 
 Expected that you have a component or engine that can execute playing a timeline. Ex: SVG has events to run the animations and report current time position. This component is meant only to visualize the position.
@@ -185,11 +241,11 @@ timeline.setTime(1000);
 Current time can be fetched by a method call or by an event:
 
 ```TypeScript
-let units = timeline.getTime();
+let value = timeline.getTime();
 
 timeline.onTimeChanged((event: TimelineTimeChangedEvent) => {
   if(event.source !== TimelineEventSource.User) {
-    units = event.var;
+    value = event.val;
   }
 });
 ```
@@ -213,6 +269,17 @@ Styles are applied by a global settings and can be overridden by a row or keyfra
 
 ## Changes
 
+## 2.2.2
+
+- Added new option timelineInteractive = true/false to control possibility for user to move timeline position.
+- Added 'nonInteractivePan' interaction mode that is allowing only to pan and change position of the timeline without changing the keyframes position.
+- Added 'none' interaction mode where no interactions are allowed.
+- Added 'play' demo to the index.html
+- Private property _findDraggable is renamed to_filterDraggableElements
+- Options are appended to the current active options, not to default.
+- Fixed order of the build (definitions and tests only after the definitions.)
+- updated build packages.
+
 ## 2.2.1
 
  TypeScript fixes, updated build packages.

+ 33 - 32
cspell.json

@@ -1,32 +1,33 @@
-{
-  "version": "0.2",
-  "words": [
-    "NESW",
-    "NWSE",
-    "Unminified",
-    "Unsubsribe",
-    "browserslist",
-    "consts",
-    "corejs",
-    "deepmerge",
-    "devtool",
-    "doubleclick",
-    "downlevel",
-    "dragfinished",
-    "dragstarted",
-    "esbenp",
-    "esnext",
-    "keyframes",
-    "khtml",
-    "minmax",
-    "pixelated",
-    "prio",
-    "scrollbar",
-    "scrollbars",
-    "sourcemap",
-    "timechanged",
-    "tslib",
-    "unittests",
-    "unminimized"
-  ]
-}
+{
+  "version": "0.2",
+  "words": [
+    "browserslist",
+    "consts",
+    "corejs",
+    "deepmerge",
+    "devtool",
+    "doubleclick",
+    "downlevel",
+    "dragfinished",
+    "dragstarted",
+    "esbenp",
+    "esnext",
+    "iteraction",
+    "keyframes",
+    "khtml",
+    "minmax",
+    "NESW",
+    "NWSE",
+    "pixelated",
+    "prio",
+    "scrollbar",
+    "scrollbars",
+    "sourcemap",
+    "timechanged",
+    "tslib",
+    "unittests",
+    "Unminified",
+    "unminimized",
+    "Unsubsribe"
+  ]
+}

+ 76 - 5
index.html

@@ -176,10 +176,21 @@
     <div class="toolbar">
       <button class="button mat-icon material-icons mat-icon-no-color" title="Timeline selection mode"
         onclick="selectMode()">tab_unselected</button>
-      <button class="button mat-icon material-icons mat-icon-no-color" title="Timeline pan mode"
-        onclick="panMode()">pan_tool</button>
+      <button class="button mat-icon material-icons mat-icon-no-color"
+        title="Timeline pan mode with the keyframe selection." onclick="panMode(true)">pan_tool_alt</button>
+      <button class="button mat-icon material-icons mat-icon-no-color" title="Timeline pan mode non interactive"
+        onclick="panMode(false)">pan_tool</button>
       <button class="button mat-icon material-icons mat-icon-no-color"
         title="Timeline zoom mode. Also ctrl + scroll can be used." onclick="zoomMode()">search</button>
+      <button class="button mat-icon material-icons mat-icon-no-color" title="Only view mode."
+        onclick="noneMode()">visibility</button>
+      <div style="width: 1px; background:gray; height: 100%;"></div>
+      <button class="button mat-icon material-icons mat-icon-no-color"
+        title="Use external player to play\stop the timeline. For the demo simple setInterval is used."
+        onclick="onPlayClick()">play_arrow</button>
+      <button class="button mat-icon material-icons mat-icon-no-color"
+        title="Use external player to play\stop the timeline. For the demo simple setInterval is used."
+        onclick="onPauseClick()">pause</button>
       <div style="flex:1"></div>
       <button class="flex-left button mat-icon material-icons mat-icon-no-color" title="Remove selected keyframe"
         onclick="removeKeyframe()">close</button>
@@ -387,8 +398,17 @@
     };
 
     timeline.onTimeChanged(function (event) {
-      document.getElementById('currentTime').innerHTML = event.val + 'ms source:' + event.source;
+      showActivePositionInformation();
     });
+    function showActivePositionInformation() {
+      if (timeline) {
+        const fromPx = timeline.getScrollLeft() - timeline._leftMargin();
+        const toPx = timeline.getScrollLeft() + timeline.getClientWidth() - timeline._leftMargin();
+        const fromMs = timeline.pxToVal(fromPx);
+        const toMs = timeline.pxToVal(toPx);
+        document.getElementById('currentTime').innerHTML = 'Timeline: ' + timeline.getTime() + 'ms. Displayed from:' + fromMs + ' To:' + toMs;
+      }
+    }
     timeline.onSelected(function (obj) {
       logMessage('selected :' + obj.selected.length + '. changed :' + obj.changed.length, 2);
     });
@@ -415,11 +435,13 @@
     timeline.onScroll(function (obj) {
       var options = timeline.getOptions();
       if (options) {
+        // Synchronize component scroll renderer with HTML list of the nodes.
         if (outlineContainer) {
           outlineContainer.style.minHeight = obj.scrollHeight + 'px';
           document.getElementById('outline-scroll-container').scrollTop = obj.scrollTop;
         }
       }
+      showActivePositionInformation();
     });
     generateHTMLOutlineListNodes(rows);
 
@@ -453,6 +475,11 @@
         timeline.setInteractionMode('zoom');
       }
     }
+    function noneMode() {
+      if (timeline) {
+        timeline.setInteractionMode('none');
+      }
+    }
 
     function removeKeyframe() {
       if (timeline) {
@@ -480,9 +507,9 @@
         generateHTMLOutlineListNodes(currentModel.rows);
       }
     }
-    function panMode() {
+    function panMode(interactive) {
       if (timeline) {
-        timeline.setInteractionMode('pan');
+        timeline.setInteractionMode(interactive ? 'pan' : "NonInteractivePan");
       }
     }
     // Set scroll back to timeline when mouse scroll over the outline
@@ -491,6 +518,50 @@
         this.timeline._handleWheelEvent(event);
       }
     }
+    playing = false;
+    playStep = 50;
+    function onPlayClick(event) {
+      playing = true;
+      if (timeline) {
+        timeline.setOptions({ timelineInteractive: false });
+      }
+    }
+    function onPauseClick(event) {
+      moveTimelineIntoTheBounds();
+      playing = false;
+      if (timeline) {
+        timeline.setOptions({ timelineInteractive: true });
+      }
+    }
+
+    function moveTimelineIntoTheBounds() {
+      if (timeline) {
+        const fromPx = timeline.getScrollLeft();
+        const toPx = timeline.getScrollLeft() + timeline.getClientWidth();
+
+        let positionInPixels = timeline.valToPx(timeline.getTime());
+        // Scroll to timeline position if timeline is out of the bounds:
+        if (positionInPixels < fromPx || positionInPixels > toPx) {
+          console.log('px timeline pos:' + positionInPixels + 'from: ' + fromPx + 'px. to:' + toPx + 'px.');
+          this.timeline.setScrollLeft(positionInPixels);
+        }
+      }
+    }
+    function initPlayer() {
+      setInterval(() => {
+        if (playing) {
+          if (timeline) {
+            timeline.setTime(timeline.getTime() + playStep);
+            moveTimelineIntoTheBounds();
+          }
+        }
+      }, playStep);
+    }
+    // Note: this can be any other player: audio, video, svg and etc. 
+    // In this case you have to synchronize events of the component and player.
+    initPlayer();
+    showActivePositionInformation();
+    window.onresize = showActivePositionInformation;
   </script>
 </body>
 

+ 154 - 46
lib/animation-timeline.js

@@ -732,7 +732,9 @@ var TimelineInteractionMode;
 (function (TimelineInteractionMode) {
   TimelineInteractionMode["Selection"] = "selection";
   TimelineInteractionMode["Pan"] = "pan";
+  TimelineInteractionMode["NonInteractivePan"] = "nonInteractivePan";
   TimelineInteractionMode["Zoom"] = "zoom";
+  TimelineInteractionMode["None"] = "none";
 })(TimelineInteractionMode || (TimelineInteractionMode = {}));
 ;// CONCATENATED MODULE: ./src/enums/timelineElementType.ts
 var TimelineElementType;
@@ -935,6 +937,10 @@ var defaultTimelineOptions = {
    * keyframes group is draggable.
    */
   keyframesDraggable: true,
+  /**
+   * Timeline can be dragged or position can be changed by user interaction. Default: true
+   */
+  timelineInteractive: true,
   min: 0,
   max: Number.MAX_VALUE
 };
@@ -1057,7 +1063,6 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     var model = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
     timeline_classCallCheck(this, Timeline);
     _this = _super.call(this);
-    // Allow to create instance without an error to perform tests.
     timeline_defineProperty(timeline_assertThisInitialized(_this), "_container", null);
     timeline_defineProperty(timeline_assertThisInitialized(_this), "_canvas", null);
     timeline_defineProperty(timeline_assertThisInitialized(_this), "_scrollContainer", null);
@@ -1110,7 +1115,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     });
     timeline_defineProperty(timeline_assertThisInitialized(_this), "_handleScrollEvent", function (args) {
       _this._clearScrollFinishedTimer();
-      // Set a timeout to run event 'scrolling end'.
+      // Set a timeout to run event 'scrolling end'. Auto scroll is used to repeat scroll when drag element outside of the area.
       _this._scrollFinishedTimerRef = window.setTimeout(function () {
         if (!_this._isPanStarted) {
           if (_this._scrollFinishedTimerRef) {
@@ -1130,6 +1135,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         var mousePos = Math.max(0, _this._getMousePos(_this._canvas, event).x || 0);
         _this._zoom(TimelineUtils.sign(event.deltaY), _this._options.zoomSpeed, mousePos);
       } else {
+        _fromScreen;
         _this._scrollContainer.scrollTop += event.deltaY;
         event.preventDefault();
       }
@@ -1153,8 +1159,13 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         y: _this._scrollContainer.scrollTop
       };
       _this._clickAllowed = true;
-      var elements = _this.elementFromPoint(_this._startPos, Math.max(2, _this._startPos.radius));
-      var target = _this._findDraggable(elements, _this._startPos.val);
+      var onlyElements = null;
+      if (_this._interactionMode === TimelineInteractionMode.NonInteractivePan || _this._interactionMode === TimelineInteractionMode.None) {
+        // Allow to select only timeline. Timeline position can be disabled/enabled by properties.
+        onlyElements = [TimelineElementType.Timeline];
+      }
+      var elements = _this.elementFromPoint(_this._startPos, Math.max(2, _this._startPos.radius), onlyElements);
+      var target = _this._filterDraggableElements(elements, _this._startPos.val);
       var event = new TimelineClickEvent();
       event.pos = _this._startPos;
       event.val = _this._startPos.val;
@@ -1225,13 +1236,19 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       _this._currentPos = _this._trackMousePos(_this._canvas, args);
       if (!_this._isPanStarted && _this._selectionRect && _this._clickTimeoutIsOver()) {
         // TODO: implement selection by rect
-        _this._selectionRectEnabled = _this._interactionMode !== TimelineInteractionMode.Zoom;
+        if (_this._interactionMode === TimelineInteractionMode.None || _this._interactionMode === TimelineInteractionMode.Zoom || _this._interactionMode === TimelineInteractionMode.NonInteractivePan) {
+          _this._selectionRectEnabled = false;
+        } else {
+          _this._selectionRectEnabled = true;
+        }
       } else {
         _this._selectionRectEnabled = false;
       }
       args = args;
       var isLeftClicked = _this.isLeftButtonClicked(args);
+      // On dragging is started.
       if (_this._startPos) {
+        // On left button is on hold by the user
         if (isLeftClicked || isTouch) {
           if (_this._drag && !_this._startedDragWithCtrl) {
             var convertedVal = _this._currentPos.val;
@@ -1257,14 +1274,16 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
               }
             }
           }
-          if (_this._interactionMode === TimelineInteractionMode.Pan && !_this._drag) {
+          if ((_this._interactionMode === TimelineInteractionMode.Pan || _this._interactionMode === TimelineInteractionMode.NonInteractivePan) && !_this._drag) {
             _this._isPanStarted = true;
             _this._setCursor(TimelineCursorType.Grabbing);
             // Track scroll by drag.
             _this._scrollByPan(_this._startPos, _this._currentPos, _this._scrollStartPos);
           } else {
-            // Track scroll by mouse or touch out of the area.
-            _this._scrollBySelectionOutOfBounds(_this._currentPos);
+            if (_this._interactionMode !== TimelineInteractionMode.None) {
+              // Track scroll by mouse or touch out of the area.
+              _this._scrollBySelectionOutOfBounds(_this._currentPos);
+            }
           }
           _this.redraw();
         } else {
@@ -1273,9 +1292,15 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           _this.redraw();
         }
       } else if (!isTouch) {
-        var elements = _this.elementFromPoint(_this._currentPos, Math.max(2, _this._currentPos.radius));
-        var target = _this._findDraggable(elements, _this._currentPos.val);
-        if (_this._isPanStarted || _this._interactionMode === TimelineInteractionMode.Pan) {
+        // Set mouse over cursors
+        var onlyElements = null;
+        if (_this._interactionMode === TimelineInteractionMode.NonInteractivePan || _this._interactionMode === TimelineInteractionMode.None) {
+          // Allow to select only timeline. Timeline position can be disabled/enabled by properties.
+          onlyElements = [TimelineElementType.Timeline];
+        }
+        var elements = _this.elementFromPoint(_this._currentPos, Math.max(2, _this._currentPos.radius), onlyElements);
+        var target = _this._filterDraggableElements(elements, _this._currentPos.val);
+        if (_this._isPanStarted || _this._interactionMode === TimelineInteractionMode.Pan || _this._interactionMode === TimelineInteractionMode.NonInteractivePan) {
           if (isLeftClicked) {
             _this._setCursor(TimelineCursorType.Grabbing);
           } else {
@@ -1326,7 +1351,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
             if (_this._selectionRect.width > 20) {
               // TODO: implement zoom by screen rect.
             }
-          } else {
+          } else if (_this._interactionMode !== TimelineInteractionMode.None) {
             var keyframes = _this._getKeyframesByRectangle(_this._selectionRect);
             var selectionMode = args.shiftKey ? TimelineSelectionMode.Append : TimelineSelectionMode.Normal;
             _this.select(keyframes, selectionMode);
@@ -1356,8 +1381,12 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       _this._renderSelectionRect();
       _this._renderTimeline();
     });
+    _this._options = _this._cloneOptions(defaultTimelineOptions);
+    // Allow to create instance without an error to perform tests.
     if (options || model) {
       _this.initialize(options, model);
+    } else {
+      console.log('No HTML element is attached to the timeline. initialize method should be called with id.');
     }
     return _this;
   }
@@ -1375,7 +1404,10 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         throw new Error("Element cannot be empty. Should be string or DOM element.");
       }
       this._generateContainers(options.id);
-      this._options = this._setOptions(options);
+      this._options = this._cloneOptions(defaultTimelineOptions);
+      if (options) {
+        this._options = this._setOptions(options);
+      }
       this._subscribeOnEvents();
       this.rescale();
       this.redraw();
@@ -1406,6 +1438,8 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       this._container.style.position = 'relative';
       // Generate size container:
       this._canvas.style.cssText = 'image-rendering: -moz-crisp-edges;' + 'image-rendering: -webkit-crisp-edges;' + 'image-rendering: pixelated;' + 'image-rendering: crisp-edges;' + 'user-select: none;' + '-webkit-user-select: none;' + '-khtml-user-select: none;' + '-moz-user-select: none;' + '-o-user-select: none;' + 'user-select: none;' + 'touch-action: none;' + 'position: relative;' + '-webkit-user-drag: none;' + '-khtml-user-drag: none;' + '-moz-user-drag: none;' + '-o-user-drag: none;' + 'user-drag: none;' + 'padding: inherit';
+
+      // Those styles are hardcoded and required for the proper scrolling.
       this._scrollContainer.style.cssText = 'overflow: scroll;' + 'position: absolute;' + 'width:  100%;' + 'height:  100%;';
       this._scrollContent.style.width = this._scrollContent.style.height = '100%';
 
@@ -1730,25 +1764,30 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     value: function _performClick(pos, drag) {
       var isChanged = false;
       if (drag && drag.type === TimelineElementType.Keyframe) {
+        var _this$_options;
         var mode = TimelineSelectionMode.Normal;
-        if (this._startedDragWithCtrl && this._controlKeyPressed(pos.args) || this._startedDragWithShiftKey && pos.args.shiftKey) {
+        if (this._startedDragWithCtrl && this._controlKeyPressed(pos.args)) {
           if (this._controlKeyPressed(pos.args)) {
             mode = TimelineSelectionMode.Revert;
           }
+        } else if (this._startedDragWithShiftKey && pos.args.shiftKey) {
+          mode = TimelineSelectionMode.Append;
         }
         // Reverse selected keyframe selection by a click:
         isChanged = this._selectInternal(this._drag.target.keyframe, mode).selectionChanged || isChanged;
-        if (pos.args.shiftKey) {
+        if (pos.args.shiftKey && ((_this$_options = this._options) === null || _this$_options === void 0 ? void 0 : _this$_options.timelineInteractive) !== false) {
           // Set current timeline position if it's not a drag or selection rect small or fast click.
           isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
         }
       } else {
+        var _this$_options2;
         // deselect keyframes if any:
         isChanged = this._selectInternal(null).selectionChanged || isChanged;
-
-        // change timeline pos:
-        // Set current timeline position if it's not a drag or selection rect small or fast click.
-        isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
+        if (((_this$_options2 = this._options) === null || _this$_options2 === void 0 ? void 0 : _this$_options2.timelineInteractive) !== false) {
+          // change timeline pos:
+          // Set current timeline position if it's not a drag or selection rect small or fast click.
+          isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
+        }
       }
       return isChanged;
     }
@@ -1841,14 +1880,17 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       });
       return selected;
     }
+    /**
+     * Get all keyframe models available in the model.
+     */
   }, {
     key: "getAllKeyframes",
     value: function getAllKeyframes() {
-      var selected = [];
+      var keyframes = [];
       this._forEachKeyframe(function (keyframe) {
-        selected.push(keyframe.model);
+        keyframes.push(keyframe.model);
       });
-      return selected;
+      return keyframes;
     }
   }, {
     key: "selectAllKeyframes",
@@ -1907,7 +1949,6 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       };
       var nodesArray = nodes;
       //const state = this.selectedSubject.getValue();
-      this.getSelectedElements();
       if (nodesArray && mode === TimelineSelectionMode.Append) {
         nodes.forEach(function (node) {
           var changed = _this4._changeNodeState(state, node, true);
@@ -2017,10 +2058,30 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       }
       return pos;
     }
+
+    /**
+     * Get scroll container client width.
+     */
+  }, {
+    key: "getClientWidth",
+    value: function getClientWidth() {
+      var _this$_scrollContaine;
+      return ((_this$_scrollContaine = this._scrollContainer) === null || _this$_scrollContaine === void 0 ? void 0 : _this$_scrollContaine.clientWidth) || 0;
+    }
+    /**
+     * Get scroll container client height.
+     */
+  }, {
+    key: "getClientHeight",
+    value: function getClientHeight() {
+      var _this$_scrollContaine2;
+      return ((_this$_scrollContaine2 = this._scrollContainer) === null || _this$_scrollContaine2 === void 0 ? void 0 : _this$_scrollContaine2.clientHeight) || 0;
+    }
   }, {
     key: "_cleanUpSelection",
     value: function _cleanUpSelection() {
-      this._emitDragFinishedEvent();
+      var forcePrevent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+      this._emitDragFinishedEvent(forcePrevent);
       this._startPos = null;
       this._drag = null;
       this._startedDragWithCtrl = false;
@@ -2089,6 +2150,10 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       this._autoPanLastActionDate = Date.now();
       return false;
     }
+
+    /**
+     * Scroll virtual canvas when pan mode is enabled.
+     */
   }, {
     key: "_scrollByPan",
     value: function _scrollByPan(start, pos, scrollStartPos) {
@@ -2950,7 +3015,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
     /**
      * Set options and render the component.
-     * Options will be merged with the defaults and control invalidated
+     * Note: Options will be merged\appended with the defaults and component will be invalidated/rendered again.
      */
   }, {
     key: "setOptions",
@@ -2961,22 +3026,40 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       // Merged options:
       return this._options;
     }
+
+    /**
+     * Private. Apply html container styles from options if any is set.
+     */
+  }, {
+    key: "_applyContainersStyles",
+    value: function _applyContainersStyles() {
+      if (this._scrollContainer && this._options) {
+        var classList = this._scrollContainer.classList;
+        if (this._options.scrollContainerClass && !classList.contains(this._options.scrollContainerClass)) {
+          classList.add(this._options.scrollContainerClass);
+        }
+        if (this._options.fillColor) {
+          this._scrollContainer.style.background = this._options.fillColor;
+        }
+      }
+    }
   }, {
     key: "_setOptions",
     value: function _setOptions(toSet) {
-      this._options = this._mergeOptions(toSet);
+      if (!toSet) {
+        return this._options;
+      }
+      this._options = this._mergeOptions(this._options, toSet);
       // Normalize and validate spans per value.
       this._options.snapStep = TimelineUtils.keepInBounds(this._options.snapStep, 0, this._options.stepVal || 0);
       this._currentZoom = this._setZoom(this._options.zoom, this._options.zoomMin, this._options.zoomMax);
       this._options.min = TimelineUtils.isNumber(this._options.min) ? this._options.min : 0;
       this._options.max = TimelineUtils.isNumber(this._options.max) ? this._options.max : Number.MAX_VALUE;
-      if (this._scrollContainer) {
-        var classList = this._scrollContainer.classList;
-        if (this._options.scrollContainerClass && classList.contains(this._options.scrollContainerClass)) {
-          classList.add(this._options.scrollContainerClass);
-        }
-        if (this._options.fillColor) {
-          this._scrollContainer.style.background = this._options.fillColor;
+      this._applyContainersStyles();
+      // Prevent current active dragging of the timeline, while it's set that it's not allowed anymore.
+      if (toSet.timelineInteractive === false) {
+        if (this._drag && this._drag.type === TimelineElementType.Timeline) {
+          this._cleanUpSelection();
         }
       }
       return this._options;
@@ -3108,14 +3191,14 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     }
 
     /**
-     * get draggable element.
-     * Filter elements and get first element by a priority.
-     * @param Array
-     * @param val current mouse value
+     * Filter and sort draggable elements by the priority to get first draggable element.
+     * Filtration is done based on the timeline styles and options.
+     * @param elements to filter and sort.
+     * @param val current mouse value to find best match.
      */
   }, {
-    key: "_findDraggable",
-    value: function _findDraggable(elements) {
+    key: "_filterDraggableElements",
+    value: function _filterDraggableElements(elements) {
       var _this9 = this;
       var val = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
       // filter and sort: Timeline, individual keyframes, groups (distance).
@@ -3141,6 +3224,11 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           if (!TimelineStyleUtils.groupDraggable(element.row, _this9._options)) {
             return false;
           }
+        } else if (element.type === TimelineElementType.Timeline) {
+          var _this9$_options;
+          if (((_this9$_options = _this9._options) === null || _this9$_options === void 0 ? void 0 : _this9$_options.timelineInteractive) === false) {
+            return false;
+          }
         } else if (element.type === TimelineElementType.Row) {
           return false;
         }
@@ -3179,6 +3267,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     value: function elementFromPoint(pos) {
       var _this10 = this;
       var clickRadius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
+      var onlyTypes = arguments.length > 2 ? arguments[2] : undefined;
       clickRadius = Math.max(clickRadius, 1);
       var toReturn = [];
       if (!pos) {
@@ -3249,19 +3338,29 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           }
         });
       }
-      return toReturn;
+      if (!onlyTypes || onlyTypes.length === 0) {
+        return toReturn;
+      } else {
+        return toReturn.filter(function (p) {
+          return onlyTypes && onlyTypes.includes(p.type);
+        });
+      }
+    }
+  }, {
+    key: "_cloneOptions",
+    value: function _cloneOptions(previousOptions) {
+      return JSON.parse(JSON.stringify(previousOptions));
     }
-
     /**
-     * Merge options with the defaults.
+     * Merge options. New keys will be added.
      */
   }, {
     key: "_mergeOptions",
-    value: function _mergeOptions(fromArg) {
-      fromArg = fromArg || {};
+    value: function _mergeOptions(previousOptions, newOptions) {
+      newOptions = newOptions || {};
       // Apply incoming options to default. (override default)
       // Deep clone default options:
-      var toArg = JSON.parse(JSON.stringify(defaultTimelineOptions));
+      var toArg = this._cloneOptions(previousOptions);
       // Merge options with the default.
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       var mergeOptionsDeep = function mergeOptionsDeep(to, from) {
@@ -3285,7 +3384,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
           }
         }
       };
-      mergeOptionsDeep(toArg, fromArg);
+      mergeOptionsDeep(toArg, newOptions);
       return toArg;
     }
     /**
@@ -3392,11 +3491,20 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       }
       return args;
     }
+    /**
+     * Private emit timeline event that dragging element is finished.
+     * @param forcePrevent - needed when during dragging components set to the state when they cannot be dragged anymore. (used only as recovery state).
+     * @returns
+     */
   }, {
     key: "_emitDragFinishedEvent",
     value: function _emitDragFinishedEvent() {
+      var forcePrevent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
       if (this._drag && this._drag.changed) {
         var args = this._getDragEventArgs();
+        if (forcePrevent) {
+          args.preventDefault();
+        }
         this.emit(TimelineEvents.DragFinished, args);
         if (args.isPrevented()) {
           this._preventDrag(args, this._drag, true);

File diff suppressed because it is too large
+ 0 - 0
lib/animation-timeline.js.map


File diff suppressed because it is too large
+ 0 - 0
lib/animation-timeline.min.js


+ 13 - 3
lib/enums/timelineInteractionMode.d.ts

@@ -1,14 +1,24 @@
 export declare enum TimelineInteractionMode {
     /**
-     * Selection tool
+     * Keyframe selection tool selecting single or group of keyframes.
      */
     Selection = "selection",
     /**
-     * Pan tool
+     * Pan tool with the possibility to select keyframes.
      */
     Pan = "pan",
+    /**
+     * Allow only pan without any keyframes interaction.
+     * Timeline still can be moved and controlled by option 'timelineInteractive'.
+     */
+    NonInteractivePan = "nonInteractivePan",
     /**
      * Zoom tool.
      */
-    Zoom = "zoom"
+    Zoom = "zoom",
+    /**
+     * No iteraction, except moving a timeline.
+     * Timeline still can be moved and controlled by option 'timelineInteractive'.
+     */
+    None = "none"
 }

+ 6 - 2
lib/settings/timelineOptions.d.ts

@@ -90,11 +90,15 @@ export interface TimelineOptions extends TimelineRanged {
      */
     scrollContainerClass?: string;
     /**
-     * keyframes group is draggable.
+     * keyframes group is draggable. Default: true
      */
     groupsDraggable?: boolean;
     /**
-     * keyframes group is draggable.
+     * keyframes group is draggable. Default: true
      */
     keyframesDraggable?: boolean;
+    /**
+     * Timeline can be dragged or position can be changed by user interaction. Default: true
+     */
+    timelineInteractive?: boolean;
 }

+ 37 - 12
lib/timeline.d.ts

@@ -20,6 +20,7 @@ import { TimelineScrollEvent } from './utils/events/timelineScrollEvent';
 import { TimelineClickEvent } from './utils/events/timelineClickEvent';
 import { TimelineDragEvent } from './utils/events/timelineDragEvent';
 import { TimelineInteractionMode } from './enums/timelineInteractionMode';
+import { TimelineElementType } from './enums/timelineElementType';
 import { TimelineEventSource } from './enums/timelineEventSource';
 import { TimelineSelectionMode } from './enums/timelineSelectionMode';
 export declare class Timeline extends TimelineEventsEmitter {
@@ -196,7 +197,10 @@ export declare class Timeline extends TimelineEventsEmitter {
     _convertToElement(row: TimelineRow, keyframe: TimelineKeyframe): TimelineElement;
     getSelectedKeyframes(): Array<TimelineKeyframe>;
     getSelectedElements(): Array<TimelineElement>;
-    getAllKeyframes(): Array<TimelineKeyframe>;
+    /**
+     * Get all keyframe models available in the model.
+     */
+    getAllKeyframes(): TimelineKeyframe[];
     selectAllKeyframes(): TimelineSelectionResults;
     deselectAll(): TimelineSelectionResults;
     private _changeNodeState;
@@ -212,7 +216,15 @@ export declare class Timeline extends TimelineEventsEmitter {
      */
     _forEachKeyframe(callback: (keyframe: TimelineCalculatedKeyframe, index?: number, newRow?: boolean) => void): void;
     _trackMousePos(canvas: HTMLCanvasElement, mouseArgs: MouseEvent | TouchEvent): TimelineMouseData;
-    _cleanUpSelection(): void;
+    /**
+     * Get scroll container client width.
+     */
+    getClientWidth(): number;
+    /**
+     * Get scroll container client height.
+     */
+    getClientHeight(): number;
+    _cleanUpSelection(forcePrevent?: boolean): void;
     /**
      * Check whether click timeout is over.
      */
@@ -229,6 +241,9 @@ export declare class Timeline extends TimelineEventsEmitter {
      * Check whether auto pan should be slowed down a bit.
      */
     _checkUpdateSpeedTooFast(): boolean;
+    /**
+     * Scroll virtual canvas when pan mode is enabled.
+     */
     _scrollByPan(start: TimelineMouseData, pos: TimelineMouseData, scrollStartPos: DOMPoint): void;
     _scrollBySelectionOutOfBounds(pos: DOMPoint): boolean;
     /**
@@ -322,9 +337,13 @@ export declare class Timeline extends TimelineEventsEmitter {
     getScrollTop(): number;
     /**
      * Set options and render the component.
-     * Options will be merged with the defaults and control invalidated
+     * Note: Options will be merged\appended with the defaults and component will be invalidated/rendered again.
      */
     setOptions(toSet: TimelineOptions): TimelineOptions;
+    /**
+     * Private. Apply html container styles from options if any is set.
+     */
+    _applyContainersStyles(): void;
     _setOptions(toSet: TimelineOptions): TimelineOptions;
     getModel(): TimelineModel;
     /**
@@ -343,20 +362,21 @@ export declare class Timeline extends TimelineEventsEmitter {
     rescale(): void;
     _rescaleInternal(newWidth?: number | null, newHeight?: number | null, scrollMode?: string): void;
     /**
-     * get draggable element.
-     * Filter elements and get first element by a priority.
-     * @param Array
-     * @param val current mouse value
+     * Filter and sort draggable elements by the priority to get first draggable element.
+     * Filtration is done based on the timeline styles and options.
+     * @param elements to filter and sort.
+     * @param val current mouse value to find best match.
      */
-    _findDraggable(elements: Array<TimelineElement>, val?: number | null): TimelineElement;
+    _filterDraggableElements(elements: TimelineElement[], val?: number | null): TimelineElement;
     /**
      * get all clickable elements by a screen point.
      */
-    elementFromPoint(pos: DOMPoint, clickRadius?: number): Array<TimelineElement>;
+    elementFromPoint(pos: DOMPoint, clickRadius?: number, onlyTypes?: TimelineElementType[] | null): TimelineElement[];
+    _cloneOptions(previousOptions: TimelineOptions): TimelineOptions;
     /**
-     * Merge options with the defaults.
+     * Merge options. New keys will be added.
      */
-    _mergeOptions(fromArg: TimelineOptions): TimelineOptions;
+    _mergeOptions(previousOptions: TimelineOptions, newOptions: TimelineOptions): TimelineOptions;
     /**
      * Subscribe on time changed.
      */
@@ -393,7 +413,12 @@ export declare class Timeline extends TimelineEventsEmitter {
     _emitScrollEvent(args: MouseEvent | null): TimelineScrollEvent;
     _emitKeyframeChanged(element: TimelineElementDragState, source?: TimelineEventSource): TimelineKeyframeChangedEvent;
     _emitDragStartedEvent(): TimelineDragEvent;
-    _emitDragFinishedEvent(): TimelineDragEvent;
+    /**
+     * Private emit timeline event that dragging element is finished.
+     * @param forcePrevent - needed when during dragging components set to the state when they cannot be dragged anymore. (used only as recovery state).
+     * @returns
+     */
+    _emitDragFinishedEvent(forcePrevent?: boolean): TimelineDragEvent;
     _preventDrag(dragArgs: TimelineDragEvent, data: TimelineDraggableData, toStart?: boolean): void;
     _emitDragEvent(): TimelineDragEvent;
     _emitKeyframesSelected(state: TimelineSelectionResults): TimelineSelectedEvent;

+ 309 - 118
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "animation-timeline-js",
-  "version": "2.2.1",
+  "version": "2.2.2",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -47,21 +47,21 @@
       "dev": true
     },
     "@babel/core": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
-      "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz",
+      "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==",
       "dev": true,
       "requires": {
         "@ampproject/remapping": "^2.1.0",
         "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.19.3",
+        "@babel/generator": "^7.19.6",
         "@babel/helper-compilation-targets": "^7.19.3",
-        "@babel/helper-module-transforms": "^7.19.0",
-        "@babel/helpers": "^7.19.0",
-        "@babel/parser": "^7.19.3",
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helpers": "^7.19.4",
+        "@babel/parser": "^7.19.6",
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.3",
-        "@babel/types": "^7.19.3",
+        "@babel/traverse": "^7.19.6",
+        "@babel/types": "^7.19.4",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -69,6 +69,94 @@
         "semver": "^6.3.0"
       },
       "dependencies": {
+        "@babel/generator": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
+          "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.20.0",
+            "@jridgewell/gen-mapping": "^0.3.2",
+            "jsesc": "^2.5.1"
+          },
+          "dependencies": {
+            "@babel/types": {
+              "version": "7.20.0",
+              "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
+              "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
+              "dev": true,
+              "requires": {
+                "@babel/helper-string-parser": "^7.19.4",
+                "@babel/helper-validator-identifier": "^7.19.1",
+                "to-fast-properties": "^2.0.0"
+              }
+            }
+          }
+        },
+        "@babel/helper-module-transforms": {
+          "version": "7.19.6",
+          "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz",
+          "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==",
+          "dev": true,
+          "requires": {
+            "@babel/helper-environment-visitor": "^7.18.9",
+            "@babel/helper-module-imports": "^7.18.6",
+            "@babel/helper-simple-access": "^7.19.4",
+            "@babel/helper-split-export-declaration": "^7.18.6",
+            "@babel/helper-validator-identifier": "^7.19.1",
+            "@babel/template": "^7.18.10",
+            "@babel/traverse": "^7.19.6",
+            "@babel/types": "^7.19.4"
+          }
+        },
+        "@babel/parser": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
+          "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
+          "dev": true
+        },
+        "@babel/traverse": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
+          "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.18.6",
+            "@babel/generator": "^7.20.1",
+            "@babel/helper-environment-visitor": "^7.18.9",
+            "@babel/helper-function-name": "^7.19.0",
+            "@babel/helper-hoist-variables": "^7.18.6",
+            "@babel/helper-split-export-declaration": "^7.18.6",
+            "@babel/parser": "^7.20.1",
+            "@babel/types": "^7.20.0",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0"
+          },
+          "dependencies": {
+            "@babel/types": {
+              "version": "7.20.0",
+              "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
+              "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
+              "dev": true,
+              "requires": {
+                "@babel/helper-string-parser": "^7.19.4",
+                "@babel/helper-validator-identifier": "^7.19.1",
+                "to-fast-properties": "^2.0.0"
+              }
+            }
+          }
+        },
+        "@jridgewell/gen-mapping": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+          "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+          "dev": true,
+          "requires": {
+            "@jridgewell/set-array": "^1.0.1",
+            "@jridgewell/sourcemap-codec": "^1.4.10",
+            "@jridgewell/trace-mapping": "^0.3.9"
+          }
+        },
         "semver": {
           "version": "6.3.0",
           "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -353,14 +441,73 @@
       }
     },
     "@babel/helpers": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz",
-      "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
+      "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.4",
-        "@babel/types": "^7.19.4"
+        "@babel/traverse": "^7.20.1",
+        "@babel/types": "^7.20.0"
+      },
+      "dependencies": {
+        "@babel/generator": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
+          "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
+          "dev": true,
+          "requires": {
+            "@babel/types": "^7.20.0",
+            "@jridgewell/gen-mapping": "^0.3.2",
+            "jsesc": "^2.5.1"
+          }
+        },
+        "@babel/parser": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
+          "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
+          "dev": true
+        },
+        "@babel/traverse": {
+          "version": "7.20.1",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
+          "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.18.6",
+            "@babel/generator": "^7.20.1",
+            "@babel/helper-environment-visitor": "^7.18.9",
+            "@babel/helper-function-name": "^7.19.0",
+            "@babel/helper-hoist-variables": "^7.18.6",
+            "@babel/helper-split-export-declaration": "^7.18.6",
+            "@babel/parser": "^7.20.1",
+            "@babel/types": "^7.20.0",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0"
+          }
+        },
+        "@babel/types": {
+          "version": "7.20.0",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
+          "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
+          "dev": true,
+          "requires": {
+            "@babel/helper-string-parser": "^7.19.4",
+            "@babel/helper-validator-identifier": "^7.19.1",
+            "to-fast-properties": "^2.0.0"
+          }
+        },
+        "@jridgewell/gen-mapping": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+          "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+          "dev": true,
+          "requires": {
+            "@jridgewell/set-array": "^1.0.1",
+            "@jridgewell/sourcemap-codec": "^1.4.10",
+            "@jridgewell/trace-mapping": "^0.3.9"
+          }
+        }
       }
     },
     "@babel/highlight": {
@@ -1230,14 +1377,14 @@
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.10.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
-      "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
+      "version": "0.11.7",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
+      "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
       "dev": true,
       "requires": {
         "@humanwhocodes/object-schema": "^1.2.1",
         "debug": "^4.1.1",
-        "minimatch": "^3.0.4"
+        "minimatch": "^3.0.5"
       }
     },
     "@humanwhocodes/module-importer": {
@@ -1396,17 +1543,24 @@
       "integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w==",
       "dev": true
     },
+    "@types/semver": {
+      "version": "7.3.13",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
+      "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
+      "dev": true
+    },
     "@typescript-eslint/eslint-plugin": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz",
-      "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz",
+      "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "5.40.0",
-        "@typescript-eslint/type-utils": "5.40.0",
-        "@typescript-eslint/utils": "5.40.0",
+        "@typescript-eslint/scope-manager": "5.42.0",
+        "@typescript-eslint/type-utils": "5.42.0",
+        "@typescript-eslint/utils": "5.42.0",
         "debug": "^4.3.4",
         "ignore": "^5.2.0",
+        "natural-compare-lite": "^1.4.0",
         "regexpp": "^3.2.0",
         "semver": "^7.3.7",
         "tsutils": "^3.21.0"
@@ -1424,53 +1578,53 @@
       }
     },
     "@typescript-eslint/parser": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz",
-      "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz",
+      "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "5.40.0",
-        "@typescript-eslint/types": "5.40.0",
-        "@typescript-eslint/typescript-estree": "5.40.0",
+        "@typescript-eslint/scope-manager": "5.42.0",
+        "@typescript-eslint/types": "5.42.0",
+        "@typescript-eslint/typescript-estree": "5.42.0",
         "debug": "^4.3.4"
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz",
-      "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz",
+      "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.40.0",
-        "@typescript-eslint/visitor-keys": "5.40.0"
+        "@typescript-eslint/types": "5.42.0",
+        "@typescript-eslint/visitor-keys": "5.42.0"
       }
     },
     "@typescript-eslint/type-utils": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz",
-      "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz",
+      "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/typescript-estree": "5.40.0",
-        "@typescript-eslint/utils": "5.40.0",
+        "@typescript-eslint/typescript-estree": "5.42.0",
+        "@typescript-eslint/utils": "5.42.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz",
-      "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz",
+      "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz",
-      "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz",
+      "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.40.0",
-        "@typescript-eslint/visitor-keys": "5.40.0",
+        "@typescript-eslint/types": "5.42.0",
+        "@typescript-eslint/visitor-keys": "5.42.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -1490,15 +1644,16 @@
       }
     },
     "@typescript-eslint/utils": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz",
-      "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz",
+      "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.9",
-        "@typescript-eslint/scope-manager": "5.40.0",
-        "@typescript-eslint/types": "5.40.0",
-        "@typescript-eslint/typescript-estree": "5.40.0",
+        "@types/semver": "^7.3.12",
+        "@typescript-eslint/scope-manager": "5.42.0",
+        "@typescript-eslint/types": "5.42.0",
+        "@typescript-eslint/typescript-estree": "5.42.0",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0",
         "semver": "^7.3.7"
@@ -1516,12 +1671,12 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "5.40.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz",
-      "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==",
+      "version": "5.42.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz",
+      "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.40.0",
+        "@typescript-eslint/types": "5.42.0",
         "eslint-visitor-keys": "^3.3.0"
       }
     },
@@ -1734,6 +1889,35 @@
         "uri-js": "^4.2.2"
       }
     },
+    "ajv-formats": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+      "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^8.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        }
+      }
+    },
     "ajv-keywords": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -1790,32 +1974,13 @@
       "dev": true
     },
     "babel-loader": {
-      "version": "8.2.5",
-      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz",
-      "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==",
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.0.1.tgz",
+      "integrity": "sha512-szYjslOXFlj/po5KfrVmiuBAcI6GVHFuAgC96Qd6mMPHdwl4lmAJkYtvjQ1RxxPjgdkKjd3LQgXDE4jxEutNuw==",
       "dev": true,
       "requires": {
-        "find-cache-dir": "^3.3.1",
-        "loader-utils": "^2.0.0",
-        "make-dir": "^3.1.0",
-        "schema-utils": "^2.6.5"
-      },
-      "dependencies": {
-        "make-dir": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-          "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
-          "dev": true,
-          "requires": {
-            "semver": "^6.0.0"
-          }
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        }
+        "find-cache-dir": "^3.3.2",
+        "schema-utils": "^4.0.0"
       }
     },
     "babel-plugin-dynamic-import-node": {
@@ -1871,12 +2036,6 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "dev": true
     },
-    "big.js": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
-      "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
-      "dev": true
-    },
     "binary-extensions": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -2171,12 +2330,6 @@
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
       "dev": true
     },
-    "emojis-list": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
-      "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
-      "dev": true
-    },
     "enhanced-resolve": {
       "version": "5.10.0",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz",
@@ -2212,14 +2365,15 @@
       "dev": true
     },
     "eslint": {
-      "version": "8.25.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
-      "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
+      "version": "8.26.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz",
+      "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==",
       "dev": true,
       "requires": {
         "@eslint/eslintrc": "^1.3.3",
-        "@humanwhocodes/config-array": "^0.10.5",
+        "@humanwhocodes/config-array": "^0.11.6",
         "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
@@ -2235,14 +2389,14 @@
         "fast-deep-equal": "^3.1.3",
         "file-entry-cache": "^6.0.1",
         "find-up": "^5.0.0",
-        "glob-parent": "^6.0.1",
+        "glob-parent": "^6.0.2",
         "globals": "^13.15.0",
-        "globby": "^11.1.0",
         "grapheme-splitter": "^1.0.4",
         "ignore": "^5.2.0",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
         "js-sdsl": "^4.1.4",
         "js-yaml": "^4.1.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
@@ -2884,6 +3038,12 @@
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true
     },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true
+    },
     "is-plain-obj": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
@@ -3018,17 +3178,6 @@
       "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
       "dev": true
     },
-    "loader-utils": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
-      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
-      "dev": true,
-      "requires": {
-        "big.js": "^5.2.2",
-        "emojis-list": "^3.0.0",
-        "json5": "^2.1.2"
-      }
-    },
     "locate-path": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -3343,6 +3492,12 @@
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
     },
+    "natural-compare-lite": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+      "dev": true
+    },
     "neo-async": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -3637,6 +3792,12 @@
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "dev": true
     },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true
+    },
     "resolve": {
       "version": "1.22.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -3702,14 +3863,44 @@
       "dev": true
     },
     "schema-utils": {
-      "version": "2.7.1",
-      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
-      "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
       "dev": true,
       "requires": {
-        "@types/json-schema": "^7.0.5",
-        "ajv": "^6.12.4",
-        "ajv-keywords": "^3.5.2"
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ajv-keywords": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+          "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.3"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        }
       }
     },
     "semver": {

+ 8 - 8
package.json

@@ -1,22 +1,22 @@
 {
   "name": "animation-timeline-js",
-  "version": "2.2.1",
+  "version": "2.2.2",
   "description": "animation timeline control based on the canvas.",
   "main": "lib/animation-timeline.min.js",
   "types": "lib/animation-timeline.d.ts",
   "devDependencies": {
     "@babel/cli": "7.19.3",
-    "@babel/core": "7.19.3",
+    "@babel/core": "7.19.6",
     "@babel/plugin-proposal-class-properties": "7.18.6",
     "@babel/preset-env": "7.19.4",
     "@babel/preset-typescript": "7.18.6",
     "@types/chai": "4.3.3",
     "@types/mocha": "10.0.0",
-    "@typescript-eslint/eslint-plugin": "5.40.0",
-    "@typescript-eslint/parser": "5.40.0",
-    "babel-loader": "8.2.5",
+    "@typescript-eslint/eslint-plugin": "5.42.0",
+    "@typescript-eslint/parser": "5.42.0",
+    "babel-loader": "9.0.1",
     "chai": "4.3.6",
-    "eslint": "8.25.0",
+    "eslint": "8.26.0",
     "eslint-config-prettier": "8.5.0",
     "eslint-plugin-prettier": "4.2.1",
     "mocha": "10.1.0",
@@ -30,7 +30,7 @@
     "start": "echo \"Run index.html in your browser. Build after files are changed.\" && exit 1",
     "test": "echo \"Run tests/unittest.html explicitly to execute tests. Build after files are changed.\" && exit 1",
     "build-ts-def": "tsc -emitDeclarationOnly",
-    "build": "webpack && npm run build-tests && npm run build-ts-def",
+    "build": "webpack && npm run build-ts-def && npm run build-tests",
     "lint": "eslint --ext .ts,.html src --ignore-path .gitignore && prettier \"src/*.ts\" --check --ignore-path .gitignore",
     "webpack": "npm run build",
     "build-tests": "tsc -p tsconfig.tests.json",
@@ -58,4 +58,4 @@
     "url": "https://github.com/ievgennaida/js-animation-timeline-control/issues"
   },
   "homepage": "https://ievgennaida.github.io/animation-timeline-control/index.html"
-}
+}

+ 13 - 2
src/enums/timelineInteractionMode.ts

@@ -1,14 +1,25 @@
 export enum TimelineInteractionMode {
   /**
-   * Selection tool
+   * Keyframe selection tool selecting single or group of keyframes.
    */
   Selection = 'selection',
   /**
-   * Pan tool
+   * Pan tool with the possibility to select keyframes.
    */
   Pan = 'pan',
+  /**
+   * Allow only pan without any keyframes interaction.
+   * Timeline still can be moved and controlled by option 'timelineInteractive'.
+   */
+  NonInteractivePan = 'nonInteractivePan',
   /**
    * Zoom tool.
    */
   Zoom = 'zoom',
+
+  /**
+   * No iteraction, except moving a timeline.
+   * Timeline still can be moved and controlled by option 'timelineInteractive'.
+   */
+  None = 'none',
 }

+ 4 - 0
src/settings/defaults.ts

@@ -134,6 +134,10 @@ export const defaultTimelineOptions = {
    * keyframes group is draggable.
    */
   keyframesDraggable: true,
+  /**
+   * Timeline can be dragged or position can be changed by user interaction. Default: true
+   */
+  timelineInteractive: true,
   min: 0,
   max: Number.MAX_VALUE,
 } as TimelineOptions;

+ 6 - 2
src/settings/timelineOptions.ts

@@ -92,11 +92,15 @@ export interface TimelineOptions extends TimelineRanged {
    */
   scrollContainerClass?: string;
   /**
-   * keyframes group is draggable.
+   * keyframes group is draggable. Default: true
    */
   groupsDraggable?: boolean;
   /**
-   * keyframes group is draggable.
+   * keyframes group is draggable. Default: true
    */
   keyframesDraggable?: boolean;
+  /**
+   * Timeline can be dragged or position can be changed by user interaction. Default: true
+   */
+  timelineInteractive?: boolean;
 }

+ 132 - 49
src/timeline.ts

@@ -114,9 +114,12 @@ export class Timeline extends TimelineEventsEmitter {
    */
   constructor(options: TimelineOptions | null = null, model: TimelineModel | null = null) {
     super();
+    this._options = this._cloneOptions(defaultTimelineOptions);
     // Allow to create instance without an error to perform tests.
     if (options || model) {
       this.initialize(options, model);
+    } else {
+      console.log('No HTML element is attached to the timeline. initialize method should be called with id.');
     }
   }
 
@@ -132,7 +135,10 @@ export class Timeline extends TimelineEventsEmitter {
     }
 
     this._generateContainers(options.id);
-    this._options = this._setOptions(options);
+    this._options = this._cloneOptions(defaultTimelineOptions);
+    if (options) {
+      this._options = this._setOptions(options);
+    }
     this._subscribeOnEvents();
     this.rescale();
     this.redraw();
@@ -184,8 +190,8 @@ export class Timeline extends TimelineEventsEmitter {
       'user-drag: none;' +
       'padding: inherit';
 
+    // Those styles are hardcoded and required for the proper scrolling.
     this._scrollContainer.style.cssText = 'overflow: scroll;' + 'position: absolute;' + 'width:  100%;' + 'height:  100%;';
-
     this._scrollContent.style.width = this._scrollContent.style.height = '100%';
 
     // add the text node to the created div
@@ -284,7 +290,7 @@ export class Timeline extends TimelineEventsEmitter {
   }
   _handleScrollEvent = (args: MouseEvent): void => {
     this._clearScrollFinishedTimer();
-    // Set a timeout to run event 'scrolling end'.
+    // Set a timeout to run event 'scrolling end'. Auto scroll is used to repeat scroll when drag element outside of the area.
     this._scrollFinishedTimerRef = window.setTimeout(() => {
       if (!this._isPanStarted) {
         if (this._scrollFinishedTimerRef) {
@@ -427,8 +433,14 @@ export class Timeline extends TimelineEventsEmitter {
       y: this._scrollContainer.scrollTop,
     } as DOMPoint;
     this._clickAllowed = true;
-    const elements = this.elementFromPoint(this._startPos, Math.max(2, this._startPos.radius));
-    const target = this._findDraggable(elements, this._startPos.val);
+    let onlyElements: TimelineElementType[] | null = null;
+    if (this._interactionMode === TimelineInteractionMode.NonInteractivePan || this._interactionMode === TimelineInteractionMode.None) {
+      // Allow to select only timeline. Timeline position can be disabled/enabled by properties.
+      onlyElements = [TimelineElementType.Timeline];
+    }
+    const elements = this.elementFromPoint(this._startPos, Math.max(2, this._startPos.radius), onlyElements);
+
+    const target = this._filterDraggableElements(elements, this._startPos.val);
     const event = new TimelineClickEvent();
     event.pos = this._startPos;
     event.val = this._startPos.val;
@@ -522,14 +534,20 @@ export class Timeline extends TimelineEventsEmitter {
     this._currentPos = this._trackMousePos(this._canvas, args);
     if (!this._isPanStarted && this._selectionRect && this._clickTimeoutIsOver()) {
       // TODO: implement selection by rect
-      this._selectionRectEnabled = this._interactionMode !== TimelineInteractionMode.Zoom;
+      if (this._interactionMode === TimelineInteractionMode.None || this._interactionMode === TimelineInteractionMode.Zoom || this._interactionMode === TimelineInteractionMode.NonInteractivePan) {
+        this._selectionRectEnabled = false;
+      } else {
+        this._selectionRectEnabled = true;
+      }
     } else {
       this._selectionRectEnabled = false;
     }
 
     args = args as MouseEvent;
     const isLeftClicked = this.isLeftButtonClicked(args);
+    // On dragging is started.
     if (this._startPos) {
+      // On left button is on hold by the user
       if (isLeftClicked || isTouch) {
         if (this._drag && !this._startedDragWithCtrl) {
           const convertedVal = this._currentPos.val;
@@ -557,14 +575,16 @@ export class Timeline extends TimelineEventsEmitter {
           }
         }
 
-        if (this._interactionMode === TimelineInteractionMode.Pan && !this._drag) {
+        if ((this._interactionMode === TimelineInteractionMode.Pan || this._interactionMode === TimelineInteractionMode.NonInteractivePan) && !this._drag) {
           this._isPanStarted = true;
           this._setCursor(TimelineCursorType.Grabbing);
           // Track scroll by drag.
           this._scrollByPan(this._startPos, this._currentPos, this._scrollStartPos);
         } else {
-          // Track scroll by mouse or touch out of the area.
-          this._scrollBySelectionOutOfBounds(this._currentPos);
+          if (this._interactionMode !== TimelineInteractionMode.None) {
+            // Track scroll by mouse or touch out of the area.
+            this._scrollBySelectionOutOfBounds(this._currentPos);
+          }
         }
 
         this.redraw();
@@ -574,9 +594,16 @@ export class Timeline extends TimelineEventsEmitter {
         this.redraw();
       }
     } else if (!isTouch) {
-      const elements = this.elementFromPoint(this._currentPos, Math.max(2, this._currentPos.radius));
-      const target = this._findDraggable(elements, this._currentPos.val);
-      if (this._isPanStarted || this._interactionMode === TimelineInteractionMode.Pan) {
+      // Set mouse over cursors
+      let onlyElements: TimelineElementType[] | null = null;
+      if (this._interactionMode === TimelineInteractionMode.NonInteractivePan || this._interactionMode === TimelineInteractionMode.None) {
+        // Allow to select only timeline. Timeline position can be disabled/enabled by properties.
+        onlyElements = [TimelineElementType.Timeline];
+      }
+
+      const elements = this.elementFromPoint(this._currentPos, Math.max(2, this._currentPos.radius), onlyElements);
+      const target = this._filterDraggableElements(elements, this._currentPos.val);
+      if (this._isPanStarted || this._interactionMode === TimelineInteractionMode.Pan || this._interactionMode === TimelineInteractionMode.NonInteractivePan) {
         if (isLeftClicked) {
           this._setCursor(TimelineCursorType.Grabbing);
         } else {
@@ -679,7 +706,7 @@ export class Timeline extends TimelineEventsEmitter {
           if (this._selectionRect.width > 20) {
             // TODO: implement zoom by screen rect.
           }
-        } else {
+        } else if (this._interactionMode !== TimelineInteractionMode.None) {
           const keyframes = this._getKeyframesByRectangle(this._selectionRect);
           const selectionMode = args.shiftKey ? TimelineSelectionMode.Append : TimelineSelectionMode.Normal;
           this.select(keyframes, selectionMode);
@@ -702,7 +729,7 @@ export class Timeline extends TimelineEventsEmitter {
   }
 
   /**
-   * Client width;
+   * Client canvas width;
    */
   _width(): number {
     if (this._canvas) {
@@ -742,15 +769,17 @@ export class Timeline extends TimelineEventsEmitter {
     let isChanged = false;
     if (drag && drag.type === TimelineElementType.Keyframe) {
       let mode = TimelineSelectionMode.Normal;
-      if ((this._startedDragWithCtrl && this._controlKeyPressed(pos.args)) || (this._startedDragWithShiftKey && pos.args.shiftKey)) {
+      if (this._startedDragWithCtrl && this._controlKeyPressed(pos.args)) {
         if (this._controlKeyPressed(pos.args)) {
           mode = TimelineSelectionMode.Revert;
         }
+      } else if (this._startedDragWithShiftKey && pos.args.shiftKey) {
+        mode = TimelineSelectionMode.Append;
       }
       // Reverse selected keyframe selection by a click:
       isChanged = this._selectInternal(this._drag.target.keyframe, mode).selectionChanged || isChanged;
 
-      if (pos.args.shiftKey) {
+      if (pos.args.shiftKey && this._options?.timelineInteractive !== false) {
         // Set current timeline position if it's not a drag or selection rect small or fast click.
         isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
       }
@@ -758,9 +787,11 @@ export class Timeline extends TimelineEventsEmitter {
       // deselect keyframes if any:
       isChanged = this._selectInternal(null).selectionChanged || isChanged;
 
-      // change timeline pos:
-      // Set current timeline position if it's not a drag or selection rect small or fast click.
-      isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
+      if (this._options?.timelineInteractive !== false) {
+        // change timeline pos:
+        // Set current timeline position if it's not a drag or selection rect small or fast click.
+        isChanged = this._setTimeInternal(pos.val, TimelineEventSource.User) || isChanged;
+      }
     }
 
     return isChanged;
@@ -843,13 +874,16 @@ export class Timeline extends TimelineEventsEmitter {
 
     return selected;
   }
-  public getAllKeyframes(): Array<TimelineKeyframe> {
-    const selected: Array<TimelineKeyframe> = [];
+  /**
+   * Get all keyframe models available in the model.
+   */
+  public getAllKeyframes(): TimelineKeyframe[] {
+    const keyframes: TimelineKeyframe[] = [];
     this._forEachKeyframe((keyframe): void => {
-      selected.push(keyframe.model);
+      keyframes.push(keyframe.model);
     });
 
-    return selected;
+    return keyframes;
   }
 
   public selectAllKeyframes(): TimelineSelectionResults {
@@ -900,7 +934,6 @@ export class Timeline extends TimelineEventsEmitter {
     } as TimelineSelectionResults;
     const nodesArray = nodes as TimelineKeyframe[];
     //const state = this.selectedSubject.getValue();
-    this.getSelectedElements();
     if (nodesArray && mode === TimelineSelectionMode.Append) {
       nodes.forEach((node) => {
         const changed = this._changeNodeState(state, node, true);
@@ -1018,8 +1051,20 @@ export class Timeline extends TimelineEventsEmitter {
     return pos;
   }
 
-  _cleanUpSelection(): void {
-    this._emitDragFinishedEvent();
+  /**
+   * Get scroll container client width.
+   */
+  getClientWidth(): number {
+    return this._scrollContainer?.clientWidth || 0;
+  }
+  /**
+   * Get scroll container client height.
+   */
+  getClientHeight(): number {
+    return this._scrollContainer?.clientHeight || 0;
+  }
+  _cleanUpSelection(forcePrevent = false): void {
+    this._emitDragFinishedEvent(forcePrevent);
     this._startPos = null;
     this._drag = null;
     this._startedDragWithCtrl = false;
@@ -1083,6 +1128,9 @@ export class Timeline extends TimelineEventsEmitter {
     return false;
   }
 
+  /**
+   * Scroll virtual canvas when pan mode is enabled.
+   */
   _scrollByPan(start: TimelineMouseData, pos: TimelineMouseData, scrollStartPos: DOMPoint): void {
     if (!start || !pos) {
       return;
@@ -1968,7 +2016,7 @@ export class Timeline extends TimelineEventsEmitter {
 
   /**
    * Set options and render the component.
-   * Options will be merged with the defaults and control invalidated
+   * Note: Options will be merged\appended with the defaults and component will be invalidated/rendered again.
    */
   public setOptions(toSet: TimelineOptions): TimelineOptions {
     this._options = this._setOptions(toSet);
@@ -1978,20 +2026,35 @@ export class Timeline extends TimelineEventsEmitter {
     return this._options;
   }
 
+  /**
+   * Private. Apply html container styles from options if any is set.
+   */
+  _applyContainersStyles(): void {
+    if (this._scrollContainer && this._options) {
+      const classList = this._scrollContainer.classList;
+      if (this._options.scrollContainerClass && !classList.contains(this._options.scrollContainerClass)) {
+        classList.add(this._options.scrollContainerClass);
+      }
+      if (this._options.fillColor) {
+        this._scrollContainer.style.background = this._options.fillColor;
+      }
+    }
+  }
   _setOptions(toSet: TimelineOptions): TimelineOptions {
-    this._options = this._mergeOptions(toSet);
+    if (!toSet) {
+      return this._options;
+    }
+    this._options = this._mergeOptions(this._options, toSet);
     // Normalize and validate spans per value.
     this._options.snapStep = TimelineUtils.keepInBounds(this._options.snapStep, 0, this._options.stepVal || 0);
     this._currentZoom = this._setZoom(this._options.zoom, this._options.zoomMin, this._options.zoomMax);
     this._options.min = TimelineUtils.isNumber(this._options.min) ? this._options.min : 0;
     this._options.max = TimelineUtils.isNumber(this._options.max) ? this._options.max : Number.MAX_VALUE;
-    if (this._scrollContainer) {
-      const classList = this._scrollContainer.classList;
-      if (this._options.scrollContainerClass && classList.contains(this._options.scrollContainerClass)) {
-        classList.add(this._options.scrollContainerClass);
-      }
-      if (this._options.fillColor) {
-        this._scrollContainer.style.background = this._options.fillColor;
+    this._applyContainersStyles();
+    // Prevent current active dragging of the timeline, while it's set that it's not allowed anymore.
+    if (toSet.timelineInteractive === false) {
+      if (this._drag && this._drag.type === TimelineElementType.Timeline) {
+        this._cleanUpSelection();
       }
     }
     return this._options;
@@ -2118,12 +2181,12 @@ export class Timeline extends TimelineEventsEmitter {
   }
 
   /**
-   * get draggable element.
-   * Filter elements and get first element by a priority.
-   * @param Array
-   * @param val current mouse value
+   * Filter and sort draggable elements by the priority to get first draggable element.
+   * Filtration is done based on the timeline styles and options.
+   * @param elements to filter and sort.
+   * @param val current mouse value to find best match.
    */
-  _findDraggable(elements: Array<TimelineElement>, val: number | null = null): TimelineElement {
+  _filterDraggableElements(elements: TimelineElement[], val: number | null = null): TimelineElement {
     // filter and sort: Timeline, individual keyframes, groups (distance).
     const getPriority = (type: TimelineElementType): number => {
       if (type === TimelineElementType.Timeline) {
@@ -2147,6 +2210,10 @@ export class Timeline extends TimelineEventsEmitter {
         if (!TimelineStyleUtils.groupDraggable(element.row, this._options)) {
           return false;
         }
+      } else if (element.type === TimelineElementType.Timeline) {
+        if (this._options?.timelineInteractive === false) {
+          return false;
+        }
       } else if (element.type === TimelineElementType.Row) {
         return false;
       }
@@ -2182,9 +2249,9 @@ export class Timeline extends TimelineEventsEmitter {
   /**
    * get all clickable elements by a screen point.
    */
-  public elementFromPoint(pos: DOMPoint, clickRadius = 2): Array<TimelineElement> {
+  public elementFromPoint(pos: DOMPoint, clickRadius = 2, onlyTypes?: TimelineElementType[] | null): TimelineElement[] {
     clickRadius = Math.max(clickRadius, 1);
-    const toReturn: Array<TimelineElement> = [];
+    const toReturn: TimelineElement[] = [];
 
     if (!pos) {
       return toReturn;
@@ -2257,17 +2324,25 @@ export class Timeline extends TimelineEventsEmitter {
         }
       });
     }
-    return toReturn;
+
+    if (!onlyTypes || onlyTypes.length === 0) {
+      return toReturn;
+    } else {
+      return toReturn.filter((p) => onlyTypes && onlyTypes.includes(p.type));
+    }
   }
 
+  _cloneOptions(previousOptions: TimelineOptions): TimelineOptions {
+    return JSON.parse(JSON.stringify(previousOptions));
+  }
   /**
-   * Merge options with the defaults.
+   * Merge options. New keys will be added.
    */
-  _mergeOptions(fromArg: TimelineOptions): TimelineOptions {
-    fromArg = fromArg || ({} as TimelineOptions);
+  _mergeOptions(previousOptions: TimelineOptions, newOptions: TimelineOptions): TimelineOptions {
+    newOptions = newOptions || ({} as TimelineOptions);
     // Apply incoming options to default. (override default)
     // Deep clone default options:
-    const toArg = JSON.parse(JSON.stringify(defaultTimelineOptions));
+    const toArg = this._cloneOptions(previousOptions);
     // Merge options with the default.
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     const mergeOptionsDeep = (to: any, from: any): void => {
@@ -2292,7 +2367,7 @@ export class Timeline extends TimelineEventsEmitter {
       }
     };
 
-    mergeOptionsDeep(toArg, fromArg);
+    mergeOptionsDeep(toArg, newOptions);
     return toArg;
   }
   /**
@@ -2375,9 +2450,17 @@ export class Timeline extends TimelineEventsEmitter {
     }
     return args;
   }
-  _emitDragFinishedEvent(): TimelineDragEvent {
+  /**
+   * Private emit timeline event that dragging element is finished.
+   * @param forcePrevent - needed when during dragging components set to the state when they cannot be dragged anymore. (used only as recovery state).
+   * @returns
+   */
+  _emitDragFinishedEvent(forcePrevent = false): TimelineDragEvent {
     if (this._drag && this._drag.changed) {
       const args = this._getDragEventArgs();
+      if (forcePrevent) {
+        args.preventDefault();
+      }
       this.emit(TimelineEvents.DragFinished, args);
       if (args.isPrevented()) {
         this._preventDrag(args, this._drag, true);

File diff suppressed because it is too large
+ 4 - 4
tests/js/settingsTests.test.js


File diff suppressed because it is too large
+ 9 - 9
tests/js/timelineTests.test.js


+ 4 - 4
tests/settingsTests.test.ts

@@ -6,7 +6,7 @@ describe('_mergeOptions', function () {
     const timeline = new Timeline();
     const defOptions = defaultTimelineOptions as TimelineOptions;
     const options = { id: 'new id', snapStep: 10, snapEnabled: true } as TimelineOptions;
-    const merged = timeline._mergeOptions(options);
+    const merged = timeline._mergeOptions(defOptions, options);
     chai.expect(merged.id).equal(options.id);
     chai.expect(merged.snapEnabled).equal(options.snapEnabled);
     chai.expect(merged.snapStep).equal(options.snapStep);
@@ -20,7 +20,7 @@ describe('_mergeOptions', function () {
   it('Default styles are merged', function () {
     const timeline = new Timeline();
     const options = { id: 'new id', snapStep: 10, snapEnabled: true } as TimelineOptions;
-    const merged = timeline._mergeOptions(options);
+    const merged = timeline._mergeOptions(defaultTimelineOptions as TimelineOptions, options);
     chai.expect(merged.id).equal(options.id);
     chai.expect(!!merged.rowsStyle).equal(true, 'Row style cannot be null');
     chai.expect(!!merged.rowsStyle?.keyframesStyle).equal(true, 'Keyframes style cannot be null');
@@ -41,7 +41,7 @@ describe('_mergeOptions', function () {
         } as TimelineKeyframeStyle,
       } as TimelineRowStyle,
     } as TimelineOptions;
-    const merged = timeline._mergeOptions(options);
+    const merged = timeline._mergeOptions(defaultTimelineOptions as TimelineOptions, options);
     chai.expect(merged.id).equal('new id');
     chai.expect(merged.headerHeight).equal(44);
     chai.expect(merged.rowsStyle?.height).equal(100);
@@ -56,7 +56,7 @@ describe('_mergeOptions', function () {
       id: 'new id',
       snapStep: 10,
     } as TimelineOptions;
-    const merged = timeline._mergeOptions(options);
+    const merged = timeline._mergeOptions(defaultTimelineOptions as TimelineOptions, options);
     chai.expect(merged.id, 'new id');
     chai.expect(merged.snapStep).equal(10);
     chai.expect(options.headerHeight === undefined).equal(true);

+ 9 - 9
tests/timelineTests.test.ts

@@ -14,7 +14,7 @@ import {
 } from '../lib/animation-timeline';
 
 describe('Timeline', function () {
-  describe('_findDraggable', function () {
+  describe('_filterDraggableElements', function () {
     it('Keyframe should be selected', function () {
       const timeline = new Timeline();
       const elements = [
@@ -27,7 +27,7 @@ describe('Timeline', function () {
           val: 5,
         } as TimelineElement,
       ];
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       if (!element) {
         throw new Error('element cannot be empty');
       }
@@ -45,7 +45,7 @@ describe('Timeline', function () {
           val: 5,
         } as TimelineElement,
       ];
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       if (!element) {
         throw new Error('element cannot be empty');
       }
@@ -71,7 +71,7 @@ describe('Timeline', function () {
           val: 5,
         } as TimelineElement,
       ];
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       if (!element) {
         throw new Error('element cannot be empty');
       }
@@ -87,7 +87,7 @@ describe('Timeline', function () {
           val: 5,
         } as TimelineElement,
       ];
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       if (!element) {
         throw new Error('element cannot be empty');
       }
@@ -109,7 +109,7 @@ describe('Timeline', function () {
           val: 9,
         } as TimelineElement,
       ];
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       chai.expect(element.val).equal(elements[1].val);
     });
     it('Keyframes are not draggable by global settings', function () {
@@ -136,7 +136,7 @@ describe('Timeline', function () {
           } as TimelineKeyframeStyle,
         } as TimelineRowStyle,
       } as TimelineOptions;
-      const element = timeline._findDraggable(elements, 5);
+      const element = timeline._filterDraggableElements(elements, 5);
       chai.expect(element.type).equal(TimelineElementType.Group, 'Group should be selected');
     });
     it('Keyframes are not draggable by row settings', function () {
@@ -162,7 +162,7 @@ describe('Timeline', function () {
         } as TimelineElement,
       ];
       // Apply global options::
-      const element = timeline._findDraggable(elements, 4);
+      const element = timeline._filterDraggableElements(elements, 4);
 
       // Keyframe with value 5 should be selected as draggable
       chai.expect(element.val).equal(5);
@@ -197,7 +197,7 @@ describe('Timeline', function () {
         } as TimelineElement,
       ];
       // Apply global options::
-      const element = timeline._findDraggable(elements, 4);
+      const element = timeline._filterDraggableElements(elements, 4);
 
       // Keyframe with value 5 should be selected as draggable
       chai.expect(element.val).equal(5);

Some files were not shown because too many files changed in this diff