@alexcambose/recjs
Version:
Lightweight user session recorder based on JSON
180 lines (176 loc) • 11.9 kB
JavaScript
import Loop from './Loop'
/** Player class */
class Player {
constructor (el, document) {
this.el = el
this.document = document
this._dataFrameIndex = 0
this._loop = null
this._playing = false
this._onEnd = null
this._previousElementFocused = null
}
/**
* Starts playing a recording
* @example
* recjs.player.play(recjs.recorder.getData(), () => {
* console.log('Finished playing')
* })
* @param {object} data - Recorded data
* @param {function} onEnd - Calls when playing finishes
*/
play (data, onEnd) {
this.data = data
this._onEnd = onEnd
this._loop = new Loop(data.fps, this._renderFrame.bind(this))
this._playing = true
this._loop.start()
}
/**
* Pauses playing
* @example
* recjs.player.pause()
*/
pause () {
this._loop.stop()
this._playing = false
}
/**
* Stops playing
* @example
* recjs.player.stop()
*/
stop () {
this.pause()
this._dataFrameIndex = 0
this._renderFrame() // render once more with the initial frame
}
/**
* Set current frame
* @example
* recjs.player.setFrameIndex(87)
* @param {number} index - Frame index
*/
setFrameIndex (index) {
if (index < this._dataFrameIndex.length) {
this._dataFrameIndex = index
} else {
console.warn(`Can't set frame index to ${index}, only ${this._dataFrameIndex.length - 1} available!`)
}
}
/**
* Get current frame
* @example
* recjs.player.currentFrame()
* @returns {object} Frame object
*/
currentFrame () {
return this.data.frames[this._dataFrameIndex]
}
/**
* Get current frame index
* @example
* recjs.player.currentFrameIndex()
* @returns {number} Frame index
*/
currentFrameIndex () {
return this._dataFrameIndex
}
/**
* Is playing
* @example
* recjs.player.isPlaying()
* @returns {boolean} Returns true if it is playing
*/
isPlaying () {
return this._playing
}
_renderFrame () {
if (this._dataFrameIndex > this.data.frames.length - 1) {
if (this._onEnd) this._onEnd()
this.stop()
return
}
if (this._dataFrameIndex === 0) { // initialize
this.el.scrollTop = this.el.scrollLeft = 0
const pointer = this.document.getElementById('recjs-pointer')
if (pointer) pointer.remove()
}
const frame = this.data.frames[this._dataFrameIndex]
if (frame.mouseX !== null && frame.mouseY !== null) {
this._mouseMove(frame.mouseX, frame.mouseY)
}
if (frame.clickX !== null && frame.clickY !== null) {
this._fireClick(frame.clickX, frame.clickY)
}
if (frame.contextX !== null && frame.contextY !== null) {
this._fireContextMenu(frame.contextX, frame.contextY)
}
if (frame.scrollY) this.el.scrollTop = frame.scrollY
if (frame.scrollX) this.el.scrollLeft = frame.scrollX
if (frame.keypress) this._keypress(frame.keypress)
this._dataFrameIndex++
}
_fireClick (x, y) {
// https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint
const absX = x + this.el.getBoundingClientRect().left
const absY = y + this.el.getBoundingClientRect().top
// we must get the emene tbehind the recjs-pointer so we do x - 1 and y - 1
const element = this.document.elementFromPoint(absX - 1, absY - 1)
if (element.tagName.toLowerCase() === 'input') {
element.focus()
this._previousElementFocused = element
} else {
element.click()
if (this._previousElementFocused) this._previousElementFocused.blur()
}
this._addDot(x, y, 'recjs-clickdot', 'red')
}
_fireContextMenu (x, y) {
this._addDot(x, y, 'recjs-contextkdot', 'blue')
}
_mouseMove (x, y) {
this.el.style.position = 'relative'
let pointer = this.document.getElementById('recjs-pointer')
if (!pointer) {
pointer = this.document.createElement('div')
pointer.id = 'recjs-pointer'
pointer.style.position = 'absolute'
pointer.style.width = '14px'
pointer.style.height = '21px'
pointer.style.backgroundImage = 'url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgaWQ9InN2ZzIiICAgc29kaXBvZGk6ZG9jbmFtZT0iTW91c2UgQ3Vyc29yIEFyb3cgKEZpeGVkKS5zdmciICAgdmlld0JveD0iMCAwIDcyMC43MTA4OSAxMDc5LjQ0OTIiICAgdmVyc2lvbj0iMS4xIiAgIGlua3NjYXBlOnZlcnNpb249IjAuOTEgcjEzNzI1IiAgIHdpZHRoPSI3MjAuNzEwODgiICAgaGVpZ2h0PSIxMDc5LjQ0OTIiPiAgPGRlZnMgICAgIGlkPSJkZWZzMTMzIiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiICAgICBpbmtzY2FwZTpzaG93cGFnZXNoYWRvdz0iZmFsc2UiICAgICBpbmtzY2FwZTp6b29tPSIwLjUiICAgICBoZWlnaHQ9IjBweCIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMiIgICAgIGlua3NjYXBlOmN4PSItNTUyLjA3MjE3IiAgICAgaW5rc2NhcGU6Y3k9IjY3OS42OTIzMSIgICAgIGlua3NjYXBlOm9iamVjdC1wYXRocz0idHJ1ZSIgICAgIGZpdC1tYXJnaW4tcmlnaHQ9IjAiICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiAgICAgaW5rc2NhcGU6c25hcC1iYm94PSJmYWxzZSIgICAgIHNob3dncmlkPSJmYWxzZSIgICAgIHdpZHRoPSIwcHgiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGlua3NjYXBlOndpbmRvdy14PSIwIiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5IiAgICAgZml0LW1hcmdpbi1ib3R0b209IjAiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGlua3NjYXBlOmJib3gtcGF0aHM9InRydWUiICAgICBpbmtzY2FwZTpiYm94LW5vZGVzPSJ0cnVlIiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA2MSIgICAgIHNob3dib3JkZXI9ImZhbHNlIiAgICAgZml0LW1hcmdpbi10b3A9IjAiICAgICBpbmtzY2FwZTpzbmFwLWludGVyc2VjdGlvbi1wYXRocz0idHJ1ZSIgICAgIGlua3NjYXBlOm9iamVjdC1ub2Rlcz0idHJ1ZSIgICAgIGlua3NjYXBlOnNuYXAtc21vb3RoLW5vZGVzPSJ0cnVlIj4gICAgPGlua3NjYXBlOmdyaWQgICAgICAgdHlwZT0ieHlncmlkIiAgICAgICBpZD0iZ3JpZDQzNTkiICAgICAgIGVtcHNwYWNpbmc9IjUwIiAgICAgICBvcmlnaW54PSIwIiAgICAgICBvcmlnaW55PSI1OC43MzgzMiIgLz4gIDwvc29kaXBvZGk6bmFtZWR2aWV3PiAgPGcgICAgIGlkPSJsYXllcjIiICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTU1NS4xODk5LC0xMTIuMDg4MzYpIj4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LXNpemU6bWVkaXVtO2xpbmUtaGVpZ2h0Om5vcm1hbDtmb250LWZhbWlseTpzYW5zLXNlcmlmO3RleHQtaW5kZW50OjA7dGV4dC1hbGlnbjpzdGFydDt0ZXh0LWRlY29yYXRpb246bm9uZTt0ZXh0LWRlY29yYXRpb24tbGluZTpub25lO3RleHQtZGVjb3JhdGlvbi1zdHlsZTpzb2xpZDt0ZXh0LWRlY29yYXRpb24tY29sb3I6IzAwMDAwMDtsZXR0ZXItc3BhY2luZzpub3JtYWw7d29yZC1zcGFjaW5nOm5vcm1hbDt0ZXh0LXRyYW5zZm9ybTpub25lO2RpcmVjdGlvbjpsdHI7YmxvY2stcHJvZ3Jlc3Npb246dGI7d3JpdGluZy1tb2RlOmxyLXRiO2Jhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO3RleHQtYW5jaG9yOnN0YXJ0O3doaXRlLXNwYWNlOm5vcm1hbDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTAwO3N0cm9rZS1saW5lY2FwOnNxdWFyZTtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSA1NTUuMTg5OSwxMTIuMDg4MzYgMCwxMjAuNzEwOTQgMCw5MjAuNzEwOSAyMzIuNDIxODgsLTIzMi40MjE4NCAxMTEuOTA0MjksMjcwLjQ0OTI0IDE2OS43NjM2MywtODQuODgyOCAtMTE0LjA5MzcxLC0yNzMuODU1NSAzMjAuNzE0ODEsMCB6IiAgICAgICBpZD0icGF0aDQzOTIiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjIiAvPiAgICA8cGF0aCAgICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxIiAgICAgICBkPSJNIDUwLDUwIDUwLDg1MCAyNTAsNjUwIDM2OS45OTYwOSw5NDAuMDAxOTUgNDQ5Ljk4ODI4LDkwMC4wMDU4NiAzMjUsNjAwIGwgMjc1LDAgeiIgICAgICAgaWQ9InJlY3Q1NiIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NjY2NjYyIgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTU1LjE4OTksMTgyLjc5OTMpIiAvPiAgPC9nPiAgPG1ldGFkYXRhICAgICBpZD0ibWV0YWRhdGExMzEiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yaz4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8Y2M6bGljZW5zZSAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9wdWJsaWNkb21haW4vemVyby8xLjAvIiAvPiAgICAgICAgPGRjOnB1Ymxpc2hlcj4gICAgICAgICAgPGNjOkFnZW50ICAgICAgICAgICAgIHJkZjphYm91dD0iaHR0cDovL29wZW5jbGlwYXJ0Lm9yZy8iPiAgICAgICAgICAgIDxkYzp0aXRsZT5PcGVuY2xpcGFydDwvZGM6dGl0bGU+ICAgICAgICAgIDwvY2M6QWdlbnQ+ICAgICAgICA8L2RjOnB1Ymxpc2hlcj4gICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPiAgICAgICAgPGRjOmRhdGU+MjAxMS0xMi0yMFQwNDozMDowNDwvZGM6ZGF0ZT4gICAgICAgIDxkYzpkZXNjcmlwdGlvbj5BIHBpeGVsLWFydCBzdHlsZSBhcnJvdyBtb3VzZSBjdXJzb3IuIE1hZGUgdXAgb2YgaW5kaXZpZHVhbCBzcXVhcmVzLCBmb3IgZWFzeSBtYW5pcHVsYXRpb248L2RjOmRlc2NyaXB0aW9uPiAgICAgICAgPGRjOnNvdXJjZT5odHRwczovL29wZW5jbGlwYXJ0Lm9yZy9kZXRhaWwvMTY2MzU2L21vdXNlLWN1cnNvci0tLWFycm93LWJ5LWhlbGxvY2F0Zm9vZDwvZGM6c291cmNlPiAgICAgICAgPGRjOmNyZWF0b3I+ICAgICAgICAgIDxjYzpBZ2VudD4gICAgICAgICAgICA8ZGM6dGl0bGU+aGVsbG9jYXRmb29kPC9kYzp0aXRsZT4gICAgICAgICAgPC9jYzpBZ2VudD4gICAgICAgIDwvZGM6Y3JlYXRvcj4gICAgICAgIDxkYzpzdWJqZWN0PiAgICAgICAgICA8cmRmOkJhZz4gICAgICAgICAgICA8cmRmOmxpPmFycm93PC9yZGY6bGk+ICAgICAgICAgICAgPHJkZjpsaT5jdXJzb3I8L3JkZjpsaT4gICAgICAgICAgICA8cmRmOmxpPm1vdXNlPC9yZGY6bGk+ICAgICAgICAgICAgPHJkZjpsaT5waXhlbDwvcmRmOmxpPiAgICAgICAgICA8L3JkZjpCYWc+ICAgICAgICA8L2RjOnN1YmplY3Q+ICAgICAgPC9jYzpXb3JrPiAgICAgIDxjYzpMaWNlbnNlICAgICAgICAgcmRmOmFib3V0PSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9wdWJsaWNkb21haW4vemVyby8xLjAvIj4gICAgICAgIDxjYzpwZXJtaXRzICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zI1JlcHJvZHVjdGlvbiIgLz4gICAgICAgIDxjYzpwZXJtaXRzICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zI0Rpc3RyaWJ1dGlvbiIgLz4gICAgICAgIDxjYzpwZXJtaXRzICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zI0Rlcml2YXRpdmVXb3JrcyIgLz4gICAgICA8L2NjOkxpY2Vuc2U+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+PC9zdmc+)'
pointer.style.backgroundSize = 'cover'
this.el.appendChild(pointer)
}
pointer.style.top = y + this.el.scrollTop + 'px'
pointer.style.left = x + this.el.scrollLeft + 'px'
}
_keypress (keycode) {
if (this._previousElementFocused) {
if (keycode === 8) { // backspace
const value = this._previousElementFocused.value
this._previousElementFocused.value = value.substring(0, value.length - 1)
} else {
this._previousElementFocused.value += String.fromCharCode(keycode)
}
}
}
_addDot (x, y, className, color) {
this.el.style.position = 'relative'
let dot = this.document.createElement('recjs')
dot.className = className
dot.style.position = 'absolute'
dot.style.width = '10px'
dot.style.height = '10px'
dot.style.backgroundColor = color
dot.style.opacity = '.6'
dot.style.borderRadius = '100%'
dot.style.top = y + this.el.scrollTop + 'px'
dot.style.left = x + this.el.scrollLeft + 'px'
this.el.appendChild(dot)
setTimeout(() => {
dot.remove()
}, 3000)
}
}
export default Player