UNPKG

terminalizer-player

Version:
1,041 lines (834 loc) 28.7 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("jquery"), require("xterm")["Terminal"]); else if(typeof define === 'function' && define.amd) define("webpackNumbers", ["jquery", ["xterm","Terminal"]], factory); else if(typeof exports === 'object') exports["webpackNumbers"] = factory(require("jquery"), require("xterm")["Terminal"]); else root["webpackNumbers"] = factory(root["$"], root["Terminal"]); })(this, function(__WEBPACK_EXTERNAL_MODULE__0__, __WEBPACK_EXTERNAL_MODULE__2__) { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 1); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE__0__; /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(4); module.exports = __webpack_require__(3); /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE__2__; /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { // extracted by mini-css-extract-plugin /***/ }), /* 4 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); // EXTERNAL MODULE: external {"commonjs":"jquery","commonjs2":"jquery","amd":"jquery","root":"$"} var external_commonjs_jquery_commonjs2_jquery_amd_jquery_root_$_ = __webpack_require__(0); var external_commonjs_jquery_commonjs2_jquery_amd_jquery_root_$_default = /*#__PURE__*/__webpack_require__.n(external_commonjs_jquery_commonjs2_jquery_amd_jquery_root_$_); // EXTERNAL MODULE: external {"commonjs":["xterm","Terminal"],"commonjs2":["xterm","Terminal"],"amd":["xterm","Terminal"],"root":"Terminal"} var external_commonjs_xterm_Terminal_commonjs2_xterm_Terminal_amd_xterm_Terminal_root_Terminal_ = __webpack_require__(2); // CONCATENATED MODULE: ./src/js/terminalizer.js // Third-party Scripts /** * Terminalizer Web Player * https://terminalizer.com * * @author Mohammad Fares <faressoft.com@gmail.com> */ function Terminalizer(element, options) { var self = this; /** * A reference to the DOM element * @type {Object} */ self.element = element; /** * A reference to the jQuery element * @type {Object} */ self.$element = $(element); /** * The terminal instance * @type {Object} */ self._terminal = null; /** * The frames * in the format [ * { * content, * delay, * duration, * startTime, * endTime * }, * .. * ] * @type {Array} */ self._frames = null; /** * The summation of the adjusted frames delays * @type {Array} */ self._totalDuration = 0; /** * The current time of the player * @type {Array} */ self._currentTime = 0; /** * The playing timer * @type {Object} */ self._timer = null; /** * The time at the last timer tick in ms * @type {Number} */ self._lastTickTime = null; /** * Is playing * @type {Boolean} */ self._isPlaying = false; /** * Is the played at least once * @type {Boolean} */ self._isStarted = false; /** * Is blocked for the rendering via a timer's tick or jumping * @type {Boolean} */ self._isRendering = false; /** * The index of the last rendered frame * @type {Number} */ self._lastRenderedFrame = -1; /** * HTML template for the start SVG icon * @type {String} */ self._startTemplate = '<?xml version="1.0" ?><svg id="Layer_1"' + ' style="enable-background:new 0 0 30 30;" version="1.1" viewBox="0 0 30 30"' + ' xml:space="preserve" xmlns="http://www.w3.org/2000/svg"' + ' xmlns:xlink="http://www.w3.org/1999/xlink">' + '<polygon points="6.583,3.186 5,4.004 5,15 26,15 26.483,14.128 "/>' + '<polygon points="6.583,26.814 5,25.996 5,15 26,15 26.483,15.872 "/>' + '<circle cx="26" cy="15" r="1"/><circle cx="6" cy="4" r="1"/>' + '<circle cx="6" cy="26" r="1"/></svg>'; /** * HTML template for the player * @type {String} */ self._playerTemplate = '<div class="terminalizer-player">' + '<div class="cover"></div>' + '<div class="start">' + self._startTemplate + '</div>' + '<div class="terminalizer"></div>' + '<div class="controller">' + '<div class="play"><span class="icon"></span></div>' + '<div class="pause"><span class="icon"></span></div>' + '<div class="timer">00:00</div>' + '<div class="progressbar-wrapper">' + '<div class="progressbar">' + '<div class="progress"></div>' + '</div></div></div></div>'; /** * HTML template for the terminal * @type {String} */ self._terminalTemplate = '<div class="terminalizer">' + '<div class="terminalizer-frame">' + '<div class="terminalizer-titlebar">' + '<div class="buttons">' + '<div class="close-button"></div>' + '<div class="minimize-button"></div>' + '<div class="maximize-button"></div>' + '</div><div class="title"></div>' + '</div>' + '<div class="terminalizer-body"></div>' + '</div></div>'; /** * Options * @type {Object} */ self._options = $.extend({ recordingFile: null, realTiming: false, speedFactor: 1.0, beforeMiddleware: null, afterMiddleware: null, controls: true, repeat: false, autoplay: false, autofocus: true, thumbnailTime: 0 }, options); // Initialize Terminalizer self._init().then(function (result) { // Autoplay is enabled if (self._options.autoplay) { return self.play(); } })["catch"](function (error) { throw new Error(error); }); return self; } /** * Initialize Terminalizer * * @return {Promise} */ Terminalizer.prototype._init = function () { var self = this; var element = self.element; var $element = self.$element; // Load the recording file return self._loadJSON(self._options.recordingFile).then(function (result) { // Store a reference to the frames self._frames = result.frames || result.records; // Marge the plugin's options with recording file's configs self._options = $.extend(result.config, self._options); // If the controls is enabled if (self._options.controls) { // Use null frame self._options.frameBox.title = null; self._options.frameBox.type = null; self._options.frameBox.style = {}; // Set the background color if (self._options.theme.background == 'transparent') { self._options.frameBox.style.background = 'black'; } else { self._options.frameBox.style.background = self._options.theme.background; } // Add paddings to the frame self._options.frameBox.style.padding = '10px'; // Remove the watermark self._options.watermark.imagePath = null; } // Create a terminal instance self._terminal = new Terminal({ cols: self._options.cols, rows: self._options.rows, cursorStyle: self._options.cursorStyle, fontFamily: self._options.fontFamily, fontSize: self._options.fontSize, lineHeight: self._options.lineHeight, letterSpacing: self._options.letterSpacing, allowTransparency: true, scrollback: 0, theme: self._options.theme }); // Insert the player template $element.html($(self._playerTemplate)); // Insert the terminal template $element.find('.terminalizer').replaceWith(self._terminalTemplate); if (self._options.frameBox.type) { $element.find('.terminalizer-frame').addClass('terminalizer-' + self._options.frameBox.type); } if (self._options.frameBox.type && self._options.frameBox.title) { $element.find('.terminalizer-frame .title').text(self._options.frameBox.title); } $element.find('.terminalizer-frame').css(self._options.frameBox.style); // If the controls is enabled if (self._options.controls) { $element.find('.terminalizer-player').addClass('controls'); } // If the frame not null, push the start button down if (self._options.frameBox.type) { $element.find('.terminalizer-player').addClass('framed'); } // Use smaller start icon if (self._options.rows < 10) { $element.find('.terminalizer-player').addClass('small'); } // Open the terminal self._terminal.open($element.find('.terminalizer-body')[0]); // A hack to keep the focus on the terminal Object.defineProperty(self._terminal._core, 'isFocused', { get: function get() { return true; } }); if (self._options.autofocus) { self._terminal.focus(); } // Initialize the controller self._initController(); // A Wrapper around the refresh event of the Terminal self._initRenderedEmitter(); // Emit the event on the Terminalizer element self._emit('init'); // Adjust the delays of the frames, considering to the options self._adjustDelays(); // Calculate and set the duration, startTime, and endTime for each frame self._calculateTiming(); // Sum the adjusted frames delays self._totalDuration = self._calculateTotalDuration(); // Start the playing timer self._lastTickTime = Date.now(); self._timer = setInterval(self._tick.bind(self), 1); // Add a watermark if (self._options.watermark.imagePath) { return self._addWatermark(self._options.watermark); } }).then(function () { return self.jump(self._options.thumbnailTime, false); }).then(function () { // Set the current time to the time of the frame self._currentTime = 0; // Update the player (time and progressbar) self._updatePlayer(); }); }; /** * Initialize the controller * * - Attach event handlers */ Terminalizer.prototype._initController = function () { var self = this; /** * A callback function for the event: * When the progressbar is clicked * * @param {Object} event */ self.$element.find('.controller .progressbar').on('click', function (event) { // Not started yet if (!self._isStarted) { return false; } var length = $(this).width(); var position = event.offsetX; self.jump(Math.floor(self._totalDuration * position / length)); return false; }); /** * A callback function for the event: * When the start button is clicked * * @param {Object} event */ self.$element.find('.cover, .start').on('click', function (event) { self.play(); return false; }); /** * A callback function for the event: * When the play button is clicked * * @param {Object} event */ self.$element.find('.controller .play').on('click', function (event) { self.play(); return false; }); /** * A callback function for the event: * When the pause button is clicked * * @param {Object} event */ self.$element.find('.controller .pause').on('click', function (event) { self.pause(); return false; }); }; /** * Wrap the refresh event of the Terminal and emit a `rendered` event * on the Terminalizer element when all the write operations are executed */ Terminalizer.prototype._initRenderedEmitter = function () { var self = this; self._terminal.on('refresh', function refreshHandler() { // Not all write operations are executed yet if (this._writeInProgress) { return; } // Emit the event on the Terminalizer element self._emit('rendered'); }); }; /** * Adjust the delays of the frames, considering to the options * * - frameDelay * - Delay between frames in ms * - If the value is `auto` use the actual recording delays * * - maxIdleTime * - Maximum delay between frames in ms * - Ignored if the `frameDelay` isn't set to `auto` * - Set to `auto` to prevnt limiting the max idle time * * - speedFactor * - Multiply the frames delays by this factor */ Terminalizer.prototype._adjustDelays = function () { var self = this; // Foreach frame self._frames.forEach(function (frame) { var delay = frame.delay; // Adjust the delay according to the options if (self._options.frameDelay != 'auto') { delay = self._options.frameDelay; } else if (self._options.maxIdleTime != 'auto' && delay > self._options.maxIdleTime) { delay = self._options.maxIdleTime; } // Apply speedFactor delay = delay * self._options.speedFactor; // Set the adjusted delay frame.delay = delay; }); }; /** * Calculate and set the duration, startTime, and endTime for each frame */ Terminalizer.prototype._calculateTiming = function () { var currentTime = 0; var framesCount = this._frames.length; var frames = this._frames; // Foreach frame frames.forEach(function (frame, index) { // Set the duration (the delay of the next frame) // The % is used to take the delay of the first frame // as the duration of the last frame var duration = frames[(index + 1) % framesCount].delay; // Set timing values for the current frame frame.duration = duration; frame.startTime = currentTime; frame.endTime = currentTime + duration; currentTime = currentTime + duration; }); }; /** * Sum the adjusted frames delays * * @return {Number} */ Terminalizer.prototype._calculateTotalDuration = function () { return this._frames.reduce(function (carry, frame) { return carry + frame.delay; }, 0); }; /** * Get the frame's index at a specific time * * @param {Number} time * @param {Number} fromIndex (default: 0) * @return {Number} */ Terminalizer.prototype._findFrameAt = function (time, fromIndex) { var frame = null; // Default value for fromIndex if (typeof fromIndex == 'undefined') { fromIndex = 0; } // Foreach frame for (var i = fromIndex; i < this._frames.length; i++) { frame = this._frames[i]; if (frame.startTime <= time && time < frame.endTime) { return i; } } // The endTime of the final frame belongs to it if (frame.startTime <= time && time <= frame.endTime) { return this._frames.length - 1; } return -1; }; /** * Check if the time belongs to the frame's duration * * @param {Number} time * @param {Number} frameIndex * @return {Number} */ Terminalizer.prototype._isFrameAt = function (time, frameIndex) { var frame = this._frames[frameIndex]; if (typeof frame == 'undefined') { return false; } if (frame.startTime <= time && time < frame.endTime) { return true; } return false; }; /** * Add a watermark and wait until it is fully loaded * * @param {Object} watermarkConfig {imagePath, style} * @return {Promise} */ Terminalizer.prototype._addWatermark = function (watermarkConfig) { var watermarkImg = document.createElement('img'); $(watermarkImg).addClass('terminalizer-watermark'); $(watermarkImg).attr('src', watermarkConfig.imagePath); $(watermarkImg).css(watermarkConfig.style); this.$element.find('.terminalizer-frame').prepend(watermarkImg); return new Promise(function (resolve, reject) { $('.terminalizer-watermark').on('load', resolve); }); }; /** * Render a frame * * Flow: * - Wait for the _options.beforeMiddleware * - Render the frame and wait for the rendring * - Wait for the _options.afterMiddleware * * @param {Number} frameIndex * @param {Boolean} skipMiddlewares * @param {Function} callback */ Terminalizer.prototype._renderFrame = function (frameIndex, skipMiddlewares, callback) { var self = this; var tasks = []; var frame = self._frames[frameIndex]; // If beforeMiddleware is set if (self._options.beforeMiddleware && !skipMiddlewares) { tasks.push(function (callback) { self._options.beforeMiddleware.call(self, frame, frameIndex, callback.bind(null, null, null)); }); } // Rendering tasks.push(function (callback) { // Render the frame self._terminal.write(frame.content); /** * A callback function for the event: * When the write operation is executed and * the changes are rendered */ self.$element.one('rendered', function () { // An extra tick to allow the browser to complete canvas painting setTimeout(function () { callback(); }); }); }); // If afterMiddleware is set if (self._options.afterMiddleware && !skipMiddlewares) { tasks.push(function (callback) { self._options.afterMiddleware.call(self, frame, frameIndex, callback.bind(null, null, null)); }); } self._series(tasks, function (error, result) { callback(); }); }; /** * Load, and parse JSON files * * @param {String} url * @return {Promise} */ Terminalizer.prototype._loadJSON = function (url) { return new Promise(function (resolve, reject) { $.getJSON(url).done(resolve).fail(function (jqxhr, textStatus, error) { reject('Failed to load ' + url); }); }); }; /** * The playing timer's callback */ Terminalizer.prototype._tick = function () { var self = this; var tickDelay = 0; tickDelay = Date.now() - self._lastTickTime; self._lastTickTime = Date.now(); // Not playing if (!self._isPlaying) { return; } // Still rendering the last frame if (self._isRendering) { return; } if (self._currentTime < self._totalDuration) { self._currentTime += tickDelay; self._updatePlayer(); } if (self._currentTime > self._totalDuration) { self._currentTime = self._totalDuration; } // Already rendered if (self._lastRenderedFrame != -1 && self._isFrameAt(self._currentTime, self._lastRenderedFrame)) { return; } // Reached the end if (self._lastRenderedFrame == self._frames.length - 1 && self._currentTime == self._totalDuration) { // Emit the event on the Terminalizer element self._emit('playingCompleted'); // Repeat is enabled if (self._options.repeat) { self._currentTime = 0; return; } return self.pause(); } // Check if current time belongs to the next frame's duration if (self._isFrameAt(self._currentTime, self._lastRenderedFrame + 1)) { self._lastRenderedFrame = self._lastRenderedFrame + 1; } else { self._lastRenderedFrame = self._findFrameAt(self._currentTime); } // The start point of the rendering cycle self._isRendering = true; // Render the frame self._renderFrame(self._lastRenderedFrame, false, function () { // To discard the time spent rendering self._lastTickTime = Date.now(); // The end point of the rendering cycle self._isRendering = false; }); }; /** * Update the player (time and progressbar) */ Terminalizer.prototype._updatePlayer = function () { var progress = this._currentTime / this._totalDuration * 100; var time = this._formatTime(this._currentTime); this.$element.find('.terminalizer-player .progress').width(progress + '%'); this.$element.find('.terminalizer-player .timer').text(time); }; /** * Emit an event on the Terminalizer element * * @param {String} eventType * @param {Array} extraParameters (Optional) */ Terminalizer.prototype._emit = function (eventType, extraParameters) { var self = this; // Default value for extraParameters if (typeof extraParameters == 'undefined') { extraParameters = []; } setTimeout(function () { self.$element.trigger(eventType, extraParameters); }); }; /** * Format time as MM:SS * * @param {Number} time time in ms * @return {Number} */ Terminalizer.prototype._formatTime = function (time) { var minutes = Math.floor(time / 60000); var seconds = parseInt((time - minutes * 60000) / 1000); if (minutes < 10) { minutes = '0' + minutes; } if (seconds < 10) { seconds = '0' + seconds; } return minutes + ':' + seconds; }; /** * Execute a set of async tasks in sequence * * @param {Array} tasks * @param {Function} callback */ Terminalizer.prototype._series = function (tasks, callback) { if (tasks.length == 0) { return callback(null); } var runTask = function runTask(index) { tasks[index](function (error, result) { if (typeof error != 'undefined' && error) { return callback(error); } // All tasks have been executed if (index == tasks.length - 1) { return callback(null); } return runTask(index + 1); }); }; runTask(0); }; /** * Get the number of frames * * @return {Number} */ Terminalizer.prototype.getFramesCount = function () { return this._frames.length; }; /** * Start/resume playing the frames * * @return {Promise} */ Terminalizer.prototype.play = function () { var self = this; return new Promise(function (resolve, reject) { // Reached the end or not started yet if (self._lastRenderedFrame == self.getFramesCount() - 1 && self._currentTime == self._totalDuration || !self._isStarted) { self._currentTime = 0; // Set the _isStarted flag self._isStarted = true; // Reset the terminal self._terminal.reset(); /** * A callback function for the event: * When the write operation is executed and * the changes are rendered */ return self.$element.one('rendered', function () { resolve(); }); } resolve(); }).then(function () { // Add started class self.$element.find('.terminalizer-player').addClass('started'); // Set the _isPlaying flag self._isPlaying = true; // Emit the event on the Terminalizer element self._emit('playingStarted'); // Add playing class self.$element.find('.terminalizer-player').addClass('playing'); }); }; /** * Pause playing the frames */ Terminalizer.prototype.pause = function () { // Unset the _isPlaying flag this._isPlaying = false; // Remove playing class this.$element.find('.terminalizer-player').removeClass('playing'); // Emit the event on the Terminalizer element this._emit('playingPaused'); }; /** * Change the current time of the player * * @param {Number} time * @param {Boolean} updatePlayer if false, just render the frame * without setting `_currentTime` and * without calling `updatePlayer()` * (Optional) (Default: true) * @return {Promise} */ Terminalizer.prototype.jump = function (time, updatePlayer) { var self = this; var tasks = []; // The frame to jump to var frameIndex = null; // Is currently playing var isPlaying = self._isPlaying; // Default value for updatePlayer if (typeof updatePlayer == 'undefined') { updatePlayer = true; } // The start point of the rendering cycle self._isRendering = true; self._isPlaying = false; if (updatePlayer) { // Set the current time to the time of the frame self._currentTime = time; // Update the player (time and progressbar) self._updatePlayer(); } return new Promise(function (resolve, reject) { /** * A callback function for the event: * When the write operation is executed and * the changes are rendered */ self.$element.one('rendered', function () { // Get the frame's index frameIndex = self._findFrameAt(time); // Foreach frame <= the frame to jump too for (var i = 0; i <= frameIndex; i++) { self._terminal.write(self._frames[i].content); } /** * A callback function for the event: * When the write operation is executed and * the changes are rendered */ self.$element.one('rendered', function () { // The end point of the rendering cycle self._isRendering = false; self._isPlaying = isPlaying; resolve(); }); }); // Reset the terminal self._terminal.reset(); }); }; // CONCATENATED MODULE: ./src/js/index.js /*! * Terminalizer Web Player * https://terminalizer.com * * @author Mohammad Fares <faressoft.com@gmail.com> */ /** * Terminalizer jQuery Plugin * * - If the first argument is an object * - A new Terminalizer instance will be attached to the element * - The passed object will be used to override the defaults * - If the first argument is a string * - The Terminalizer's method that matches the string will be called * - Any extra arguments will be passed to the method * * Methods: * ------------ * - getFramesCount * - play * - pause * - jump * * Events: * ------------ * - init * - playingCompleted * - playingStarted * - playingPaused * - rendered */ external_commonjs_jquery_commonjs2_jquery_amd_jquery_root_$_default.a.fn.terminalizer = function () { var options = {}; var method = {}; var methodArgs = []; if (arguments.length == 0) { throw new Error('The options argument is required'); } if (typeof arguments[0] == 'string') { method = arguments[0]; methodArgs = Array.prototype.slice.call(arguments, 1); if (typeof this.data('terminalizer') == 'undefined') { throw new Error('It is not Terminalizer element'); } return this.data('terminalizer')[method].apply(this.data('terminalizer'), methodArgs); } if (typeof this.data('terminalizer') != 'undefined') { return this; } options = arguments[0]; this.data('terminalizer', new Terminalizer(this[0], options)); return this; }; /***/ }) /******/ ]); }); //# sourceMappingURL=terminalizer.js.map