Ver Fonte

extended demo: allow to add keyframe on right click. Added context menu. Some styles set as optional.

Ievgen Naida há 1 ano atrás
pai
commit
b7a7678b61

+ 8 - 0
CHANGELOG.md

@@ -2,6 +2,14 @@
 
 ## Changes
 
+## [2.3.3] - 10.04.2024
+
+### Changed
+
+- Added context menu event.
+- Extended demo to show different styling options.
+- Updated dev packages to the latest versions.
+
 ## [2.3.2] - 10.04.2024
 
 ### Changed

+ 3 - 2
README.md

@@ -246,11 +246,12 @@ Example on how to add a keyframe to existing model:
 | dragStarted     | emitted on drag started. args type: TimelineDragEvent                                       |
 | drag            | emitted when dragging. args type: TimelineDragEvent                                         |
 | dragFinished    | emitted when drag finished. args type: TimelineDragEvent                                    |
-| KeyframeChanged | emitted when drag finished. args type: TimelineKeyframeChangedEvent                         |
+| keyframeChanged | emitted when drag finished. args type: TimelineKeyframeChangedEvent                         |
+| onContextMenu   | emitted on context menu displayed. args type: TimelineKeyframeChangedEvent                  |
 
 Events can be prevented by calling args.preventDefault()
 
-Example of the type strict event subscription:
+Events subscription is performed in the JavaScript (not a DOM events):
 
 ```TypeScript
 this.timeline.onDragStarted((args: TimelineDragEvent) => {

+ 569 - 0
demo/demo.js

@@ -0,0 +1,569 @@
+/**
+ * Javascript timeline initialization example. 
+ * Consumed by index.html
+ */
+
+// @ts-check
+var outlineContainer = document.getElementById('outline-container');
+
+function generateModel() {
+    const groupA = {
+        style: {
+            fillColor: '#6B9080',
+            marginTop: 4,
+        },
+        keyframesStyle: {
+            shape: 'rect',
+        },
+    };
+    const groupB = {
+        style: {
+            marginTop: 6,
+        },
+    };
+    /** @type {import('../lib/animation-timeline').TimelineModel} */
+    let timelineModel = {
+        rows: [
+            {
+                selected: false,
+                draggable: false,
+
+                keyframes: [
+                    {
+                        val: 40,
+                        shape: 'rhomb',
+                    },
+                    {
+                        shape: 'rhomb',
+                        val: 3000,
+                        selected: false,
+                    },
+                ],
+            },
+            {
+                selected: false,
+                keyframes: [
+                    {
+                        style: {
+                            cursor: 'default',
+                        },
+                        val: 2000,
+                    },
+                    {
+                        val: 2500,
+                    },
+                    {
+                        val: 2600,
+                    },
+                ],
+            },
+            {
+                keyframes: [
+                    {
+                        val: 1000,
+                    },
+                    {
+                        val: 1500,
+                    },
+                    {
+                        val: 2000,
+                    },
+                ],
+            },
+            {
+                title: 'Groups (Limited)',
+                keyframes: [
+                    {
+                        val: 40,
+                        max: 850,
+                        group: 'a',
+                    },
+                    {
+                        val: 800,
+                        max: 900,
+                        group: 'a',
+                    },
+                    {
+                        min: 1000,
+                        max: 3400,
+                        val: 1900,
+                        group: 'b',
+                    },
+                    {
+                        val: 3000,
+                        max: 3500,
+                        group: 'b',
+                    },
+                    {
+                        min: 3500,
+                        val: 4000,
+                        group: 'c',
+                    },
+                ],
+            },
+            {
+                title: 'Show 3 lanes as one outline',
+                keyframesDraggable: false,
+                style: {
+                    fillColor: "black",
+                    marginBottom: 0,
+
+                    keyframesStyle: {
+                        shape: "none"
+                    },
+                    height: 10,
+                    groupsStyle: {
+                        marginTop: 0,
+                        height: 10,
+                        fillColor: "gray"
+                    }
+                },
+                keyframes: [
+                    {
+                        val: 400,
+                    },
+                    {
+                        val: 5800,
+                    }
+                ],
+            },
+            {
+                title: 'Show 3 lanes as one outline',
+                keyframesDraggable: false,
+                style: {
+                    fillColor: "black",
+
+                    marginBottom: 0,
+                    keyframesStyle: {
+                        shape: "none"
+                    },
+                    groupsStyle: {
+                        marginTop: 0,
+                        height: 10,
+                        fillColor: "lightgray"
+                    },
+                    height: 10
+                },
+                keyframes: [
+                    {
+                        val: 500,
+                    },
+                    {
+                        val: 1800
+                    }
+                ],
+            },
+            {
+                title: 'Show 3 lanes as one outline',
+                keyframesDraggable: false,
+                style: {
+                    fillColor: "black",
+                    marginBottom: 0,
+                    keyframesStyle: {
+                        shape: "none"
+                    },
+                    groupsStyle: {
+                        marginTop: 0,
+                        height: 12,
+                        fillColor: "gray"
+                    },
+                    height: 10
+                },
+                keyframes: [
+                    {
+                        val: 650
+                    },
+                    {
+                        val: 2500
+                    }
+                ],
+            },
+            {
+                title: 'Groups Different Styles',
+                keyframes: [
+                    {
+                        val: 100,
+                        max: 850,
+                        group: groupA,
+                    },
+                    {
+                        val: 500,
+                        max: 900,
+                        group: groupA,
+                    },
+                    {
+                        min: 900,
+                        max: 3400,
+                        val: 1900,
+                        group: groupB,
+                    },
+                    {
+                        val: 4000,
+                        group: groupB,
+                    },
+                ],
+            },
+            {
+                keyframes: [
+                    {
+                        val: 100,
+                    },
+                    {
+                        val: 3410,
+                    },
+                    {
+                        val: 2000,
+                    },
+                ],
+            },
+            {
+                title: 'Keyframe Style Customized',
+                style: {
+                    groupsStyle: {
+                        height: 5,
+                        marginTop: 'auto',
+                    },
+                    keyframesStyle: {
+                        shape: 'rect',
+                        width: 5,
+                        height: 20,
+                    },
+                },
+                keyframes: [
+                    {
+                        val: 90,
+                    },
+                    {
+                        val: 3000,
+                    },
+                ],
+            },
+            {},
+            {
+                title: 'Max Value (Not Draggable)',
+                max: 4000,
+                keyframes: [
+                    {
+                        style: {
+                            width: 4,
+                            height: 20,
+                            group: 'block',
+                            shape: 'rect',
+                            fillColor: 'Red',
+                            strokeColor: 'Black',
+                        },
+                        val: 4000,
+                        selectable: false,
+                        draggable: false,
+                    },
+                    {
+                        val: 1500,
+                    },
+                    {
+                        val: 2500,
+                    },
+                ],
+            },
+            {},
+            {},
+            {
+                title: 'Custom Height',
+                style: {
+                    height: 100,
+                    keyframesStyle: {
+                        shape: 'rect',
+                        width: 4,
+                        height: 70,
+                    },
+                },
+
+                keyframes: [
+                    {
+                        val: 40,
+                        max: 850,
+                        group: 'a',
+                    },
+                    {
+                        val: 8600,
+                        group: 'a',
+                    },
+                ],
+            },
+        ]
+    };
+    return timelineModel;
+}
+const timelineModel = generateModel();
+
+// Log message to the screen
+var logMessage = function (message, logPanel = 1) {
+    if (message) {
+        let el = document.getElementById('output' + logPanel);
+        if (el) {
+            el.innerHTML = message + '<br/>' + el.innerHTML;
+        }
+    }
+};
+
+var logDraggingMessage = function (object, eventName) {
+    if (object.elements) {
+        logMessage('Keyframe value: ' + object.elements[0].val + '. Selected (' + object.elements.length + ').' + eventName);
+    }
+};
+/** @type {import('../lib/animation-timeline').Timeline} */
+// @ts-ignore
+var timeline = new timelineModule.Timeline();
+
+timeline.initialize({ id: 'timeline', headerHeight: 45 }, timelineModel);
+
+// Select all elements on key down
+document.addEventListener('keydown', function (args) {
+    if (args.which === 65 && timeline._controlKeyPressed(args)) {
+        timeline.selectAllKeyframes();
+        args.preventDefault();
+    }
+});
+
+timeline.onTimeChanged(function (event) {
+    showActivePositionInformation();
+});
+
+function showActivePositionInformation() {
+    if (timeline) {
+        var fromPx = timeline.scrollLeft;
+        var toPx = timeline.scrollLeft + timeline.getClientWidth();
+        var fromMs = timeline.pxToVal(fromPx - timeline._leftMargin());
+        var toMs = timeline.pxToVal(toPx - timeline._leftMargin());
+        var positionInPixels = timeline.valToPx(timeline.getTime()) + timeline._leftMargin();
+        var message = 'Timeline in ms: ' + timeline.getTime() + 'ms. Displayed from:' + fromMs.toFixed() + 'ms to: ' + toMs.toFixed() + 'ms.';
+        message += '<br>';
+        message += 'Timeline in px: ' + positionInPixels + 'px. Displayed from: ' + fromPx + 'px to: ' + toPx + 'px';
+        var currentElement = document.getElementById('currentTime');
+        if (currentElement) {
+            currentElement.innerHTML = message;
+        }
+    }
+}
+
+timeline.onSelected(function (obj) {
+    logMessage('Selected Event: (' + obj.selected.length + '). changed selection :' + obj.changed.length, 2);
+});
+
+timeline.onDragStarted(function (obj) {
+    logDraggingMessage(obj, 'dragstarted');
+});
+
+timeline.onDrag(function (obj) {
+    logDraggingMessage(obj, 'drag');
+});
+
+timeline.onKeyframeChanged(function (obj) {
+    console.log('keyframe: ' + obj.val);
+});
+
+timeline.onDragFinished(function (obj) {
+    logDraggingMessage(obj, 'dragfinished');
+});
+
+timeline.onContextMenu(function (obj) {
+    if (obj.args) {
+        obj.args.preventDefault();
+    }
+    logDraggingMessage(obj, 'addKeyframe');
+
+    obj.elements.forEach(p => {
+        if (p.type === "row") {
+            if (!p.keyframes) {
+                p.keyframes = []
+            }
+            p.keyframes?.push({ val: obj.point?.val || 0 });
+        }
+    })
+    timeline.redraw();
+
+
+});
+
+timeline.onMouseDown(function (obj) {
+    var type = obj.target ? obj.target.type : '';
+    if (obj.pos) {
+        logMessage('mousedown:' + obj.val + '.  target:' + type + '. ' + Math.floor(obj.pos.x) + 'x' + Math.floor(obj.pos.y), 2);
+    }
+});
+
+timeline.onDoubleClick(function (obj) {
+    var type = obj.target ? obj.target.type : '';
+    if (obj.pos) {
+        logMessage('doubleclick:' + obj.val + '.  target:' + type + '. ' + Math.floor(obj.pos.x) + 'x' + Math.floor(obj.pos.y), 2);
+    }
+});
+
+// Synchronize component scroll renderer with HTML list of the nodes.
+timeline.onScroll(function (obj) {
+    var options = timeline.getOptions();
+    if (options) {
+        if (outlineContainer) {
+            outlineContainer.style.minHeight = obj.scrollHeight + 'px';
+            const outlineElement = document.getElementById('outline-scroll-container');
+            if (outlineElement) {
+                outlineElement.scrollTop = obj.scrollTop;
+            }
+        }
+    }
+    showActivePositionInformation();
+});
+
+timeline.onScrollFinished(function (_) {
+    // Stop move component screen to the timeline when user start manually scrolling.
+    logMessage('on scroll finished', 2);
+});
+
+generateHTMLOutlineListNodes(timelineModel.rows);
+
+/**
+ * Generate html for the left menu for each row.
+ * */
+function generateHTMLOutlineListNodes(rows) {
+    var options = timeline.getOptions();
+    var headerElement = document.getElementById('outline-header');
+    if (!headerElement) {
+        return;
+    }
+    headerElement.style.maxHeight = headerElement.style.minHeight = options.headerHeight + 'px';
+    // headerElement.style.backgroundColor = options.headerFillColor;
+    if (!outlineContainer) {
+        console.log("Error: Cannot find html element to output outline/tree view")
+        return;
+    }
+    outlineContainer.innerHTML = '';
+
+    rows.forEach(function (row, index) {
+        var div = document.createElement('div');
+        div.classList.add('outline-node');
+        const h = (row.style ? row.style.height : 0) || (options.rowsStyle ? options.rowsStyle.height : 0);
+        div.style.maxHeight = div.style.minHeight = h + 'px';
+        div.style.marginBottom = ((options.rowsStyle ? options.rowsStyle.marginBottom : 0) || 0) + 'px';
+        div.innerText = row.title || 'Track ' + index;
+        div.id = div.innerText;
+        var alreadyAddedWithSuchNameElement = document.getElementById(div.innerText)
+        // Combine outlines with the same name:
+        if (alreadyAddedWithSuchNameElement) {
+            var increaseSize = Number.parseInt(alreadyAddedWithSuchNameElement.style.maxHeight) + h;
+            alreadyAddedWithSuchNameElement.style.maxHeight = alreadyAddedWithSuchNameElement.style.minHeight = increaseSize + 'px';
+
+            return
+        }
+        if (outlineContainer) {
+            outlineContainer.appendChild(div);
+        }
+
+    });
+}
+
+// Handle events from html page
+function selectMode() {
+    if (timeline) {
+        timeline.setInteractionMode('selection');
+    }
+}
+function zoomMode() {
+    if (timeline) {
+        timeline.setInteractionMode('zoom');
+    }
+}
+function noneMode() {
+    if (timeline) {
+        timeline.setInteractionMode('none');
+    }
+}
+
+function removeKeyframe() {
+    if (timeline) {
+        // Add keyframe
+        const currentModel = timeline.getModel();
+        if (currentModel && currentModel.rows) {
+            currentModel.rows.forEach((row) => {
+                if (row.keyframes) {
+                    row.keyframes = row.keyframes.filter((p) => !p.selected);
+                }
+            });
+            timeline.setModel(currentModel);
+        }
+    }
+}
+function addKeyframe() {
+    if (timeline) {
+        // Add keyframe
+        const currentModel = timeline.getModel();
+        if (!currentModel) {
+            return;
+        }
+        currentModel.rows.push({ keyframes: [{ val: timeline.getTime() }] });
+        timeline.setModel(currentModel);
+
+        // Generate outline list menu
+        generateHTMLOutlineListNodes(currentModel.rows);
+    }
+}
+function panMode(interactive) {
+    if (timeline) {
+        timeline.setInteractionMode(interactive ? 'pan' : 'nonInteractivePan');
+    }
+}
+// Set scroll back to timeline when mouse scroll over the outline
+function outlineMouseWheel(event) {
+    if (timeline) {
+        this.timeline._handleWheelEvent(event);
+    }
+}
+var playing = false;
+var playStep = 50;
+// Automatic tracking should be turned off when user interaction happened.
+var trackTimelineMovement = false;
+function onPlayClick(event) {
+    playing = true;
+    trackTimelineMovement = true;
+    if (timeline) {
+        this.moveTimelineIntoTheBounds();
+        // Don't allow to manipulate timeline during playing (optional).
+        timeline.setOptions({ timelineDraggable: false });
+    }
+}
+function onPauseClick(event) {
+    playing = false;
+    if (timeline) {
+        timeline.setOptions({ timelineDraggable: true });
+    }
+}
+
+function moveTimelineIntoTheBounds() {
+    if (timeline) {
+        if (timeline._startPosMouseArgs || timeline._scrollAreaClickOrDragStarted) {
+            // User is manipulating items, don't move screen in this case.
+            return;
+        }
+        const fromPx = timeline.scrollLeft;
+        const toPx = timeline.scrollLeft + timeline.getClientWidth();
+
+        let positionInPixels = timeline.valToPx(timeline.getTime()) + timeline._leftMargin();
+        // Scroll to timeline position if timeline is out of the bounds:
+        if (positionInPixels <= fromPx || positionInPixels >= toPx) {
+            this.timeline.scrollLeft = 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;

+ 1 - 431
index.html

@@ -205,436 +205,6 @@
         <div id="timeline"></div>
       </footer>
     </div>
-    <script type="text/javascript">
-      // @ts-check
-      var outlineContainer = document.getElementById('outline-container');
-
-      function generateModel() {
-        const groupA = {
-          style: {
-            fillColor: '#6B9080',
-            marginTop: 4,
-          },
-          keyframesStyle: {
-            shape: 'rect',
-          },
-        };
-        const groupB = {
-          style: {
-            marginTop: 6,
-          },
-        };
-        let rows = [
-          {
-            selected: false,
-            draggable: false,
-
-            keyframes: [
-              {
-                val: 40,
-                shape: 'rhomb',
-              },
-              {
-                shape: 'rhomb',
-                val: 3000,
-                selected: false,
-              },
-            ],
-          },
-          {
-            selected: false,
-            keyframes: [
-              {
-                style: {
-                  cursor: 'default',
-                },
-                val: 2000,
-              },
-              {
-                val: 2500,
-              },
-              {
-                val: 2600,
-              },
-            ],
-          },
-          {
-            keyframes: [
-              {
-                val: 1000,
-              },
-              {
-                val: 1500,
-              },
-              {
-                val: 2000,
-              },
-            ],
-          },
-          {
-            title: 'Groups (Limited)',
-            keyframes: [
-              {
-                val: 40,
-                max: 850,
-                group: 'a',
-              },
-              {
-                val: 800,
-                max: 900,
-                group: 'a',
-              },
-              {
-                min: 1000,
-                max: 3400,
-                val: 1900,
-                group: 'b',
-              },
-              {
-                val: 3000,
-                max: 3500,
-                group: 'b',
-              },
-              {
-                min: 3500,
-                val: 4000,
-                group: 'c',
-              },
-            ],
-          },
-          {
-            title: 'Groups Different Styles',
-            keyframes: [
-              {
-                val: 100,
-                max: 850,
-                group: groupA,
-              },
-              {
-                val: 500,
-                max: 900,
-                group: groupA,
-              },
-              {
-                min: 900,
-                max: 3400,
-                val: 1900,
-                group: groupB,
-              },
-              {
-                val: 4000,
-                group: groupB,
-              },
-            ],
-          },
-          {
-            keyframes: [
-              {
-                val: 100,
-              },
-              {
-                val: 3410,
-              },
-              {
-                val: 2000,
-              },
-            ],
-          },
-          {
-            title: 'Keyframe Style Customized',
-            style: {
-              groupsStyle: {
-                height: 5,
-                marginTop: 'auto',
-              },
-              keyframesStyle: {
-                shape: 'rect',
-                width: 5,
-                height: 20,
-              },
-            },
-            keyframes: [
-              {
-                val: 90,
-              },
-              {
-                val: 3000,
-              },
-            ],
-          },
-          {},
-          {
-            title: 'Max Value (Not Draggable)',
-            max: 4000,
-            keyframes: [
-              {
-                style: {
-                  width: 4,
-                  height: 20,
-                  group: 'block',
-                  shape: 'rect',
-                  fillColor: 'Red',
-                  strokeColor: 'Black',
-                },
-                val: 4000,
-                selectable: false,
-                draggable: false,
-              },
-              {
-                val: 1500,
-              },
-              {
-                val: 2500,
-              },
-            ],
-          },
-          {},
-          {},
-          {
-            title: 'Custom Height',
-            style: {
-              height: 100,
-              keyframesStyle: {
-                shape: 'rect',
-                width: 4,
-                height: 70,
-              },
-            },
-
-            keyframes: [
-              {
-                val: 40,
-                max: 850,
-                group: 'a',
-              },
-              {
-                val: 8600,
-                group: 'a',
-              },
-            ],
-          },
-        ];
-        return rows;
-      }
-      const rows = generateModel();
-
-      var logMessage = function (message, logPanel = 1) {
-        if (message) {
-          let el = document.getElementById('output' + logPanel);
-          el.innerHTML = message + '<br/>' + el.innerHTML;
-        }
-      };
-
-      var logDraggingMessage = function (object, eventName) {
-        if (object.elements) {
-          logMessage('Keyframe value: ' + object.elements[0].val + '. Selected (' + object.elements.length + ').' + eventName);
-        }
-      };
-      /** @type {import('./lib/animation-timeline').Timeline} */
-      var timeline = new timelineModule.Timeline();
-
-      timeline.initialize({ id: 'timeline', headerHeight: 45 });
-      timeline.setModel({ rows: rows });
-
-      // Select all elements on key down
-      document.addEventListener('keydown', function (args) {
-        if (args.which === 65 && timeline._controlKeyPressed(args)) {
-          timeline.selectAllKeyframes();
-          args.preventDefault();
-        }
-      });
-
-      timeline.onTimeChanged(function (event) {
-        showActivePositionInformation();
-      });
-
-      function showActivePositionInformation() {
-        if (timeline) {
-          const fromPx = timeline.scrollLeft;
-          const toPx = timeline.scrollLeft + timeline.getClientWidth();
-          const fromMs = timeline.pxToVal(fromPx - timeline._leftMargin());
-          const toMs = timeline.pxToVal(toPx - timeline._leftMargin());
-          let positionInPixels = timeline.valToPx(timeline.getTime()) + timeline._leftMargin();
-          let message = 'Timeline in ms: ' + timeline.getTime() + 'ms. Displayed from:' + fromMs.toFixed() + 'ms to: ' + toMs.toFixed() + 'ms.';
-          message += '<br>';
-          message += 'Timeline in px: ' + positionInPixels + 'px. Displayed from: ' + fromPx + 'px to: ' + toPx + 'px';
-          document.getElementById('currentTime').innerHTML = message;
-        }
-      }
-
-      timeline.onSelected(function (obj) {
-        logMessage('Selected Event: (' + obj.selected.length + '). changed selection :' + obj.changed.length, 2);
-      });
-
-      timeline.onDragStarted(function (obj) {
-        logDraggingMessage(obj, 'dragstarted');
-      });
-
-      timeline.onDrag(function (obj) {
-        logDraggingMessage(obj, 'drag');
-      });
-
-      timeline.onKeyframeChanged(function (obj) {
-        console.log('keyframe: ' + obj.val);
-      });
-
-      timeline.onDragFinished(function (obj) {
-        logDraggingMessage(obj, 'dragfinished');
-      });
-
-      timeline.onMouseDown(function (obj) {
-        var type = obj.target ? obj.target.type : '';
-        logMessage('mousedown:' + obj.val + '.  target:' + type + '. ' + Math.floor(obj.pos.x) + 'x' + Math.floor(obj.pos.y), 2);
-      });
-
-      timeline.onDoubleClick(function (obj) {
-        var type = obj.target ? obj.target.type : '';
-        logMessage('doubleclick:' + obj.val + '.  target:' + type + '. ' + Math.floor(obj.pos.x) + 'x' + Math.floor(obj.pos.y), 2);
-      });
-
-      // Synchronize component scroll renderer with HTML list of the nodes.
-      timeline.onScroll(function (obj) {
-        var options = timeline.getOptions();
-        if (options) {
-          if (outlineContainer) {
-            outlineContainer.style.minHeight = obj.scrollHeight + 'px';
-            document.getElementById('outline-scroll-container').scrollTop = obj.scrollTop;
-          }
-        }
-        showActivePositionInformation();
-      });
-
-      timeline.onScrollFinished(function (obj) {
-        // Stop move component screen to the timeline when user start manually scrolling.
-        logMessage('on scroll finished', 2);
-      });
-      generateHTMLOutlineListNodes(rows);
-
-      /**
-       * Generate html for the left menu for each row.
-       * */
-      function generateHTMLOutlineListNodes(rows) {
-        var options = timeline.getOptions();
-        var headerElement = document.getElementById('outline-header');
-        headerElement.style.maxHeight = headerElement.style.minHeight = options.headerHeight + 'px';
-        // headerElement.style.backgroundColor = options.headerFillColor;
-        outlineContainer.innerHTML = '';
-        rows.forEach(function (row, index) {
-          var div = document.createElement('div');
-          div.classList.add('outline-node');
-          const h = (row.style ? row.style.height : 0) || options.rowsStyle.height;
-          div.style.maxHeight = div.style.minHeight = h + 'px';
-          div.style.marginBottom = options.rowsStyle.marginBottom + 'px';
-          div.innerText = row.title || 'Track ' + index;
-          outlineContainer.appendChild(div);
-        });
-      }
-
-      /*Handle events from html page*/
-      function selectMode() {
-        if (timeline) {
-          timeline.setInteractionMode('selector');
-        }
-      }
-      function zoomMode() {
-        if (timeline) {
-          timeline.setInteractionMode('zoom');
-        }
-      }
-      function noneMode() {
-        if (timeline) {
-          timeline.setInteractionMode('none');
-        }
-      }
-
-      function removeKeyframe() {
-        if (timeline) {
-          // Add keyframe
-          const currentModel = timeline.getModel();
-          if (currentModel && currentModel.rows) {
-            currentModel.rows.forEach((row) => {
-              if (row.keyframes) {
-                row.keyframes = row.keyframes.filter((p) => !p.selected);
-              }
-            });
-          }
-
-          timeline.setModel(currentModel);
-        }
-      }
-      function addKeyframe() {
-        if (timeline) {
-          // Add keyframe
-          const currentModel = timeline.getModel();
-          currentModel.rows.push({ keyframes: [{ val: timeline.getTime() }] });
-          timeline.setModel(currentModel);
-
-          // Generate outline list menu
-          generateHTMLOutlineListNodes(currentModel.rows);
-        }
-      }
-      function panMode(interactive) {
-        if (timeline) {
-          timeline.setInteractionMode(interactive ? 'pan' : 'nonInteractivePan');
-        }
-      }
-      // Set scroll back to timeline when mouse scroll over the outline
-      function outlineMouseWheel(event) {
-        if (timeline) {
-          this.timeline._handleWheelEvent(event);
-        }
-      }
-      playing = false;
-      playStep = 50;
-      // Automatic tracking should be turned off when user interaction happened.
-      trackTimelineMovement = false;
-      function onPlayClick(event) {
-        playing = true;
-        trackTimelineMovement = true;
-        if (timeline) {
-          this.moveTimelineIntoTheBounds();
-          // Don't allow to manipulate timeline during playing (optional).
-          timeline.setOptions({ timelineDraggable: false });
-        }
-      }
-      function onPauseClick(event) {
-        playing = false;
-        if (timeline) {
-          timeline.setOptions({ timelineDraggable: true });
-        }
-      }
-
-      function moveTimelineIntoTheBounds() {
-        if (timeline) {
-          if (timeline._startPos || timeline._scrollAreaClickOrDragStarted) {
-            // User is manipulating items, don't move screen in this case.
-            return;
-          }
-          const fromPx = timeline.scrollLeft;
-          const toPx = timeline.scrollLeft + timeline.getClientWidth();
-
-          let positionInPixels = timeline.valToPx(timeline.getTime()) + timeline._leftMargin();
-          // Scroll to timeline position if timeline is out of the bounds:
-          if (positionInPixels <= fromPx || positionInPixels >= toPx) {
-            this.timeline.scrollLeft = 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>
+  <script src="./demo/demo.js" type="text/javascript"></script>
 </html>

+ 33 - 6
lib/animation-timeline.js

@@ -144,7 +144,6 @@ var TimelineEventsEmitter = /*#__PURE__*/function () {
      * @param topic Event name.
      * @param args Event arguments.
      */
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
   }, {
     key: "emit",
     value: function emit(topic, args) {
@@ -550,8 +549,6 @@ function timelineStyleUtils_createClass(e, r, t) { return r && timelineStyleUtil
 function timelineStyleUtils_toPropertyKey(t) { var i = timelineStyleUtils_toPrimitive(t, "string"); return "symbol" == timelineStyleUtils_typeof(i) ? i : i + ""; }
 function timelineStyleUtils_toPrimitive(t, r) { if ("object" != timelineStyleUtils_typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != timelineStyleUtils_typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
 function timelineStyleUtils_typeof(o) { "@babel/helpers - typeof"; return timelineStyleUtils_typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, timelineStyleUtils_typeof(o); }
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
 
 
 
@@ -1237,6 +1234,7 @@ var TimelineEvents = /*#__PURE__*/function (TimelineEvents) {
   TimelineEvents["DragFinished"] = "dragFinished";
   TimelineEvents["Scroll"] = "scroll";
   TimelineEvents["ScrollFinished"] = "scrollFinished";
+  TimelineEvents["ContextMenu"] = "onContextMenu";
   TimelineEvents["DoubleClick"] = "doubleClick";
   TimelineEvents["MouseDown"] = "mouseDown";
   return TimelineEvents;
@@ -1680,6 +1678,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       if (_this._canvas) {
         _this._canvas.addEventListener('touchstart', _this._handleMouseDownEvent, false);
         _this._canvas.addEventListener('mousedown', _this._handleMouseDownEvent, false);
+        _this._canvas.addEventListener('contextmenu', _this._handleContextMenu, false);
       }
       window.addEventListener('mousemove', _this._handleMouseMoveEvent, false);
       window.addEventListener('touchmove', _this._handleMouseMoveEvent, false);
@@ -1706,6 +1705,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       if (_this._canvas) {
         _this._canvas.removeEventListener('touchstart', _this._handleMouseDownEvent);
         _this._canvas.removeEventListener('mousedown', _this._handleMouseDownEvent);
+        _this._canvas.removeEventListener('contextmenu', _this._handleContextMenu);
       } else {
         console.warn("Cannot unsubscribe canvas while it's already empty");
       }
@@ -1911,6 +1911,27 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       var defaultValue = _this._consts.clickDetectionMinRadius || 1;
       return Math.max(defaultValue, (point === null || point === void 0 ? void 0 : point.radius) || defaultValue);
     });
+    timeline_defineProperty(_this, "_handleContextMenu", function (args) {
+      // Prevent drag of the canvas if canvas is selected as text:
+      TimelineUtils.clearBrowserSelection();
+      if (!_this._canvas || !_this._scrollContainer) {
+        _this._cleanUpSelection();
+        return;
+      }
+      var mousePosTimeline = _this._trackMousePos(_this._canvas, args);
+      var clickRadius = _this._getClickDetectionRadius(mousePosTimeline);
+      var elements = _this.elementFromPoint(mousePosTimeline.pos, clickRadius, []);
+      var target = _this._findDraggableElement(elements, mousePosTimeline.val);
+      // Create click event
+      var event = new TimelineClickEvent();
+      event.point = mousePosTimeline;
+      event.args = args;
+      // all elements under the click:
+      event.elements = elements;
+      // target element.
+      event.target = target;
+      _get((_this, timeline_getPrototypeOf(Timeline.prototype)), "emit", _this).call(_this, TimelineEvents.ContextMenu, event);
+    });
     /**
      * @param args
      */
@@ -2973,6 +2994,7 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
 
           // Performance FIX: use clip only  when we are in the collision! Clip is slow!
           // Other keyframes should be hidden by bounds check.
+          // Note: clip with just render part of the keyframe
           if (bounds && bounds.overlapY) {
             _this._ctx.beginPath();
             _this._ctx.rect(0, TimelineStyleUtils.headerHeight(_this._options), _this._canvasClientWidth(), _this._canvasClientWidth());
@@ -3109,8 +3131,8 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
         var capSize = capStyle.width || 0;
         var capHeight = capStyle.height || 0;
         if (capSize && capHeight) {
-          _this._ctx.strokeStyle = capStyle.strokeColor;
-          _this._ctx.fillStyle = capStyle.fillColor;
+          _this._ctx.strokeStyle = capStyle.strokeColor || '';
+          _this._ctx.fillStyle = capStyle.fillColor || 'white';
           if (capStyle.capType === TimelineCapShape.Triangle) {
             _this._ctx.beginPath();
             _this._ctx.moveTo(timeLinePos - capSize / 2, y);
@@ -3306,7 +3328,6 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
       _this.rescale();
       _this.redraw();
     });
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     timeline_defineProperty(_this, "_getMousePos", function (canvas, e) {
       var radius = 1;
       var clientX = 0;
@@ -3657,6 +3678,12 @@ var Timeline = /*#__PURE__*/function (_TimelineEventsEmitte) {
     timeline_defineProperty(_this, "onScrollFinished", function (callback) {
       _this.on(TimelineEvents.ScrollFinished, callback);
     });
+    /**
+     * Subscribe on canvas context menu event.
+     */
+    timeline_defineProperty(_this, "onContextMenu", function (callback) {
+      _this.on(TimelineEvents.ContextMenu, callback);
+    });
     /**
      * Private.
      * Emit internally scroll eve

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
lib/animation-timeline.js.map


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
lib/animation-timeline.min.js


+ 1 - 0
lib/enums/timelineEvents.d.ts

@@ -10,6 +10,7 @@ export declare enum TimelineEvents {
     DragFinished = "dragFinished",
     Scroll = "scroll",
     ScrollFinished = "scrollFinished",
+    ContextMenu = "onContextMenu",
     DoubleClick = "doubleClick",
     MouseDown = "mouseDown"
 }

+ 1 - 1
lib/models/timelineGroup.d.ts

@@ -11,7 +11,7 @@ export interface TimelineGroup {
     /**
      * Child keyframes style.
      */
-    keyframesStyle: TimelineKeyframeStyle;
+    keyframesStyle?: TimelineKeyframeStyle;
     /**
      * Whether group is draggable.
      * Considered to be false when really set as false.

+ 2 - 2
lib/settings/styles/timelineCapStyle.d.ts

@@ -14,11 +14,11 @@ export interface TimelineCapStyle {
     /**
      * Cap stroke color.
      */
-    strokeColor: string;
+    strokeColor?: string;
     /**
      * Cap fill color.
      */
-    fillColor: string;
+    fillColor?: string;
     /**
      * Cap type
      */

+ 5 - 0
lib/timeline.d.ts

@@ -214,6 +214,7 @@ export declare class Timeline extends TimelineEventsEmitter {
      */
     getZoom: () => number;
     _getClickDetectionRadius: (point: TimelineMouseData) => number;
+    _handleContextMenu: (args: MouseEvent | TouchEvent) => void;
     /**
      * @param args
      */
@@ -532,6 +533,10 @@ export declare class Timeline extends TimelineEventsEmitter {
      * Subscribe on scroll finished event.
      */
     onScrollFinished: (callback: (eventArgs: TimelineScrollEvent) => void) => void;
+    /**
+     * Subscribe on canvas context menu event.
+     */
+    onContextMenu: (callback: (eventArgs: TimelineClickEvent) => void) => void;
     /**
      * Private.
      * Emit internally scroll eve

+ 1 - 0
src/enums/timelineEvents.ts

@@ -10,6 +10,7 @@ export enum TimelineEvents {
   DragFinished = 'dragFinished',
   Scroll = 'scroll',
   ScrollFinished = 'scrollFinished',
+  ContextMenu = 'onContextMenu',
   DoubleClick = 'doubleClick',
   MouseDown = 'mouseDown',
 }

+ 1 - 1
src/models/timelineGroup.ts

@@ -11,7 +11,7 @@ export interface TimelineGroup {
   /**
    * Child keyframes style.
    */
-  keyframesStyle: TimelineKeyframeStyle;
+  keyframesStyle?: TimelineKeyframeStyle;
   /**
    * Whether group is draggable.
    * Considered to be false when really set as false.

+ 2 - 2
src/settings/styles/timelineCapStyle.ts

@@ -15,11 +15,11 @@ export interface TimelineCapStyle {
   /**
    * Cap stroke color.
    */
-  strokeColor: string;
+  strokeColor?: string;
   /**
    * Cap fill color.
    */
-  fillColor: string;
+  fillColor?: string;
   /**
    * Cap type
    */

+ 38 - 2
src/timeline.ts

@@ -293,6 +293,7 @@ export class Timeline extends TimelineEventsEmitter {
     if (this._canvas) {
       this._canvas.addEventListener('touchstart', this._handleMouseDownEvent, false);
       this._canvas.addEventListener('mousedown', this._handleMouseDownEvent, false);
+      this._canvas.addEventListener('contextmenu', this._handleContextMenu, false);
     }
     window.addEventListener('mousemove', this._handleMouseMoveEvent, false);
     window.addEventListener('touchmove', this._handleMouseMoveEvent, false);
@@ -320,6 +321,7 @@ export class Timeline extends TimelineEventsEmitter {
     if (this._canvas) {
       this._canvas.removeEventListener('touchstart', this._handleMouseDownEvent);
       this._canvas.removeEventListener('mousedown', this._handleMouseDownEvent);
+      this._canvas.removeEventListener('contextmenu', this._handleContextMenu);
     } else {
       console.warn(`Cannot unsubscribe canvas while it's already empty`);
     }
@@ -525,6 +527,32 @@ export class Timeline extends TimelineEventsEmitter {
     const defaultValue = this._consts.clickDetectionMinRadius || 1;
     return Math.max(defaultValue, point?.radius || defaultValue);
   };
+
+  _handleContextMenu = (args: MouseEvent | TouchEvent): void => {
+    // Prevent drag of the canvas if canvas is selected as text:
+    TimelineUtils.clearBrowserSelection();
+    if (!this._canvas || !this._scrollContainer) {
+      this._cleanUpSelection();
+      return;
+    }
+    const mousePosTimeline = this._trackMousePos(this._canvas, args);
+
+    const clickRadius = this._getClickDetectionRadius(mousePosTimeline);
+    const elements = this.elementFromPoint(mousePosTimeline.pos, clickRadius, []);
+
+    const target = this._findDraggableElement(elements, mousePosTimeline.val);
+    // Create click event
+    const event = new TimelineClickEvent();
+    event.point = mousePosTimeline;
+    event.args = args;
+    // all elements under the click:
+    event.elements = elements;
+    // target element.
+    event.target = target;
+
+    super.emit(TimelineEvents.ContextMenu, event);
+  };
+
   /**
    * @param args
    */
@@ -1959,6 +1987,7 @@ export class Timeline extends TimelineEventsEmitter {
 
         // Performance FIX: use clip only  when we are in the collision! Clip is slow!
         // Other keyframes should be hidden by bounds check.
+        // Note: clip with just render part of the keyframe
         if (bounds && bounds.overlapY) {
           this._ctx.beginPath();
           this._ctx.rect(0, TimelineStyleUtils.headerHeight(this._options), this._canvasClientWidth(), this._canvasClientWidth());
@@ -2110,8 +2139,8 @@ export class Timeline extends TimelineEventsEmitter {
       const capSize = capStyle.width || 0;
       const capHeight = capStyle.height || 0;
       if (capSize && capHeight) {
-        this._ctx.strokeStyle = capStyle.strokeColor;
-        this._ctx.fillStyle = capStyle.fillColor;
+        this._ctx.strokeStyle = capStyle.strokeColor || '';
+        this._ctx.fillStyle = capStyle.fillColor || 'white';
         if (capStyle.capType === TimelineCapShape.Triangle) {
           this._ctx.beginPath();
           this._ctx.moveTo(timeLinePos - capSize / 2, y);
@@ -2698,6 +2727,13 @@ export class Timeline extends TimelineEventsEmitter {
   public onScrollFinished = (callback: (eventArgs: TimelineScrollEvent) => void): void => {
     this.on(TimelineEvents.ScrollFinished, callback);
   };
+  /**
+   * Subscribe on canvas context menu event.
+   */
+  public onContextMenu = (callback: (eventArgs: TimelineClickEvent) => void): void => {
+    this.on(TimelineEvents.ContextMenu, callback);
+  };
+
   /**
    * Private.
    * Emit internally scroll eve

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff