diff --git a/.gitignore b/.gitignore index cb2395f..7799064 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea distribution node_modules +tsup.config.bundled_* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..65220e0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": false, + "trailingComma": "none", + "singleQuote": true, + "printWidth": 80, + "arrowParens": "always", + "max-len": ["error", 140, 2], + "tabWidth": 2, + "useTabs": false +} diff --git a/build-settings.json b/build-settings.json index ccacf64..7621fb7 100644 --- a/build-settings.json +++ b/build-settings.json @@ -3,6 +3,7 @@ "VokaAdvertisementPlugin": false, "VokaStatisticsPlugin": false, "VokaQualityPlugin": false, - "VokaCaptionsPlugin": false + "VokaCaptionsPlugin": false, + "hotkeys": true } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b24b727..26d0d2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "keycode": "^2.2.1", "postcss": "^8.5.1", "postcss-preset-env": "^10.1.3", + "prettier": "^3.5.3", "ts-bus": "^2.3.1", "ts-node": "^10.9.2", "tsup": "^8.3.6", @@ -4537,6 +4538,21 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index 4bf8d35..4e23091 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "keycode": "^2.2.1", "postcss": "^8.5.1", "postcss-preset-env": "^10.1.3", + "prettier": "^3.5.3", "ts-bus": "^2.3.1", "ts-node": "^10.9.2", "tsup": "^8.3.6", diff --git a/src/components/BigPlayButton.ts b/src/components/BigPlayButton.ts index 3190625..d813db5 100644 --- a/src/components/BigPlayButton.ts +++ b/src/components/BigPlayButton.ts @@ -57,7 +57,7 @@ class BigPlayButton extends Button { handleKeyDown(event: Event) { this.mouseused_ = false - + super.handleKeyDown(event) } diff --git a/src/components/Button.ts b/src/components/Button.ts index f487f88..db6d219 100644 --- a/src/components/Button.ts +++ b/src/components/Button.ts @@ -1,10 +1,10 @@ import videojs from 'video.js' const Component = videojs.getComponent('component') -import ClickableComponent from './ClickableComponent' const log = videojs.log +const Dom = videojs.dom +import ClickableComponent from './ClickableComponent' import { assign } from '../utilities/obj' import keycode from 'keycode' -const Dom = videojs.dom /** * Base class for all buttons. @@ -115,7 +115,7 @@ class Button extends ClickableComponent { super.disable() this.el().setAttribute('disabled', 'disabled') } - + /** * This gets called when a `Button` has focus and `keydown` is triggered via a key * press. diff --git a/src/components/CentralPanel.ts b/src/components/CentralPanel.ts index 184e277..efcd4f1 100644 --- a/src/components/CentralPanel.ts +++ b/src/components/CentralPanel.ts @@ -3,6 +3,7 @@ import ClickableComponent from './ClickableComponent' import ClickableComponentOptions = videojs.ClickableComponentOptions import { VideoJsPlayer } from '@types/video.js' +import VokaEvent from "@/constants/VokaEvent" import './BigPlayButton' /** @@ -23,17 +24,6 @@ class CentralPanel extends ClickableComponent { // this.bigPlayButton = this.getChild('BigPlayButton') as videojs.Component this.isClickDisabled = false - - if (this.bigPlayButton) { - this.bigPlayButton.el().addEventListener('animationend', () => { - ;(this.bigPlayButton as videojs.Component).removeClass( - 'animation-press-play' - ) - ;(this.bigPlayButton as videojs.Component).removeClass( - 'animation-press-pause' - ) - }) - } } enableClick() { @@ -52,8 +42,8 @@ class CentralPanel extends ClickableComponent { */ createEl() { return videojs.dom.createEl('div', { - className: 'voka-central-panel' - }); + className: 'voka-central-panel', + }) } handleClick(event: Event) { @@ -62,12 +52,21 @@ class CentralPanel extends ClickableComponent { if (this.isClickDisabled) return - if (this.player_.paused()) { - this.player_.play() + if (this.player().paused()) { + if (this.player().ended()) { + this.player().trigger(VokaEvent.Replay) + } + + this.player().play() } else { - this.player_.pause() + this.player().pause() } } + + handleKeyDown(event: Event) { + // Pass keypress handling up for unsupported keys + super.handleKeyDown(event) + } } videojs.registerComponent('CentralPanel', CentralPanel) diff --git a/src/components/ClickableComponent.ts b/src/components/ClickableComponent.ts index 01898f4..481b607 100644 --- a/src/components/ClickableComponent.ts +++ b/src/components/ClickableComponent.ts @@ -1,11 +1,11 @@ import videojs from 'video.js' -const Component = videojs.getComponent('Component') -// import ClickableComponentOptions = videojs.ClickableComponentOptions +import { VideoJsPlayer, VideoJsPlayerOptions } from '@types/video.js' +import keycode from 'keycode' + +const Component = videojs.getComponent('component') const Dom = videojs.dom -const browser = videojs.browser const log = videojs.log import { assign } from '../utilities/obj' -import keycode from 'keycode' export const enum TooltipPosition { top = 'top', @@ -38,16 +38,32 @@ class ClickableComponent extends Component { * The `Player` that this class should be attached to. * * @param {Object} [options] - * The key/value store of player options. + * The key/value store of component options. * * @param {function} [options.clickHandler] * The function to call when the button is clicked / activated + * + * @param {string} [options.controlText] + * The text to set on the button + * + * @param {string} [options.className] + * A class or space separated list of classes to add the component + * */ - constructor(player: any, options: any) { + constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) { super(player, options) - this.options_ = options + + if (this.options_.controlText) { + this.controlText(this.options_.controlText) + } + + this.handleMouseOver_ = (e) => this.handleMouseOver(e) + this.handleMouseOut_ = (e) => this.handleMouseOut(e) + this.handleClick_ = (e) => this.handleClick(e) + this.handleKeyDown_ = (e) => this.handleKeyDown(e) this.emitTapEvents() + this.enable() } @@ -66,8 +82,8 @@ class ClickableComponent extends Component { * @return {Element} * The element that gets created. */ - createEl(tag: string, props?: any, attributes?: any) { - props = assign( + createEl(tag: string = 'div', props?: any = {}, attributes?: any = {}) { + props = Object.assign( { className: this.buildCSSClass(), tabIndex: -1 @@ -77,7 +93,7 @@ class ClickableComponent extends Component { if (tag === 'button') { log.error( - `Creating a ClickableComponent with an HTML element of ${tag} is not supported use a Button instead.` + `Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.` ) } @@ -91,38 +107,76 @@ class ClickableComponent extends Component { this.tabIndex_ = props.tabIndex - const el = Dom.createEl(tag, props, attributes, '') + const el = Dom.createEl(tag, props, attributes) - el.appendChild( - Dom.createEl( - 'span', - { - className: 'voka-icon-placeholder' - }, - { - 'aria-hidden': true - }, - '' + if (!this.player_.options_.experimentalSvgIcons) { + el.appendChild( + Dom.createEl( + 'span', + { + className: 'voka-icon-placeholder' + }, + { + 'aria-hidden': true + } + ) ) - ) + } - this.controlText(this.controlText_, el) + this.createControlTextEl(el) return el } dispose() { // remove controlTextEl_ on dispose - // this.controlTextEl_ = null + this.controlTextEl_ = null super.dispose() } + /** + * Create a control text element on this `ClickableComponent` + * + * @param {Element} [el] + * Parent element for the control text. + * + * @return {Element} + * The control text element that gets created. + */ + createControlTextEl(el) { + this.controlTextEl_ = Dom.createEl( + 'span', + { + className: 'voka-control-text' + }, + { + // let the screen reader user know that the text of the element may change + 'aria-live': 'polite' + } + ) + + if (el) { + el.appendChild(this.controlTextEl_) + } + + this.controlText(this.controlText_, el) + + return this.controlTextEl_ + } + /** * Get or set the localize text to use for the controls on the `ClickableComponent`. * + * @param {string} [text] + * Control text for element. + * + * @param {Element} [el=this.el()] + * Element to set the title on. + * + * @return {string} + * - The control text when getting */ - // @ts-ignore controlText( text?: string, el: Element = this.el(), @@ -164,12 +218,12 @@ class ClickableComponent extends Component { if (!this.enabled_) { this.enabled_ = true this.removeClass('vjs-disabled') - this.el().setAttribute('aria-disabled', 'false') + this.el_.setAttribute('aria-disabled', 'false') if (typeof this.tabIndex_ !== 'undefined') { - this.el().setAttribute('tabIndex', this.tabIndex_) + this.el_.setAttribute('tabIndex', this.tabIndex_) } - this.on(['tap', 'click'], this.handleClick) - this.on('keydown', this.handleKeyDown) + this.on(['tap', 'click'], this.handleClick_) + this.on('keydown', this.handleKeyDown_) } } @@ -179,19 +233,16 @@ class ClickableComponent extends Component { disable() { this.enabled_ = false this.addClass('vjs-disabled') - this.el().setAttribute('aria-disabled', 'true') + this.el_.setAttribute('aria-disabled', 'true') if (typeof this.tabIndex_ !== 'undefined') { - this.el().removeAttribute('tabIndex') + this.el_.removeAttribute('tabIndex') } - this.off('mouseover', this.handleMouseOver) - this.off('mouseout', this.handleMouseOut) - this.off(['tap', 'click'], this.handleClick) - this.off('keydown', this.handleKeyDown) + this.off('mouseover', this.handleMouseOver_) + this.off('mouseout', this.handleMouseOut_) + this.off(['tap', 'click'], this.handleClick_) + this.off('keydown', this.handleKeyDown_) } - handleMouseOver(e: Event) {} - handleMouseOut(e: Event) {} - /** * Handles language change in ClickableComponent for the player in components * @@ -205,7 +256,7 @@ class ClickableComponent extends Component { * Event handler that is called when a `ClickableComponent` receives a * `click` or `tap` event. * - * @param {EventTarget~Event} event + * @param {Event} event * The `tap` or `click` event that caused this function to be called. * * @listens tap @@ -213,8 +264,9 @@ class ClickableComponent extends Component { * @abstract */ handleClick(event: Event) { + event.stopPropagation() if (this.options_.clickHandler) { - this.options_.clickHandler() + this.options_.clickHandler.call(this, arguments) } } @@ -224,25 +276,21 @@ class ClickableComponent extends Component { * * By default, if the key is Space or Enter, it will trigger a `click` event. * - * @param {EventTarget~Event} event + * @param {KeyboardEvent} event * The `keydown` event that caused this function to be called. * * @listens keydown */ - handleKeyDown(event: Event) { + handleKeyDown(event: KeyboardEvent) { // Support Space or Enter key operation to fire a click event. Also, // prevent the event from propagating through the DOM and triggering // Player hotkeys. - if ( - keycode.isEventKey(event, 'Space') || - keycode.isEventKey(event, 'Enter') - ) { + if (['space', 'enter'].includes(keycode(event))) { event.preventDefault() event.stopPropagation() this.trigger('click') } else { // Pass keypress handling up for unsupported keys - // @ts-ignore @todo убрать ignore после PR https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60138 super.handleKeyDown(event) } } diff --git a/src/internal/player/VokaPlayerImpl.ts b/src/internal/player/VokaPlayerImpl.ts index 4f52760..a935684 100644 --- a/src/internal/player/VokaPlayerImpl.ts +++ b/src/internal/player/VokaPlayerImpl.ts @@ -4,6 +4,7 @@ import { EventBus } from 'ts-bus' import { IVokaPlayer } from '@/public/IVokaPlayer' import { AutoplayChecker } from '@/internal/utils/AutoplayChecker' import * as languages from '@/languages.json' +import '../../plugins/hotkeys' import '../../components/Skin' @@ -85,6 +86,7 @@ export class VokaPlayerImpl implements IVokaPlayer { //plugins.vokaStatisticsPlugin = {} //plugins.vokaAdvertisementPlugin = {} //plugins.vokaCaptionsPlugin = {} + plugins.hotkeys = {} const childrenComponents = [ // Non-visual component, not in UI layer list. diff --git a/src/plugins/hotkeys.ts b/src/plugins/hotkeys.ts new file mode 100644 index 0000000..7866da1 --- /dev/null +++ b/src/plugins/hotkeys.ts @@ -0,0 +1,68 @@ +import videojs from "video.js"; +import { VideoJsPlayer } from "@types/video.js" +import keycode from 'keycode' +import VokaEvent from "@/constants/VokaEvent" + +const Plugin = videojs.getPlugin('plugin'); + +export class Hotkeys extends Plugin { + player!: VideoJsPlayer + volumeStep: number + + constructor(player: VideoJsPlayer) { + super(player) + this.volumeStep = 0.1 + + this.player.el().addEventListener('keydown', (e) => this.handleKeyDown(e)); + } + + handleKeyDown(event: Event) { + event.preventDefault() + event.stopPropagation() + + switch (keycode(event)) { + case 'up': + this.changeVolume(this.volumeStep) + break + + case 'down': + this.changeVolume(-this.volumeStep) + break + + case 'm': + this.mute() + break + + case 'space': + case 'enter': + this.togglePlay() + break + + default: + break + } + } + + togglePlay() { + if (this.player.paused()) { + if (this.player.ended()) { + this.player.trigger(VokaEvent.Replay) + } + + this.player.play() + } else { + this.player.pause() + } + } + + changeVolume(diff: number) { + const value = this.player.volume() + diff + this.player.volume(value) + } + + mute() { + this.player.muted(!this.player.muted()) + } +} + +videojs.registerPlugin('hotkeys', Hotkeys); \ No newline at end of file