From 44d632be2a368837570ad8403a37048353292c69 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 10 Feb 2022 17:31:57 +0100 Subject: [PATCH] Implements pdf-player custom element --- .../CustomHtmlElements/pdf-player.ts | 400 ++++++++++++++++++ api/js/etemplate/et2_widget_video.ts | 4 +- package-lock.json | 11 + package.json | 1 + 4 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 api/js/etemplate/CustomHtmlElements/pdf-player.ts diff --git a/api/js/etemplate/CustomHtmlElements/pdf-player.ts b/api/js/etemplate/CustomHtmlElements/pdf-player.ts new file mode 100644 index 0000000000..dac1ab4e91 --- /dev/null +++ b/api/js/etemplate/CustomHtmlElements/pdf-player.ts @@ -0,0 +1,400 @@ +/** + * EGroupware Custom Html Elements - pdf player Web Components + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link https://www.egroupware.org + * @author Hadi Nategh + * @copyright EGroupware GmbH + */ + +import pdfjs from "@bundled-es-modules/pdfjs-dist/build/pdf"; + +/* + This web component allows to display and play pdf file like a video player widget/element. Its attributes and + methodes are mostley identical as video html. No controls attribute supported yet. +*/ +pdfjs.GlobalWorkerOptions.workerSrc = 'node_modules/@bundled-es-modules/pdfjs-dist/build/pdf.worker.js'; + +/** + * + */ +type PdfViewArray = { + pdf: any, + zoom: number, + currentPage: number +}; + +// Create a class for the element +class pdf_player extends HTMLElement { + + /** + * shadow dom container + * @private + */ + private readonly _shadow = null; + /** + * wrapper container holds canvas + * @private + */ + private readonly _wrapper : HTMLDivElement; + /** + * Canvas element to render pdf + * @private + */ + private _canvas : HTMLCanvasElement; + /** + * Styling contianer + * @private + */ + private readonly _style : HTMLStyleElement; + /** + * keeps duration time internally + * @private + */ + private _duration : number = 0; + /** + * keeps currentTime internally + * @private + */ + private _currentTime : number = 0; + /** + * Keeps playing state internally + * @private + */ + private __playing: boolean = false; + /** + * keeps playing interval id + * @private + */ + private __playingInterval : number = 0; + /** + * keeps play back rate + * @private + */ + private _playBackRate : number = 1000; + /** + * keeps ended state of playing pdf + * @private + */ + private _ended : boolean = false; + /** + * keep paused state + * @private + */ + private _paused : boolean = false; + /** + * keeps pdf doc states + * @private + */ + private __pdfViewState : PdfViewArray = { + pdf: null, + currentPage: 1, + zoom: 1 + }; + + constructor() { + + super(); + + // Create a shadow root + this._shadow = this.attachShadow({mode: 'open'}); + + // Create wrapper + this._wrapper = document.createElement('div'); + this._wrapper.setAttribute('class','wrapper'); + + // Create some CSS to apply to the shadow dom + this._style = document.createElement('style'); + + this._style.textContent = '.wrapper {' + + 'width: 100%;' + + 'height: auto;' + + 'display: block;' + + '}'+ + '.wrapper canvas {' + + 'width: 100%;'+ + 'height: auto;'+ + '}'; + + // attach to the shadow dom + this._shadow.appendChild(this._style); + this._shadow.appendChild(this._wrapper); + } + + /** + * set observable attributes + * @return {string[]} + */ + static get observedAttributes() { + return ['src', 'type']; + } + + /** + * Gets called on observable attributes changes + * @param name attribute name + * @param _ + * @param newVal new value + */ + attributeChangedCallback(name, _, newVal) { + switch(name) + { + case 'src': + this.__buildPDFView(newVal); + break; + case 'type': + // do nothing + break; + } + } + + /** + * init/update pdf tag + * @param _value + * @private + */ + private __buildPDFView(_value) + { + this._canvas = document.createElement('canvas'); + this._wrapper.appendChild(this._canvas); + let longTask = pdfjs.getDocument(_value); + longTask.promise.then((pdf) => { + + this.__pdfViewState.pdf = pdf; + this._duration = this.__pdfViewState.pdf._pdfInfo.numPages; + + // initiate the pdf file viewer for the first time after loading + this.__render(1).then(_ =>{ + this.__pushEvent('loadedmetadata'); + }); + }); + } + + /** + * Render given page from pdf into the canvas container + * + * @param _page + * @private + */ + private __render(_page) + { + if (!this.__pdfViewState.pdf) return; + let p = _page || this.__pdfViewState.currentPage; + let self = this; + return this.__pdfViewState.pdf.getPage(p).then((page) => { + let canvasContext = self._canvas.getContext('2d'); + let viewport = page.getViewport({scale:self.__pdfViewState.zoom}); + self._canvas.width = viewport.width; + self._canvas.height = viewport.height; + // don't ty to render if page is under proccess + if (typeof page.intentStates.display == 'undefined') + { + page.render({ + canvasContext: canvasContext, + viewport: viewport + }); + } + }); + } + + /** + * Creates event and dispatches it + * @param _name + */ + private __pushEvent(_name: string) + { + let event = document.createEvent("Event"); + event.initEvent(_name, true, true); + this.dispatchEvent(event); + } + + /**************************** PUBLIC ATTRIBUTES & METHODES *************************************************/ + + /****************************** ATTRIBUTES **************************************/ + + /** + * set src + * @param _value + */ + set src(_value) + { + this._wrapper.children.forEach(_ch=>{ + _ch.remove(); + }); + this.__buildPDFView(_value); + } + + /** + * get src + * @return string returns comma separated sources + */ + get src () + { + return this.src; + } + + /** + * currentTime + * @param _time + */ + set currentTime(_time : number) + { + let time = Math.floor(_time < 1 ? 1 : _time); + + if (time>this._duration) + { + // set ended state to true as it's the last page of pdf + this._ended = true; + // don't go further because it's litterally the last page + return; + } + + // set ended state to false as it's not the end of the pdf + this._ended = false; + this._currentTime = time; + this.__pdfViewState.currentPage = time; + this.__render(time); + } + + /** + * get currentTime + * @return {number} + */ + get currentTime() + { + return this._currentTime; + } + + /** + * get duration time + */ + get duration() + { + return this._duration; + } + + /** + * get paused attribute + */ + get paused() + { + return this._paused; + } + + /** + * set muted attribute + * @param _value + */ + set muted(_value: boolean) + { + return; + } + + /** + * get muted attribute + */ + get muted() + { + return true; + } + + set ended (_value : boolean) + { + this._ended = _value; + } + + /** + * get ended attribute + */ + get ended() + { + return this._ended; + } + + /** + * set playbackRate + * @param _value + */ + set playbackRate(_value: number) + { + this._playBackRate = _value*1000; + } + + /** + * get playbackRate + */ + get playbackRate() + { + return this._playBackRate; + } + + /** + * set volume + */ + set volume(_value: number) + { + return; + } + + /** + * get volume + */ + get volume() + { + return 0; + } + + + /************************************************* METHODES ******************************************/ + + /** + * Play + */ + play() + { + this.__playing = true; + let self = this; + return new Promise(function(_resolve, _reject){ + self.__playingInterval = window.setInterval(_=>{ + if (self.currentTime >= self._duration) + { + self.ended = true; + self.pause(); + } + self.currentTime +=1; + self.__pushEvent('timeupdate'); + }, self._playBackRate); + _resolve(); + }); + } + + /** + * pause + */ + pause() + { + this.__playing = false; + this._paused = true; + window.clearInterval(this.__playingInterval); + } + + /** + * seek previous page + */ + prevPage() + { + this.currentTime -=1; + } + + /** + * seek next page + */ + nextPage() + { + this.currentTime +=1; + } +} + +// Define pdf-player element +customElements.define('pdf-player', pdf_player); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_video.ts b/api/js/etemplate/et2_widget_video.ts index a284773733..701ea2024d 100644 --- a/api/js/etemplate/et2_widget_video.ts +++ b/api/js/etemplate/et2_widget_video.ts @@ -20,6 +20,7 @@ import {ClassWithAttributes} from "./et2_core_inheritance"; import {WidgetConfig, et2_register_widget} from "./et2_core_widget"; import {et2_IDOMNode} from "./et2_core_interfaces"; import "./CustomHtmlElements/multi-video"; +import "./CustomHtmlElements/pdf-player"; /** * This widget represents the HTML5 video tag with all its optional attributes @@ -174,7 +175,8 @@ export class et2_video extends et2_baseWidget implements et2_IDOMNode return; } //Create Video tag - this.video = jQuery(document.createElement(this._isYoutube()?"div":(this.options.multi_src ? "multi-video" : "video"))) + this.video = jQuery(document.createElement(this._isYoutube() ? "div" : + (_type.match('pdf') ? "pdf-player" : (this.options.multi_src ? 'multi-video' : 'video' )))) .addClass('et2_video') .attr('id', this.dom_id); diff --git a/package-lock.json b/package-lock.json index ac0d09e13f..5a4f6b5ae5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-2.0", "dependencies": { "@andxor/jquery-ui-touch-punch-fix": "^1.0.2", + "@bundled-es-modules/pdfjs-dist": "^2.5.207-rc1", "@lion/button": "^0.14.2", "@lion/core": "^0.18.2", "@lion/input": "^0.15.4", @@ -533,6 +534,11 @@ "resolved": "https://registry.npmjs.org/@bundled-es-modules/message-format/-/message-format-6.0.4.tgz", "integrity": "sha512-NGUoPxqsBzDwvRhY3A3L/AhS1hzS9OWappfyDOyCwE7G3W4ua28gau7QwvJz7QzA6ArbAdeb8c1mLjvd1WUFAA==" }, + "node_modules/@bundled-es-modules/pdfjs-dist": { + "version": "2.5.207-rc1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/pdfjs-dist/-/pdfjs-dist-2.5.207-rc1.tgz", + "integrity": "sha512-e/UVP1g6dwjQLnu4MPf/mlESCIvyr/KgpoMUyxGcv4evCIuJwKR/fcfhG3p1NYo+49gJsd0hL2yz9kzhkCZ32A==" + }, "node_modules/@esm-bundle/chai": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@esm-bundle/chai/-/chai-4.3.4.tgz", @@ -6480,6 +6486,11 @@ "resolved": "https://registry.npmjs.org/@bundled-es-modules/message-format/-/message-format-6.0.4.tgz", "integrity": "sha512-NGUoPxqsBzDwvRhY3A3L/AhS1hzS9OWappfyDOyCwE7G3W4ua28gau7QwvJz7QzA6ArbAdeb8c1mLjvd1WUFAA==" }, + "@bundled-es-modules/pdfjs-dist": { + "version": "2.5.207-rc1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/pdfjs-dist/-/pdfjs-dist-2.5.207-rc1.tgz", + "integrity": "sha512-e/UVP1g6dwjQLnu4MPf/mlESCIvyr/KgpoMUyxGcv4evCIuJwKR/fcfhG3p1NYo+49gJsd0hL2yz9kzhkCZ32A==" + }, "@esm-bundle/chai": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@esm-bundle/chai/-/chai-4.3.4.tgz", diff --git a/package.json b/package.json index c5d14aeeae..1cd7c3011d 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@andxor/jquery-ui-touch-punch-fix": "^1.0.2", + "@bundled-es-modules/pdfjs-dist": "^2.5.207-rc1", "@lion/button": "^0.14.2", "@lion/core": "^0.18.2", "@lion/input": "^0.15.4",