mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-21 21:01:20 -08:00
refactor(core): CORE-17 type input events and own interaction lifetimes
This commit is contained in:
1 parent
88aef830df
commit
a842058136
72 files changed
+31156
-1219
No files matched your search
@@ -4,6 +4,132 @@ ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hoo
|
||||
The production entry is still `src/index.js`; this document marks actual migrated
|
||||
boundaries rather than describing the entire core as TypeScript.
|
||||
|
||||
## Keyboard input (CORE-17)
|
||||
|
||||
hotkey.ts retains the art/keys own fields, init/add/remove prototype methods, plain
|
||||
keys-object prototype, caller-visible callback arrays and chainable registration.
|
||||
input/hotkey-types.ts describes its minimal generic host and callback receiver;
|
||||
input/hotkey-defaults.ts creates stable default handlers so repeated init neither
|
||||
multiplies callbacks nor prevents explicitly restoring removed defaults. Public init
|
||||
still enables manual mobile keyboard handling; automatic construction stays desktop-only.
|
||||
|
||||
input/keyboard-focus.ts resolves the event document, falls back to the player's current
|
||||
ownerDocument and follows open shadow focus/composed targets. Native input, textarea,
|
||||
select, inherited/plaintext editing and IME composition suppress hotkeys while preserving
|
||||
the generic keydown event. Existing modifier suppression remains. This fixes typing being
|
||||
intercepted after bindGlobalEvents or inside shadow roots. The global binding itself is
|
||||
owned by events/globalInit.ts, described below.
|
||||
|
||||
The instance scope removes the one keyboard subscription and callbacks check destruction
|
||||
before continuing or emitting success. Ordinary synchronous errors still propagate with
|
||||
their identity. Callback dispatch intentionally retains historical live-array mutation
|
||||
and this=art semantics. Own-property registration makes prototype-named keys safe without
|
||||
changing the public keys prototype. Direct editing of those arrays remains observable.
|
||||
|
||||
Run `node --test test/hotkey.test.js test/playback.test.js` and
|
||||
`yarn test:browser test/browser/hotkey.spec.js`. Browser tests exercise native typing and
|
||||
play/pause/seek/volume/Escape, iframe and shadow editors, controlled IME and mobile-UA
|
||||
initialization, and callback destruction. Controlled composition/mobile UA is not physical
|
||||
keyboard/IME/device certification. Input and scheduling share instance ownership;
|
||||
the refactor progress record tracks final installed UMD/legacy acceptance.
|
||||
|
||||
## Native listeners and global targets (CORE-17)
|
||||
|
||||
events/index.ts keeps the compatibility facade with the original bound proxy/hover
|
||||
methods and destroyEvents Set. events/listener-registry.ts owns native registration,
|
||||
capture snapshots, array rollback, abort and explicit cleanup. Native callback identity
|
||||
is preserved: direct removeEventListener with the original callback still works. Null
|
||||
options keep their historical native defaults. Duplicate callbacks retain native
|
||||
deduplication; disposal is not reference counted. Native once removes the listener itself,
|
||||
while its bookkeeping remains until explicit disposal or owner cleanup. Abort removes
|
||||
both the listener and its owned record. Failed removals warn and remain retryable;
|
||||
a target that permanently refuses removal cannot be forcibly cleaned up.
|
||||
|
||||
events/global-types.ts defines the minimal host, registry and source interfaces.
|
||||
events/globalInit.ts stages document/window listeners, forwards only from the committed
|
||||
binding, and retains the old binding if registration fails. A generation counter stops
|
||||
an interrupted binding from overwriting a newer nested bind; owner destruction prevents
|
||||
late activation. The original event object, forwarding names, void return and independent
|
||||
document/window fallback rules remain. Manual Events.destroy still permits later reuse
|
||||
while the player is alive. events/types.ts composes the typed initializer hosts without
|
||||
adding runtime fields or changing initializer order.
|
||||
|
||||
Run `node --test test/listener-registry.test.js test/global-events.test.js` and
|
||||
`yarn test:browser test/browser/listener-registry.spec.js test/browser/global-events.spec.js`.
|
||||
Tests cover actual iframe targets and native once/abort/callback identity; injected
|
||||
registration/removal failures exercise cleanup without asserting platform failure rates.
|
||||
|
||||
## Pointer interaction (CORE-17)
|
||||
|
||||
events/clickInit.ts retains synchronous click counting, the inclusive double-click
|
||||
threshold, live constructor settings and existing desktop/mobile actions. It checks
|
||||
instance closure after caller callbacks, preventing playback/fullscreen actions after
|
||||
destruction. input/pointer-focus.ts owns click/contextmenu emitter subscriptions while
|
||||
preserving isInput's historical INPUT-only meaning and composed-path focus checks.
|
||||
input/pointer-types.ts keeps host and native registry requirements explicit.
|
||||
|
||||
events/hoverInit.ts and moveInit.ts retain original events and class-before-notification
|
||||
ordering, with guards against stale callbacks. Emitter listeners still use their explicit
|
||||
ctx argument; unlike hotkey callbacks, they do not acquire this=art automatically.
|
||||
Public player methods remain non-configurable and are not replaced by these changes.
|
||||
|
||||
`test/pointer-events.test.js` verifies boundaries, nesting, callback failures, rejected
|
||||
playback, destruction and owned focus cleanup. `test/browser/pointer-events.spec.js`
|
||||
compares old/new events, controlled failure paths and actual trusted mouse input with
|
||||
real media playback. Android UA coverage exercises branch logic, not a physical device.
|
||||
|
||||
## Touch gestures (CORE-17)
|
||||
|
||||
events/gestureInit.ts installs the mobile-only listeners; gesture=false still retains
|
||||
progress touches and isLive skips gesture installation. input/gesture-controller.ts
|
||||
owns the active touch, source snapshot and orientation, plus end/cancel/lock subscriptions.
|
||||
input/gesture-direction.ts preserves the two-pixel threshold and diagonal boundaries;
|
||||
gesture-types.ts describes the minimal host and drag state. Normal video gestures keep
|
||||
TOUCH_MOVE_RATIO, progress gestures keep their full-width multiplier and rotated gestures
|
||||
use the vertical axis. Invalid geometry or a cancelled/replaced session cannot resume
|
||||
the previous drag. Callback errors retain identity; nested gestures supersede interrupted
|
||||
work, and destruction prevents subsequent seek/bar/notice writes.
|
||||
|
||||
control/progress/position.ts now accepts a minimal position host and an optional lifetime
|
||||
predicate between its bar notification and seek. Existing two-argument desktop callers
|
||||
retain their default behavior. Global events add document:touchcancel with the original
|
||||
Event payload; its public declaration is additive. Direct local cancellation also works
|
||||
when document forwarding has been rebound. Source changes cancel through source identity
|
||||
rather than depending on delayed media events.
|
||||
|
||||
`test/gesture.test.js` covers axes, absolute/relative ordering, reentry, invalid geometry,
|
||||
source/rotation/lock/finger changes and cleanup. Browser gestures use controlled touch
|
||||
payloads with actual media and DOM targets. They check requests before first play and
|
||||
actual seeking after playback initializes the decoder; they do not certify physical
|
||||
touch delivery, native scrolling arbitration or mobile OS cancellation behavior.
|
||||
|
||||
## Input scheduling and Events facade (CORE-17)
|
||||
|
||||
events/resizeInit.ts retains debounce coalescing, live RESIZE_TIME, normal auto-size,
|
||||
aspect-ratio restoration and notice cleanup. It guards later writes after caller reentry
|
||||
and listens to the owner's screen orientation by event capability rather than onchange
|
||||
being non-null. events/viewInit.ts owns a leading-only throttle, captures SCROLL_TIME at
|
||||
installation, reads SCROLL_GAP live and preserves synchronous nested dispatch. Its reset
|
||||
timer and internal subscriptions are released at destruction. Public throttle/debounce
|
||||
utilities retain their existing independent behavior.
|
||||
|
||||
events/viewport.ts uses the container's current ownerDocument/defaultView, preserving
|
||||
existing edge and gap arithmetic after iframe adoption. events/updateInit.ts keeps RAF
|
||||
opt-in, the initial playing emission and one pending owned frame; paused players do not
|
||||
emit raf, and destruction inside a callback cannot schedule another frame. An explicit
|
||||
destroy notification still cancels the pending frame while the owner is alive.
|
||||
|
||||
events/subscriptions.ts provides typed, guarded, owner-scoped emitter subscriptions;
|
||||
scheduling-types.ts describes the event maps and minimal scheduling hosts. The TS Events
|
||||
facade keeps its own destroyEvents/proxy/hover/bindGlobalEvents properties, prototype
|
||||
methods, bound proxy/hover behavior and scalar/array disposer returns. It declares the
|
||||
rebinding property without emitting an extra class field during construction.
|
||||
|
||||
`test/event-scheduling.test.js` verifies debounce/throttle/RAF ownership and reentry.
|
||||
`test/browser/event-scheduling.spec.js` compares public facade shape, real animation
|
||||
frames/playback, orientation event capability, timer cleanup and adopted iframe visibility.
|
||||
Controlled orientation targets do not certify physical device orientation.
|
||||
|
||||
## Display modes (CORE-16, in progress)
|
||||
|
||||
player/autoSizeMix.ts, autoHeightMix.ts, aspectRatioMix.ts and flipMix.ts retain the
|
||||
|
||||
Vendored
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,n),i(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),a(s,l,c),i(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),a(l,c,d),i(l,c,d),o(l,c,d)})}}(t,e,n),t}function a(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function i(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var a=r(o);throw"string"===a?new Error(o):"error"===a?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),a="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
|
||||
+1
-1
@@ -67,4 +67,4 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
|
||||
+578
-355
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,15 @@
|
||||
import type { ControlHost } from '../types'
|
||||
import { clamp, getRect, isMobile, secondToTime } from '../../utils'
|
||||
|
||||
export type ProgressEvent = MouseEvent | TouchEvent
|
||||
type ProgressPositionHost = Pick<ControlHost, 'template' | 'duration' | 'isRotate' | 'top' | 'height' | 'emit' | 'seek'>
|
||||
export interface ProgressPositionHost {
|
||||
template: { $progress: HTMLElement }
|
||||
duration: number
|
||||
isRotate: boolean
|
||||
top: number
|
||||
height: number
|
||||
seek: number | string
|
||||
emit: (name: 'setBar', kind: 'played', percentage: number, event: ProgressEvent) => unknown
|
||||
}
|
||||
|
||||
export function getPosFromEvent(art: ProgressPositionHost, event: ProgressEvent) {
|
||||
const { $progress } = art.template
|
||||
@@ -15,16 +22,18 @@ export function getPosFromEvent(art: ProgressPositionHost, event: ProgressEvent)
|
||||
return { second, time, width, percentage }
|
||||
}
|
||||
|
||||
export function setCurrentTime(art: ProgressPositionHost, event: ProgressEvent) {
|
||||
export function setCurrentTime(art: ProgressPositionHost, event: ProgressEvent, active: () => boolean = () => true) {
|
||||
if (art.isRotate) {
|
||||
const percentage = ((event as TouchEvent).touches[0]!.clientY - art.top) / art.height
|
||||
const second = percentage * art.duration
|
||||
art.emit('setBar', 'played', percentage, event)
|
||||
art.seek = second
|
||||
if (active())
|
||||
art.seek = second
|
||||
}
|
||||
else {
|
||||
const { second, percentage } = getPosFromEvent(art, event)
|
||||
art.emit('setBar', 'played', percentage, event)
|
||||
art.seek = second
|
||||
if (active())
|
||||
art.seek = second
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { includeFromEvent, isMobile, silencePromise } from '../utils'
|
||||
|
||||
export default function clickInit(art, events) {
|
||||
const {
|
||||
constructor,
|
||||
template: { $player, $video },
|
||||
} = art
|
||||
|
||||
function onDocumentClick(event) {
|
||||
if (includeFromEvent(event, $player)) {
|
||||
art.isInput = event.target.tagName === 'INPUT'
|
||||
art.isFocus = true
|
||||
art.emit('focus', event)
|
||||
}
|
||||
else {
|
||||
art.isInput = false
|
||||
art.isFocus = false
|
||||
art.emit('blur', event)
|
||||
}
|
||||
}
|
||||
|
||||
art.on('document:click', onDocumentClick)
|
||||
art.on('document:contextmenu', onDocumentClick)
|
||||
|
||||
let clickTimes = []
|
||||
events.proxy($video, 'click', (event) => {
|
||||
const now = Date.now()
|
||||
clickTimes.push(now)
|
||||
const { MOBILE_CLICK_PLAY, DBCLICK_TIME, MOBILE_DBCLICK_PLAY, DBCLICK_FULLSCREEN } = constructor
|
||||
|
||||
const clicks = clickTimes.filter(t => now - t <= DBCLICK_TIME)
|
||||
switch (clicks.length) {
|
||||
case 1:
|
||||
art.emit('click', event)
|
||||
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_CLICK_PLAY) {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
}
|
||||
else {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
clickTimes = clicks
|
||||
break
|
||||
case 2:
|
||||
art.emit('dblclick', event)
|
||||
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_DBCLICK_PLAY) {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (DBCLICK_FULLSCREEN) {
|
||||
art.fullscreen = !art.fullscreen
|
||||
}
|
||||
}
|
||||
clickTimes = []
|
||||
break
|
||||
default:
|
||||
clickTimes = []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ClickHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { pointerFocus } from '../input/pointer-focus'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
import { silencePromise } from '../utils/error'
|
||||
|
||||
export default function clickInit(art: ClickHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { constructor, template: { $video } } = art
|
||||
pointerFocus(art)
|
||||
|
||||
let clickTimes: number[] = []
|
||||
events.proxy($video, 'click', (event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const now = Date.now()
|
||||
clickTimes.push(now)
|
||||
const { MOBILE_CLICK_PLAY, DBCLICK_TIME, MOBILE_DBCLICK_PLAY, DBCLICK_FULLSCREEN } = constructor
|
||||
const clicks = clickTimes.filter(t => now - t <= DBCLICK_TIME)
|
||||
switch (clicks.length) {
|
||||
case 1:
|
||||
art.emit('click', event)
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!isMobile || (!art.isLock && MOBILE_CLICK_PLAY))
|
||||
silencePromise(art.toggle())
|
||||
clickTimes = clicks
|
||||
break
|
||||
case 2:
|
||||
art.emit('dblclick', event)
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_DBCLICK_PLAY)
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
else if (DBCLICK_FULLSCREEN) {
|
||||
art.fullscreen = !art.fullscreen
|
||||
}
|
||||
clickTimes = []
|
||||
break
|
||||
default:
|
||||
clickTimes = []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { setCurrentTime } from '../control/progress'
|
||||
import { clamp, isMobile, secondToTime } from '../utils'
|
||||
|
||||
function GetSlideAngle(dx, dy) {
|
||||
return (Math.atan2(dy, dx) * 180) / Math.PI
|
||||
}
|
||||
|
||||
function GetSlideDirection(startX, startY, endX, endY) {
|
||||
const dy = startY - endY
|
||||
const dx = endX - startX
|
||||
let result = 0
|
||||
|
||||
if (Math.abs(dx) < 2 && Math.abs(dy) < 2) {
|
||||
return result
|
||||
}
|
||||
|
||||
const angle = GetSlideAngle(dx, dy)
|
||||
if (angle >= -45 && angle < 45) {
|
||||
result = 4
|
||||
}
|
||||
else if (angle >= 45 && angle < 135) {
|
||||
result = 1
|
||||
}
|
||||
else if (angle >= -135 && angle < -45) {
|
||||
result = 2
|
||||
}
|
||||
else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) {
|
||||
result = 3
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default function gestureInit(art, events) {
|
||||
if (isMobile && !art.option.isLive) {
|
||||
const { $video, $progress } = art.template
|
||||
|
||||
let touchTarget = null
|
||||
let isDragging = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let startTime = 0
|
||||
|
||||
const onTouchStart = (event) => {
|
||||
if (event.touches.length === 1 && !art.isLock) {
|
||||
if (touchTarget === $progress) {
|
||||
setCurrentTime(art, event)
|
||||
}
|
||||
|
||||
isDragging = true
|
||||
const { pageX, pageY } = event.touches[0]
|
||||
startX = pageX
|
||||
startY = pageY
|
||||
startTime = art.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchMove = (event) => {
|
||||
if (event.touches.length === 1 && isDragging && art.duration) {
|
||||
const { pageX, pageY } = event.touches[0]
|
||||
const direction = GetSlideDirection(startX, startY, pageX, pageY)
|
||||
const isHorizontal = [3, 4].includes(direction)
|
||||
const isVertical = [1, 2].includes(direction)
|
||||
const isLegal = (isHorizontal && !art.isRotate) || (isVertical && art.isRotate)
|
||||
if (isLegal) {
|
||||
const ratioX = clamp((pageX - startX) / art.width, -1, 1)
|
||||
const ratioY = clamp((pageY - startY) / art.height, -1, 1)
|
||||
const ratio = art.isRotate ? ratioY : ratioX
|
||||
const TOUCH_MOVE_RATIO = touchTarget === $video ? art.constructor.TOUCH_MOVE_RATIO : 1
|
||||
const currentTime = clamp(startTime + art.duration * ratio * TOUCH_MOVE_RATIO, 0, art.duration)
|
||||
art.seek = currentTime
|
||||
art.emit('setBar', 'played', clamp(currentTime / art.duration, 0, 1), event)
|
||||
art.notice.show = `${secondToTime(currentTime)} / ${secondToTime(art.duration)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (isDragging) {
|
||||
startX = 0
|
||||
startY = 0
|
||||
startTime = 0
|
||||
isDragging = false
|
||||
touchTarget = null
|
||||
}
|
||||
}
|
||||
|
||||
if (art.option.gesture) {
|
||||
events.proxy($video, 'touchstart', (event) => {
|
||||
touchTarget = $video
|
||||
onTouchStart(event)
|
||||
})
|
||||
events.proxy($video, 'touchmove', onTouchMove)
|
||||
}
|
||||
|
||||
events.proxy($progress, 'touchstart', (event) => {
|
||||
touchTarget = $progress
|
||||
onTouchStart(event)
|
||||
})
|
||||
|
||||
events.proxy($progress, 'touchmove', onTouchMove)
|
||||
art.on('document:touchend', onTouchEnd)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { GestureHost } from '../input/gesture-types'
|
||||
import type { PointerRegistry } from '../input/pointer-types'
|
||||
import { gestureController } from '../input/gesture-controller'
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
|
||||
export default function gestureInit(art: GestureHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
if (!isMobile || art.option.isLive)
|
||||
return
|
||||
const { $video, $progress } = art.template
|
||||
const gesture = gestureController(art)
|
||||
const bind = (target: EventTarget) => {
|
||||
events.proxy(target, 'touchstart', event => gesture.start(target, event as TouchEvent))
|
||||
events.proxy(target, 'touchmove', event => gesture.move(event as TouchEvent))
|
||||
events.proxy(target, 'touchcancel', gesture.cancel)
|
||||
}
|
||||
if (art.option.gesture)
|
||||
bind($video)
|
||||
bind($progress)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Disposer, Listener } from './listener-registry'
|
||||
|
||||
export interface GlobalEventSource {
|
||||
document?: Document
|
||||
window?: Window
|
||||
}
|
||||
|
||||
export interface GlobalEventHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: string, event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface GlobalEventRegistry {
|
||||
proxy: (target: EventTarget, name: string, listener: Listener) => Disposer
|
||||
remove: (dispose: Disposer) => void
|
||||
bindGlobalEvents?: (source?: GlobalEventSource) => void
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
export default function globalInit(art, events) {
|
||||
const documentEvents = [
|
||||
'click',
|
||||
'mouseup',
|
||||
'keydown',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
'pointerup',
|
||||
'contextmenu',
|
||||
'pointermove',
|
||||
'visibilitychange',
|
||||
'webkitfullscreenchange',
|
||||
]
|
||||
|
||||
const windowEvents = [
|
||||
'resize',
|
||||
'scroll',
|
||||
'orientationchange',
|
||||
]
|
||||
|
||||
const destroyEvents = []
|
||||
|
||||
function bindGlobalEvents(source = {}) {
|
||||
for (let index = 0; index < destroyEvents.length; index++) {
|
||||
events.remove(destroyEvents[index])
|
||||
}
|
||||
|
||||
destroyEvents.length = 0
|
||||
|
||||
const { $player } = art.template
|
||||
|
||||
documentEvents.forEach((name) => {
|
||||
const doc = source.document || $player.ownerDocument || document
|
||||
const destroy = events.proxy(doc, name, (event) => {
|
||||
art.emit(`document:${name}`, event)
|
||||
})
|
||||
destroyEvents.push(destroy)
|
||||
})
|
||||
|
||||
windowEvents.forEach((name) => {
|
||||
const win = source.window || $player.ownerDocument?.defaultView || window
|
||||
const destroy = events.proxy(win, name, (event) => {
|
||||
art.emit(`window:${name}`, event)
|
||||
})
|
||||
destroyEvents.push(destroy)
|
||||
})
|
||||
}
|
||||
|
||||
bindGlobalEvents()
|
||||
events.bindGlobalEvents = bindGlobalEvents
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { GlobalEventHost, GlobalEventRegistry, GlobalEventSource } from './global-types'
|
||||
import type { Disposer } from './listener-registry'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
const documentEvents = [
|
||||
'click',
|
||||
'mouseup',
|
||||
'keydown',
|
||||
'touchend',
|
||||
'touchcancel',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
'pointerup',
|
||||
'contextmenu',
|
||||
'pointermove',
|
||||
'visibilitychange',
|
||||
'webkitfullscreenchange',
|
||||
]
|
||||
const windowEvents = ['resize', 'scroll', 'orientationchange']
|
||||
|
||||
interface Binding {
|
||||
disposers: Disposer[]
|
||||
}
|
||||
|
||||
export default function globalInit(art: GlobalEventHost, events: GlobalEventRegistry): void {
|
||||
let active: Binding | undefined
|
||||
let generation = 0
|
||||
getScope(art).add(() => {
|
||||
active = undefined
|
||||
generation++
|
||||
return undefined
|
||||
})
|
||||
|
||||
const release = (binding: Binding | undefined) => {
|
||||
if (binding) {
|
||||
for (const dispose of binding.disposers)
|
||||
events.remove(dispose)
|
||||
binding.disposers.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
function bindGlobalEvents(source: GlobalEventSource = {}): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++generation
|
||||
const binding: Binding = { disposers: [] }
|
||||
const cancelled = () => current !== generation || isClosing(art)
|
||||
try {
|
||||
const { $player } = art.template
|
||||
const doc = source.document || $player.ownerDocument || document
|
||||
const win = source.window || $player.ownerDocument?.defaultView || window
|
||||
const register = (target: EventTarget, names: string[], prefix: string) => {
|
||||
for (const name of names) {
|
||||
if (cancelled())
|
||||
return
|
||||
binding.disposers.push(events.proxy(target, name, (event) => {
|
||||
if (active === binding && !isClosing(art))
|
||||
art.emit(`${prefix}:${name}`, event)
|
||||
}))
|
||||
}
|
||||
}
|
||||
register(doc, documentEvents, 'document')
|
||||
register(win, windowEvents, 'window')
|
||||
}
|
||||
catch (error) {
|
||||
release(binding)
|
||||
throw error
|
||||
}
|
||||
if (cancelled()) {
|
||||
release(binding)
|
||||
return
|
||||
}
|
||||
const previous = active
|
||||
active = binding
|
||||
release(previous)
|
||||
}
|
||||
|
||||
bindGlobalEvents()
|
||||
events.bindGlobalEvents = bindGlobalEvents
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { addClass, removeClass } from '../utils'
|
||||
|
||||
export default function hoverInit(art, events) {
|
||||
const { $player } = art.template
|
||||
|
||||
events.hover(
|
||||
$player,
|
||||
(event) => {
|
||||
addClass($player, 'art-hover')
|
||||
art.emit('hover', true, event)
|
||||
},
|
||||
(event) => {
|
||||
removeClass($player, 'art-hover')
|
||||
art.emit('hover', false, event)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { HoverHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { addClass, removeClass } from '../utils/dom'
|
||||
|
||||
export default function hoverInit(art: HoverHost, events: Pick<PointerRegistry, 'hover'>): void {
|
||||
const { $player } = art.template
|
||||
events.hover(
|
||||
$player,
|
||||
(event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
addClass($player, 'art-hover')
|
||||
art.emit('hover', true, event)
|
||||
},
|
||||
(event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
removeClass($player, 'art-hover')
|
||||
art.emit('hover', false, event)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import clickInit from './clickInit'
|
||||
import gestureInit from './gestureInit'
|
||||
import globalInit from './globalInit'
|
||||
import hoverInit from './hoverInit'
|
||||
import moveInit from './moveInit'
|
||||
import resizeInit from './resizeInit'
|
||||
import updateInit from './updateInit'
|
||||
import viewInit from './viewInit'
|
||||
|
||||
const scopes = new WeakMap()
|
||||
|
||||
export default class Events {
|
||||
constructor(art) {
|
||||
this.destroyEvents = new Set()
|
||||
const scope = getScope(art)
|
||||
scopes.set(this, scope)
|
||||
scope.add(() => this.destroy())
|
||||
this.proxy = this.proxy.bind(this)
|
||||
this.hover = this.hover.bind(this)
|
||||
|
||||
clickInit(art, this)
|
||||
hoverInit(art, this)
|
||||
moveInit(art, this)
|
||||
resizeInit(art, this)
|
||||
gestureInit(art, this)
|
||||
viewInit(art, this)
|
||||
globalInit(art, this)
|
||||
updateInit(art, this)
|
||||
}
|
||||
|
||||
proxy(target, name, callback, option = {}) {
|
||||
if (Array.isArray(name)) {
|
||||
return name.map(item => this.proxy(target, item, callback, option))
|
||||
}
|
||||
|
||||
if (scopes.get(this).closed)
|
||||
return () => {}
|
||||
|
||||
target.addEventListener(name, callback, option)
|
||||
const destroy = () => target.removeEventListener(name, callback, option)
|
||||
this.destroyEvents.add(destroy)
|
||||
return destroy
|
||||
}
|
||||
|
||||
hover(target, mouseenter, mouseleave) {
|
||||
if (mouseenter) {
|
||||
this.proxy(target, 'mouseenter', mouseenter)
|
||||
}
|
||||
if (mouseleave) {
|
||||
this.proxy(target, 'mouseleave', mouseleave)
|
||||
}
|
||||
}
|
||||
|
||||
remove(destroyEvent) {
|
||||
if (this.destroyEvents.has(destroyEvent)) {
|
||||
try {
|
||||
destroyEvent()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to remove event listener:', error)
|
||||
}
|
||||
finally {
|
||||
this.destroyEvents.delete(destroyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const destroyEvent of this.destroyEvents) {
|
||||
try {
|
||||
destroyEvent()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to destroy event listener:', error)
|
||||
}
|
||||
}
|
||||
this.destroyEvents.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { GlobalEventSource } from './global-types'
|
||||
import type { Disposer, Listener } from './listener-registry'
|
||||
import type { EventsHost } from './types'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import clickInit from './clickInit'
|
||||
import gestureInit from './gestureInit'
|
||||
import globalInit from './globalInit'
|
||||
import hoverInit from './hoverInit'
|
||||
import { destroyListeners, ownListeners, proxyListener, removeListener } from './listener-registry'
|
||||
import moveInit from './moveInit'
|
||||
import resizeInit from './resizeInit'
|
||||
import updateInit from './updateInit'
|
||||
import viewInit from './viewInit'
|
||||
|
||||
export default class Events {
|
||||
declare destroyEvents: Set<Disposer>
|
||||
declare bindGlobalEvents: (source?: GlobalEventSource) => void
|
||||
|
||||
constructor(art: EventsHost) {
|
||||
this.destroyEvents = new Set()
|
||||
ownListeners(this, art)
|
||||
getScope(art).add(() => {
|
||||
this.destroy()
|
||||
})
|
||||
this.proxy = this.proxy.bind(this)
|
||||
this.hover = this.hover.bind(this)
|
||||
|
||||
clickInit(art, this)
|
||||
hoverInit(art, this)
|
||||
moveInit(art, this)
|
||||
resizeInit(art, this)
|
||||
gestureInit(art, this)
|
||||
viewInit(art)
|
||||
globalInit(art, this)
|
||||
updateInit(art)
|
||||
}
|
||||
|
||||
proxy(target: EventTarget, name: string, callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer
|
||||
proxy(target: EventTarget, name: string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer[]
|
||||
proxy(target: EventTarget, name: string | string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer | Disposer[]
|
||||
proxy(target: EventTarget, name: string | string[], callback: Listener, option: boolean | AddEventListenerOptions | null = {}): Disposer | Disposer[] {
|
||||
return Array.isArray(name) ? proxyListener(this, target, name, callback, option) : proxyListener(this, target, name, callback, option)
|
||||
}
|
||||
|
||||
hover(target: EventTarget, mouseenter?: (event: Event) => unknown, mouseleave?: (event: Event) => unknown): void {
|
||||
if (mouseenter)
|
||||
this.proxy(target, 'mouseenter', mouseenter)
|
||||
if (mouseleave)
|
||||
this.proxy(target, 'mouseleave', mouseleave)
|
||||
}
|
||||
|
||||
remove(dispose: Disposer): void {
|
||||
removeListener(this, dispose)
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
destroyListeners(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
|
||||
export type Disposer = () => void
|
||||
export type Listener = EventListenerOrEventListenerObject | null
|
||||
export interface ListenerRegistry {
|
||||
destroyEvents: Set<Disposer>
|
||||
}
|
||||
|
||||
interface State {
|
||||
scope: ResourceScope
|
||||
cleaning: boolean
|
||||
}
|
||||
const states = new WeakMap<ListenerRegistry, State>()
|
||||
|
||||
export function ownListeners(registry: ListenerRegistry, art: object): void {
|
||||
states.set(registry, { scope: getScope(art), cleaning: false })
|
||||
}
|
||||
|
||||
function stateOf(registry: ListenerRegistry): State {
|
||||
const state = states.get(registry)
|
||||
if (!state)
|
||||
throw new Error('ArtPlayer event registry has not been initialized')
|
||||
return state
|
||||
}
|
||||
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer[]
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string, callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string | string[], callback: Listener, option: boolean | AddEventListenerOptions | null = {}): Disposer | Disposer[] {
|
||||
if (Array.isArray(names)) {
|
||||
const created: Disposer[] = []
|
||||
try {
|
||||
return names.map((name) => {
|
||||
const dispose = proxyListener(registry, target, name, callback, option)
|
||||
created.push(dispose)
|
||||
return dispose
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
for (const dispose of created.reverse()) {
|
||||
try {
|
||||
dispose()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('Failed to roll back event listener:', cleanupError)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const name = names
|
||||
const state = stateOf(registry)
|
||||
if (state.scope.closed || state.cleaning)
|
||||
return () => {}
|
||||
// Snapshot native dictionary values, especially capture, without wrapping the callback.
|
||||
const options = typeof option === 'boolean'
|
||||
? option
|
||||
: {
|
||||
capture: Boolean(option?.capture),
|
||||
once: Boolean(option?.once),
|
||||
passive: option?.passive,
|
||||
signal: option?.signal,
|
||||
}
|
||||
const capture = typeof options === 'boolean' ? options : options.capture
|
||||
const signal = typeof options === 'boolean' ? undefined : options.signal
|
||||
if (state.scope.closed || state.cleaning || signal?.aborted)
|
||||
return () => {}
|
||||
let active = true
|
||||
function clean(force = false): void {
|
||||
if (!active && !force)
|
||||
return
|
||||
target.removeEventListener(name, callback, capture)
|
||||
signal?.removeEventListener('abort', dispose)
|
||||
active = false
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
function dispose(): void {
|
||||
clean()
|
||||
}
|
||||
registry.destroyEvents.add(dispose)
|
||||
try {
|
||||
target.addEventListener(name, callback, options)
|
||||
// A custom target may synchronously destroy the player before adding the listener.
|
||||
if (!active || state.scope.closed || state.cleaning)
|
||||
clean(true)
|
||||
else if (signal?.aborted)
|
||||
dispose()
|
||||
else
|
||||
signal?.addEventListener('abort', dispose, { once: true })
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
clean(true)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('Failed to roll back event listener:', cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return dispose
|
||||
}
|
||||
|
||||
export function removeListener(registry: ListenerRegistry, dispose: Disposer): void {
|
||||
if (!registry.destroyEvents.has(dispose))
|
||||
return
|
||||
try {
|
||||
dispose()
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
catch (error) {
|
||||
// Retain a failed removal so a later remove/destroy can retry it.
|
||||
console.warn('Failed to remove event listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyListeners(registry: ListenerRegistry): void {
|
||||
const state = stateOf(registry)
|
||||
if (state.cleaning)
|
||||
return
|
||||
state.cleaning = true
|
||||
try {
|
||||
for (const dispose of registry.destroyEvents) {
|
||||
try {
|
||||
dispose()
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to destroy event listener:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
state.cleaning = false
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function moveInit(art, events) {
|
||||
const { $player } = art.template
|
||||
|
||||
events.proxy($player, 'mousemove', (event) => {
|
||||
art.emit('mousemove', event)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { MoveHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
|
||||
export default function moveInit(art: MoveHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { $player } = art.template
|
||||
events.proxy($player, 'mousemove', (event) => {
|
||||
if (!isClosing(art))
|
||||
art.emit('mousemove', event)
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
|
||||
export default function resizeInit(art, events) {
|
||||
const { option, constructor } = art
|
||||
|
||||
art.on('resize', () => {
|
||||
const { aspectRatio, notice } = art
|
||||
if (art.state === 'standard' && option.autoSize) {
|
||||
art.autoSize()
|
||||
}
|
||||
art.aspectRatio = aspectRatio
|
||||
notice.show = ''
|
||||
})
|
||||
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const resizeFn = () => {
|
||||
cancel()
|
||||
cancel = timeout(scope, () => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
}
|
||||
|
||||
art.on('window:orientationchange', () => resizeFn())
|
||||
art.on('window:resize', () => resizeFn())
|
||||
|
||||
if (screen && screen.orientation && screen.orientation.onchange) {
|
||||
events.proxy(screen.orientation, 'change', () => resizeFn())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PointerRegistry } from '../input/pointer-types'
|
||||
import type { ResizeEvents, ResizeHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
|
||||
export default function resizeInit(art: ResizeHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { option, constructor } = art
|
||||
const on = eventSubscriptions<ResizeEvents>(art)
|
||||
on('resize', () => {
|
||||
const { aspectRatio, notice } = art
|
||||
if (art.state === 'standard' && option.autoSize)
|
||||
art.autoSize()
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.aspectRatio = aspectRatio
|
||||
if (!isClosing(art))
|
||||
notice.show = ''
|
||||
})
|
||||
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const resize = () => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
cancel()
|
||||
cancel = timeout(scope, () => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
}
|
||||
on('window:orientationchange', resize)
|
||||
on('window:resize', resize)
|
||||
|
||||
const orientation = art.template.$player.ownerDocument.defaultView?.screen?.orientation
|
||||
if (typeof orientation?.addEventListener === 'function')
|
||||
events.proxy(orientation, 'change', resize)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
|
||||
export interface ResizeEvents {
|
||||
'resize': []
|
||||
'window:resize': [Event]
|
||||
'window:orientationchange': [Event]
|
||||
}
|
||||
|
||||
export interface ResizeHost extends SubscriptionHost<ResizeEvents> {
|
||||
template: { $player: HTMLElement }
|
||||
option: { autoSize: boolean }
|
||||
constructor: { RESIZE_TIME: number }
|
||||
aspectRatio: string
|
||||
state: string
|
||||
autoSize: () => unknown
|
||||
notice: { show: string }
|
||||
emit: (name: 'resize') => unknown
|
||||
}
|
||||
|
||||
export interface ViewEvents { 'window:scroll': [Event], 'view': [boolean] }
|
||||
export interface ViewHost extends SubscriptionHost<ViewEvents> {
|
||||
template: { $container: HTMLElement }
|
||||
option: { autoMini: boolean }
|
||||
constructor: { SCROLL_GAP: number, SCROLL_TIME: number }
|
||||
mini: boolean
|
||||
emit: (name: 'view', visible: boolean) => unknown
|
||||
}
|
||||
|
||||
export interface RafEvents { destroy: [] }
|
||||
export interface RafHost extends SubscriptionHost<RafEvents> {
|
||||
constructor: { USE_RAF: boolean }
|
||||
playing: boolean
|
||||
emit: (name: 'raf') => unknown
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
export function eventSubscriptions<Events extends { [Name in keyof Events]: readonly unknown[] }>(art: SubscriptionHost<Events>) {
|
||||
return <Name extends keyof Events>(name: Name, callback: (...args: [...Events[Name]]) => unknown): void => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const listener = (...args: [...Events[Name]]) => {
|
||||
if (!isClosing(art))
|
||||
return callback(...args)
|
||||
}
|
||||
art.on(name, listener)
|
||||
getScope(art).add(() => {
|
||||
art.off(name, listener)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { GestureHost } from '../input/gesture-types'
|
||||
import type { ClickHost, HoverHost, MoveHost } from '../input/pointer-types'
|
||||
import type { GlobalEventHost } from './global-types'
|
||||
import type { RafHost, ResizeHost, ViewHost } from './scheduling-types'
|
||||
|
||||
export type EventsHost = ClickHost & HoverHost & MoveHost & GestureHost & GlobalEventHost & RafHost & ResizeHost & ViewHost
|
||||
@@ -1,23 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { animationFrame } from '../lifecycle/resources'
|
||||
|
||||
export default function updateInit(art) {
|
||||
if (art.constructor.USE_RAF) {
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {};
|
||||
|
||||
(function update() {
|
||||
if (art.playing) {
|
||||
art.emit('raf')
|
||||
}
|
||||
|
||||
if (!art.isDestroy) {
|
||||
cancel = animationFrame(scope, update)
|
||||
}
|
||||
})()
|
||||
|
||||
art.on('destroy', () => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RafEvents, RafHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { animationFrame } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
|
||||
export default function updateInit(art: RafHost): void {
|
||||
if (!art.constructor.USE_RAF)
|
||||
return
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const update = () => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (art.playing)
|
||||
art.emit('raf')
|
||||
if (!isClosing(art))
|
||||
cancel = animationFrame(scope, update)
|
||||
}
|
||||
update()
|
||||
eventSubscriptions<RafEvents>(art)('destroy', () => cancel())
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { isInViewport, throttle } from '../utils'
|
||||
|
||||
export default function viewInit(art) {
|
||||
const {
|
||||
option,
|
||||
constructor,
|
||||
template: { $container },
|
||||
} = art
|
||||
|
||||
const scrollFn = throttle(() => {
|
||||
art.emit('view', isInViewport($container, constructor.SCROLL_GAP))
|
||||
}, constructor.SCROLL_TIME)
|
||||
|
||||
art.on('window:scroll', () => scrollFn())
|
||||
|
||||
art.on('view', (state) => {
|
||||
if (option.autoMini) {
|
||||
art.mini = !state
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ViewEvents, ViewHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
import { inViewport } from './viewport'
|
||||
|
||||
export default function viewInit(art: ViewHost): void {
|
||||
const { option, constructor, template: { $container } } = art
|
||||
const delay = constructor.SCROLL_TIME
|
||||
const scope = getScope(art)
|
||||
const on = eventSubscriptions<ViewEvents>(art)
|
||||
let waiting = false
|
||||
on('window:scroll', () => {
|
||||
if (waiting)
|
||||
return
|
||||
art.emit('view', inViewport($container, constructor.SCROLL_GAP))
|
||||
if (isClosing(art))
|
||||
return
|
||||
// Keep the leading-only throttle and its historical synchronous reentry timing.
|
||||
waiting = true
|
||||
timeout(scope, () => {
|
||||
waiting = false
|
||||
}, delay)
|
||||
})
|
||||
on('view', (visible) => {
|
||||
if (option.autoMini)
|
||||
art.mini = !visible
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function inViewport(element: HTMLElement, offset: number): boolean {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const document = element.ownerDocument
|
||||
const window = document.defaultView
|
||||
const height = window?.innerHeight || document.documentElement.clientHeight
|
||||
const width = window?.innerWidth || document.documentElement.clientWidth
|
||||
// Retain the historical edge/gap calculations while using the actual owner viewport.
|
||||
const vertical = rect.top - offset <= height && rect.top + rect.height + offset >= 0
|
||||
const horizontal = rect.left - offset <= width + offset && rect.left + rect.width + offset >= 0
|
||||
return vertical && horizontal
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { isMobile, silencePromise } from './utils'
|
||||
|
||||
export default class Hotkey {
|
||||
constructor(art) {
|
||||
this.art = art
|
||||
this.keys = {}
|
||||
|
||||
if (!isMobile) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
const { constructor } = this.art
|
||||
|
||||
if (this.art.option.hotkey) {
|
||||
this.add('Escape', () => {
|
||||
if (this.art.fullscreenWeb) {
|
||||
this.art.fullscreenWeb = false
|
||||
}
|
||||
})
|
||||
|
||||
this.add('Space', () => {
|
||||
silencePromise(this.art.toggle())
|
||||
})
|
||||
|
||||
this.add('ArrowLeft', () => {
|
||||
this.art.backward = constructor.SEEK_STEP
|
||||
})
|
||||
|
||||
this.add('ArrowUp', () => {
|
||||
this.art.volume += constructor.VOLUME_STEP
|
||||
})
|
||||
|
||||
this.add('ArrowRight', () => {
|
||||
this.art.forward = constructor.SEEK_STEP
|
||||
})
|
||||
|
||||
this.add('ArrowDown', () => {
|
||||
this.art.volume -= constructor.VOLUME_STEP
|
||||
})
|
||||
}
|
||||
|
||||
this.art.on('document:keydown', (event) => {
|
||||
if (this.art.isFocus) {
|
||||
const tag = document.activeElement.tagName.toUpperCase()
|
||||
const editable = document.activeElement.getAttribute('contenteditable')
|
||||
if (
|
||||
tag !== 'INPUT'
|
||||
&& tag !== 'TEXTAREA'
|
||||
&& editable !== ''
|
||||
&& editable !== 'true'
|
||||
&& !event.altKey
|
||||
&& !event.ctrlKey
|
||||
&& !event.metaKey
|
||||
&& !event.shiftKey
|
||||
) {
|
||||
const events = this.keys[event.code]
|
||||
if (events) {
|
||||
event.preventDefault()
|
||||
for (let index = 0; index < events.length; index++) {
|
||||
events[index].call(this.art, event)
|
||||
}
|
||||
this.art.emit('hotkey', event)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.art.emit('keydown', event)
|
||||
})
|
||||
}
|
||||
|
||||
add(key, event) {
|
||||
if (this.keys[key]) {
|
||||
if (!this.keys[key].includes(event)) {
|
||||
this.keys[key].push(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.keys[key] = [event]
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
remove(key, event) {
|
||||
if (this.keys[key]) {
|
||||
const index = this.keys[key].indexOf(event)
|
||||
if (index !== -1) {
|
||||
this.keys[key].splice(index, 1)
|
||||
}
|
||||
if (this.keys[key].length === 0) {
|
||||
delete this.keys[key]
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { HotkeyCallback, HotkeyHost } from './input/hotkey-types'
|
||||
import { defaultHotkeys } from './input/hotkey-defaults'
|
||||
import { acceptsHotkey } from './input/keyboard-focus'
|
||||
import { getScope, isClosing } from './lifecycle/instance'
|
||||
import { isMobile } from './utils/compatibility'
|
||||
|
||||
interface State {
|
||||
defaults?: () => void
|
||||
subscribed: boolean
|
||||
}
|
||||
const states = new WeakMap<object, State>()
|
||||
|
||||
export default class Hotkey<Host extends HotkeyHost> {
|
||||
declare art: Host
|
||||
declare keys: Record<string, HotkeyCallback<Host>[]>
|
||||
|
||||
constructor(art: Host) {
|
||||
this.art = art
|
||||
this.keys = {}
|
||||
states.set(this, { subscribed: false })
|
||||
if (!isMobile)
|
||||
this.init()
|
||||
}
|
||||
|
||||
init(): void {
|
||||
const art = this.art
|
||||
if (isClosing(art))
|
||||
return
|
||||
const state = states.get(this)!
|
||||
if (art.option.hotkey) {
|
||||
if (!state.defaults) {
|
||||
const callbacks = defaultHotkeys(art)
|
||||
state.defaults = () => {
|
||||
for (const key of Object.keys(callbacks))
|
||||
this.add(key, callbacks[key]!)
|
||||
}
|
||||
}
|
||||
state.defaults()
|
||||
}
|
||||
if (state.subscribed)
|
||||
return
|
||||
state.subscribed = true
|
||||
const onKeydown = (event: KeyboardEvent) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (art.isFocus && acceptsHotkey(event, art.template.$player.ownerDocument)) {
|
||||
const callbacks = Object.prototype.hasOwnProperty.call(this.keys, event.code) ? this.keys[event.code] : undefined
|
||||
if (callbacks) {
|
||||
event.preventDefault()
|
||||
// Keep the historical live-array mutation and callback receiver contracts.
|
||||
for (let index = 0; index < callbacks.length; index++) {
|
||||
if (isClosing(art))
|
||||
return
|
||||
callbacks[index]!.call(art, event)
|
||||
}
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.emit('hotkey', event)
|
||||
}
|
||||
}
|
||||
if (!isClosing(art))
|
||||
art.emit('keydown', event)
|
||||
}
|
||||
art.on('document:keydown', onKeydown)
|
||||
getScope(art).add(() => {
|
||||
state.subscribed = false
|
||||
art.off('document:keydown', onKeydown)
|
||||
})
|
||||
}
|
||||
|
||||
add(key: string, callback: HotkeyCallback<Host>): this {
|
||||
const existing = Object.prototype.hasOwnProperty.call(this.keys, key) ? this.keys[key] : undefined
|
||||
if (existing) {
|
||||
if (!existing.includes(callback))
|
||||
existing.push(callback)
|
||||
}
|
||||
else {
|
||||
Object.defineProperty(this.keys, key, { value: [callback], enumerable: true, configurable: true, writable: true })
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
remove(key: string, callback: HotkeyCallback<Host>): this {
|
||||
const existing = Object.prototype.hasOwnProperty.call(this.keys, key) ? this.keys[key] : undefined
|
||||
if (existing) {
|
||||
const index = existing.indexOf(callback)
|
||||
if (index !== -1)
|
||||
existing.splice(index, 1)
|
||||
if (existing.length === 0)
|
||||
delete this.keys[key]
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Drag, GestureHost } from './gesture-types'
|
||||
import { setCurrentTime } from '../control/progress/position'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { captureSource } from '../source/operation'
|
||||
import { clamp, secondToTime } from '../utils'
|
||||
import { slideDirection } from './gesture-direction'
|
||||
|
||||
export function gestureController(art: GestureHost) {
|
||||
let drag: Drag | undefined
|
||||
const cancel = () => {
|
||||
drag = undefined
|
||||
}
|
||||
const active = (current: Drag) => drag === current && !isClosing(art) && !art.isLock
|
||||
&& !art.option.isLive && art.isRotate === current.rotated && current.sourceActive()
|
||||
const onLock = (locked: boolean) => {
|
||||
if (locked)
|
||||
cancel()
|
||||
}
|
||||
art.on('document:touchend', cancel)
|
||||
art.on('document:touchcancel', cancel)
|
||||
art.on('lock', onLock)
|
||||
getScope(art).add(() => {
|
||||
cancel()
|
||||
art.off('document:touchend', cancel)
|
||||
art.off('document:touchcancel', cancel)
|
||||
art.off('lock', onLock)
|
||||
})
|
||||
|
||||
const start = (target: EventTarget, event: TouchEvent) => {
|
||||
cancel()
|
||||
const touch = event.touches[0]
|
||||
if (isClosing(art) || art.isLock || art.option.isLive || event.touches.length !== 1 || !touch)
|
||||
return
|
||||
if (![touch.pageX, touch.pageY, touch.clientX, touch.clientY, art.currentTime].every(Number.isFinite))
|
||||
return
|
||||
const current: Drag = {
|
||||
target,
|
||||
identifier: touch.identifier,
|
||||
x: touch.pageX,
|
||||
y: touch.pageY,
|
||||
time: art.currentTime,
|
||||
rotated: art.isRotate,
|
||||
sourceActive: captureSource(art),
|
||||
}
|
||||
drag = current
|
||||
if (target === art.template.$progress) {
|
||||
const size = art.isRotate ? art.height : art.template.$progress.clientWidth
|
||||
if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(art.duration) || art.duration <= 0) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
try {
|
||||
setCurrentTime(art, event, () => active(current))
|
||||
}
|
||||
catch (error) {
|
||||
if (drag === current)
|
||||
cancel()
|
||||
throw error
|
||||
}
|
||||
if (active(current))
|
||||
current.time = art.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
const move = (event: TouchEvent) => {
|
||||
const current = drag
|
||||
const touch = event.touches[0]
|
||||
if (!current)
|
||||
return
|
||||
if (!active(current) || event.touches.length !== 1 || !touch || touch.identifier !== current.identifier) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
const size = current.rotated ? art.height : art.width
|
||||
const multiplier = current.target === art.template.$video ? art.constructor.TOUCH_MOVE_RATIO : 1
|
||||
if (![touch.pageX, touch.pageY, size, art.duration, multiplier].every(Number.isFinite) || size <= 0 || art.duration <= 0) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
const direction = slideDirection(current.x, current.y, touch.pageX, touch.pageY)
|
||||
if (!(current.rotated ? direction === 1 || direction === 2 : direction === 3 || direction === 4))
|
||||
return
|
||||
const distance = current.rotated ? touch.pageY - current.y : touch.pageX - current.x
|
||||
const ratio = clamp(distance / size, -1, 1)
|
||||
const time = clamp(current.time + art.duration * ratio * multiplier, 0, art.duration)
|
||||
art.seek = time
|
||||
if (!active(current))
|
||||
return
|
||||
art.emit('setBar', 'played', clamp(time / art.duration, 0, 1), event)
|
||||
if (active(current))
|
||||
art.notice.show = `${secondToTime(time)} / ${secondToTime(art.duration)}`
|
||||
}
|
||||
return { start, move, cancel }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function slideDirection(startX: number, startY: number, endX: number, endY: number): 0 | 1 | 2 | 3 | 4 {
|
||||
const dy = startY - endY
|
||||
const dx = endX - startX
|
||||
if (Math.abs(dx) < 2 && Math.abs(dy) < 2)
|
||||
return 0
|
||||
const angle = Math.atan2(dy, dx) * 180 / Math.PI
|
||||
if (angle >= -45 && angle < 45)
|
||||
return 4
|
||||
if (angle >= 45 && angle < 135)
|
||||
return 1
|
||||
if (angle >= -135 && angle < -45)
|
||||
return 2
|
||||
if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135))
|
||||
return 3
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { ProgressPositionHost } from '../control/progress/position'
|
||||
|
||||
export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
|
||||
'document:touchend': [Event]
|
||||
'document:touchcancel': [Event]
|
||||
'lock': [boolean]
|
||||
}> {
|
||||
template: { $video: EventTarget, $progress: HTMLElement }
|
||||
option: { isLive: boolean, gesture: boolean }
|
||||
constructor: { TOUCH_MOVE_RATIO: number }
|
||||
isLock: boolean
|
||||
width: number
|
||||
currentTime: number
|
||||
notice: { show: string }
|
||||
}
|
||||
|
||||
export interface Drag {
|
||||
target: EventTarget
|
||||
identifier: number
|
||||
x: number
|
||||
y: number
|
||||
time: number
|
||||
rotated: boolean
|
||||
sourceActive: () => boolean
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { HotkeyCallback, HotkeyHost } from './hotkey-types'
|
||||
import { silencePromise } from '../utils/error'
|
||||
|
||||
export function defaultHotkeys<Host extends HotkeyHost>(art: Host): Record<string, HotkeyCallback<Host>> {
|
||||
const { constructor } = art
|
||||
return {
|
||||
Escape: () => {
|
||||
if (art.fullscreenWeb)
|
||||
art.fullscreenWeb = false
|
||||
},
|
||||
Space: () => {
|
||||
silencePromise(art.toggle())
|
||||
},
|
||||
ArrowLeft: () => {
|
||||
art.backward = constructor.SEEK_STEP
|
||||
},
|
||||
ArrowUp: () => {
|
||||
art.volume += constructor.VOLUME_STEP
|
||||
},
|
||||
ArrowRight: () => {
|
||||
art.forward = constructor.SEEK_STEP
|
||||
},
|
||||
ArrowDown: () => {
|
||||
art.volume -= constructor.VOLUME_STEP
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
|
||||
export interface HotkeyHost extends SubscriptionHost<{ 'document:keydown': [KeyboardEvent] }> {
|
||||
constructor: { SEEK_STEP: number, VOLUME_STEP: number }
|
||||
option: { hotkey?: boolean }
|
||||
template: { $player: HTMLElement }
|
||||
isFocus: boolean
|
||||
fullscreenWeb: boolean
|
||||
backward: number
|
||||
forward: number
|
||||
volume: number
|
||||
toggle: () => unknown
|
||||
emit: (name: 'hotkey' | 'keydown', event: KeyboardEvent) => unknown
|
||||
}
|
||||
|
||||
export type HotkeyCallback<Host> = (this: Host, event: KeyboardEvent) => unknown
|
||||
@@ -0,0 +1,30 @@
|
||||
function asElement(target: EventTarget | null | undefined): HTMLElement | undefined {
|
||||
if (target && (target as Node).nodeType === 1)
|
||||
return target as HTMLElement
|
||||
}
|
||||
|
||||
function eventDocument(event: KeyboardEvent, fallback: Document): Document {
|
||||
const target = event.target as Node | null
|
||||
return target?.nodeType === 9 ? target as Document : target?.ownerDocument || event.view?.document || fallback
|
||||
}
|
||||
|
||||
function editable(target: HTMLElement): boolean {
|
||||
const tag = target.tagName.toUpperCase()
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
|
||||
return true
|
||||
return target.isContentEditable
|
||||
}
|
||||
|
||||
export function acceptsHotkey(event: KeyboardEvent, fallback: Document): boolean {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.isComposing || event.keyCode === 229)
|
||||
return false
|
||||
const doc = eventDocument(event, fallback)
|
||||
let active = asElement(doc.activeElement)
|
||||
while (active?.shadowRoot?.activeElement)
|
||||
active = asElement(active.shadowRoot.activeElement)
|
||||
if (active && editable(active))
|
||||
return false
|
||||
// The composed path exposes an editable descendant behind a retargeted shadow host.
|
||||
const first = asElement(event.composedPath?.()[0]) || asElement(event.target)
|
||||
return !first || !editable(first)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FocusHost } from './pointer-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { includeFromEvent } from '../utils/dom'
|
||||
|
||||
export function pointerFocus(art: FocusHost): void {
|
||||
const { $player } = art.template
|
||||
const onDocumentClick = (event: Event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const inside = includeFromEvent(event, $player)
|
||||
art.isInput = inside && (event.target as Element | null)?.tagName === 'INPUT'
|
||||
art.isFocus = inside
|
||||
art.emit(inside ? 'focus' : 'blur', event)
|
||||
}
|
||||
art.on('document:click', onDocumentClick)
|
||||
art.on('document:contextmenu', onDocumentClick)
|
||||
getScope(art).add(() => {
|
||||
art.off('document:click', onDocumentClick)
|
||||
art.off('document:contextmenu', onDocumentClick)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { Disposer, Listener } from '../events/listener-registry'
|
||||
|
||||
export interface PointerRegistry {
|
||||
proxy: (target: EventTarget, name: string, callback: Listener) => Disposer
|
||||
hover: (target: EventTarget, enter?: (event: Event) => unknown, leave?: (event: Event) => unknown) => void
|
||||
}
|
||||
|
||||
export interface FocusHost extends SubscriptionHost<{ 'document:click': [Event], 'document:contextmenu': [Event] }> {
|
||||
template: { $player: HTMLElement }
|
||||
isInput: boolean
|
||||
isFocus: boolean
|
||||
emit: (name: 'focus' | 'blur', event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface ClickHost extends FocusHost {
|
||||
constructor: {
|
||||
MOBILE_CLICK_PLAY: boolean
|
||||
MOBILE_DBCLICK_PLAY: boolean
|
||||
DBCLICK_TIME: number
|
||||
DBCLICK_FULLSCREEN: boolean
|
||||
}
|
||||
|
||||
template: { $player: HTMLElement, $video: EventTarget }
|
||||
isLock: boolean
|
||||
fullscreen: boolean
|
||||
toggle: () => unknown
|
||||
emit: (name: 'focus' | 'blur' | 'click' | 'dblclick', event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface HoverHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: 'hover', state: boolean, event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface MoveHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: 'mousemove', event: Event) => unknown
|
||||
}
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ export interface Events {
|
||||
'document:mouseup': [event: Event]
|
||||
'document:keydown': [event: Event]
|
||||
'document:touchend': [event: Event]
|
||||
'document:touchcancel': [event: Event]
|
||||
'document:touchmove': [event: Event]
|
||||
'document:mousemove': [event: Event]
|
||||
'document:pointerup': [event: Event]
|
||||
|
||||
Reference in new issue
Block a user