| /* | 
 | * Licensed to the Apache Software Foundation (ASF) under one | 
 | * or more contributor license agreements.  See the NOTICE file | 
 | * distributed with this work for additional information | 
 | * regarding copyright ownership.  The ASF licenses this file | 
 | * to you under the Apache License, Version 2.0 (the | 
 | * "License"); you may not use this file except in compliance | 
 | * with the License.  You may obtain a copy of the License at | 
 | * | 
 | *   http://www.apache.org/licenses/LICENSE-2.0 | 
 | * | 
 | * Unless required by applicable law or agreed to in writing, | 
 | * software distributed under the License is distributed on an | 
 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
 | * KIND, either express or implied.  See the License for the | 
 | * specific language governing permissions and limitations | 
 | * under the License. | 
 | */ | 
 |  | 
 | import * as timeline from './timeline'; | 
 |  | 
 | function waitTime(time) { | 
 |     return new Promise(resolve => { | 
 |         setTimeout(() => { | 
 |             resolve(); | 
 |         }, time); | 
 |     }); | 
 | }; | 
 |  | 
 | export class ActionPlayback { | 
 |  | 
 |     constructor() { | 
 |         this._timer = 0; | 
 |         this._current = 0; | 
 |  | 
 |         this._ops = []; | 
 |         this._currentOpIndex = 0; | 
 |  | 
 |         this._isLastOpMousewheel = false; | 
 |     } | 
 |  | 
 |     getContext() { | 
 |         return { | 
 |             elapsedTime: this._elapsedTime, | 
 |             currentOpIndex: this._currentOpIndex, | 
 |             isLastOpMouseWheel: this._isLastOpMousewheel | 
 |         } | 
 |     } | 
 |  | 
 |     _reset() { | 
 |         this._currentOpIndex = 0; | 
 |         this._current = Date.now(); | 
 |         this._elapsedTime = 0; | 
 |         this._isLastOpMousewheel = false; | 
 |     } | 
 |  | 
 |     _restoreContext(ctx) { | 
 |         this._elapsedTime = ctx.elapsedTime; | 
 |         this._currentOpIndex = ctx.currentOpIndex; | 
 |         this._isLastOpMousewheel = ctx.isLastOpMouseWheel; | 
 |     } | 
 |  | 
 |     async runAction(action, ctxToRestore) { | 
 |         this.stop(); | 
 |  | 
 |         if (!action.ops.length) { | 
 |             return; | 
 |         } | 
 |  | 
 |         this._ops = action.ops.slice().sort((a, b) => { | 
 |             return a.time - b.time; | 
 |         }); | 
 |         let firstOp = this._ops[0]; | 
 |         this._ops.forEach(op => { | 
 |             op.time -= firstOp.time; | 
 |         }); | 
 |  | 
 |         this._reset(); | 
 |  | 
 |         if (ctxToRestore) { | 
 |             this._restoreContext(ctxToRestore); | 
 |             // Usually restore context happens when page is reloaded after mouseup. | 
 |             // In this case the _currentOpIndex is not increased yet. | 
 |             this._currentOpIndex++; | 
 |         } | 
 |  | 
 |         let self = this; | 
 |  | 
 |         async function takeScreenshot() { | 
 |             // Pause timeline when doing screenshot to avoid screenshot needs taking a while. | 
 |             timeline.pause(); | 
 |             await __VRT_ACTION_SCREENSHOT__(action); | 
 |             timeline.resume(); | 
 |         } | 
 |  | 
 |         return new Promise((resolve, reject) => { | 
 |             async function tick() { | 
 |                 let current = Date.now(); | 
 |                 let dTime = current - self._current; | 
 |                 self._elapsedTime += dTime; | 
 |                 self._current = current; | 
 |  | 
 |                 try { | 
 |                     // Execute all if there are multiple ops in one frame. | 
 |                     do { | 
 |                         const executed = await self._update(takeScreenshot); | 
 |                         if (!executed) { | 
 |                             break; | 
 |                         } | 
 |                     } while (true); | 
 |                 } | 
 |                 catch (e) { | 
 |                     // Stop running and throw error. | 
 |                     reject(e); | 
 |                     return; | 
 |                 } | 
 |  | 
 |                 if (self._currentOpIndex >= self._ops.length) { | 
 |                     // Finished | 
 |                     resolve(); | 
 |                 } | 
 |                 else { | 
 |                     self._timer = setTimeout(tick, 0); | 
 |                 } | 
 |             } | 
 |             tick(); | 
 |         }); | 
 |     } | 
 |  | 
 |  | 
 |     stop() { | 
 |         if (this._timer) { | 
 |             clearTimeout(this._timer); | 
 |             this._timer = 0; | 
 |         } | 
 |     } | 
 |  | 
 |     async _update(takeScreenshot) { | 
 |         let op = this._ops[this._currentOpIndex]; | 
 |  | 
 |         if (!op || (op.time > this._elapsedTime)) { | 
 |             // Not yet. | 
 |             return; | 
 |         } | 
 |  | 
 |         let screenshotTaken = false; | 
 |         switch (op.type) { | 
 |             case 'mousedown': | 
 |                 // Pause timeline to avoid frame not sync. | 
 |                 timeline.pause(); | 
 |                 await __VRT_MOUSE_MOVE__(op.x, op.y); | 
 |                 await __VRT_MOUSE_DOWN__(); | 
 |                 timeline.resume(); | 
 |                 break; | 
 |             case 'mouseup': | 
 |                 timeline.pause(); | 
 |                 await __VRT_MOUSE_MOVE__(op.x, op.y); | 
 |                 await __VRT_MOUSE_UP__(); | 
 |                 if (window.__VRT_RELOAD_TRIGGERED__) { | 
 |                     return; | 
 |                 } | 
 |                 timeline.resume(); | 
 |                 break; | 
 |             case 'mousemove': | 
 |                 timeline.pause(); | 
 |                 await __VRT_MOUSE_MOVE__(op.x, op.y); | 
 |                 timeline.resume(); | 
 |                 break; | 
 |             case 'mousewheel': | 
 |                 let element = document.elementFromPoint(op.x, op.y); | 
 |                 // Here dispatch mousewheel event because echarts used it. | 
 |                 // TODO Consider upgrade? | 
 |                 let event = new WheelEvent('mousewheel', { | 
 |                     // PENDING | 
 |                     // Needs inverse delta? | 
 |                     deltaY: op.deltaY, | 
 |                     clientX: op.x, clientY: op.y, | 
 |                     // Needs bubble to parent container | 
 |                     bubbles: true | 
 |                 }); | 
 |                 element.dispatchEvent(event); | 
 |                 this._isLastOpMousewheel = true; | 
 |                 break; | 
 |             case 'screenshot': | 
 |                 await takeScreenshot(); | 
 |                 screenshotTaken = true; | 
 |                 break; | 
 |             case 'valuechange': | 
 |                 const selector = document.querySelector(op.selector); | 
 |                 selector.value = op.value; | 
 |                 // changing value via js won't trigger `change` event, so trigger it manually | 
 |                 selector.dispatchEvent(new Event('change')); | 
 |                 break; | 
 |         } | 
 |  | 
 |         this._currentOpIndex++; | 
 |  | 
 |         // If next op is an auto screenshot | 
 |         let nextOp = this._ops[this._currentOpIndex]; | 
 |         if (nextOp && nextOp.type === 'screenshot-auto') { | 
 |             let delay = nextOp.delay == null ? 400 : nextOp.delay; | 
 |             // TODO Configuration time | 
 |             await waitTime(delay); | 
 |             await takeScreenshot(); | 
 |             screenshotTaken = true; | 
 |             this._currentOpIndex++; | 
 |         } | 
 |  | 
 |         if (this._isLastOpMousewheel && op.type !== 'mousewheel') { | 
 |             // Only take screenshot after mousewheel finished | 
 |             if (!screenshotTaken) { | 
 |                 await takeScreenshot(); | 
 |             } | 
 |             this._isLastOpMousewheel = false; | 
 |         } | 
 |  | 
 |         return true; | 
 |     } | 
 | }; |