{ "version": 3, "sources": ["../../../node_modules/@rails/actioncable/src/adapters.js", "../../../node_modules/@rails/actioncable/src/logger.js", "../../../node_modules/@rails/actioncable/src/connection_monitor.js", "../../../node_modules/@rails/actioncable/src/internal.js", "../../../node_modules/@rails/actioncable/src/connection.js", "../../../node_modules/@rails/actioncable/src/subscription.js", "../../../node_modules/@rails/actioncable/src/subscription_guarantor.js", "../../../node_modules/@rails/actioncable/src/subscriptions.js", "../../../node_modules/@rails/actioncable/src/consumer.js", "../../../node_modules/@rails/actioncable/src/index.js", "../../../node_modules/autocompleter/autocomplete.ts", "../../../node_modules/recaptcha-v3/dist/ReCaptchaInstance.js", "../../../node_modules/recaptcha-v3/dist/ReCaptchaLoader.js", "../../../node_modules/recaptcha-v3/dist/ReCaptcha.js", "../../../node_modules/plyr/dist/plyr.js", "../../../node_modules/plyr/dist/node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs", "../../../node_modules/plyr/dist/src/js/utils/is.js", "../../../node_modules/plyr/dist/src/js/utils/animation.js", "../../../node_modules/plyr/dist/src/js/utils/browser.js", "../../../node_modules/plyr/dist/src/js/utils/objects.js", "../../../node_modules/plyr/dist/src/js/utils/elements.js", "../../../node_modules/plyr/dist/src/js/support.js", "../../../node_modules/plyr/dist/src/js/utils/events.js", "../../../node_modules/plyr/dist/src/js/utils/promise.js", "../../../node_modules/plyr/dist/src/js/utils/arrays.js", "../../../node_modules/plyr/dist/src/js/utils/style.js", "../../../node_modules/plyr/dist/src/js/html5.js", "../../../node_modules/plyr/dist/src/js/utils/strings.js", "../../../node_modules/plyr/dist/src/js/utils/i18n.js", "../../../node_modules/plyr/dist/src/js/storage.js", "../../../node_modules/plyr/dist/src/js/utils/fetch.js", "../../../node_modules/plyr/dist/src/js/utils/load-sprite.js", "../../../node_modules/plyr/dist/src/js/utils/time.js", "../../../node_modules/plyr/dist/src/js/controls.js", "../../../node_modules/plyr/dist/src/js/utils/urls.js", "../../../node_modules/plyr/dist/src/js/captions.js", "../../../node_modules/plyr/dist/src/js/config/defaults.js", "../../../node_modules/plyr/dist/src/js/config/states.js", "../../../node_modules/plyr/dist/src/js/config/types.js", "../../../node_modules/plyr/dist/src/js/console.js", "../../../node_modules/plyr/dist/src/js/fullscreen.js", "../../../node_modules/plyr/dist/src/js/utils/load-image.js", "../../../node_modules/plyr/dist/src/js/ui.js", "../../../node_modules/plyr/dist/src/js/listeners.js", "../../../node_modules/plyr/dist/node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js", "../../../node_modules/plyr/dist/src/js/utils/load-script.js", "../../../node_modules/plyr/dist/src/js/plugins/vimeo.js", "../../../node_modules/plyr/dist/src/js/plugins/youtube.js", "../../../node_modules/plyr/dist/src/js/media.js", "../../../node_modules/plyr/dist/src/js/plugins/ads.js", "../../../node_modules/plyr/dist/src/js/utils/numbers.js", "../../../node_modules/plyr/dist/src/js/plugins/preview-thumbnails.js", "../../../node_modules/plyr/dist/src/js/source.js", "../../../node_modules/plyr/dist/src/js/plyr.js", "../../../node_modules/feather-icons/dist/webpack:/feather/webpack/universalModuleDefinition", "../../../node_modules/feather-icons/dist/webpack:/feather/webpack/bootstrap", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/classnames/dedupe.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/es/array/from.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/a-function.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/an-object.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/array-from.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/array-includes.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/bind-context.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/call-with-safe-iteration-closing.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/check-correctness-of-iteration.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/classof-raw.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/classof.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/copy-constructor-properties.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/correct-prototype-getter.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/create-iterator-constructor.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/create-property-descriptor.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/create-property.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/define-iterator.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/descriptors.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/document-create-element.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/enum-bug-keys.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/export.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/fails.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/function-to-string.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/get-iterator-method.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/global.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/has.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/hidden-keys.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/hide.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/html.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/ie8-dom-define.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/indexed-object.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/internal-state.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/is-array-iterator-method.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/is-forced.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/is-object.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/is-pure.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/iterators-core.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/iterators.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/native-symbol.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/native-weak-map.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-create.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-define-properties.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-define-property.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-get-own-property-descriptor.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-get-own-property-names.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-get-own-property-symbols.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-get-prototype-of.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-keys-internal.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-keys.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-property-is-enumerable.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/object-set-prototype-of.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/own-keys.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/path.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/redefine.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/require-object-coercible.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/set-global.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/set-to-string-tag.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/shared-key.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/shared.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/string-at.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-absolute-index.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-indexed-object.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-integer.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-length.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-object.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/to-primitive.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/uid.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/validate-set-prototype-of-arguments.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/internals/well-known-symbol.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/modules/es.array.from.js", "../../../node_modules/feather-icons/dist/webpack:/feather/node_modules/core-js/modules/es.string.iterator.js", "../../../node_modules/feather-icons/dist/webpack:/feather/(webpack)/buildin/global.js", "../../../node_modules/feather-icons/dist/webpack:/feather/src/icon.js", "../../../node_modules/feather-icons/dist/webpack:/feather/src/icons.js", "../../../node_modules/feather-icons/dist/webpack:/feather/src/index.js", "../../../node_modules/feather-icons/dist/webpack:/feather/src/replace.js", "../../../node_modules/feather-icons/dist/webpack:/feather/src/to-svg.js", "../../javascript/redactorx.min.js", "../../javascript/custom/prism.js", "../../../node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js", "../../../node_modules/@hotwired/turbo-rails/app/javascript/turbo/cable.js", "../../../node_modules/@hotwired/turbo-rails/app/javascript/turbo/snakeize.js", "../../../node_modules/@hotwired/turbo-rails/app/javascript/turbo/cable_stream_source_element.js", "../../../node_modules/@hotwired/turbo-rails/app/javascript/turbo/fetch_requests.js", "../../../node_modules/@hotwired/turbo-rails/app/javascript/turbo/index.js", "../../../node_modules/@hotwired/stimulus/dist/stimulus.js", "../../javascript/controllers/application.js", "../../javascript/controllers/autocomplete_controller.js", "../../../node_modules/@rails/request.js/src/fetch_response.js", "../../../node_modules/@rails/request.js/src/request_interceptor.js", "../../../node_modules/@rails/request.js/src/lib/utils.js", "../../../node_modules/@rails/request.js/src/fetch_request.js", "../../../node_modules/@rails/request.js/src/verbs.js", "../../javascript/controllers/calculator_controller.js", "../../javascript/controllers/counter_controller.js", "../../javascript/controllers/dark_mode_controller.js", "../../javascript/controllers/hello_controller.js", "../../javascript/controllers/menu_controller.js", "../../../node_modules/stimulus-use/dist/index.js", "../../javascript/controllers/modal_controller.js", "../../../node_modules/tiny-slider/src/helpers/raf.js", "../../../node_modules/tiny-slider/src/helpers/caf.js", "../../../node_modules/tiny-slider/src/helpers/extend.js", "../../../node_modules/tiny-slider/src/helpers/checkStorageValue.js", "../../../node_modules/tiny-slider/src/helpers/setLocalStorage.js", "../../../node_modules/tiny-slider/src/helpers/getSlideId.js", "../../../node_modules/tiny-slider/src/helpers/getBody.js", "../../../node_modules/tiny-slider/src/helpers/docElement.js", "../../../node_modules/tiny-slider/src/helpers/setFakeBody.js", "../../../node_modules/tiny-slider/src/helpers/resetFakeBody.js", "../../../node_modules/tiny-slider/src/helpers/calc.js", "../../../node_modules/tiny-slider/src/helpers/percentageLayout.js", "../../../node_modules/tiny-slider/src/helpers/mediaquerySupport.js", "../../../node_modules/tiny-slider/src/helpers/createStyleSheet.js", "../../../node_modules/tiny-slider/src/helpers/addCSSRule.js", "../../../node_modules/tiny-slider/src/helpers/removeCSSRule.js", "../../../node_modules/tiny-slider/src/helpers/getCssRulesLength.js", "../../../node_modules/tiny-slider/src/helpers/toDegree.js", "../../../node_modules/tiny-slider/src/helpers/getTouchDirection.js", "../../../node_modules/tiny-slider/src/helpers/forEach.js", "../../../node_modules/tiny-slider/src/helpers/classListSupport.js", "../../../node_modules/tiny-slider/src/helpers/hasClass.js", "../../../node_modules/tiny-slider/src/helpers/addClass.js", "../../../node_modules/tiny-slider/src/helpers/removeClass.js", "../../../node_modules/tiny-slider/src/helpers/hasAttr.js", "../../../node_modules/tiny-slider/src/helpers/getAttr.js", "../../../node_modules/tiny-slider/src/helpers/isNodeList.js", "../../../node_modules/tiny-slider/src/helpers/setAttrs.js", "../../../node_modules/tiny-slider/src/helpers/removeAttrs.js", "../../../node_modules/tiny-slider/src/helpers/arrayFromNodeList.js", "../../../node_modules/tiny-slider/src/helpers/hideElement.js", "../../../node_modules/tiny-slider/src/helpers/showElement.js", "../../../node_modules/tiny-slider/src/helpers/isVisible.js", "../../../node_modules/tiny-slider/src/helpers/whichProperty.js", "../../../node_modules/tiny-slider/src/helpers/has3DTransforms.js", "../../../node_modules/tiny-slider/src/helpers/getEndProperty.js", "../../../node_modules/tiny-slider/src/helpers/passiveOption.js", "../../../node_modules/tiny-slider/src/helpers/addEvents.js", "../../../node_modules/tiny-slider/src/helpers/removeEvents.js", "../../../node_modules/tiny-slider/src/helpers/events.js", "../../../node_modules/tiny-slider/src/helpers/jsTransform.js", "../../../node_modules/tiny-slider/src/tiny-slider.js", "../../javascript/controllers/slider_controller.js", "../../../node_modules/hotkeys-js/dist/hotkeys.esm.js", "../../../node_modules/stimulus-dropdown/node_modules/stimulus-use/dist/index.js", "../../../node_modules/stimulus-dropdown/dist/stimulus-dropdown.mjs", "../../javascript/controllers/country_autocomplete_controller.js", "../../javascript/controllers/signup_controller.js", "../../../node_modules/ky/source/errors/HTTPError.ts", "../../../node_modules/ky/source/errors/TimeoutError.ts", "../../../node_modules/ky/source/utils/is.ts", "../../../node_modules/ky/source/utils/merge.ts", "../../../node_modules/ky/source/core/constants.ts", "../../../node_modules/ky/source/utils/normalize.ts", "../../../node_modules/ky/source/utils/timeout.ts", "../../../node_modules/ky/source/errors/DOMException.ts", "../../../node_modules/ky/source/utils/delay.ts", "../../../node_modules/ky/source/core/Ky.ts", "../../../node_modules/ky/source/index.ts", "../../javascript/controllers/accordion_controller.js", "../../javascript/controllers/index.js", "../../../node_modules/trix/dist/trix.esm.min.js", "../../../node_modules/@rails/actiontext/app/assets/javascripts/actiontext.js", "../../javascript/application.js", "../../javascript/custom/app.js", "../../javascript/custom/plugins.init.js", "../../javascript/custom/alignment.min.js", "../../javascript/custom/imageposition.min.js", "../../javascript/custom/removeformat.min.js", "../../javascript/custom/definedlinks.min.js", "../../javascript/custom/imageresize.min.js", "../../javascript/custom/myplugin.js"], "sourcesContent": ["export default {\n logger: self.console,\n WebSocket: self.WebSocket\n}\n", "import adapters from \"./adapters\"\n\n// The logger is disabled by default. You can enable it with:\n//\n// ActionCable.logger.enabled = true\n//\n// Example:\n//\n// import * as ActionCable from '@rails/actioncable'\n//\n// ActionCable.logger.enabled = true\n// ActionCable.logger.log('Connection Established.')\n//\n\nexport default {\n log(...messages) {\n if (this.enabled) {\n messages.push(Date.now())\n adapters.logger.log(\"[ActionCable]\", ...messages)\n }\n },\n}\n", "import logger from \"./logger\"\n\n// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting\n// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.\n\nconst now = () => new Date().getTime()\n\nconst secondsSince = time => (now() - time) / 1000\n\nclass ConnectionMonitor {\n constructor(connection) {\n this.visibilityDidChange = this.visibilityDidChange.bind(this)\n this.connection = connection\n this.reconnectAttempts = 0\n }\n\n start() {\n if (!this.isRunning()) {\n this.startedAt = now()\n delete this.stoppedAt\n this.startPolling()\n addEventListener(\"visibilitychange\", this.visibilityDidChange)\n logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`)\n }\n }\n\n stop() {\n if (this.isRunning()) {\n this.stoppedAt = now()\n this.stopPolling()\n removeEventListener(\"visibilitychange\", this.visibilityDidChange)\n logger.log(\"ConnectionMonitor stopped\")\n }\n }\n\n isRunning() {\n return this.startedAt && !this.stoppedAt\n }\n\n recordPing() {\n this.pingedAt = now()\n }\n\n recordConnect() {\n this.reconnectAttempts = 0\n this.recordPing()\n delete this.disconnectedAt\n logger.log(\"ConnectionMonitor recorded connect\")\n }\n\n recordDisconnect() {\n this.disconnectedAt = now()\n logger.log(\"ConnectionMonitor recorded disconnect\")\n }\n\n // Private\n\n startPolling() {\n this.stopPolling()\n this.poll()\n }\n\n stopPolling() {\n clearTimeout(this.pollTimeout)\n }\n\n poll() {\n this.pollTimeout = setTimeout(() => {\n this.reconnectIfStale()\n this.poll()\n }\n , this.getPollInterval())\n }\n\n getPollInterval() {\n const { staleThreshold, reconnectionBackoffRate } = this.constructor\n const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10))\n const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate\n const jitter = jitterMax * Math.random()\n return staleThreshold * 1000 * backoff * (1 + jitter)\n }\n\n reconnectIfStale() {\n if (this.connectionIsStale()) {\n logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)\n this.reconnectAttempts++\n if (this.disconnectedRecently()) {\n logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`)\n } else {\n logger.log(\"ConnectionMonitor reopening\")\n this.connection.reopen()\n }\n }\n }\n\n get refreshedAt() {\n return this.pingedAt ? this.pingedAt : this.startedAt\n }\n\n connectionIsStale() {\n return secondsSince(this.refreshedAt) > this.constructor.staleThreshold\n }\n\n disconnectedRecently() {\n return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold)\n }\n\n visibilityDidChange() {\n if (document.visibilityState === \"visible\") {\n setTimeout(() => {\n if (this.connectionIsStale() || !this.connection.isOpen()) {\n logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`)\n this.connection.reopen()\n }\n }\n , 200)\n }\n }\n\n}\n\nConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)\nConnectionMonitor.reconnectionBackoffRate = 0.15\n\nexport default ConnectionMonitor\n", "export default {\n \"message_types\": {\n \"welcome\": \"welcome\",\n \"disconnect\": \"disconnect\",\n \"ping\": \"ping\",\n \"confirmation\": \"confirm_subscription\",\n \"rejection\": \"reject_subscription\"\n },\n \"disconnect_reasons\": {\n \"unauthorized\": \"unauthorized\",\n \"invalid_request\": \"invalid_request\",\n \"server_restart\": \"server_restart\"\n },\n \"default_mount_path\": \"/cable\",\n \"protocols\": [\n \"actioncable-v1-json\",\n \"actioncable-unsupported\"\n ]\n}\n", "import adapters from \"./adapters\"\nimport ConnectionMonitor from \"./connection_monitor\"\nimport INTERNAL from \"./internal\"\nimport logger from \"./logger\"\n\n// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.\n\nconst {message_types, protocols} = INTERNAL\nconst supportedProtocols = protocols.slice(0, protocols.length - 1)\n\nconst indexOf = [].indexOf\n\nclass Connection {\n constructor(consumer) {\n this.open = this.open.bind(this)\n this.consumer = consumer\n this.subscriptions = this.consumer.subscriptions\n this.monitor = new ConnectionMonitor(this)\n this.disconnected = true\n }\n\n send(data) {\n if (this.isOpen()) {\n this.webSocket.send(JSON.stringify(data))\n return true\n } else {\n return false\n }\n }\n\n open() {\n if (this.isActive()) {\n logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)\n return false\n } else {\n logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)\n if (this.webSocket) { this.uninstallEventHandlers() }\n this.webSocket = new adapters.WebSocket(this.consumer.url, protocols)\n this.installEventHandlers()\n this.monitor.start()\n return true\n }\n }\n\n close({allowReconnect} = {allowReconnect: true}) {\n if (!allowReconnect) { this.monitor.stop() }\n // Avoid closing websockets in a \"connecting\" state due to Safari 15.1+ bug. See: https://github.com/rails/rails/issues/43835#issuecomment-1002288478\n if (this.isOpen()) {\n return this.webSocket.close()\n }\n }\n\n reopen() {\n logger.log(`Reopening WebSocket, current state is ${this.getState()}`)\n if (this.isActive()) {\n try {\n return this.close()\n } catch (error) {\n logger.log(\"Failed to reopen WebSocket\", error)\n }\n finally {\n logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)\n setTimeout(this.open, this.constructor.reopenDelay)\n }\n } else {\n return this.open()\n }\n }\n\n getProtocol() {\n if (this.webSocket) {\n return this.webSocket.protocol\n }\n }\n\n isOpen() {\n return this.isState(\"open\")\n }\n\n isActive() {\n return this.isState(\"open\", \"connecting\")\n }\n\n // Private\n\n isProtocolSupported() {\n return indexOf.call(supportedProtocols, this.getProtocol()) >= 0\n }\n\n isState(...states) {\n return indexOf.call(states, this.getState()) >= 0\n }\n\n getState() {\n if (this.webSocket) {\n for (let state in adapters.WebSocket) {\n if (adapters.WebSocket[state] === this.webSocket.readyState) {\n return state.toLowerCase()\n }\n }\n }\n return null\n }\n\n installEventHandlers() {\n for (let eventName in this.events) {\n const handler = this.events[eventName].bind(this)\n this.webSocket[`on${eventName}`] = handler\n }\n }\n\n uninstallEventHandlers() {\n for (let eventName in this.events) {\n this.webSocket[`on${eventName}`] = function() {}\n }\n }\n\n}\n\nConnection.reopenDelay = 500\n\nConnection.prototype.events = {\n message(event) {\n if (!this.isProtocolSupported()) { return }\n const {identifier, message, reason, reconnect, type} = JSON.parse(event.data)\n switch (type) {\n case message_types.welcome:\n this.monitor.recordConnect()\n return this.subscriptions.reload()\n case message_types.disconnect:\n logger.log(`Disconnecting. Reason: ${reason}`)\n return this.close({allowReconnect: reconnect})\n case message_types.ping:\n return this.monitor.recordPing()\n case message_types.confirmation:\n this.subscriptions.confirmSubscription(identifier)\n return this.subscriptions.notify(identifier, \"connected\")\n case message_types.rejection:\n return this.subscriptions.reject(identifier)\n default:\n return this.subscriptions.notify(identifier, \"received\", message)\n }\n },\n\n open() {\n logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)\n this.disconnected = false\n if (!this.isProtocolSupported()) {\n logger.log(\"Protocol is unsupported. Stopping monitor and disconnecting.\")\n return this.close({allowReconnect: false})\n }\n },\n\n close(event) {\n logger.log(\"WebSocket onclose event\")\n if (this.disconnected) { return }\n this.disconnected = true\n this.monitor.recordDisconnect()\n return this.subscriptions.notifyAll(\"disconnected\", {willAttemptReconnect: this.monitor.isRunning()})\n },\n\n error() {\n logger.log(\"WebSocket onerror event\")\n }\n}\n\nexport default Connection\n", "// A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.\n// It provides a number of callbacks and a method for calling remote procedure calls on the corresponding\n// Channel instance on the server side.\n//\n// An example demonstrates the basic functionality:\n//\n// App.appearance = App.cable.subscriptions.create(\"AppearanceChannel\", {\n// connected() {\n// // Called once the subscription has been successfully completed\n// },\n//\n// disconnected({ willAttemptReconnect: boolean }) {\n// // Called when the client has disconnected with the server.\n// // The object will have an `willAttemptReconnect` property which\n// // says whether the client has the intention of attempting\n// // to reconnect.\n// },\n//\n// appear() {\n// this.perform('appear', {appearing_on: this.appearingOn()})\n// },\n//\n// away() {\n// this.perform('away')\n// },\n//\n// appearingOn() {\n// $('main').data('appearing-on')\n// }\n// })\n//\n// The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server\n// by calling the `perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away).\n// The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter.\n//\n// This is how the server component would look:\n//\n// class AppearanceChannel < ApplicationActionCable::Channel\n// def subscribed\n// current_user.appear\n// end\n//\n// def unsubscribed\n// current_user.disappear\n// end\n//\n// def appear(data)\n// current_user.appear on: data['appearing_on']\n// end\n//\n// def away\n// current_user.away\n// end\n// end\n//\n// The \"AppearanceChannel\" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.\n// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method.\n\nconst extend = function(object, properties) {\n if (properties != null) {\n for (let key in properties) {\n const value = properties[key]\n object[key] = value\n }\n }\n return object\n}\n\nexport default class Subscription {\n constructor(consumer, params = {}, mixin) {\n this.consumer = consumer\n this.identifier = JSON.stringify(params)\n extend(this, mixin)\n }\n\n // Perform a channel action with the optional data passed as an attribute\n perform(action, data = {}) {\n data.action = action\n return this.send(data)\n }\n\n send(data) {\n return this.consumer.send({command: \"message\", identifier: this.identifier, data: JSON.stringify(data)})\n }\n\n unsubscribe() {\n return this.consumer.subscriptions.remove(this)\n }\n}\n", "import logger from \"./logger\"\n\n// Responsible for ensuring channel subscribe command is confirmed, retrying until confirmation is received.\n// Internal class, not intended for direct user manipulation.\n\nclass SubscriptionGuarantor {\n constructor(subscriptions) {\n this.subscriptions = subscriptions\n this.pendingSubscriptions = []\n }\n\n guarantee(subscription) {\n if(this.pendingSubscriptions.indexOf(subscription) == -1){ \n logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`)\n this.pendingSubscriptions.push(subscription) \n }\n else {\n logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`)\n }\n this.startGuaranteeing()\n }\n\n forget(subscription) {\n logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`)\n this.pendingSubscriptions = (this.pendingSubscriptions.filter((s) => s !== subscription))\n }\n\n startGuaranteeing() {\n this.stopGuaranteeing()\n this.retrySubscribing()\n }\n \n stopGuaranteeing() {\n clearTimeout(this.retryTimeout)\n }\n\n retrySubscribing() {\n this.retryTimeout = setTimeout(() => {\n if (this.subscriptions && typeof(this.subscriptions.subscribe) === \"function\") {\n this.pendingSubscriptions.map((subscription) => {\n logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`)\n this.subscriptions.subscribe(subscription)\n })\n }\n }\n , 500)\n }\n}\n\nexport default SubscriptionGuarantor", "import Subscription from \"./subscription\"\nimport SubscriptionGuarantor from \"./subscription_guarantor\"\nimport logger from \"./logger\"\n\n// Collection class for creating (and internally managing) channel subscriptions.\n// The only method intended to be triggered by the user is ActionCable.Subscriptions#create,\n// and it should be called through the consumer like so:\n//\n// App = {}\n// App.cable = ActionCable.createConsumer(\"ws://example.com/accounts/1\")\n// App.appearance = App.cable.subscriptions.create(\"AppearanceChannel\")\n//\n// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.\n\nexport default class Subscriptions {\n constructor(consumer) {\n this.consumer = consumer\n this.guarantor = new SubscriptionGuarantor(this)\n this.subscriptions = []\n }\n\n create(channelName, mixin) {\n const channel = channelName\n const params = typeof channel === \"object\" ? channel : {channel}\n const subscription = new Subscription(this.consumer, params, mixin)\n return this.add(subscription)\n }\n\n // Private\n\n add(subscription) {\n this.subscriptions.push(subscription)\n this.consumer.ensureActiveConnection()\n this.notify(subscription, \"initialized\")\n this.subscribe(subscription)\n return subscription\n }\n\n remove(subscription) {\n this.forget(subscription)\n if (!this.findAll(subscription.identifier).length) {\n this.sendCommand(subscription, \"unsubscribe\")\n }\n return subscription\n }\n\n reject(identifier) {\n return this.findAll(identifier).map((subscription) => {\n this.forget(subscription)\n this.notify(subscription, \"rejected\")\n return subscription\n })\n }\n\n forget(subscription) {\n this.guarantor.forget(subscription)\n this.subscriptions = (this.subscriptions.filter((s) => s !== subscription))\n return subscription\n }\n\n findAll(identifier) {\n return this.subscriptions.filter((s) => s.identifier === identifier)\n }\n\n reload() {\n return this.subscriptions.map((subscription) =>\n this.subscribe(subscription))\n }\n\n notifyAll(callbackName, ...args) {\n return this.subscriptions.map((subscription) =>\n this.notify(subscription, callbackName, ...args))\n }\n\n notify(subscription, callbackName, ...args) {\n let subscriptions\n if (typeof subscription === \"string\") {\n subscriptions = this.findAll(subscription)\n } else {\n subscriptions = [subscription]\n }\n\n return subscriptions.map((subscription) =>\n (typeof subscription[callbackName] === \"function\" ? subscription[callbackName](...args) : undefined))\n }\n\n subscribe(subscription) {\n if (this.sendCommand(subscription, \"subscribe\")) {\n this.guarantor.guarantee(subscription)\n }\n }\n\n confirmSubscription(identifier) {\n logger.log(`Subscription confirmed ${identifier}`)\n this.findAll(identifier).map((subscription) =>\n this.guarantor.forget(subscription))\n }\n\n sendCommand(subscription, command) {\n const {identifier} = subscription\n return this.consumer.send({command, identifier})\n }\n}\n", "import Connection from \"./connection\"\nimport Subscriptions from \"./subscriptions\"\n\n// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,\n// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.\n// The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription\n// method.\n//\n// The following example shows how this can be set up:\n//\n// App = {}\n// App.cable = ActionCable.createConsumer(\"ws://example.com/accounts/1\")\n// App.appearance = App.cable.subscriptions.create(\"AppearanceChannel\")\n//\n// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.\n//\n// When a consumer is created, it automatically connects with the server.\n//\n// To disconnect from the server, call\n//\n// App.cable.disconnect()\n//\n// and to restart the connection:\n//\n// App.cable.connect()\n//\n// Any channel subscriptions which existed prior to disconnecting will\n// automatically resubscribe.\n\nexport default class Consumer {\n constructor(url) {\n this._url = url\n this.subscriptions = new Subscriptions(this)\n this.connection = new Connection(this)\n }\n\n get url() {\n return createWebSocketURL(this._url)\n }\n\n send(data) {\n return this.connection.send(data)\n }\n\n connect() {\n return this.connection.open()\n }\n\n disconnect() {\n return this.connection.close({allowReconnect: false})\n }\n\n ensureActiveConnection() {\n if (!this.connection.isActive()) {\n return this.connection.open()\n }\n }\n}\n\nexport function createWebSocketURL(url) {\n if (typeof url === \"function\") {\n url = url()\n }\n\n if (url && !/^wss?:/i.test(url)) {\n const a = document.createElement(\"a\")\n a.href = url\n // Fix populating Location properties in IE. Otherwise, protocol will be blank.\n a.href = a.href\n a.protocol = a.protocol.replace(\"http\", \"ws\")\n return a.href\n } else {\n return url\n }\n}\n", "import Connection from \"./connection\"\nimport ConnectionMonitor from \"./connection_monitor\"\nimport Consumer, { createWebSocketURL } from \"./consumer\"\nimport INTERNAL from \"./internal\"\nimport Subscription from \"./subscription\"\nimport Subscriptions from \"./subscriptions\"\nimport SubscriptionGuarantor from \"./subscription_guarantor\"\nimport adapters from \"./adapters\"\nimport logger from \"./logger\"\n\nexport {\n Connection,\n ConnectionMonitor,\n Consumer,\n INTERNAL,\n Subscription,\n Subscriptions,\n SubscriptionGuarantor,\n adapters,\n createWebSocketURL,\n logger,\n}\n\nexport function createConsumer(url = getConfig(\"url\") || INTERNAL.default_mount_path) {\n return new Consumer(url)\n}\n\nexport function getConfig(name) {\n const element = document.head.querySelector(`meta[name='action-cable-${name}']`)\n if (element) {\n return element.getAttribute(\"content\")\n }\n}\n", "/**\n * Copyright (c) 2016 Denys Krasnoshchok\n * \n * Homepage: https://smartscheduling.com/en/documentation/autocomplete\n * Source: https://github.com/kraaden/autocomplete\n * \n * MIT License\n */\n\nexport const enum EventTrigger {\n Keyboard = 0,\n Focus = 1,\n Mouse = 2,\n /**\n * Fetch is triggered manually by calling `fetch` function returned in `AutocompleteResult`\n */\n Manual = 3\n}\n\n/**\n * Enum for controlling form submission when `ENTER` key is pressed in the autocomplete input field.\n */\nexport const enum PreventSubmit {\n Never = 0,\n Always = 1,\n /**\n * Form submission is prevented only when an item is selected from the autocomplete list.\n */\n OnSelect = 2\n}\n\nexport interface AutocompleteItem {\n label?: string;\n group?: string;\n}\n\nexport interface AutocompleteEvent {\n /**\n * Native event object passed by browser to the event handler\n */\n event: T;\n\n /**\n * Fetch data and display autocomplete\n */\n fetch: () => void;\n}\n\nexport interface AutocompleteSettings {\n /**\n * Autocomplete will be attached to this element.\n */\n input: HTMLInputElement | HTMLTextAreaElement;\n\n /**\n * Provide your own container for the widget.\n * If not specified, a new DIV element will be created.\n */\n container?: HTMLDivElement;\n\n /**\n * This method allows you to override the default rendering function for items.\n * It must return a DIV element or undefined to skip rendering.\n */\n render?: (item: T, currentValue: string, index: number) => HTMLDivElement | undefined;\n\n /**\n * This method allows you to override the default rendering function for item groups.\n * It must return a DIV element or undefined to skip rendering.\n */\n renderGroup?: (name: string, currentValue: string) => HTMLDivElement | undefined;\n\n /**\n * If specified, the autocomplete DOM element will have this class assigned to it.\n */\n className?: string;\n\n /**\n * Specify the minimum text length required to show autocomplete.\n */\n minLength?: number;\n\n /**\n * The message that will be showed when there are no suggestions that match the entered value.\n */\n emptyMsg?: string;\n\n /**\n * This method will be called when user choose an item in autocomplete. The selected item will be passed as the first parameter.\n */\n onSelect: (item: T, input: HTMLInputElement | HTMLTextAreaElement) => void;\n\n /**\n * Show autocomplete on focus event. Focus event will ignore the `minLength` property and will always call `fetch`.\n */\n showOnFocus?: boolean;\n\n /**\n * This method will be called to prepare suggestions and then pass them to autocomplete.\n * @param {string} text - text in the input field\n * @param {(items: T[] | false) => void} update - a callback function that must be called after suggestions are prepared\n * @param {EventTrigger} trigger - type of the event that triggered the fetch\n * @param {number} cursorPos - position of the cursor in the input field\n */\n fetch: (text: string, update: (items: T[] | false) => void, trigger: EventTrigger, cursorPos: number) => void;\n\n /**\n * Enforces that the fetch function will only be called once within the specified time frame (in milliseconds) and\n * delays execution. This prevents flooding your server with AJAX requests.\n */\n debounceWaitMs?: number;\n\n /**\n * Callback for additional autocomplete customization\n * @param {HTMLInputElement | HTMLTextAreaElement} input - input box associated with autocomplete\n * @param {ClientRect | DOMRect} inputRect - size of the input box and its position relative to the viewport\n * @param {HTMLDivElement} container - container with suggestions\n * @param {number} maxHeight - max height that can be used by autocomplete\n */\n customize?: (input: HTMLInputElement | HTMLTextAreaElement, inputRect: ClientRect | DOMRect, container: HTMLDivElement, maxHeight: number) => void;\n\n /**\n * Controls form submission when the ENTER key is pressed in a input field.\n */\n preventSubmit?: PreventSubmit;\n\n /**\n * Prevents the first item in the list from being selected automatically. This option allows you\n * to submit a custom text by pressing ENTER even when autocomplete is displayed.\n */\n disableAutoSelect?: boolean;\n\n /**\n * Provide your keyup event handler to display autocomplete when a key is pressed that doesn't modify the content. You can also perform some additional actions.\n */\n keyup?: (e: AutocompleteEvent) => void;\n\n /**\n * Allows to display autocomplete on mouse clicks or perform some additional actions.\n */\n click?: (e: AutocompleteEvent) => void;\n}\n\nexport interface AutocompleteResult {\n /**\n * Remove event handlers, DOM elements and ARIA/accessibility attributes created by the widget.\n */\n destroy: () => void;\n\n /**\n * This function allows to manually start data fetching and display autocomplete. Note that\n * it does not automatically place focus on the input field, so you may need to do so manually\n * in certain situations.\n */\n fetch: () => void;\n}\n\nexport default function autocomplete(settings: AutocompleteSettings): AutocompleteResult {\n\n // just an alias to minimize JS file size\n const doc = document;\n\n const container: HTMLDivElement = settings.container || doc.createElement('div');\n const preventSubmit: PreventSubmit = settings.preventSubmit || PreventSubmit.Never;\n\n container.id = container.id || 'autocomplete-' + uid();\n const containerStyle = container.style;\n const debounceWaitMs = settings.debounceWaitMs || 0;\n const disableAutoSelect = settings.disableAutoSelect || false;\n const customContainerParent = container.parentElement;\n\n let items: T[] = [];\n let inputValue = '';\n let minLen = 2;\n const showOnFocus = settings.showOnFocus;\n let selected: T | undefined;\n let fetchCounter = 0;\n let debounceTimer: number | undefined;\n let destroyed = false;\n\n // Fixes #104: autocomplete selection is broken on Firefox for Android\n let suppressAutocomplete = false;\n\n if (settings.minLength !== undefined) {\n minLen = settings.minLength;\n }\n\n if (!settings.input) {\n throw new Error('input undefined');\n }\n\n const input: HTMLInputElement | HTMLTextAreaElement = settings.input;\n\n container.className = 'autocomplete ' + (settings.className || '');\n container.setAttribute('role', 'listbox');\n\n input.setAttribute('role', 'combobox');\n input.setAttribute('aria-expanded', 'false');\n input.setAttribute('aria-autocomplete', 'list');\n input.setAttribute('aria-controls', container.id);\n input.setAttribute('aria-owns', container.id);\n input.setAttribute('aria-activedescendant', '');\n input.setAttribute('aria-haspopup', 'listbox');\n\n // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning\n containerStyle.position = 'absolute';\n\n /**\n * Generate a very complex textual ID that greatly reduces the chance of a collision with another ID or text.\n */\n function uid(): string {\n return Date.now().toString(36) + Math.random().toString(36).substring(2);\n }\n\n /**\n * Detach the container from DOM\n */\n function detach() {\n const parent = container.parentNode;\n if (parent) {\n parent.removeChild(container);\n }\n }\n\n /**\n * Clear debouncing timer if assigned\n */\n function clearDebounceTimer() {\n if (debounceTimer) {\n window.clearTimeout(debounceTimer);\n }\n }\n\n /**\n * Attach the container to DOM\n */\n function attach() {\n if (!container.parentNode) {\n (customContainerParent || doc.body).appendChild(container);\n }\n }\n\n /**\n * Check if container for autocomplete is displayed\n */\n function containerDisplayed(): boolean {\n return !!container.parentNode;\n }\n\n /**\n * Clear autocomplete state and hide container\n */\n function clear() {\n // prevent the update call if there are pending AJAX requests\n fetchCounter++;\n\n items = [];\n inputValue = '';\n selected = undefined;\n input.setAttribute('aria-activedescendant', '');\n input.setAttribute('aria-expanded', 'false');\n detach();\n }\n\n /**\n * Update autocomplete position\n */\n function updatePosition() {\n if (!containerDisplayed()) {\n return;\n }\n\n input.setAttribute('aria-expanded', 'true');\n\n containerStyle.height = 'auto';\n containerStyle.width = input.offsetWidth + 'px';\n\n let maxHeight = 0;\n let inputRect: DOMRect | undefined;\n\n function calc() {\n const docEl = doc.documentElement as HTMLElement;\n const clientTop = docEl.clientTop || doc.body.clientTop || 0;\n const clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;\n const scrollTop = window.pageYOffset || docEl.scrollTop;\n const scrollLeft = window.pageXOffset || docEl.scrollLeft;\n\n inputRect = input.getBoundingClientRect();\n\n const top = inputRect.top + input.offsetHeight + scrollTop - clientTop;\n const left = inputRect.left + scrollLeft - clientLeft;\n\n containerStyle.top = top + 'px';\n containerStyle.left = left + 'px';\n\n maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);\n\n if (maxHeight < 0) {\n maxHeight = 0;\n }\n\n containerStyle.top = top + 'px';\n containerStyle.bottom = '';\n containerStyle.left = left + 'px';\n containerStyle.maxHeight = maxHeight + 'px';\n }\n\n // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)\n calc();\n calc();\n\n if (settings.customize && inputRect) {\n settings.customize(input, inputRect, container, maxHeight);\n }\n }\n\n /**\n * Redraw the autocomplete div element with suggestions\n */\n function update() {\n\n container.innerHTML = '';\n input.setAttribute('aria-activedescendant', '');\n\n // function for rendering autocomplete suggestions\n let render = function (item: T, _: string, __: number): HTMLDivElement | undefined {\n const itemElement = doc.createElement('div');\n itemElement.textContent = item.label || '';\n return itemElement;\n };\n if (settings.render) {\n render = settings.render;\n }\n\n // function to render autocomplete groups\n let renderGroup = function (groupName: string, _: string): HTMLDivElement | undefined {\n const groupDiv = doc.createElement('div');\n groupDiv.textContent = groupName;\n return groupDiv;\n };\n if (settings.renderGroup) {\n renderGroup = settings.renderGroup;\n }\n\n const fragment = doc.createDocumentFragment();\n let prevGroup = uid();\n\n items.forEach(function (item: T, index: number): void {\n if (item.group && item.group !== prevGroup) {\n prevGroup = item.group;\n const groupDiv = renderGroup(item.group, inputValue);\n if (groupDiv) {\n groupDiv.className += ' group';\n fragment.appendChild(groupDiv);\n }\n }\n const div = render(item, inputValue, index);\n if (div) {\n div.id = `${container.id}_${index}`;\n div.setAttribute('role', 'option');\n div.addEventListener('click', function (ev: MouseEvent): void {\n suppressAutocomplete = true;\n try {\n settings.onSelect(item, input);\n } finally {\n suppressAutocomplete = false;\n }\n clear();\n ev.preventDefault();\n ev.stopPropagation();\n });\n if (item === selected) {\n div.className += ' selected';\n div.setAttribute('aria-selected', 'true');\n input.setAttribute('aria-activedescendant', div.id);\n }\n fragment.appendChild(div);\n }\n });\n container.appendChild(fragment);\n if (items.length < 1) {\n if (settings.emptyMsg) {\n const empty = doc.createElement('div');\n empty.id = `${container.id}_${uid()}`;\n empty.className = 'empty';\n empty.textContent = settings.emptyMsg;\n container.appendChild(empty);\n input.setAttribute('aria-activedescendant', empty.id);\n } else {\n clear();\n return;\n }\n }\n\n attach();\n updatePosition();\n\n updateScroll();\n }\n\n function updateIfDisplayed() {\n if (containerDisplayed()) {\n update();\n }\n }\n\n function resizeEventHandler() {\n updateIfDisplayed();\n }\n\n function scrollEventHandler(e: Event) {\n if (e.target !== container) {\n updateIfDisplayed();\n } else {\n e.preventDefault();\n }\n }\n\n function inputEventHandler() {\n if (!suppressAutocomplete) {\n fetch(EventTrigger.Keyboard);\n }\n }\n\n /**\n * Automatically move scroll bar if selected item is not visible\n */\n function updateScroll() {\n const elements = container.getElementsByClassName('selected');\n if (elements.length > 0) {\n let element = elements[0] as HTMLDivElement;\n\n // make group visible\n const previous = element.previousElementSibling as HTMLDivElement;\n if (previous && previous.className.indexOf('group') !== -1 && !previous.previousElementSibling) {\n element = previous;\n }\n\n if (element.offsetTop < container.scrollTop) {\n container.scrollTop = element.offsetTop;\n } else {\n const selectBottom = element.offsetTop + element.offsetHeight;\n const containerBottom = container.scrollTop + container.offsetHeight;\n if (selectBottom > containerBottom) {\n container.scrollTop += selectBottom - containerBottom;\n }\n }\n }\n }\n\n function selectPreviousSuggestion() {\n const index = items.indexOf(selected!);\n\n selected = index === -1\n ? undefined\n : items[(index + items.length - 1) % items.length];\n\n updateSelectedSuggestion(index);\n }\n\n function selectNextSuggestion() {\n const index = items.indexOf(selected!);\n\n selected = items.length < 1\n ? undefined\n : index === -1\n ? items[0]\n : items[(index + 1) % items.length];\n\n updateSelectedSuggestion(index);\n }\n\n function updateSelectedSuggestion(index: number) {\n if (items.length > 0) {\n unselectSuggestion(index);\n selectSuggestion(items.indexOf(selected!));\n updateScroll();\n }\n }\n\n function selectSuggestion(index: number) {\n var element = doc.getElementById(container.id + \"_\" + index);\n if (element) {\n element.classList.add('selected');\n element.setAttribute('aria-selected', 'true');\n input.setAttribute('aria-activedescendant', element.id);\n }\n }\n\n function unselectSuggestion(index: number) {\n var element = doc.getElementById(container.id + \"_\" + index);\n if (element) {\n element.classList.remove('selected');\n element.removeAttribute('aria-selected');\n input.removeAttribute('aria-activedescendant');\n }\n }\n\n function handleArrowAndEscapeKeys(ev: KeyboardEvent, key: 'ArrowUp' | 'ArrowDown' | 'Escape') {\n const containerIsDisplayed = containerDisplayed();\n\n if (key === 'Escape') {\n clear();\n } else {\n if (!containerIsDisplayed || items.length < 1) {\n return;\n }\n key === 'ArrowUp'\n ? selectPreviousSuggestion()\n : selectNextSuggestion();\n }\n\n ev.preventDefault();\n\n if (containerIsDisplayed) {\n ev.stopPropagation();\n }\n }\n\n function handleEnterKey(ev: KeyboardEvent) {\n if (selected) {\n if (preventSubmit === PreventSubmit.OnSelect) {\n ev.preventDefault();\n }\n suppressAutocomplete = true;\n try {\n settings.onSelect(selected, input);\n } finally {\n suppressAutocomplete = false;\n }\n clear();\n }\n\n if (preventSubmit === PreventSubmit.Always) {\n ev.preventDefault();\n }\n }\n\n function keydownEventHandler(ev: KeyboardEvent) {\n const key = ev.key;\n\n switch (key) {\n case 'ArrowUp':\n case 'ArrowDown':\n case 'Escape':\n handleArrowAndEscapeKeys(ev, key);\n break;\n case 'Enter':\n handleEnterKey(ev);\n break;\n default:\n break;\n }\n }\n\n function focusEventHandler() {\n if (showOnFocus) {\n fetch(EventTrigger.Focus);\n }\n }\n\n function fetch(trigger: EventTrigger) {\n if (input.value.length >= minLen || trigger === EventTrigger.Focus) {\n clearDebounceTimer();\n debounceTimer = window.setTimeout(\n () => startFetch(input.value, trigger, input.selectionStart || 0),\n trigger === EventTrigger.Keyboard || trigger === EventTrigger.Mouse ? debounceWaitMs : 0);\n } else {\n clear();\n }\n }\n\n function startFetch(inputText: string, trigger: EventTrigger, cursorPos: number) {\n if (destroyed) return;\n const savedFetchCounter = ++fetchCounter;\n settings.fetch(inputText, function (elements: T[] | false): void {\n if (fetchCounter === savedFetchCounter && elements) {\n items = elements;\n inputValue = inputText;\n selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];\n update();\n }\n }, trigger, cursorPos);\n }\n\n function keyupEventHandler(e: KeyboardEvent) {\n if (settings.keyup) {\n settings.keyup({\n event: e,\n fetch: () => fetch(EventTrigger.Keyboard)\n });\n return;\n }\n\n if (!containerDisplayed() && e.key === 'ArrowDown') {\n fetch(EventTrigger.Keyboard);\n }\n }\n\n function clickEventHandler(e: MouseEvent) {\n settings.click && settings.click({\n event: e,\n fetch: () => fetch(EventTrigger.Mouse)\n });\n }\n\n function blurEventHandler() {\n // when an item is selected by mouse click, the blur event will be initiated before the click event and remove DOM elements,\n // so that the click event will never be triggered. In order to avoid this issue, DOM removal should be delayed.\n setTimeout(() => {\n if (doc.activeElement !== input) {\n clear();\n }\n }, 200);\n }\n\n function manualFetch() {\n startFetch(input.value, EventTrigger.Manual, input.selectionStart || 0);\n }\n\n /**\n * Fixes #26: on long clicks focus will be lost and onSelect method will not be called\n */\n container.addEventListener('mousedown', function (evt: Event) {\n evt.stopPropagation();\n evt.preventDefault();\n });\n\n /**\n * Fixes #30: autocomplete closes when scrollbar is clicked in IE\n * See: https://stackoverflow.com/a/9210267/13172349\n */\n container.addEventListener('focus', () => input.focus());\n\n /**\n * This function will remove DOM elements and clear event handlers\n */\n function destroy() {\n input.removeEventListener('focus', focusEventHandler);\n input.removeEventListener('keyup', keyupEventHandler as EventListenerOrEventListenerObject)\n input.removeEventListener('click', clickEventHandler as EventListenerOrEventListenerObject)\n input.removeEventListener('keydown', keydownEventHandler as EventListenerOrEventListenerObject);\n input.removeEventListener('input', inputEventHandler as EventListenerOrEventListenerObject);\n input.removeEventListener('blur', blurEventHandler);\n window.removeEventListener('resize', resizeEventHandler);\n doc.removeEventListener('scroll', scrollEventHandler, true);\n input.removeAttribute('role');\n input.removeAttribute('aria-expanded');\n input.removeAttribute('aria-autocomplete');\n input.removeAttribute('aria-controls');\n input.removeAttribute('aria-activedescendant');\n input.removeAttribute('aria-owns');\n input.removeAttribute('aria-haspopup');\n clearDebounceTimer();\n clear();\n destroyed = true;\n }\n\n // setup event handlers\n input.addEventListener('keyup', keyupEventHandler as EventListenerOrEventListenerObject);\n input.addEventListener('click', clickEventHandler as EventListenerOrEventListenerObject);\n input.addEventListener('keydown', keydownEventHandler as EventListenerOrEventListenerObject);\n input.addEventListener('input', inputEventHandler as EventListenerOrEventListenerObject);\n input.addEventListener('blur', blurEventHandler);\n input.addEventListener('focus', focusEventHandler);\n window.addEventListener('resize', resizeEventHandler);\n doc.addEventListener('scroll', scrollEventHandler, true);\n\n return {\n destroy,\n fetch: manualFetch\n };\n}\n", "\"use strict\";\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __generator = (this && this.__generator) || function (thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.ReCaptchaInstance = void 0;\nvar ReCaptchaInstance = (function () {\n function ReCaptchaInstance(siteKey, recaptchaID, recaptcha) {\n this.siteKey = siteKey;\n this.recaptchaID = recaptchaID;\n this.recaptcha = recaptcha;\n this.styleContainer = null;\n }\n ReCaptchaInstance.prototype.execute = function (action) {\n return __awaiter(this, void 0, void 0, function () {\n var _a;\n return __generator(this, function (_b) {\n switch (_b.label) {\n case 0:\n if (!this.recaptcha.enterprise) return [3, 2];\n return [4, this.recaptcha.enterprise.execute(this.recaptchaID, { action: action })];\n case 1:\n _a = _b.sent();\n return [3, 4];\n case 2: return [4, this.recaptcha.execute(this.recaptchaID, { action: action })];\n case 3:\n _a = _b.sent();\n _b.label = 4;\n case 4: return [2, _a];\n }\n });\n });\n };\n ReCaptchaInstance.prototype.getSiteKey = function () {\n return this.siteKey;\n };\n ReCaptchaInstance.prototype.hideBadge = function () {\n if (this.styleContainer !== null) {\n return;\n }\n this.styleContainer = document.createElement(\"style\");\n this.styleContainer.innerHTML =\n \".grecaptcha-badge{visibility:hidden !important;}\";\n document.head.appendChild(this.styleContainer);\n };\n ReCaptchaInstance.prototype.showBadge = function () {\n if (this.styleContainer === null) {\n return;\n }\n document.head.removeChild(this.styleContainer);\n this.styleContainer = null;\n };\n return ReCaptchaInstance;\n}());\nexports.ReCaptchaInstance = ReCaptchaInstance;\n", "\"use strict\";\nvar __assign = (this && this.__assign) || function () {\n __assign = Object.assign || function(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))\n t[p] = s[p];\n }\n return t;\n };\n return __assign.apply(this, arguments);\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.getInstance = exports.load = void 0;\nvar ReCaptchaInstance_1 = require(\"./ReCaptchaInstance\");\nvar ELoadingState;\n(function (ELoadingState) {\n ELoadingState[ELoadingState[\"NOT_LOADED\"] = 0] = \"NOT_LOADED\";\n ELoadingState[ELoadingState[\"LOADING\"] = 1] = \"LOADING\";\n ELoadingState[ELoadingState[\"LOADED\"] = 2] = \"LOADED\";\n})(ELoadingState || (ELoadingState = {}));\nvar ReCaptchaLoader = (function () {\n function ReCaptchaLoader() {\n }\n ReCaptchaLoader.load = function (siteKey, options) {\n if (options === void 0) { options = {}; }\n if (typeof document === \"undefined\") {\n return Promise.reject(new Error(\"This is a library for the browser!\"));\n }\n if (ReCaptchaLoader.getLoadingState() === ELoadingState.LOADED) {\n if (ReCaptchaLoader.instance.getSiteKey() === siteKey) {\n return Promise.resolve(ReCaptchaLoader.instance);\n }\n else {\n return Promise.reject(new Error(\"reCAPTCHA already loaded with different site key!\"));\n }\n }\n if (ReCaptchaLoader.getLoadingState() === ELoadingState.LOADING) {\n if (siteKey !== ReCaptchaLoader.instanceSiteKey) {\n return Promise.reject(new Error(\"reCAPTCHA already loaded with different site key!\"));\n }\n return new Promise(function (resolve, reject) {\n ReCaptchaLoader.successfulLoadingConsumers.push(function (instance) { return resolve(instance); });\n ReCaptchaLoader.errorLoadingRunnable.push(function (reason) {\n return reject(reason);\n });\n });\n }\n ReCaptchaLoader.instanceSiteKey = siteKey;\n ReCaptchaLoader.setLoadingState(ELoadingState.LOADING);\n var loader = new ReCaptchaLoader();\n return new Promise(function (resolve, reject) {\n loader\n .loadScript(siteKey, options.useRecaptchaNet || false, options.useEnterprise || false, options.renderParameters ? options.renderParameters : {}, options.customUrl)\n .then(function () {\n ReCaptchaLoader.setLoadingState(ELoadingState.LOADED);\n var widgetID = loader.doExplicitRender(grecaptcha, siteKey, options.explicitRenderParameters\n ? options.explicitRenderParameters\n : {}, options.useEnterprise || false);\n var instance = new ReCaptchaInstance_1.ReCaptchaInstance(siteKey, widgetID, grecaptcha);\n ReCaptchaLoader.successfulLoadingConsumers.forEach(function (v) {\n return v(instance);\n });\n ReCaptchaLoader.successfulLoadingConsumers = [];\n if (options.autoHideBadge) {\n instance.hideBadge();\n }\n ReCaptchaLoader.instance = instance;\n resolve(instance);\n })\n .catch(function (error) {\n ReCaptchaLoader.errorLoadingRunnable.forEach(function (v) { return v(error); });\n ReCaptchaLoader.errorLoadingRunnable = [];\n reject(error);\n });\n });\n };\n ReCaptchaLoader.getInstance = function () {\n return ReCaptchaLoader.instance;\n };\n ReCaptchaLoader.setLoadingState = function (state) {\n ReCaptchaLoader.loadingState = state;\n };\n ReCaptchaLoader.getLoadingState = function () {\n if (ReCaptchaLoader.loadingState === null) {\n return ELoadingState.NOT_LOADED;\n }\n else {\n return ReCaptchaLoader.loadingState;\n }\n };\n ReCaptchaLoader.prototype.loadScript = function (siteKey, useRecaptchaNet, useEnterprise, renderParameters, customUrl) {\n var _this = this;\n if (useRecaptchaNet === void 0) { useRecaptchaNet = false; }\n if (useEnterprise === void 0) { useEnterprise = false; }\n if (renderParameters === void 0) { renderParameters = {}; }\n if (customUrl === void 0) { customUrl = \"\"; }\n var scriptElement = document.createElement(\"script\");\n scriptElement.setAttribute(\"recaptcha-v3-script\", \"\");\n scriptElement.setAttribute(\"async\", \"\");\n scriptElement.setAttribute(\"defer\", \"\");\n var scriptBase = \"https://www.google.com/recaptcha/api.js\";\n if (useRecaptchaNet) {\n if (useEnterprise) {\n scriptBase = \"https://recaptcha.net/recaptcha/enterprise.js\";\n }\n else {\n scriptBase = \"https://recaptcha.net/recaptcha/api.js\";\n }\n }\n else if (useEnterprise) {\n scriptBase = \"https://www.google.com/recaptcha/enterprise.js\";\n }\n if (customUrl) {\n scriptBase = customUrl;\n }\n if (renderParameters.render) {\n renderParameters.render = undefined;\n }\n var parametersQuery = this.buildQueryString(renderParameters);\n scriptElement.src = scriptBase + \"?render=explicit\" + parametersQuery;\n return new Promise(function (resolve, reject) {\n scriptElement.addEventListener(\"load\", _this.waitForScriptToLoad(function () {\n resolve(scriptElement);\n }, useEnterprise), false);\n scriptElement.onerror = function (error) {\n ReCaptchaLoader.setLoadingState(ELoadingState.NOT_LOADED);\n reject(error);\n };\n document.head.appendChild(scriptElement);\n });\n };\n ReCaptchaLoader.prototype.buildQueryString = function (parameters) {\n var parameterKeys = Object.keys(parameters);\n if (parameterKeys.length < 1) {\n return \"\";\n }\n return (\"&\" +\n Object.keys(parameters)\n .filter(function (parameterKey) {\n return !!parameters[parameterKey];\n })\n .map(function (parameterKey) {\n return parameterKey + \"=\" + parameters[parameterKey];\n })\n .join(\"&\"));\n };\n ReCaptchaLoader.prototype.waitForScriptToLoad = function (callback, useEnterprise) {\n var _this = this;\n return function () {\n if (window.grecaptcha === undefined) {\n setTimeout(function () {\n _this.waitForScriptToLoad(callback, useEnterprise);\n }, ReCaptchaLoader.SCRIPT_LOAD_DELAY);\n }\n else {\n if (useEnterprise) {\n window.grecaptcha.enterprise.ready(function () {\n callback();\n });\n }\n else {\n window.grecaptcha.ready(function () {\n callback();\n });\n }\n }\n };\n };\n ReCaptchaLoader.prototype.doExplicitRender = function (grecaptcha, siteKey, parameters, isEnterprise) {\n var augmentedParameters = __assign({ sitekey: siteKey }, parameters);\n if (parameters.container) {\n if (isEnterprise) {\n return grecaptcha.enterprise.render(parameters.container, augmentedParameters);\n }\n else {\n return grecaptcha.render(parameters.container, augmentedParameters);\n }\n }\n else {\n if (isEnterprise) {\n return grecaptcha.enterprise.render(augmentedParameters);\n }\n else {\n return grecaptcha.render(augmentedParameters);\n }\n }\n };\n ReCaptchaLoader.loadingState = null;\n ReCaptchaLoader.instance = null;\n ReCaptchaLoader.instanceSiteKey = null;\n ReCaptchaLoader.successfulLoadingConsumers = [];\n ReCaptchaLoader.errorLoadingRunnable = [];\n ReCaptchaLoader.SCRIPT_LOAD_DELAY = 25;\n return ReCaptchaLoader;\n}());\nexports.load = ReCaptchaLoader.load;\nexports.getInstance = ReCaptchaLoader.getInstance;\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.ReCaptchaInstance = exports.getInstance = exports.load = void 0;\nvar ReCaptchaLoader_1 = require(\"./ReCaptchaLoader\");\nObject.defineProperty(exports, \"load\", { enumerable: true, get: function () { return ReCaptchaLoader_1.load; } });\nObject.defineProperty(exports, \"getInstance\", { enumerable: true, get: function () { return ReCaptchaLoader_1.getInstance; } });\nvar ReCaptchaInstance_1 = require(\"./ReCaptchaInstance\");\nObject.defineProperty(exports, \"ReCaptchaInstance\", { enumerable: true, get: function () { return ReCaptchaInstance_1.ReCaptchaInstance; } });\n", "typeof navigator === \"object\" && (function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define('Plyr', factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());\n})(this, (function () { 'use strict';\n\n function _defineProperty$1(obj, key, value) {\n key = _toPropertyKey(key);\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n return obj;\n }\n function _toPrimitive(input, hint) {\n if (typeof input !== \"object\" || input === null) return input;\n var prim = input[Symbol.toPrimitive];\n if (prim !== undefined) {\n var res = prim.call(input, hint || \"default\");\n if (typeof res !== \"object\") return res;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (hint === \"string\" ? String : Number)(input);\n }\n function _toPropertyKey(arg) {\n var key = _toPrimitive(arg, \"string\");\n return typeof key === \"symbol\" ? key : String(key);\n }\n\n function _classCallCheck(e, t) {\n if (!(e instanceof t)) throw new TypeError(\"Cannot call a class as a function\");\n }\n function _defineProperties(e, t) {\n for (var n = 0; n < t.length; n++) {\n var r = t[n];\n r.enumerable = r.enumerable || !1, r.configurable = !0, \"value\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\n }\n }\n function _createClass(e, t, n) {\n return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\n }\n function _defineProperty(e, t, n) {\n return t in e ? Object.defineProperty(e, t, {\n value: n,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[t] = n, e;\n }\n function ownKeys(e, t) {\n var n = Object.keys(e);\n if (Object.getOwnPropertySymbols) {\n var r = Object.getOwnPropertySymbols(e);\n t && (r = r.filter(function (t) {\n return Object.getOwnPropertyDescriptor(e, t).enumerable;\n })), n.push.apply(n, r);\n }\n return n;\n }\n function _objectSpread2(e) {\n for (var t = 1; t < arguments.length; t++) {\n var n = null != arguments[t] ? arguments[t] : {};\n t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\n _defineProperty(e, t, n[t]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\n Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\n });\n }\n return e;\n }\n var defaults$1 = {\n addCSS: !0,\n thumbWidth: 15,\n watch: !0\n };\n function matches$1(e, t) {\n return function () {\n return Array.from(document.querySelectorAll(t)).includes(this);\n }.call(e, t);\n }\n function trigger(e, t) {\n if (e && t) {\n var n = new Event(t, {\n bubbles: !0\n });\n e.dispatchEvent(n);\n }\n }\n var getConstructor$1 = function (e) {\n return null != e ? e.constructor : null;\n },\n instanceOf$1 = function (e, t) {\n return !!(e && t && e instanceof t);\n },\n isNullOrUndefined$1 = function (e) {\n return null == e;\n },\n isObject$1 = function (e) {\n return getConstructor$1(e) === Object;\n },\n isNumber$1 = function (e) {\n return getConstructor$1(e) === Number && !Number.isNaN(e);\n },\n isString$1 = function (e) {\n return getConstructor$1(e) === String;\n },\n isBoolean$1 = function (e) {\n return getConstructor$1(e) === Boolean;\n },\n isFunction$1 = function (e) {\n return getConstructor$1(e) === Function;\n },\n isArray$1 = function (e) {\n return Array.isArray(e);\n },\n isNodeList$1 = function (e) {\n return instanceOf$1(e, NodeList);\n },\n isElement$1 = function (e) {\n return instanceOf$1(e, Element);\n },\n isEvent$1 = function (e) {\n return instanceOf$1(e, Event);\n },\n isEmpty$1 = function (e) {\n return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\n },\n is$1 = {\n nullOrUndefined: isNullOrUndefined$1,\n object: isObject$1,\n number: isNumber$1,\n string: isString$1,\n boolean: isBoolean$1,\n function: isFunction$1,\n array: isArray$1,\n nodeList: isNodeList$1,\n element: isElement$1,\n event: isEvent$1,\n empty: isEmpty$1\n };\n function getDecimalPlaces(e) {\n var t = \"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);\n return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\n }\n function round(e, t) {\n if (1 > t) {\n var n = getDecimalPlaces(t);\n return parseFloat(e.toFixed(n));\n }\n return Math.round(e / t) * t;\n }\n var RangeTouch = function () {\n function e(t, n) {\n _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\n }\n return _createClass(e, [{\n key: \"init\",\n value: function () {\n e.enabled && (this.config.addCSS && (this.element.style.userSelect = \"none\", this.element.style.webKitUserSelect = \"none\", this.element.style.touchAction = \"manipulation\"), this.listeners(!0), this.element.rangeTouch = this);\n }\n }, {\n key: \"destroy\",\n value: function () {\n e.enabled && (this.config.addCSS && (this.element.style.userSelect = \"\", this.element.style.webKitUserSelect = \"\", this.element.style.touchAction = \"\"), this.listeners(!1), this.element.rangeTouch = null);\n }\n }, {\n key: \"listeners\",\n value: function (e) {\n var t = this,\n n = e ? \"addEventListener\" : \"removeEventListener\";\n [\"touchstart\", \"touchmove\", \"touchend\"].forEach(function (e) {\n t.element[n](e, function (e) {\n return t.set(e);\n }, !1);\n });\n }\n }, {\n key: \"get\",\n value: function (t) {\n if (!e.enabled || !is$1.event(t)) return null;\n var n,\n r = t.target,\n i = t.changedTouches[0],\n o = parseFloat(r.getAttribute(\"min\")) || 0,\n s = parseFloat(r.getAttribute(\"max\")) || 100,\n u = parseFloat(r.getAttribute(\"step\")) || 1,\n c = r.getBoundingClientRect(),\n a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\n return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\n }\n }, {\n key: \"set\",\n value: function (t) {\n e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \"touchend\" === t.type ? \"change\" : \"input\"));\n }\n }], [{\n key: \"setup\",\n value: function (t) {\n var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\n r = null;\n if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\"range\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\n var i = _objectSpread2({}, defaults$1, {}, n);\n if (is$1.string(t) && i.watch) {\n var o = new MutationObserver(function (n) {\n Array.from(n).forEach(function (n) {\n Array.from(n.addedNodes).forEach(function (n) {\n is$1.element(n) && matches$1(n, t) && new e(n, i);\n });\n });\n });\n o.observe(document.body, {\n childList: !0,\n subtree: !0\n });\n }\n return r.map(function (t) {\n return new e(t, n);\n });\n }\n }, {\n key: \"enabled\",\n get: function () {\n return \"ontouchstart\" in document.documentElement;\n }\n }]), e;\n }();\n\n // ==========================================================================\n // Type checking utils\n // ==========================================================================\n\n const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\n const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\n const isNullOrUndefined = input => input === null || typeof input === 'undefined';\n const isObject = input => getConstructor(input) === Object;\n const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\n const isString = input => getConstructor(input) === String;\n const isBoolean = input => getConstructor(input) === Boolean;\n const isFunction = input => typeof input === 'function';\n const isArray = input => Array.isArray(input);\n const isWeakMap = input => instanceOf(input, WeakMap);\n const isNodeList = input => instanceOf(input, NodeList);\n const isTextNode = input => getConstructor(input) === Text;\n const isEvent = input => instanceOf(input, Event);\n const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\n const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\n const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\n const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\n const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\n const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\n const isUrl = input => {\n // Accept a URL object\n if (instanceOf(input, window.URL)) {\n return true;\n }\n\n // Must be string from here\n if (!isString(input)) {\n return false;\n }\n\n // Add the protocol if required\n let string = input;\n if (!input.startsWith('http://') || !input.startsWith('https://')) {\n string = `http://${input}`;\n }\n try {\n return !isEmpty(new URL(string).hostname);\n } catch (_) {\n return false;\n }\n };\n var is = {\n nullOrUndefined: isNullOrUndefined,\n object: isObject,\n number: isNumber,\n string: isString,\n boolean: isBoolean,\n function: isFunction,\n array: isArray,\n weakMap: isWeakMap,\n nodeList: isNodeList,\n element: isElement,\n textNode: isTextNode,\n event: isEvent,\n keyboardEvent: isKeyboardEvent,\n cue: isCue,\n track: isTrack,\n promise: isPromise,\n url: isUrl,\n empty: isEmpty\n };\n\n // ==========================================================================\n const transitionEndEvent = (() => {\n const element = document.createElement('span');\n const events = {\n WebkitTransition: 'webkitTransitionEnd',\n MozTransition: 'transitionend',\n OTransition: 'oTransitionEnd otransitionend',\n transition: 'transitionend'\n };\n const type = Object.keys(events).find(event => element.style[event] !== undefined);\n return is.string(type) ? events[type] : false;\n })();\n\n // Force repaint of element\n function repaint(element, delay) {\n setTimeout(() => {\n try {\n // eslint-disable-next-line no-param-reassign\n element.hidden = true;\n\n // eslint-disable-next-line no-unused-expressions\n element.offsetHeight;\n\n // eslint-disable-next-line no-param-reassign\n element.hidden = false;\n } catch (_) {\n // Do nothing\n }\n }, delay);\n }\n\n // ==========================================================================\n // Browser sniffing\n // Unfortunately, due to mixed support, UA sniffing is required\n // ==========================================================================\n\n const isIE = Boolean(window.document.documentMode);\n const isEdge = /Edge/g.test(navigator.userAgent);\n const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\n const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\n // navigator.platform may be deprecated but this check is still required\n const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\n const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\n var browser = {\n isIE,\n isEdge,\n isWebKit,\n isIPhone,\n isIPadOS,\n isIos\n };\n\n // ==========================================================================\n\n // Clone nested objects\n function cloneDeep(object) {\n return JSON.parse(JSON.stringify(object));\n }\n\n // Get a nested value in an object\n function getDeep(object, path) {\n return path.split('.').reduce((obj, key) => obj && obj[key], object);\n }\n\n // Deep extend destination object with N more objects\n function extend(target = {}, ...sources) {\n if (!sources.length) {\n return target;\n }\n const source = sources.shift();\n if (!is.object(source)) {\n return target;\n }\n Object.keys(source).forEach(key => {\n if (is.object(source[key])) {\n if (!Object.keys(target).includes(key)) {\n Object.assign(target, {\n [key]: {}\n });\n }\n extend(target[key], source[key]);\n } else {\n Object.assign(target, {\n [key]: source[key]\n });\n }\n });\n return extend(target, ...sources);\n }\n\n // ==========================================================================\n\n // Wrap an element\n function wrap(elements, wrapper) {\n // Convert `elements` to an array, if necessary.\n const targets = elements.length ? elements : [elements];\n\n // Loops backwards to prevent having to clone the wrapper on the\n // first element (see `child` below).\n Array.from(targets).reverse().forEach((element, index) => {\n const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\n // Cache the current parent and sibling.\n const parent = element.parentNode;\n const sibling = element.nextSibling;\n\n // Wrap the element (is automatically removed from its current\n // parent).\n child.appendChild(element);\n\n // If the element had a sibling, insert the wrapper before\n // the sibling to maintain the HTML structure; otherwise, just\n // append it to the parent.\n if (sibling) {\n parent.insertBefore(child, sibling);\n } else {\n parent.appendChild(child);\n }\n });\n }\n\n // Set attributes\n function setAttributes(element, attributes) {\n if (!is.element(element) || is.empty(attributes)) return;\n\n // Assume null and undefined attributes should be left out,\n // Setting them would otherwise convert them to \"null\" and \"undefined\"\n Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\n }\n\n // Create a DocumentFragment\n function createElement(type, attributes, text) {\n // Create a new \n const element = document.createElement(type);\n\n // Set all passed attributes\n if (is.object(attributes)) {\n setAttributes(element, attributes);\n }\n\n // Add text node\n if (is.string(text)) {\n element.innerText = text;\n }\n\n // Return built element\n return element;\n }\n\n // Insert an element after another\n function insertAfter(element, target) {\n if (!is.element(element) || !is.element(target)) return;\n target.parentNode.insertBefore(element, target.nextSibling);\n }\n\n // Insert a DocumentFragment\n function insertElement(type, parent, attributes, text) {\n if (!is.element(parent)) return;\n parent.appendChild(createElement(type, attributes, text));\n }\n\n // Remove element(s)\n function removeElement(element) {\n if (is.nodeList(element) || is.array(element)) {\n Array.from(element).forEach(removeElement);\n return;\n }\n if (!is.element(element) || !is.element(element.parentNode)) {\n return;\n }\n element.parentNode.removeChild(element);\n }\n\n // Remove all child elements\n function emptyElement(element) {\n if (!is.element(element)) return;\n let {\n length\n } = element.childNodes;\n while (length > 0) {\n element.removeChild(element.lastChild);\n length -= 1;\n }\n }\n\n // Replace element\n function replaceElement(newChild, oldChild) {\n if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\n oldChild.parentNode.replaceChild(newChild, oldChild);\n return newChild;\n }\n\n // Get an attribute object from a string selector\n function getAttributesFromSelector(sel, existingAttributes) {\n // For example:\n // '.test' to { class: 'test' }\n // '#test' to { id: 'test' }\n // '[data-test=\"test\"]' to { 'data-test': 'test' }\n\n if (!is.string(sel) || is.empty(sel)) return {};\n const attributes = {};\n const existing = extend({}, existingAttributes);\n sel.split(',').forEach(s => {\n // Remove whitespace\n const selector = s.trim();\n const className = selector.replace('.', '');\n const stripped = selector.replace(/[[\\]]/g, '');\n // Get the parts and value\n const parts = stripped.split('=');\n const [key] = parts;\n const value = parts.length > 1 ? parts[1].replace(/[\"']/g, '') : '';\n // Get the first character\n const start = selector.charAt(0);\n switch (start) {\n case '.':\n // Add to existing classname\n if (is.string(existing.class)) {\n attributes.class = `${existing.class} ${className}`;\n } else {\n attributes.class = className;\n }\n break;\n case '#':\n // ID selector\n attributes.id = selector.replace('#', '');\n break;\n case '[':\n // Attribute selector\n attributes[key] = value;\n break;\n }\n });\n return extend(existing, attributes);\n }\n\n // Toggle hidden\n function toggleHidden(element, hidden) {\n if (!is.element(element)) return;\n let hide = hidden;\n if (!is.boolean(hide)) {\n hide = !element.hidden;\n }\n\n // eslint-disable-next-line no-param-reassign\n element.hidden = hide;\n }\n\n // Mirror Element.classList.toggle, with IE compatibility for \"force\" argument\n function toggleClass(element, className, force) {\n if (is.nodeList(element)) {\n return Array.from(element).map(e => toggleClass(e, className, force));\n }\n if (is.element(element)) {\n let method = 'toggle';\n if (typeof force !== 'undefined') {\n method = force ? 'add' : 'remove';\n }\n element.classList[method](className);\n return element.classList.contains(className);\n }\n return false;\n }\n\n // Has class name\n function hasClass(element, className) {\n return is.element(element) && element.classList.contains(className);\n }\n\n // Element matches selector\n function matches(element, selector) {\n const {\n prototype\n } = Element;\n function match() {\n return Array.from(document.querySelectorAll(selector)).includes(this);\n }\n const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\n return method.call(element, selector);\n }\n\n // Closest ancestor element matching selector (also tests element itself)\n function closest$1(element, selector) {\n const {\n prototype\n } = Element;\n\n // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\n function closestElement() {\n let el = this;\n do {\n if (matches.matches(el, selector)) return el;\n el = el.parentElement || el.parentNode;\n } while (el !== null && el.nodeType === 1);\n return null;\n }\n const method = prototype.closest || closestElement;\n return method.call(element, selector);\n }\n\n // Find all elements\n function getElements(selector) {\n return this.elements.container.querySelectorAll(selector);\n }\n\n // Find a single element\n function getElement(selector) {\n return this.elements.container.querySelector(selector);\n }\n\n // Set focus and tab focus class\n function setFocus(element = null, focusVisible = false) {\n if (!is.element(element)) return;\n\n // Set regular focus\n element.focus({\n preventScroll: true,\n focusVisible\n });\n }\n\n // ==========================================================================\n\n // Default codecs for checking mimetype support\n const defaultCodecs = {\n 'audio/ogg': 'vorbis',\n 'audio/wav': '1',\n 'video/webm': 'vp8, vorbis',\n 'video/mp4': 'avc1.42E01E, mp4a.40.2',\n 'video/ogg': 'theora'\n };\n\n // Check for feature support\n const support = {\n // Basic support\n audio: 'canPlayType' in document.createElement('audio'),\n video: 'canPlayType' in document.createElement('video'),\n // Check for support\n // Basic functionality vs full UI\n check(type, provider) {\n const api = support[type] || provider !== 'html5';\n const ui = api && support.rangeInput;\n return {\n api,\n ui\n };\n },\n // Picture-in-picture support\n // Safari & Chrome only currently\n pip: (() => {\n // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\n // It will throw the following error when trying to enter picture-in-picture\n // `NotSupportedError: The Picture-in-Picture mode is not supported.`\n if (browser.isIPhone) {\n return false;\n }\n\n // Safari\n // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\n if (is.function(createElement('video').webkitSetPresentationMode)) {\n return true;\n }\n\n // Chrome\n // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\n if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\n return true;\n }\n return false;\n })(),\n // Airplay support\n // Safari only currently\n airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\n // Inline playback support\n // https://webkit.org/blog/6784/new-video-policies-for-ios/\n playsinline: 'playsInline' in document.createElement('video'),\n // Check for mime type support against a player instance\n // Credits: http://diveintohtml5.info/everything.html\n // Related: http://www.leanbackplayer.com/test/h5mt.html\n mime(input) {\n if (is.empty(input)) {\n return false;\n }\n const [mediaType] = input.split('/');\n let type = input;\n\n // Verify we're using HTML5 and there's no media type mismatch\n if (!this.isHTML5 || mediaType !== this.type) {\n return false;\n }\n\n // Add codec if required\n if (Object.keys(defaultCodecs).includes(type)) {\n type += `; codecs=\"${defaultCodecs[input]}\"`;\n }\n try {\n return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\n } catch (_) {\n return false;\n }\n },\n // Check for textTracks support\n textTracks: 'textTracks' in document.createElement('video'),\n // Sliders\n rangeInput: (() => {\n const range = document.createElement('input');\n range.type = 'range';\n return range.type === 'range';\n })(),\n // Touch\n // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\n touch: 'ontouchstart' in document.documentElement,\n // Detect transitions support\n transitions: transitionEndEvent !== false,\n // Reduced motion iOS & MacOS setting\n // https://webkit.org/blog/7551/responsive-design-for-motion/\n reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\n };\n\n // ==========================================================================\n\n // Check for passive event listener support\n // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\n // https://www.youtube.com/watch?v=NPM6172J22g\n const supportsPassiveListeners = (() => {\n // Test via a getter in the options object to see if the passive property is accessed\n let supported = false;\n try {\n const options = Object.defineProperty({}, 'passive', {\n get() {\n supported = true;\n return null;\n }\n });\n window.addEventListener('test', null, options);\n window.removeEventListener('test', null, options);\n } catch (_) {\n // Do nothing\n }\n return supported;\n })();\n\n // Toggle event listener\n function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\n // Bail if no element, event, or callback\n if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\n return;\n }\n\n // Allow multiple events\n const events = event.split(' ');\n // Build options\n // Default to just the capture boolean for browsers with no passive listener support\n let options = capture;\n\n // If passive events listeners are supported\n if (supportsPassiveListeners) {\n options = {\n // Whether the listener can be passive (i.e. default never prevented)\n passive,\n // Whether the listener is a capturing listener or not\n capture\n };\n }\n\n // If a single node is passed, bind the event listener\n events.forEach(type => {\n if (this && this.eventListeners && toggle) {\n // Cache event listener\n this.eventListeners.push({\n element,\n type,\n callback,\n options\n });\n }\n element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\n });\n }\n\n // Bind event handler\n function on(element, events = '', callback, passive = true, capture = false) {\n toggleListener.call(this, element, events, callback, true, passive, capture);\n }\n\n // Unbind event handler\n function off(element, events = '', callback, passive = true, capture = false) {\n toggleListener.call(this, element, events, callback, false, passive, capture);\n }\n\n // Bind once-only event handler\n function once(element, events = '', callback, passive = true, capture = false) {\n const onceCallback = (...args) => {\n off(element, events, onceCallback, passive, capture);\n callback.apply(this, args);\n };\n toggleListener.call(this, element, events, onceCallback, true, passive, capture);\n }\n\n // Trigger event\n function triggerEvent(element, type = '', bubbles = false, detail = {}) {\n // Bail if no element\n if (!is.element(element) || is.empty(type)) {\n return;\n }\n\n // Create and dispatch the event\n const event = new CustomEvent(type, {\n bubbles,\n detail: {\n ...detail,\n plyr: this\n }\n });\n\n // Dispatch the event\n element.dispatchEvent(event);\n }\n\n // Unbind all cached event listeners\n function unbindListeners() {\n if (this && this.eventListeners) {\n this.eventListeners.forEach(item => {\n const {\n element,\n type,\n callback,\n options\n } = item;\n element.removeEventListener(type, callback, options);\n });\n this.eventListeners = [];\n }\n }\n\n // Run method when / if player is ready\n function ready() {\n return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\n }\n\n /**\n * Silence a Promise-like object.\n * This is useful for avoiding non-harmful, but potentially confusing \"uncaught\n * play promise\" rejection error messages.\n * @param {Object} value An object that may or may not be `Promise`-like.\n */\n function silencePromise(value) {\n if (is.promise(value)) {\n value.then(null, () => {});\n }\n }\n\n // ==========================================================================\n\n // Remove duplicates in an array\n function dedupe(array) {\n if (!is.array(array)) {\n return array;\n }\n return array.filter((item, index) => array.indexOf(item) === index);\n }\n\n // Get the closest value in an array\n function closest(array, value) {\n if (!is.array(array) || !array.length) {\n return null;\n }\n return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\n }\n\n // ==========================================================================\n\n // Check support for a CSS declaration\n function supportsCSS(declaration) {\n if (!window || !window.CSS) {\n return false;\n }\n return window.CSS.supports(declaration);\n }\n\n // Standard/common aspect ratios\n const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\n ...out,\n [x / y]: [x, y]\n }), {});\n\n // Validate an aspect ratio\n function validateAspectRatio(input) {\n if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\n return false;\n }\n const ratio = is.array(input) ? input : input.split(':');\n return ratio.map(Number).every(is.number);\n }\n\n // Reduce an aspect ratio to it's lowest form\n function reduceAspectRatio(ratio) {\n if (!is.array(ratio) || !ratio.every(is.number)) {\n return null;\n }\n const [width, height] = ratio;\n const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\n const divider = getDivider(width, height);\n return [width / divider, height / divider];\n }\n\n // Calculate an aspect ratio\n function getAspectRatio(input) {\n const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\n // Try provided ratio\n let ratio = parse(input);\n\n // Get from config\n if (ratio === null) {\n ratio = parse(this.config.ratio);\n }\n\n // Get from embed\n if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\n ({\n ratio\n } = this.embed);\n }\n\n // Get from HTML5 video\n if (ratio === null && this.isHTML5) {\n const {\n videoWidth,\n videoHeight\n } = this.media;\n ratio = [videoWidth, videoHeight];\n }\n return reduceAspectRatio(ratio);\n }\n\n // Set aspect ratio for responsive container\n function setAspectRatio(input) {\n if (!this.isVideo) {\n return {};\n }\n const {\n wrapper\n } = this.elements;\n const ratio = getAspectRatio.call(this, input);\n if (!is.array(ratio)) {\n return {};\n }\n const [x, y] = reduceAspectRatio(ratio);\n const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\n const padding = 100 / x * y;\n if (useNative) {\n wrapper.style.aspectRatio = `${x}/${y}`;\n } else {\n wrapper.style.paddingBottom = `${padding}%`;\n }\n\n // For Vimeo we have an extra
to hide the standard controls and UI\n if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\n const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\n const offset = (height - padding) / (height / 50);\n if (this.fullscreen.active) {\n wrapper.style.paddingBottom = null;\n } else {\n this.media.style.transform = `translateY(-${offset}%)`;\n }\n } else if (this.isHTML5) {\n wrapper.classList.add(this.config.classNames.videoFixedRatio);\n }\n return {\n padding,\n ratio\n };\n }\n\n // Round an aspect ratio to closest standard ratio\n function roundAspectRatio(x, y, tolerance = 0.05) {\n const ratio = x / y;\n const closestRatio = closest(Object.keys(standardRatios), ratio);\n\n // Check match is within tolerance\n if (Math.abs(closestRatio - ratio) <= tolerance) {\n return standardRatios[closestRatio];\n }\n\n // No match\n return [x, y];\n }\n\n // Get the size of the viewport\n // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\n function getViewportSize() {\n const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\n const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\n return [width, height];\n }\n\n // ==========================================================================\n const html5 = {\n getSources() {\n if (!this.isHTML5) {\n return [];\n }\n const sources = Array.from(this.media.querySelectorAll('source'));\n\n // Filter out unsupported sources (if type is specified)\n return sources.filter(source => {\n const type = source.getAttribute('type');\n if (is.empty(type)) {\n return true;\n }\n return support.mime.call(this, type);\n });\n },\n // Get quality levels\n getQualityOptions() {\n // Whether we're forcing all options (e.g. for streaming)\n if (this.config.quality.forced) {\n return this.config.quality.options;\n }\n\n // Get sizes from elements\n return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\n },\n setup() {\n if (!this.isHTML5) {\n return;\n }\n const player = this;\n\n // Set speed options from config\n player.options.speed = player.config.speed.options;\n\n // Set aspect ratio if fixed\n if (!is.empty(this.config.ratio)) {\n setAspectRatio.call(player);\n }\n\n // Quality\n Object.defineProperty(player.media, 'quality', {\n get() {\n // Get sources\n const sources = html5.getSources.call(player);\n const source = sources.find(s => s.getAttribute('src') === player.source);\n\n // Return size, if match is found\n return source && Number(source.getAttribute('size'));\n },\n set(input) {\n if (player.quality === input) {\n return;\n }\n\n // If we're using an external handler...\n if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\n player.config.quality.onChange(input);\n } else {\n // Get sources\n const sources = html5.getSources.call(player);\n // Get first match for requested size\n const source = sources.find(s => Number(s.getAttribute('size')) === input);\n\n // No matching source found\n if (!source) {\n return;\n }\n\n // Get current state\n const {\n currentTime,\n paused,\n preload,\n readyState,\n playbackRate\n } = player.media;\n\n // Set new source\n player.media.src = source.getAttribute('src');\n\n // Prevent loading if preload=\"none\" and the current source isn't loaded (#1044)\n if (preload !== 'none' || readyState) {\n // Restore time\n player.once('loadedmetadata', () => {\n player.speed = playbackRate;\n player.currentTime = currentTime;\n\n // Resume playing\n if (!paused) {\n silencePromise(player.play());\n }\n });\n\n // Load new source\n player.media.load();\n }\n }\n\n // Trigger change event\n triggerEvent.call(player, player.media, 'qualitychange', false, {\n quality: input\n });\n }\n });\n },\n // Cancel current network requests\n // See https://github.com/sampotts/plyr/issues/174\n cancelRequests() {\n if (!this.isHTML5) {\n return;\n }\n\n // Remove child sources\n removeElement(html5.getSources.call(this));\n\n // Set blank video src attribute\n // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\n // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\n this.media.setAttribute('src', this.config.blankVideo);\n\n // Load the new empty source\n // This will cancel existing requests\n // See https://github.com/sampotts/plyr/issues/174\n this.media.load();\n\n // Debugging\n this.debug.log('Cancelled network requests');\n }\n };\n\n // ==========================================================================\n\n // Generate a random ID\n function generateId(prefix) {\n return `${prefix}-${Math.floor(Math.random() * 10000)}`;\n }\n\n // Format string\n function format(input, ...args) {\n if (is.empty(input)) return input;\n return input.toString().replace(/{(\\d+)}/g, (_, i) => args[i].toString());\n }\n\n // Get percentage\n function getPercentage(current, max) {\n if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\n return 0;\n }\n return (current / max * 100).toFixed(2);\n }\n\n // Replace all occurrences of a string in a string\n const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g, '\\\\$1'), 'g'), replace.toString());\n\n // Convert to title case\n const toTitleCase = (input = '') => input.toString().replace(/\\w\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\n\n // Convert string to pascalCase\n function toPascalCase(input = '') {\n let string = input.toString();\n\n // Convert kebab case\n string = replaceAll(string, '-', ' ');\n\n // Convert snake case\n string = replaceAll(string, '_', ' ');\n\n // Convert to title case\n string = toTitleCase(string);\n\n // Convert to pascal case\n return replaceAll(string, ' ', '');\n }\n\n // Convert string to pascalCase\n function toCamelCase(input = '') {\n let string = input.toString();\n\n // Convert to pascal case\n string = toPascalCase(string);\n\n // Convert first character to lowercase\n return string.charAt(0).toLowerCase() + string.slice(1);\n }\n\n // Remove HTML from a string\n function stripHTML(source) {\n const fragment = document.createDocumentFragment();\n const element = document.createElement('div');\n fragment.appendChild(element);\n element.innerHTML = source;\n return fragment.firstChild.innerText;\n }\n\n // Like outerHTML, but also works for DocumentFragment\n function getHTML(element) {\n const wrapper = document.createElement('div');\n wrapper.appendChild(element);\n return wrapper.innerHTML;\n }\n\n // ==========================================================================\n\n // Skip i18n for abbreviations and brand names\n const resources = {\n pip: 'PIP',\n airplay: 'AirPlay',\n html5: 'HTML5',\n vimeo: 'Vimeo',\n youtube: 'YouTube'\n };\n const i18n = {\n get(key = '', config = {}) {\n if (is.empty(key) || is.empty(config)) {\n return '';\n }\n let string = getDeep(config.i18n, key);\n if (is.empty(string)) {\n if (Object.keys(resources).includes(key)) {\n return resources[key];\n }\n return '';\n }\n const replace = {\n '{seektime}': config.seekTime,\n '{title}': config.title\n };\n Object.entries(replace).forEach(([k, v]) => {\n string = replaceAll(string, k, v);\n });\n return string;\n }\n };\n\n class Storage {\n constructor(player) {\n _defineProperty$1(this, \"get\", key => {\n if (!Storage.supported || !this.enabled) {\n return null;\n }\n const store = window.localStorage.getItem(this.key);\n if (is.empty(store)) {\n return null;\n }\n const json = JSON.parse(store);\n return is.string(key) && key.length ? json[key] : json;\n });\n _defineProperty$1(this, \"set\", object => {\n // Bail if we don't have localStorage support or it's disabled\n if (!Storage.supported || !this.enabled) {\n return;\n }\n\n // Can only store objectst\n if (!is.object(object)) {\n return;\n }\n\n // Get current storage\n let storage = this.get();\n\n // Default to empty object\n if (is.empty(storage)) {\n storage = {};\n }\n\n // Update the working copy of the values\n extend(storage, object);\n\n // Update storage\n try {\n window.localStorage.setItem(this.key, JSON.stringify(storage));\n } catch (_) {\n // Do nothing\n }\n });\n this.enabled = player.config.storage.enabled;\n this.key = player.config.storage.key;\n }\n\n // Check for actual support (see if we can use it)\n static get supported() {\n try {\n if (!('localStorage' in window)) {\n return false;\n }\n const test = '___test';\n\n // Try to use it (it might be disabled, e.g. user is in private mode)\n // see: https://github.com/sampotts/plyr/issues/131\n window.localStorage.setItem(test, test);\n window.localStorage.removeItem(test);\n return true;\n } catch (_) {\n return false;\n }\n }\n }\n\n // ==========================================================================\n // Fetch wrapper\n // Using XHR to avoid issues with older browsers\n // ==========================================================================\n\n function fetch(url, responseType = 'text') {\n return new Promise((resolve, reject) => {\n try {\n const request = new XMLHttpRequest();\n\n // Check for CORS support\n if (!('withCredentials' in request)) {\n return;\n }\n request.addEventListener('load', () => {\n if (responseType === 'text') {\n try {\n resolve(JSON.parse(request.responseText));\n } catch (_) {\n resolve(request.responseText);\n }\n } else {\n resolve(request.response);\n }\n });\n request.addEventListener('error', () => {\n throw new Error(request.status);\n });\n request.open('GET', url, true);\n\n // Set the required response type\n request.responseType = responseType;\n request.send();\n } catch (error) {\n reject(error);\n }\n });\n }\n\n // ==========================================================================\n\n // Load an external SVG sprite\n function loadSprite(url, id) {\n if (!is.string(url)) {\n return;\n }\n const prefix = 'cache';\n const hasId = is.string(id);\n let isCached = false;\n const exists = () => document.getElementById(id) !== null;\n const update = (container, data) => {\n // eslint-disable-next-line no-param-reassign\n container.innerHTML = data;\n\n // Check again incase of race condition\n if (hasId && exists()) {\n return;\n }\n\n // Inject the SVG to the body\n document.body.insertAdjacentElement('afterbegin', container);\n };\n\n // Only load once if ID set\n if (!hasId || !exists()) {\n const useStorage = Storage.supported;\n // Create container\n const container = document.createElement('div');\n container.setAttribute('hidden', '');\n if (hasId) {\n container.setAttribute('id', id);\n }\n\n // Check in cache\n if (useStorage) {\n const cached = window.localStorage.getItem(`${prefix}-${id}`);\n isCached = cached !== null;\n if (isCached) {\n const data = JSON.parse(cached);\n update(container, data.content);\n }\n }\n\n // Get the sprite\n fetch(url).then(result => {\n if (is.empty(result)) {\n return;\n }\n if (useStorage) {\n try {\n window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\n content: result\n }));\n } catch (_) {\n // Do nothing\n }\n }\n update(container, result);\n }).catch(() => {});\n }\n }\n\n // ==========================================================================\n\n // Time helpers\n const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\n const getMinutes = value => Math.trunc(value / 60 % 60, 10);\n const getSeconds = value => Math.trunc(value % 60, 10);\n\n // Format time to UI friendly string\n function formatTime(time = 0, displayHours = false, inverted = false) {\n // Bail if the value isn't a number\n if (!is.number(time)) {\n return formatTime(undefined, displayHours, inverted);\n }\n\n // Format time component to add leading zero\n const format = value => `0${value}`.slice(-2);\n // Breakdown to hours, mins, secs\n let hours = getHours(time);\n const mins = getMinutes(time);\n const secs = getSeconds(time);\n\n // Do we need to display hours?\n if (displayHours || hours > 0) {\n hours = `${hours}:`;\n } else {\n hours = '';\n }\n\n // Render\n return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\n }\n\n // ==========================================================================\n\n // TODO: Don't export a massive object - break down and create class\n const controls = {\n // Get icon URL\n getIconUrl() {\n const url = new URL(this.config.iconUrl, window.location);\n const host = window.location.host ? window.location.host : window.top.location.host;\n const cors = url.host !== host || browser.isIE && !window.svg4everybody;\n return {\n url: this.config.iconUrl,\n cors\n };\n },\n // Find the UI controls\n findElements() {\n try {\n this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\n\n // Buttons\n this.elements.buttons = {\n play: getElements.call(this, this.config.selectors.buttons.play),\n pause: getElement.call(this, this.config.selectors.buttons.pause),\n restart: getElement.call(this, this.config.selectors.buttons.restart),\n rewind: getElement.call(this, this.config.selectors.buttons.rewind),\n fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\n mute: getElement.call(this, this.config.selectors.buttons.mute),\n pip: getElement.call(this, this.config.selectors.buttons.pip),\n airplay: getElement.call(this, this.config.selectors.buttons.airplay),\n settings: getElement.call(this, this.config.selectors.buttons.settings),\n captions: getElement.call(this, this.config.selectors.buttons.captions),\n fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\n };\n\n // Progress\n this.elements.progress = getElement.call(this, this.config.selectors.progress);\n\n // Inputs\n this.elements.inputs = {\n seek: getElement.call(this, this.config.selectors.inputs.seek),\n volume: getElement.call(this, this.config.selectors.inputs.volume)\n };\n\n // Display\n this.elements.display = {\n buffer: getElement.call(this, this.config.selectors.display.buffer),\n currentTime: getElement.call(this, this.config.selectors.display.currentTime),\n duration: getElement.call(this, this.config.selectors.display.duration)\n };\n\n // Seek tooltip\n if (is.element(this.elements.progress)) {\n this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\n }\n return true;\n } catch (error) {\n // Log it\n this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\n\n // Restore native video controls\n this.toggleNativeControls(true);\n return false;\n }\n },\n // Create icon\n createIcon(type, attributes) {\n const namespace = 'http://www.w3.org/2000/svg';\n const iconUrl = controls.getIconUrl.call(this);\n const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\n // Create \n const icon = document.createElementNS(namespace, 'svg');\n setAttributes(icon, extend(attributes, {\n 'aria-hidden': 'true',\n focusable: 'false'\n }));\n\n // Create the to reference sprite\n const use = document.createElementNS(namespace, 'use');\n const path = `${iconPath}-${type}`;\n\n // Set `href` attributes\n // https://github.com/sampotts/plyr/issues/460\n // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\n if ('href' in use) {\n use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\n }\n\n // Always set the older attribute even though it's \"deprecated\" (it'll be around for ages)\n use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\n\n // Add to \n icon.appendChild(use);\n return icon;\n },\n // Create hidden text label\n createLabel(key, attr = {}) {\n const text = i18n.get(key, this.config);\n const attributes = {\n ...attr,\n class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\n };\n return createElement('span', attributes, text);\n },\n // Create a badge\n createBadge(text) {\n if (is.empty(text)) {\n return null;\n }\n const badge = createElement('span', {\n class: this.config.classNames.menu.value\n });\n badge.appendChild(createElement('span', {\n class: this.config.classNames.menu.badge\n }, text));\n return badge;\n },\n // Create a
`);\n }\n\n // Set position\n tipElement.style.left = `${percent}%`;\n\n // Show/hide the tooltip\n // If the event is a moues in/out and percentage is inside bounds\n if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\n toggle(event.type === 'mouseenter');\n }\n },\n // Handle time change event\n timeUpdate(event) {\n // Only invert if only one time element is displayed and used for both duration and currentTime\n const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\n\n // Duration\n controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\n\n // Ignore updates while seeking\n if (event && event.type === 'timeupdate' && this.media.seeking) {\n return;\n }\n\n // Playing progress\n controls.updateProgress.call(this, event);\n },\n // Show the duration on metadataloaded or durationchange events\n durationUpdate() {\n // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\n if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\n return;\n }\n\n // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\n // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\n // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\n // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\n if (this.duration >= 2 ** 32) {\n toggleHidden(this.elements.display.currentTime, true);\n toggleHidden(this.elements.progress, true);\n return;\n }\n\n // Update ARIA values\n if (is.element(this.elements.inputs.seek)) {\n this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\n }\n\n // If there's a spot to display duration\n const hasDuration = is.element(this.elements.display.duration);\n\n // If there's only one time display, display duration there\n if (!hasDuration && this.config.displayDuration && this.paused) {\n controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\n }\n\n // If there's a duration element, update content\n if (hasDuration) {\n controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\n }\n if (this.config.markers.enabled) {\n controls.setMarkers.call(this);\n }\n\n // Update the tooltip (if visible)\n controls.updateSeekTooltip.call(this);\n },\n // Hide/show a tab\n toggleMenuButton(setting, toggle) {\n toggleHidden(this.elements.settings.buttons[setting], !toggle);\n },\n // Update the selected setting\n updateSetting(setting, container, input) {\n const pane = this.elements.settings.panels[setting];\n let value = null;\n let list = container;\n if (setting === 'captions') {\n value = this.currentTrack;\n } else {\n value = !is.empty(input) ? input : this[setting];\n\n // Get default\n if (is.empty(value)) {\n value = this.config[setting].default;\n }\n\n // Unsupported value\n if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\n this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\n return;\n }\n\n // Disabled value\n if (!this.config[setting].options.includes(value)) {\n this.debug.warn(`Disabled value of '${value}' for ${setting}`);\n return;\n }\n }\n\n // Get the list if we need to\n if (!is.element(list)) {\n list = pane && pane.querySelector('[role=\"menu\"]');\n }\n\n // If there's no list it means it's not been rendered...\n if (!is.element(list)) {\n return;\n }\n\n // Update the label\n const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\n label.innerHTML = controls.getLabel.call(this, setting, value);\n\n // Find the radio option and check it\n const target = list && list.querySelector(`[value=\"${value}\"]`);\n if (is.element(target)) {\n target.checked = true;\n }\n },\n // Translate a value into a nice label\n getLabel(setting, value) {\n switch (setting) {\n case 'speed':\n return value === 1 ? i18n.get('normal', this.config) : `${value}×`;\n case 'quality':\n if (is.number(value)) {\n const label = i18n.get(`qualityLabel.${value}`, this.config);\n if (!label.length) {\n return `${value}p`;\n }\n return label;\n }\n return toTitleCase(value);\n case 'captions':\n return captions.getLabel.call(this);\n default:\n return null;\n }\n },\n // Set the quality menu\n setQualityMenu(options) {\n // Menu required\n if (!is.element(this.elements.settings.panels.quality)) {\n return;\n }\n const type = 'quality';\n const list = this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');\n\n // Set options if passed and filter based on uniqueness and config\n if (is.array(options)) {\n this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\n }\n\n // Toggle the pane and tab\n const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\n controls.toggleMenuButton.call(this, type, toggle);\n\n // Empty the menu\n emptyElement(list);\n\n // Check if we need to toggle the parent\n controls.checkMenu.call(this);\n\n // If we're hiding, nothing more to do\n if (!toggle) {\n return;\n }\n\n // Get the badge HTML for HD, 4K etc\n const getBadge = quality => {\n const label = i18n.get(`qualityBadge.${quality}`, this.config);\n if (!label.length) {\n return null;\n }\n return controls.createBadge.call(this, label);\n };\n\n // Sort options by the config and then render options\n this.options.quality.sort((a, b) => {\n const sorting = this.config.quality.options;\n return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\n }).forEach(quality => {\n controls.createMenuItem.call(this, {\n value: quality,\n list,\n type,\n title: controls.getLabel.call(this, 'quality', quality),\n badge: getBadge(quality)\n });\n });\n controls.updateSetting.call(this, type, list);\n },\n // Set the looping options\n /* setLoopMenu() {\n // Menu required\n if (!is.element(this.elements.settings.panels.loop)) {\n return;\n }\n const options = ['start', 'end', 'all', 'reset'];\n const list = this.elements.settings.panels.loop.querySelector('[role=\"menu\"]');\n // Show the pane and tab\n toggleHidden(this.elements.settings.buttons.loop, false);\n toggleHidden(this.elements.settings.panels.loop, false);\n // Toggle the pane and tab\n const toggle = !is.empty(this.loop.options);\n controls.toggleMenuButton.call(this, 'loop', toggle);\n // Empty the menu\n emptyElement(list);\n options.forEach(option => {\n const item = createElement('li');\n const button = createElement(\n 'button',\n extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\n type: 'button',\n class: this.config.classNames.control,\n 'data-plyr-loop-action': option,\n }),\n i18n.get(option, this.config)\n );\n if (['start', 'end'].includes(option)) {\n const badge = controls.createBadge.call(this, '00:00');\n button.appendChild(badge);\n }\n item.appendChild(button);\n list.appendChild(item);\n });\n }, */\n\n // Get current selected caption language\n // TODO: rework this to user the getter in the API?\n\n // Set a list of available captions languages\n setCaptionsMenu() {\n // Menu required\n if (!is.element(this.elements.settings.panels.captions)) {\n return;\n }\n\n // TODO: Captions or language? Currently it's mixed\n const type = 'captions';\n const list = this.elements.settings.panels.captions.querySelector('[role=\"menu\"]');\n const tracks = captions.getTracks.call(this);\n const toggle = Boolean(tracks.length);\n\n // Toggle the pane and tab\n controls.toggleMenuButton.call(this, type, toggle);\n\n // Empty the menu\n emptyElement(list);\n\n // Check if we need to toggle the parent\n controls.checkMenu.call(this);\n\n // If there's no captions, bail\n if (!toggle) {\n return;\n }\n\n // Generate options data\n const options = tracks.map((track, value) => ({\n value,\n checked: this.captions.toggled && this.currentTrack === value,\n title: captions.getLabel.call(this, track),\n badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\n list,\n type: 'language'\n }));\n\n // Add the \"Disabled\" option to turn off captions\n options.unshift({\n value: -1,\n checked: !this.captions.toggled,\n title: i18n.get('disabled', this.config),\n list,\n type: 'language'\n });\n\n // Generate options\n options.forEach(controls.createMenuItem.bind(this));\n controls.updateSetting.call(this, type, list);\n },\n // Set a list of available captions languages\n setSpeedMenu() {\n // Menu required\n if (!is.element(this.elements.settings.panels.speed)) {\n return;\n }\n const type = 'speed';\n const list = this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');\n\n // Filter out invalid speeds\n this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\n\n // Toggle the pane and tab\n const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\n controls.toggleMenuButton.call(this, type, toggle);\n\n // Empty the menu\n emptyElement(list);\n\n // Check if we need to toggle the parent\n controls.checkMenu.call(this);\n\n // If we're hiding, nothing more to do\n if (!toggle) {\n return;\n }\n\n // Create items\n this.options.speed.forEach(speed => {\n controls.createMenuItem.call(this, {\n value: speed,\n list,\n type,\n title: controls.getLabel.call(this, 'speed', speed)\n });\n });\n controls.updateSetting.call(this, type, list);\n },\n // Check if we need to hide/show the settings menu\n checkMenu() {\n const {\n buttons\n } = this.elements.settings;\n const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\n toggleHidden(this.elements.settings.menu, !visible);\n },\n // Focus the first menu item in a given (or visible) menu\n focusFirstMenuItem(pane, focusVisible = false) {\n if (this.elements.settings.popup.hidden) {\n return;\n }\n let target = pane;\n if (!is.element(target)) {\n target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\n }\n const firstItem = target.querySelector('[role^=\"menuitem\"]');\n setFocus.call(this, firstItem, focusVisible);\n },\n // Show/hide menu\n toggleMenu(input) {\n const {\n popup\n } = this.elements.settings;\n const button = this.elements.buttons.settings;\n\n // Menu and button are required\n if (!is.element(popup) || !is.element(button)) {\n return;\n }\n\n // True toggle by default\n const {\n hidden\n } = popup;\n let show = hidden;\n if (is.boolean(input)) {\n show = input;\n } else if (is.keyboardEvent(input) && input.key === 'Escape') {\n show = false;\n } else if (is.event(input)) {\n // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\n // Element in the shadowDOM. The path, if available, is complete.\n const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\n const isMenuItem = popup.contains(target);\n\n // If the click was inside the menu or if the click\n // wasn't the button or menu item and we're trying to\n // show the menu (a doc click shouldn't show the menu)\n if (isMenuItem || !isMenuItem && input.target !== button && show) {\n return;\n }\n }\n\n // Set button attributes\n button.setAttribute('aria-expanded', show);\n\n // Show the actual popup\n toggleHidden(popup, !show);\n\n // Add class hook\n toggleClass(this.elements.container, this.config.classNames.menu.open, show);\n\n // Focus the first item if key interaction\n if (show && is.keyboardEvent(input)) {\n controls.focusFirstMenuItem.call(this, null, true);\n } else if (!show && !hidden) {\n // If closing, re-focus the button\n setFocus.call(this, button, is.keyboardEvent(input));\n }\n },\n // Get the natural size of a menu panel\n getMenuSize(tab) {\n const clone = tab.cloneNode(true);\n clone.style.position = 'absolute';\n clone.style.opacity = 0;\n clone.removeAttribute('hidden');\n\n // Append to parent so we get the \"real\" size\n tab.parentNode.appendChild(clone);\n\n // Get the sizes before we remove\n const width = clone.scrollWidth;\n const height = clone.scrollHeight;\n\n // Remove from the DOM\n removeElement(clone);\n return {\n width,\n height\n };\n },\n // Show a panel in the menu\n showMenuPanel(type = '', focusVisible = false) {\n const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\n\n // Nothing to show, bail\n if (!is.element(target)) {\n return;\n }\n\n // Hide all other panels\n const container = target.parentNode;\n const current = Array.from(container.children).find(node => !node.hidden);\n\n // If we can do fancy animations, we'll animate the height/width\n if (support.transitions && !support.reducedMotion) {\n // Set the current width as a base\n container.style.width = `${current.scrollWidth}px`;\n container.style.height = `${current.scrollHeight}px`;\n\n // Get potential sizes\n const size = controls.getMenuSize.call(this, target);\n\n // Restore auto height/width\n const restore = event => {\n // We're only bothered about height and width on the container\n if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\n return;\n }\n\n // Revert back to auto\n container.style.width = '';\n container.style.height = '';\n\n // Only listen once\n off.call(this, container, transitionEndEvent, restore);\n };\n\n // Listen for the transition finishing and restore auto height/width\n on.call(this, container, transitionEndEvent, restore);\n\n // Set dimensions to target\n container.style.width = `${size.width}px`;\n container.style.height = `${size.height}px`;\n }\n\n // Set attributes on current tab\n toggleHidden(current, true);\n\n // Set attributes on target\n toggleHidden(target, false);\n\n // Focus the first item\n controls.focusFirstMenuItem.call(this, target, focusVisible);\n },\n // Set the download URL\n setDownloadUrl() {\n const button = this.elements.buttons.download;\n\n // Bail if no button\n if (!is.element(button)) {\n return;\n }\n\n // Set attribute\n button.setAttribute('href', this.download);\n },\n // Build the default HTML\n create(data) {\n const {\n bindMenuItemShortcuts,\n createButton,\n createProgress,\n createRange,\n createTime,\n setQualityMenu,\n setSpeedMenu,\n showMenuPanel\n } = controls;\n this.elements.controls = null;\n\n // Larger overlaid play button\n if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\n this.elements.container.appendChild(createButton.call(this, 'play-large'));\n }\n\n // Create the container\n const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\n this.elements.controls = container;\n\n // Default item attributes\n const defaultAttributes = {\n class: 'plyr__controls__item'\n };\n\n // Loop through controls in order\n dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\n // Restart button\n if (control === 'restart') {\n container.appendChild(createButton.call(this, 'restart', defaultAttributes));\n }\n\n // Rewind button\n if (control === 'rewind') {\n container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\n }\n\n // Play/Pause button\n if (control === 'play') {\n container.appendChild(createButton.call(this, 'play', defaultAttributes));\n }\n\n // Fast forward button\n if (control === 'fast-forward') {\n container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\n }\n\n // Progress\n if (control === 'progress') {\n const progressContainer = createElement('div', {\n class: `${defaultAttributes.class} plyr__progress__container`\n });\n const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\n\n // Seek range slider\n progress.appendChild(createRange.call(this, 'seek', {\n id: `plyr-seek-${data.id}`\n }));\n\n // Buffer progress\n progress.appendChild(createProgress.call(this, 'buffer'));\n\n // TODO: Add loop display indicator\n\n // Seek tooltip\n if (this.config.tooltips.seek) {\n const tooltip = createElement('span', {\n class: this.config.classNames.tooltip\n }, '00:00');\n progress.appendChild(tooltip);\n this.elements.display.seekTooltip = tooltip;\n }\n this.elements.progress = progress;\n progressContainer.appendChild(this.elements.progress);\n container.appendChild(progressContainer);\n }\n\n // Media current time display\n if (control === 'current-time') {\n container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\n }\n\n // Media duration display\n if (control === 'duration') {\n container.appendChild(createTime.call(this, 'duration', defaultAttributes));\n }\n\n // Volume controls\n if (control === 'mute' || control === 'volume') {\n let {\n volume\n } = this.elements;\n\n // Create the volume container if needed\n if (!is.element(volume) || !container.contains(volume)) {\n volume = createElement('div', extend({}, defaultAttributes, {\n class: `${defaultAttributes.class} plyr__volume`.trim()\n }));\n this.elements.volume = volume;\n container.appendChild(volume);\n }\n\n // Toggle mute button\n if (control === 'mute') {\n volume.appendChild(createButton.call(this, 'mute'));\n }\n\n // Volume range control\n // Ignored on iOS as it's handled globally\n // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\n if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\n // Set the attributes\n const attributes = {\n max: 1,\n step: 0.05,\n value: this.config.volume\n };\n\n // Create the volume range slider\n volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\n id: `plyr-volume-${data.id}`\n })));\n }\n }\n\n // Toggle captions button\n if (control === 'captions') {\n container.appendChild(createButton.call(this, 'captions', defaultAttributes));\n }\n\n // Settings button / menu\n if (control === 'settings' && !is.empty(this.config.settings)) {\n const wrapper = createElement('div', extend({}, defaultAttributes, {\n class: `${defaultAttributes.class} plyr__menu`.trim(),\n hidden: ''\n }));\n wrapper.appendChild(createButton.call(this, 'settings', {\n 'aria-haspopup': true,\n 'aria-controls': `plyr-settings-${data.id}`,\n 'aria-expanded': false\n }));\n const popup = createElement('div', {\n class: 'plyr__menu__container',\n id: `plyr-settings-${data.id}`,\n hidden: ''\n });\n const inner = createElement('div');\n const home = createElement('div', {\n id: `plyr-settings-${data.id}-home`\n });\n\n // Create the menu\n const menu = createElement('div', {\n role: 'menu'\n });\n home.appendChild(menu);\n inner.appendChild(home);\n this.elements.settings.panels.home = home;\n\n // Build the menu items\n this.config.settings.forEach(type => {\n // TODO: bundle this with the createMenuItem helper and bindings\n const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\n type: 'button',\n class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\n role: 'menuitem',\n 'aria-haspopup': true,\n hidden: ''\n }));\n\n // Bind menu shortcuts for keyboard users\n bindMenuItemShortcuts.call(this, menuItem, type);\n\n // Show menu on click\n on.call(this, menuItem, 'click', () => {\n showMenuPanel.call(this, type, false);\n });\n const flex = createElement('span', null, i18n.get(type, this.config));\n const value = createElement('span', {\n class: this.config.classNames.menu.value\n });\n\n // Speed contains HTML entities\n value.innerHTML = data[type];\n flex.appendChild(value);\n menuItem.appendChild(flex);\n menu.appendChild(menuItem);\n\n // Build the panes\n const pane = createElement('div', {\n id: `plyr-settings-${data.id}-${type}`,\n hidden: ''\n });\n\n // Back button\n const backButton = createElement('button', {\n type: 'button',\n class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\n });\n\n // Visible label\n backButton.appendChild(createElement('span', {\n 'aria-hidden': true\n }, i18n.get(type, this.config)));\n\n // Screen reader label\n backButton.appendChild(createElement('span', {\n class: this.config.classNames.hidden\n }, i18n.get('menuBack', this.config)));\n\n // Go back via keyboard\n on.call(this, pane, 'keydown', event => {\n if (event.key !== 'ArrowLeft') return;\n\n // Prevent seek\n event.preventDefault();\n event.stopPropagation();\n\n // Show the respective menu\n showMenuPanel.call(this, 'home', true);\n }, false);\n\n // Go back via button click\n on.call(this, backButton, 'click', () => {\n showMenuPanel.call(this, 'home', false);\n });\n\n // Add to pane\n pane.appendChild(backButton);\n\n // Menu\n pane.appendChild(createElement('div', {\n role: 'menu'\n }));\n inner.appendChild(pane);\n this.elements.settings.buttons[type] = menuItem;\n this.elements.settings.panels[type] = pane;\n });\n popup.appendChild(inner);\n wrapper.appendChild(popup);\n container.appendChild(wrapper);\n this.elements.settings.popup = popup;\n this.elements.settings.menu = wrapper;\n }\n\n // Picture in picture button\n if (control === 'pip' && support.pip) {\n container.appendChild(createButton.call(this, 'pip', defaultAttributes));\n }\n\n // Airplay button\n if (control === 'airplay' && support.airplay) {\n container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\n }\n\n // Download button\n if (control === 'download') {\n const attributes = extend({}, defaultAttributes, {\n element: 'a',\n href: this.download,\n target: '_blank'\n });\n\n // Set download attribute for HTML5 only\n if (this.isHTML5) {\n attributes.download = '';\n }\n const {\n download\n } = this.config.urls;\n if (!is.url(download) && this.isEmbed) {\n extend(attributes, {\n icon: `logo-${this.provider}`,\n label: this.provider\n });\n }\n container.appendChild(createButton.call(this, 'download', attributes));\n }\n\n // Toggle fullscreen button\n if (control === 'fullscreen') {\n container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\n }\n });\n\n // Set available quality levels\n if (this.isHTML5) {\n setQualityMenu.call(this, html5.getQualityOptions.call(this));\n }\n setSpeedMenu.call(this);\n return container;\n },\n // Insert controls\n inject() {\n // Sprite\n if (this.config.loadSprite) {\n const icon = controls.getIconUrl.call(this);\n\n // Only load external sprite using AJAX\n if (icon.cors) {\n loadSprite(icon.url, 'sprite-plyr');\n }\n }\n\n // Create a unique ID\n this.id = Math.floor(Math.random() * 10000);\n\n // Null by default\n let container = null;\n this.elements.controls = null;\n\n // Set template properties\n const props = {\n id: this.id,\n seektime: this.config.seekTime,\n title: this.config.title\n };\n let update = true;\n\n // If function, run it and use output\n if (is.function(this.config.controls)) {\n this.config.controls = this.config.controls.call(this, props);\n }\n\n // Convert falsy controls to empty array (primarily for empty strings)\n if (!this.config.controls) {\n this.config.controls = [];\n }\n if (is.element(this.config.controls) || is.string(this.config.controls)) {\n // HTMLElement or Non-empty string passed as the option\n container = this.config.controls;\n } else {\n // Create controls\n container = controls.create.call(this, {\n id: this.id,\n seektime: this.config.seekTime,\n speed: this.speed,\n quality: this.quality,\n captions: captions.getLabel.call(this)\n // TODO: Looping\n // loop: 'None',\n });\n\n update = false;\n }\n\n // Replace props with their value\n const replace = input => {\n let result = input;\n Object.entries(props).forEach(([key, value]) => {\n result = replaceAll(result, `{${key}}`, value);\n });\n return result;\n };\n\n // Update markup\n if (update) {\n if (is.string(this.config.controls)) {\n container = replace(container);\n }\n }\n\n // Controls container\n let target;\n\n // Inject to custom location\n if (is.string(this.config.selectors.controls.container)) {\n target = document.querySelector(this.config.selectors.controls.container);\n }\n\n // Inject into the container by default\n if (!is.element(target)) {\n target = this.elements.container;\n }\n\n // Inject controls HTML (needs to be before captions, hence \"afterbegin\")\n const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\n target[insertMethod]('afterbegin', container);\n\n // Find the elements if need be\n if (!is.element(this.elements.controls)) {\n controls.findElements.call(this);\n }\n\n // Add pressed property to buttons\n if (!is.empty(this.elements.buttons)) {\n const addProperty = button => {\n const className = this.config.classNames.controlPressed;\n button.setAttribute('aria-pressed', 'false');\n Object.defineProperty(button, 'pressed', {\n configurable: true,\n enumerable: true,\n get() {\n return hasClass(button, className);\n },\n set(pressed = false) {\n toggleClass(button, className, pressed);\n button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\n }\n });\n };\n\n // Toggle classname when pressed property is set\n Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\n if (is.array(button) || is.nodeList(button)) {\n Array.from(button).filter(Boolean).forEach(addProperty);\n } else {\n addProperty(button);\n }\n });\n }\n\n // Edge sometimes doesn't finish the paint so force a repaint\n if (browser.isEdge) {\n repaint(target);\n }\n\n // Setup tooltips\n if (this.config.tooltips.controls) {\n const {\n classNames,\n selectors\n } = this.config;\n const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\n const labels = getElements.call(this, selector);\n Array.from(labels).forEach(label => {\n toggleClass(label, this.config.classNames.hidden, false);\n toggleClass(label, this.config.classNames.tooltip, true);\n });\n }\n },\n // Set media metadata\n setMediaMetadata() {\n try {\n if ('mediaSession' in navigator) {\n navigator.mediaSession.metadata = new window.MediaMetadata({\n title: this.config.mediaMetadata.title,\n artist: this.config.mediaMetadata.artist,\n album: this.config.mediaMetadata.album,\n artwork: this.config.mediaMetadata.artwork\n });\n }\n } catch (_) {\n // Do nothing\n }\n },\n // Add markers\n setMarkers() {\n var _this$config$markers2, _this$config$markers3;\n if (!this.duration || this.elements.markers) return;\n\n // Get valid points\n const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\n time\n }) => time > 0 && time < this.duration);\n if (!(points !== null && points !== void 0 && points.length)) return;\n const containerFragment = document.createDocumentFragment();\n const pointsFragment = document.createDocumentFragment();\n let tipElement = null;\n const tipVisible = `${this.config.classNames.tooltip}--visible`;\n const toggleTip = show => toggleClass(tipElement, tipVisible, show);\n\n // Inject markers to progress container\n points.forEach(point => {\n const markerElement = createElement('span', {\n class: this.config.classNames.marker\n }, '');\n const left = `${point.time / this.duration * 100}%`;\n if (tipElement) {\n // Show on hover\n markerElement.addEventListener('mouseenter', () => {\n if (point.label) return;\n tipElement.style.left = left;\n tipElement.innerHTML = point.label;\n toggleTip(true);\n });\n\n // Hide on leave\n markerElement.addEventListener('mouseleave', () => {\n toggleTip(false);\n });\n }\n markerElement.addEventListener('click', () => {\n this.currentTime = point.time;\n });\n markerElement.style.left = left;\n pointsFragment.appendChild(markerElement);\n });\n containerFragment.appendChild(pointsFragment);\n\n // Inject a tooltip if needed\n if (!this.config.tooltips.seek) {\n tipElement = createElement('span', {\n class: this.config.classNames.tooltip\n }, '');\n containerFragment.appendChild(tipElement);\n }\n this.elements.markers = {\n points: pointsFragment,\n tip: tipElement\n };\n this.elements.progress.appendChild(containerFragment);\n }\n };\n\n // ==========================================================================\n\n /**\n * Parse a string to a URL object\n * @param {String} input - the URL to be parsed\n * @param {Boolean} safe - failsafe parsing\n */\n function parseUrl(input, safe = true) {\n let url = input;\n if (safe) {\n const parser = document.createElement('a');\n parser.href = url;\n url = parser.href;\n }\n try {\n return new URL(url);\n } catch (_) {\n return null;\n }\n }\n\n // Convert object to URLSearchParams\n function buildUrlParams(input) {\n const params = new URLSearchParams();\n if (is.object(input)) {\n Object.entries(input).forEach(([key, value]) => {\n params.set(key, value);\n });\n }\n return params;\n }\n\n // ==========================================================================\n const captions = {\n // Setup captions\n setup() {\n // Requires UI support\n if (!this.supported.ui) {\n return;\n }\n\n // Only Vimeo and HTML5 video supported at this point\n if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\n // Clear menu and hide\n if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\n controls.setCaptionsMenu.call(this);\n }\n return;\n }\n\n // Inject the container\n if (!is.element(this.elements.captions)) {\n this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\n this.elements.captions.setAttribute('dir', 'auto');\n insertAfter(this.elements.captions, this.elements.wrapper);\n }\n\n // Fix IE captions if CORS is used\n // Fetch captions and inject as blobs instead (data URIs not supported!)\n if (browser.isIE && window.URL) {\n const elements = this.media.querySelectorAll('track');\n Array.from(elements).forEach(track => {\n const src = track.getAttribute('src');\n const url = parseUrl(src);\n if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\n fetch(src, 'blob').then(blob => {\n track.setAttribute('src', window.URL.createObjectURL(blob));\n }).catch(() => {\n removeElement(track);\n });\n }\n });\n }\n\n // Get and set initial data\n // The \"preferred\" options are not realized unless / until the wanted language has a match\n // * languages: Array of user's browser languages.\n // * language: The language preferred by user settings or config\n // * active: The state preferred by user settings or config\n // * toggled: The real captions state\n\n const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\n const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\n let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\n\n // Use first browser language when language is 'auto'\n if (language === 'auto') {\n [language] = languages;\n }\n let active = this.storage.get('captions');\n if (!is.boolean(active)) {\n ({\n active\n } = this.config.captions);\n }\n Object.assign(this.captions, {\n toggled: false,\n active,\n language,\n languages\n });\n\n // Watch changes to textTracks and update captions menu\n if (this.isHTML5) {\n const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\n on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\n }\n\n // Update available languages in list next tick (the event must not be triggered before the listeners)\n setTimeout(captions.update.bind(this), 0);\n },\n // Update available language options in settings based on tracks\n update() {\n const tracks = captions.getTracks.call(this, true);\n // Get the wanted language\n const {\n active,\n language,\n meta,\n currentTrackNode\n } = this.captions;\n const languageExists = Boolean(tracks.find(track => track.language === language));\n\n // Handle tracks (add event listener and \"pseudo\"-default)\n if (this.isHTML5 && this.isVideo) {\n tracks.filter(track => !meta.get(track)).forEach(track => {\n this.debug.log('Track added', track);\n\n // Attempt to store if the original dom element was \"default\"\n meta.set(track, {\n default: track.mode === 'showing'\n });\n\n // Turn off native caption rendering to avoid double captions\n // Note: mode='hidden' forces a track to download. To ensure every track\n // isn't downloaded at once, only 'showing' tracks should be reassigned\n // eslint-disable-next-line no-param-reassign\n if (track.mode === 'showing') {\n // eslint-disable-next-line no-param-reassign\n track.mode = 'hidden';\n }\n\n // Add event listener for cue changes\n on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\n });\n }\n\n // Update language first time it matches, or if the previous matching track was removed\n if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\n captions.setLanguage.call(this, language);\n captions.toggle.call(this, active && languageExists);\n }\n\n // Enable or disable captions based on track length\n if (this.elements) {\n toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\n }\n\n // Update available languages in list\n if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\n controls.setCaptionsMenu.call(this);\n }\n },\n // Toggle captions display\n // Used internally for the toggleCaptions method, with the passive option forced to false\n toggle(input, passive = true) {\n // If there's no full support\n if (!this.supported.ui) {\n return;\n }\n const {\n toggled\n } = this.captions; // Current state\n const activeClass = this.config.classNames.captions.active;\n // Get the next state\n // If the method is called without parameter, toggle based on current value\n const active = is.nullOrUndefined(input) ? !toggled : input;\n\n // Update state and trigger event\n if (active !== toggled) {\n // When passive, don't override user preferences\n if (!passive) {\n this.captions.active = active;\n this.storage.set({\n captions: active\n });\n }\n\n // Force language if the call isn't passive and there is no matching language to toggle to\n if (!this.language && active && !passive) {\n const tracks = captions.getTracks.call(this);\n const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\n\n // Override user preferences to avoid switching languages if a matching track is added\n this.captions.language = track.language;\n\n // Set caption, but don't store in localStorage as user preference\n captions.set.call(this, tracks.indexOf(track));\n return;\n }\n\n // Toggle button if it's enabled\n if (this.elements.buttons.captions) {\n this.elements.buttons.captions.pressed = active;\n }\n\n // Add class hook\n toggleClass(this.elements.container, activeClass, active);\n this.captions.toggled = active;\n\n // Update settings menu\n controls.updateSetting.call(this, 'captions');\n\n // Trigger event (not used internally)\n triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\n }\n\n // Wait for the call stack to clear before setting mode='hidden'\n // on the active track - forcing the browser to download it\n setTimeout(() => {\n if (active && this.captions.toggled) {\n this.captions.currentTrackNode.mode = 'hidden';\n }\n });\n },\n // Set captions by track index\n // Used internally for the currentTrack setter with the passive option forced to false\n set(index, passive = true) {\n const tracks = captions.getTracks.call(this);\n\n // Disable captions if setting to -1\n if (index === -1) {\n captions.toggle.call(this, false, passive);\n return;\n }\n if (!is.number(index)) {\n this.debug.warn('Invalid caption argument', index);\n return;\n }\n if (!(index in tracks)) {\n this.debug.warn('Track not found', index);\n return;\n }\n if (this.captions.currentTrack !== index) {\n this.captions.currentTrack = index;\n const track = tracks[index];\n const {\n language\n } = track || {};\n\n // Store reference to node for invalidation on remove\n this.captions.currentTrackNode = track;\n\n // Update settings menu\n controls.updateSetting.call(this, 'captions');\n\n // When passive, don't override user preferences\n if (!passive) {\n this.captions.language = language;\n this.storage.set({\n language\n });\n }\n\n // Handle Vimeo captions\n if (this.isVimeo) {\n this.embed.enableTextTrack(language);\n }\n\n // Trigger event\n triggerEvent.call(this, this.media, 'languagechange');\n }\n\n // Show captions\n captions.toggle.call(this, true, passive);\n if (this.isHTML5 && this.isVideo) {\n // If we change the active track while a cue is already displayed we need to update it\n captions.updateCues.call(this);\n }\n },\n // Set captions by language\n // Used internally for the language setter with the passive option forced to false\n setLanguage(input, passive = true) {\n if (!is.string(input)) {\n this.debug.warn('Invalid language argument', input);\n return;\n }\n // Normalize\n const language = input.toLowerCase();\n this.captions.language = language;\n\n // Set currentTrack\n const tracks = captions.getTracks.call(this);\n const track = captions.findTrack.call(this, [language]);\n captions.set.call(this, tracks.indexOf(track), passive);\n },\n // Get current valid caption tracks\n // If update is false it will also ignore tracks without metadata\n // This is used to \"freeze\" the language options when captions.update is false\n getTracks(update = false) {\n // Handle media or textTracks missing or null\n const tracks = Array.from((this.media || {}).textTracks || []);\n // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\n // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\n return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\n },\n // Match tracks based on languages and get the first\n findTrack(languages, force = false) {\n const tracks = captions.getTracks.call(this);\n const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\n const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\n let track;\n languages.every(language => {\n track = sorted.find(t => t.language === language);\n return !track; // Break iteration if there is a match\n });\n\n // If no match is found but is required, get first\n return track || (force ? sorted[0] : undefined);\n },\n // Get the current track\n getCurrentTrack() {\n return captions.getTracks.call(this)[this.currentTrack];\n },\n // Get UI label for track\n getLabel(track) {\n let currentTrack = track;\n if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\n currentTrack = captions.getCurrentTrack.call(this);\n }\n if (is.track(currentTrack)) {\n if (!is.empty(currentTrack.label)) {\n return currentTrack.label;\n }\n if (!is.empty(currentTrack.language)) {\n return track.language.toUpperCase();\n }\n return i18n.get('enabled', this.config);\n }\n return i18n.get('disabled', this.config);\n },\n // Update captions using current track's active cues\n // Also optional array argument in case there isn't any track (ex: vimeo)\n updateCues(input) {\n // Requires UI\n if (!this.supported.ui) {\n return;\n }\n if (!is.element(this.elements.captions)) {\n this.debug.warn('No captions element to render to');\n return;\n }\n\n // Only accept array or empty input\n if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\n this.debug.warn('updateCues: Invalid input', input);\n return;\n }\n let cues = input;\n\n // Get cues from track\n if (!cues) {\n const track = captions.getCurrentTrack.call(this);\n cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\n }\n\n // Set new caption text\n const content = cues.map(cueText => cueText.trim()).join('\\n');\n const changed = content !== this.elements.captions.innerHTML;\n if (changed) {\n // Empty the container and create a new child element\n emptyElement(this.elements.captions);\n const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\n caption.innerHTML = content;\n this.elements.captions.appendChild(caption);\n\n // Trigger event\n triggerEvent.call(this, this.media, 'cuechange');\n }\n }\n };\n\n // ==========================================================================\n // Plyr default config\n // ==========================================================================\n\n const defaults = {\n // Disable\n enabled: true,\n // Custom media title\n title: '',\n // Logging to console\n debug: false,\n // Auto play (if supported)\n autoplay: false,\n // Only allow one media playing at once (vimeo only)\n autopause: true,\n // Allow inline playback on iOS\n playsinline: true,\n // Default time to skip when rewind/fast forward\n seekTime: 10,\n // Default volume\n volume: 1,\n muted: false,\n // Pass a custom duration\n duration: null,\n // Display the media duration on load in the current time position\n // If you have opted to display both duration and currentTime, this is ignored\n displayDuration: true,\n // Invert the current time to be a countdown\n invertTime: true,\n // Clicking the currentTime inverts it's value to show time left rather than elapsed\n toggleInvert: true,\n // Force an aspect ratio\n // The format must be `'w:h'` (e.g. `'16:9'`)\n ratio: null,\n // Click video container to play/pause\n clickToPlay: true,\n // Auto hide the controls\n hideControls: true,\n // Reset to start when playback ended\n resetOnEnd: false,\n // Disable the standard context menu\n disableContextMenu: true,\n // Sprite (for icons)\n loadSprite: true,\n iconPrefix: 'plyr',\n iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\n // Blank video (used to prevent errors on source change)\n blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\n // Quality default\n quality: {\n default: 576,\n // The options to display in the UI, if available for the source media\n options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\n forced: false,\n onChange: null\n },\n // Set loops\n loop: {\n active: false\n // start: null,\n // end: null,\n },\n\n // Speed default and options to display\n speed: {\n selected: 1,\n // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\n options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\n },\n // Keyboard shortcut settings\n keyboard: {\n focused: true,\n global: false\n },\n // Display tooltips\n tooltips: {\n controls: false,\n seek: true\n },\n // Captions settings\n captions: {\n active: false,\n language: 'auto',\n // Listen to new tracks added after Plyr is initialized.\n // This is needed for streaming captions, but may result in unselectable options\n update: false\n },\n // Fullscreen settings\n fullscreen: {\n enabled: true,\n // Allow fullscreen?\n fallback: true,\n // Fallback using full viewport/window\n iosNative: false // Use the native fullscreen in iOS (disables custom controls)\n // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\n // Non-ancestors of the player element will be ignored\n // container: null, // defaults to the player element\n },\n\n // Local storage\n storage: {\n enabled: true,\n key: 'plyr'\n },\n // Default controls\n controls: ['play-large',\n // 'restart',\n // 'rewind',\n 'play',\n // 'fast-forward',\n 'progress', 'current-time',\n // 'duration',\n 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\n // 'download',\n 'fullscreen'],\n settings: ['captions', 'quality', 'speed'],\n // Localisation\n i18n: {\n restart: 'Restart',\n rewind: 'Rewind {seektime}s',\n play: 'Play',\n pause: 'Pause',\n fastForward: 'Forward {seektime}s',\n seek: 'Seek',\n seekLabel: '{currentTime} of {duration}',\n played: 'Played',\n buffered: 'Buffered',\n currentTime: 'Current time',\n duration: 'Duration',\n volume: 'Volume',\n mute: 'Mute',\n unmute: 'Unmute',\n enableCaptions: 'Enable captions',\n disableCaptions: 'Disable captions',\n download: 'Download',\n enterFullscreen: 'Enter fullscreen',\n exitFullscreen: 'Exit fullscreen',\n frameTitle: 'Player for {title}',\n captions: 'Captions',\n settings: 'Settings',\n pip: 'PIP',\n menuBack: 'Go back to previous menu',\n speed: 'Speed',\n normal: 'Normal',\n quality: 'Quality',\n loop: 'Loop',\n start: 'Start',\n end: 'End',\n all: 'All',\n reset: 'Reset',\n disabled: 'Disabled',\n enabled: 'Enabled',\n advertisement: 'Ad',\n qualityBadge: {\n 2160: '4K',\n 1440: 'HD',\n 1080: 'HD',\n 720: 'HD',\n 576: 'SD',\n 480: 'SD'\n }\n },\n // URLs\n urls: {\n download: null,\n vimeo: {\n sdk: 'https://player.vimeo.com/api/player.js',\n iframe: 'https://player.vimeo.com/video/{0}?{1}',\n api: 'https://vimeo.com/api/oembed.json?url={0}'\n },\n youtube: {\n sdk: 'https://www.youtube.com/iframe_api',\n api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\n },\n googleIMA: {\n sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\n }\n },\n // Custom control listeners\n listeners: {\n seek: null,\n play: null,\n pause: null,\n restart: null,\n rewind: null,\n fastForward: null,\n mute: null,\n volume: null,\n captions: null,\n download: null,\n fullscreen: null,\n pip: null,\n airplay: null,\n speed: null,\n quality: null,\n loop: null,\n language: null\n },\n // Events to watch and bubble\n events: [\n // Events to watch on HTML5 media elements and bubble\n // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\n 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\n // Custom events\n 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\n // YouTube\n 'statechange',\n // Quality\n 'qualitychange',\n // Ads\n 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\n // Selectors\n // Change these to match your template if using custom HTML\n selectors: {\n editable: 'input, textarea, select, [contenteditable]',\n container: '.plyr',\n controls: {\n container: null,\n wrapper: '.plyr__controls'\n },\n labels: '[data-plyr]',\n buttons: {\n play: '[data-plyr=\"play\"]',\n pause: '[data-plyr=\"pause\"]',\n restart: '[data-plyr=\"restart\"]',\n rewind: '[data-plyr=\"rewind\"]',\n fastForward: '[data-plyr=\"fast-forward\"]',\n mute: '[data-plyr=\"mute\"]',\n captions: '[data-plyr=\"captions\"]',\n download: '[data-plyr=\"download\"]',\n fullscreen: '[data-plyr=\"fullscreen\"]',\n pip: '[data-plyr=\"pip\"]',\n airplay: '[data-plyr=\"airplay\"]',\n settings: '[data-plyr=\"settings\"]',\n loop: '[data-plyr=\"loop\"]'\n },\n inputs: {\n seek: '[data-plyr=\"seek\"]',\n volume: '[data-plyr=\"volume\"]',\n speed: '[data-plyr=\"speed\"]',\n language: '[data-plyr=\"language\"]',\n quality: '[data-plyr=\"quality\"]'\n },\n display: {\n currentTime: '.plyr__time--current',\n duration: '.plyr__time--duration',\n buffer: '.plyr__progress__buffer',\n loop: '.plyr__progress__loop',\n // Used later\n volume: '.plyr__volume--display'\n },\n progress: '.plyr__progress',\n captions: '.plyr__captions',\n caption: '.plyr__caption'\n },\n // Class hooks added to the player in different states\n classNames: {\n type: 'plyr--{0}',\n provider: 'plyr--{0}',\n video: 'plyr__video-wrapper',\n embed: 'plyr__video-embed',\n videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\n embedContainer: 'plyr__video-embed__container',\n poster: 'plyr__poster',\n posterEnabled: 'plyr__poster-enabled',\n ads: 'plyr__ads',\n control: 'plyr__control',\n controlPressed: 'plyr__control--pressed',\n playing: 'plyr--playing',\n paused: 'plyr--paused',\n stopped: 'plyr--stopped',\n loading: 'plyr--loading',\n hover: 'plyr--hover',\n tooltip: 'plyr__tooltip',\n cues: 'plyr__cues',\n marker: 'plyr__progress__marker',\n hidden: 'plyr__sr-only',\n hideControls: 'plyr--hide-controls',\n isTouch: 'plyr--is-touch',\n uiSupported: 'plyr--full-ui',\n noTransition: 'plyr--no-transition',\n display: {\n time: 'plyr__time'\n },\n menu: {\n value: 'plyr__menu__value',\n badge: 'plyr__badge',\n open: 'plyr--menu-open'\n },\n captions: {\n enabled: 'plyr--captions-enabled',\n active: 'plyr--captions-active'\n },\n fullscreen: {\n enabled: 'plyr--fullscreen-enabled',\n fallback: 'plyr--fullscreen-fallback'\n },\n pip: {\n supported: 'plyr--pip-supported',\n active: 'plyr--pip-active'\n },\n airplay: {\n supported: 'plyr--airplay-supported',\n active: 'plyr--airplay-active'\n },\n previewThumbnails: {\n // Tooltip thumbs\n thumbContainer: 'plyr__preview-thumb',\n thumbContainerShown: 'plyr__preview-thumb--is-shown',\n imageContainer: 'plyr__preview-thumb__image-container',\n timeContainer: 'plyr__preview-thumb__time-container',\n // Scrubbing\n scrubbingContainer: 'plyr__preview-scrubbing',\n scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\n }\n },\n // Embed attributes\n attributes: {\n embed: {\n provider: 'data-plyr-provider',\n id: 'data-plyr-embed-id',\n hash: 'data-plyr-embed-hash'\n }\n },\n // Advertisements plugin\n // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\n ads: {\n enabled: false,\n publisherId: '',\n tagUrl: ''\n },\n // Preview Thumbnails plugin\n previewThumbnails: {\n enabled: false,\n src: ''\n },\n // Vimeo plugin\n vimeo: {\n byline: false,\n portrait: false,\n title: false,\n speed: true,\n transparent: false,\n // Custom settings from Plyr\n customControls: true,\n referrerPolicy: null,\n // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\n // Whether the owner of the video has a Pro or Business account\n // (which allows us to properly hide controls without CSS hacks, etc)\n premium: false\n },\n // YouTube plugin\n youtube: {\n rel: 0,\n // No related vids\n showinfo: 0,\n // Hide info\n iv_load_policy: 3,\n // Hide annotations\n modestbranding: 1,\n // Hide logos as much as possible (they still show one in the corner when paused)\n // Custom settings from Plyr\n customControls: true,\n noCookie: false // Whether to use an alternative version of YouTube without cookies\n },\n\n // Media Metadata\n mediaMetadata: {\n title: '',\n artist: '',\n album: '',\n artwork: []\n },\n // Markers\n markers: {\n enabled: false,\n points: []\n }\n };\n\n // ==========================================================================\n // Plyr states\n // ==========================================================================\n\n const pip = {\n active: 'picture-in-picture',\n inactive: 'inline'\n };\n\n // ==========================================================================\n // Plyr supported types and providers\n // ==========================================================================\n\n const providers = {\n html5: 'html5',\n youtube: 'youtube',\n vimeo: 'vimeo'\n };\n const types = {\n audio: 'audio',\n video: 'video'\n };\n\n /**\n * Get provider by URL\n * @param {String} url\n */\n function getProviderByUrl(url) {\n // YouTube\n if (/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(url)) {\n return providers.youtube;\n }\n\n // Vimeo\n if (/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(url)) {\n return providers.vimeo;\n }\n return null;\n }\n\n // ==========================================================================\n // Console wrapper\n // ==========================================================================\n\n const noop = () => {};\n class Console {\n constructor(enabled = false) {\n this.enabled = window.console && enabled;\n if (this.enabled) {\n this.log('Debugging enabled');\n }\n }\n get log() {\n // eslint-disable-next-line no-console\n return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\n }\n get warn() {\n // eslint-disable-next-line no-console\n return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\n }\n get error() {\n // eslint-disable-next-line no-console\n return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\n }\n }\n\n class Fullscreen {\n constructor(player) {\n _defineProperty$1(this, \"onChange\", () => {\n if (!this.supported) return;\n\n // Update toggle button\n const button = this.player.elements.buttons.fullscreen;\n if (is.element(button)) {\n button.pressed = this.active;\n }\n\n // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\n const target = this.target === this.player.media ? this.target : this.player.elements.container;\n // Trigger an event\n triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\n });\n _defineProperty$1(this, \"toggleFallback\", (toggle = false) => {\n // Store or restore scroll position\n if (toggle) {\n this.scrollPosition = {\n x: window.scrollX ?? 0,\n y: window.scrollY ?? 0\n };\n } else {\n window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\n }\n\n // Toggle scroll\n document.body.style.overflow = toggle ? 'hidden' : '';\n\n // Toggle class hook\n toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\n\n // Force full viewport on iPhone X+\n if (browser.isIos) {\n let viewport = document.head.querySelector('meta[name=\"viewport\"]');\n const property = 'viewport-fit=cover';\n\n // Inject the viewport meta if required\n if (!viewport) {\n viewport = document.createElement('meta');\n viewport.setAttribute('name', 'viewport');\n }\n\n // Check if the property already exists\n const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\n if (toggle) {\n this.cleanupViewport = !hasProperty;\n if (!hasProperty) viewport.content += `,${property}`;\n } else if (this.cleanupViewport) {\n viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\n }\n }\n\n // Toggle button and fire events\n this.onChange();\n });\n // Trap focus inside container\n _defineProperty$1(this, \"trapFocus\", event => {\n // Bail if iOS/iPadOS, not active, not the tab key\n if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\n\n // Get the current focused element\n const focused = document.activeElement;\n const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\n const [first] = focusable;\n const last = focusable[focusable.length - 1];\n if (focused === last && !event.shiftKey) {\n // Move focus to first element that can be tabbed if Shift isn't used\n first.focus();\n event.preventDefault();\n } else if (focused === first && event.shiftKey) {\n // Move focus to last element that can be tabbed if Shift is used\n last.focus();\n event.preventDefault();\n }\n });\n // Update UI\n _defineProperty$1(this, \"update\", () => {\n if (this.supported) {\n let mode;\n if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\n this.player.debug.log(`${mode} fullscreen enabled`);\n } else {\n this.player.debug.log('Fullscreen not supported and fallback disabled');\n }\n\n // Add styling hook to show button\n toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\n });\n // Make an element fullscreen\n _defineProperty$1(this, \"enter\", () => {\n if (!this.supported) return;\n\n // iOS native fullscreen doesn't need the request step\n if (browser.isIos && this.player.config.fullscreen.iosNative) {\n if (this.player.isVimeo) {\n this.player.embed.requestFullscreen();\n } else {\n this.target.webkitEnterFullscreen();\n }\n } else if (!Fullscreen.nativeSupported || this.forceFallback) {\n this.toggleFallback(true);\n } else if (!this.prefix) {\n this.target.requestFullscreen({\n navigationUI: 'hide'\n });\n } else if (!is.empty(this.prefix)) {\n this.target[`${this.prefix}Request${this.property}`]();\n }\n });\n // Bail from fullscreen\n _defineProperty$1(this, \"exit\", () => {\n if (!this.supported) return;\n\n // iOS native fullscreen\n if (browser.isIos && this.player.config.fullscreen.iosNative) {\n if (this.player.isVimeo) {\n this.player.embed.exitFullscreen();\n } else {\n this.target.webkitEnterFullscreen();\n }\n silencePromise(this.player.play());\n } else if (!Fullscreen.nativeSupported || this.forceFallback) {\n this.toggleFallback(false);\n } else if (!this.prefix) {\n (document.cancelFullScreen || document.exitFullscreen).call(document);\n } else if (!is.empty(this.prefix)) {\n const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\n document[`${this.prefix}${action}${this.property}`]();\n }\n });\n // Toggle state\n _defineProperty$1(this, \"toggle\", () => {\n if (!this.active) this.enter();else this.exit();\n });\n // Keep reference to parent\n this.player = player;\n\n // Get prefix\n this.prefix = Fullscreen.prefix;\n this.property = Fullscreen.property;\n\n // Scroll position\n this.scrollPosition = {\n x: 0,\n y: 0\n };\n\n // Force the use of 'full window/browser' rather than fullscreen\n this.forceFallback = player.config.fullscreen.fallback === 'force';\n\n // Get the fullscreen element\n // Checks container is an ancestor, defaults to null\n this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\n\n // Register event listeners\n // Handle event (incase user presses escape etc)\n on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\n // TODO: Filter for target??\n this.onChange();\n });\n\n // Fullscreen toggle on double click\n on.call(this.player, this.player.elements.container, 'dblclick', event => {\n // Ignore double click in controls\n if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\n return;\n }\n this.player.listeners.proxy(event, this.toggle, 'fullscreen');\n });\n\n // Tap focus when in fullscreen\n on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\n\n // Update the UI\n this.update();\n }\n\n // Determine if native supported\n static get nativeSupported() {\n return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\n }\n\n // If we're actually using native\n get useNative() {\n return Fullscreen.nativeSupported && !this.forceFallback;\n }\n\n // Get the prefix for handlers\n static get prefix() {\n // No prefix\n if (is.function(document.exitFullscreen)) return '';\n\n // Check for fullscreen support by vendor prefix\n let value = '';\n const prefixes = ['webkit', 'moz', 'ms'];\n prefixes.some(pre => {\n if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\n value = pre;\n return true;\n }\n return false;\n });\n return value;\n }\n static get property() {\n return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\n }\n\n // Determine if fullscreen is supported\n get supported() {\n return [\n // Fullscreen is enabled in config\n this.player.config.fullscreen.enabled,\n // Must be a video\n this.player.isVideo,\n // Either native is supported or fallback enabled\n Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\n // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\n // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\n !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\n }\n\n // Get active state\n get active() {\n if (!this.supported) return false;\n\n // Fallback using classname\n if (!Fullscreen.nativeSupported || this.forceFallback) {\n return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\n }\n const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\n return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\n }\n\n // Get target element\n get target() {\n return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\n }\n }\n\n // ==========================================================================\n // Load image avoiding xhr/fetch CORS issues\n // Server status can't be obtained this way unfortunately, so this uses \"naturalWidth\" to determine if the image has loaded\n // By default it checks if it is at least 1px, but you can add a second argument to change this\n // ==========================================================================\n\n function loadImage(src, minWidth = 1) {\n return new Promise((resolve, reject) => {\n const image = new Image();\n const handler = () => {\n delete image.onload;\n delete image.onerror;\n (image.naturalWidth >= minWidth ? resolve : reject)(image);\n };\n Object.assign(image, {\n onload: handler,\n onerror: handler,\n src\n });\n });\n }\n\n // ==========================================================================\n const ui = {\n addStyleHook() {\n toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\n toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\n },\n // Toggle native HTML5 media controls\n toggleNativeControls(toggle = false) {\n if (toggle && this.isHTML5) {\n this.media.setAttribute('controls', '');\n } else {\n this.media.removeAttribute('controls');\n }\n },\n // Setup the UI\n build() {\n // Re-attach media element listeners\n // TODO: Use event bubbling?\n this.listeners.media();\n\n // Don't setup interface if no support\n if (!this.supported.ui) {\n this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\n\n // Restore native controls\n ui.toggleNativeControls.call(this, true);\n\n // Bail\n return;\n }\n\n // Inject custom controls if not present\n if (!is.element(this.elements.controls)) {\n // Inject custom controls\n controls.inject.call(this);\n\n // Re-attach control listeners\n this.listeners.controls();\n }\n\n // Remove native controls\n ui.toggleNativeControls.call(this);\n\n // Setup captions for HTML5\n if (this.isHTML5) {\n captions.setup.call(this);\n }\n\n // Reset volume\n this.volume = null;\n\n // Reset mute state\n this.muted = null;\n\n // Reset loop state\n this.loop = null;\n\n // Reset quality setting\n this.quality = null;\n\n // Reset speed\n this.speed = null;\n\n // Reset volume display\n controls.updateVolume.call(this);\n\n // Reset time display\n controls.timeUpdate.call(this);\n\n // Reset duration display\n controls.durationUpdate.call(this);\n\n // Update the UI\n ui.checkPlaying.call(this);\n\n // Check for picture-in-picture support\n toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\n\n // Check for airplay support\n toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\n\n // Add touch class\n toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\n\n // Ready for API calls\n this.ready = true;\n\n // Ready event at end of execution stack\n setTimeout(() => {\n triggerEvent.call(this, this.media, 'ready');\n }, 0);\n\n // Set the title\n ui.setTitle.call(this);\n\n // Assure the poster image is set, if the property was added before the element was created\n if (this.poster) {\n ui.setPoster.call(this, this.poster, false).catch(() => {});\n }\n\n // Manually set the duration if user has overridden it.\n // The event listeners for it doesn't get called if preload is disabled (#701)\n if (this.config.duration) {\n controls.durationUpdate.call(this);\n }\n\n // Media metadata\n if (this.config.mediaMetadata) {\n controls.setMediaMetadata.call(this);\n }\n },\n // Setup aria attribute for play and iframe title\n setTitle() {\n // Find the current text\n let label = i18n.get('play', this.config);\n\n // If there's a media title set, use that for the label\n if (is.string(this.config.title) && !is.empty(this.config.title)) {\n label += `, ${this.config.title}`;\n }\n\n // If there's a play button, set label\n Array.from(this.elements.buttons.play || []).forEach(button => {\n button.setAttribute('aria-label', label);\n });\n\n // Set iframe title\n // https://github.com/sampotts/plyr/issues/124\n if (this.isEmbed) {\n const iframe = getElement.call(this, 'iframe');\n if (!is.element(iframe)) {\n return;\n }\n\n // Default to media type\n const title = !is.empty(this.config.title) ? this.config.title : 'video';\n const format = i18n.get('frameTitle', this.config);\n iframe.setAttribute('title', format.replace('{title}', title));\n }\n },\n // Toggle poster\n togglePoster(enable) {\n toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\n },\n // Set the poster image (async)\n // Used internally for the poster setter, with the passive option forced to false\n setPoster(poster, passive = true) {\n // Don't override if call is passive\n if (passive && this.poster) {\n return Promise.reject(new Error('Poster already set'));\n }\n\n // Set property synchronously to respect the call order\n this.media.setAttribute('data-poster', poster);\n\n // Show the poster\n this.elements.poster.removeAttribute('hidden');\n\n // Wait until ui is ready\n return ready.call(this)\n // Load image\n .then(() => loadImage(poster)).catch(error => {\n // Hide poster on error unless it's been set by another call\n if (poster === this.poster) {\n ui.togglePoster.call(this, false);\n }\n // Rethrow\n throw error;\n }).then(() => {\n // Prevent race conditions\n if (poster !== this.poster) {\n throw new Error('setPoster cancelled by later call to setPoster');\n }\n }).then(() => {\n Object.assign(this.elements.poster.style, {\n backgroundImage: `url('${poster}')`,\n // Reset backgroundSize as well (since it can be set to \"cover\" for padded thumbnails for youtube)\n backgroundSize: ''\n });\n ui.togglePoster.call(this, true);\n return poster;\n });\n },\n // Check playing state\n checkPlaying(event) {\n // Class hooks\n toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\n toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\n toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\n\n // Set state\n Array.from(this.elements.buttons.play || []).forEach(target => {\n Object.assign(target, {\n pressed: this.playing\n });\n target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\n });\n\n // Only update controls on non timeupdate events\n if (is.event(event) && event.type === 'timeupdate') {\n return;\n }\n\n // Toggle controls\n ui.toggleControls.call(this);\n },\n // Check if media is loading\n checkLoading(event) {\n this.loading = ['stalled', 'waiting'].includes(event.type);\n\n // Clear timer\n clearTimeout(this.timers.loading);\n\n // Timer to prevent flicker when seeking\n this.timers.loading = setTimeout(() => {\n // Update progress bar loading class state\n toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\n\n // Update controls visibility\n ui.toggleControls.call(this);\n }, this.loading ? 250 : 0);\n },\n // Toggle controls based on state and `force` argument\n toggleControls(force) {\n const {\n controls: controlsElement\n } = this.elements;\n if (controlsElement && this.config.hideControls) {\n // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\n const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\n\n // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\n this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\n }\n },\n // Migrate any custom properties from the media to the parent\n migrateStyles() {\n // Loop through values (as they are the keys when the object is spread 🤔)\n Object.values({\n ...this.media.style\n })\n // We're only fussed about Plyr specific properties\n .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\n // Set on the container\n this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\n\n // Clean up from media element\n this.media.style.removeProperty(key);\n });\n\n // Remove attribute if empty\n if (is.empty(this.media.style)) {\n this.media.removeAttribute('style');\n }\n }\n };\n\n class Listeners {\n constructor(_player) {\n // Device is touch enabled\n _defineProperty$1(this, \"firstTouch\", () => {\n const {\n player\n } = this;\n const {\n elements\n } = player;\n player.touch = true;\n\n // Add touch class\n toggleClass(elements.container, player.config.classNames.isTouch, true);\n });\n // Global window & document listeners\n _defineProperty$1(this, \"global\", (toggle = true) => {\n const {\n player\n } = this;\n\n // Keyboard shortcuts\n if (player.config.keyboard.global) {\n toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\n }\n\n // Click anywhere closes menu\n toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\n\n // Detect touch by events\n once.call(player, document.body, 'touchstart', this.firstTouch);\n });\n // Container listeners\n _defineProperty$1(this, \"container\", () => {\n const {\n player\n } = this;\n const {\n config,\n elements,\n timers\n } = player;\n\n // Keyboard shortcuts\n if (!config.keyboard.global && config.keyboard.focused) {\n on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\n }\n\n // Toggle controls on mouse events and entering fullscreen\n on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\n const {\n controls: controlsElement\n } = elements;\n\n // Remove button states for fullscreen\n if (controlsElement && event.type === 'enterfullscreen') {\n controlsElement.pressed = false;\n controlsElement.hover = false;\n }\n\n // Show, then hide after a timeout unless another control event occurs\n const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\n let delay = 0;\n if (show) {\n ui.toggleControls.call(player, true);\n // Use longer timeout for touch devices\n delay = player.touch ? 3000 : 2000;\n }\n\n // Clear timer\n clearTimeout(timers.controls);\n\n // Set new timer to prevent flicker when seeking\n timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\n });\n\n // Set a gutter for Vimeo\n const setGutter = () => {\n if (!player.isVimeo || player.config.vimeo.premium) {\n return;\n }\n const target = elements.wrapper;\n const {\n active\n } = player.fullscreen;\n const [videoWidth, videoHeight] = getAspectRatio.call(player);\n const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\n\n // If not active, remove styles\n if (!active) {\n if (useNativeAspectRatio) {\n target.style.width = null;\n target.style.height = null;\n } else {\n target.style.maxWidth = null;\n target.style.margin = null;\n }\n return;\n }\n\n // Determine which dimension will overflow and constrain view\n const [viewportWidth, viewportHeight] = getViewportSize();\n const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\n if (useNativeAspectRatio) {\n target.style.width = overflow ? 'auto' : '100%';\n target.style.height = overflow ? '100%' : 'auto';\n } else {\n target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\n target.style.margin = overflow ? '0 auto' : null;\n }\n };\n\n // Handle resizing\n const resized = () => {\n clearTimeout(timers.resized);\n timers.resized = setTimeout(setGutter, 50);\n };\n on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\n const {\n target\n } = player.fullscreen;\n\n // Ignore events not from target\n if (target !== elements.container) {\n return;\n }\n\n // If it's not an embed and no ratio specified\n if (!player.isEmbed && is.empty(player.config.ratio)) {\n return;\n }\n\n // Set Vimeo gutter\n setGutter();\n\n // Watch for resizes\n const method = event.type === 'enterfullscreen' ? on : off;\n method.call(player, window, 'resize', resized);\n });\n });\n // Listen for media events\n _defineProperty$1(this, \"media\", () => {\n const {\n player\n } = this;\n const {\n elements\n } = player;\n\n // Time change on media\n on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\n\n // Display duration\n on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\n\n // Handle the media finishing\n on.call(player, player.media, 'ended', () => {\n // Show poster on end\n if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\n // Restart\n player.restart();\n\n // Call pause otherwise IE11 will start playing the video again\n player.pause();\n }\n });\n\n // Check for buffer progress\n on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\n\n // Handle volume changes\n on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\n\n // Handle play/pause\n on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\n\n // Loading state\n on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\n\n // Click video\n if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\n // Re-fetch the wrapper\n const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\n\n // Bail if there's no wrapper (this should never happen)\n if (!is.element(wrapper)) {\n return;\n }\n\n // On click play, pause or restart\n on.call(player, elements.container, 'click', event => {\n const targets = [elements.container, wrapper];\n\n // Ignore if click if not container or in video wrapper\n if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\n return;\n }\n\n // Touch devices will just show controls (if hidden)\n if (player.touch && player.config.hideControls) {\n return;\n }\n if (player.ended) {\n this.proxy(event, player.restart, 'restart');\n this.proxy(event, () => {\n silencePromise(player.play());\n }, 'play');\n } else {\n this.proxy(event, () => {\n silencePromise(player.togglePlay());\n }, 'play');\n }\n });\n }\n\n // Disable right click\n if (player.supported.ui && player.config.disableContextMenu) {\n on.call(player, elements.wrapper, 'contextmenu', event => {\n event.preventDefault();\n }, false);\n }\n\n // Volume change\n on.call(player, player.media, 'volumechange', () => {\n // Save to storage\n player.storage.set({\n volume: player.volume,\n muted: player.muted\n });\n });\n\n // Speed change\n on.call(player, player.media, 'ratechange', () => {\n // Update UI\n controls.updateSetting.call(player, 'speed');\n\n // Save to storage\n player.storage.set({\n speed: player.speed\n });\n });\n\n // Quality change\n on.call(player, player.media, 'qualitychange', event => {\n // Update UI\n controls.updateSetting.call(player, 'quality', null, event.detail.quality);\n });\n\n // Update download link when ready and if quality changes\n on.call(player, player.media, 'ready qualitychange', () => {\n controls.setDownloadUrl.call(player);\n });\n\n // Proxy events to container\n // Bubble up key events for Edge\n const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\n on.call(player, player.media, proxyEvents, event => {\n let {\n detail = {}\n } = event;\n\n // Get error details from media\n if (event.type === 'error') {\n detail = player.media.error;\n }\n triggerEvent.call(player, elements.container, event.type, true, detail);\n });\n });\n // Run default and custom handlers\n _defineProperty$1(this, \"proxy\", (event, defaultHandler, customHandlerKey) => {\n const {\n player\n } = this;\n const customHandler = player.config.listeners[customHandlerKey];\n const hasCustomHandler = is.function(customHandler);\n let returned = true;\n\n // Execute custom handler\n if (hasCustomHandler) {\n returned = customHandler.call(player, event);\n }\n\n // Only call default handler if not prevented in custom handler\n if (returned !== false && is.function(defaultHandler)) {\n defaultHandler.call(player, event);\n }\n });\n // Trigger custom and default handlers\n _defineProperty$1(this, \"bind\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\n const {\n player\n } = this;\n const customHandler = player.config.listeners[customHandlerKey];\n const hasCustomHandler = is.function(customHandler);\n on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\n });\n // Listen for control events\n _defineProperty$1(this, \"controls\", () => {\n const {\n player\n } = this;\n const {\n elements\n } = player;\n // IE doesn't support input event, so we fallback to change\n const inputEvent = browser.isIE ? 'change' : 'input';\n\n // Play/pause toggle\n if (elements.buttons.play) {\n Array.from(elements.buttons.play).forEach(button => {\n this.bind(button, 'click', () => {\n silencePromise(player.togglePlay());\n }, 'play');\n });\n }\n\n // Pause\n this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\n\n // Rewind\n this.bind(elements.buttons.rewind, 'click', () => {\n // Record seek time so we can prevent hiding controls for a few seconds after rewind\n player.lastSeekTime = Date.now();\n player.rewind();\n }, 'rewind');\n\n // Rewind\n this.bind(elements.buttons.fastForward, 'click', () => {\n // Record seek time so we can prevent hiding controls for a few seconds after fast forward\n player.lastSeekTime = Date.now();\n player.forward();\n }, 'fastForward');\n\n // Mute toggle\n this.bind(elements.buttons.mute, 'click', () => {\n player.muted = !player.muted;\n }, 'mute');\n\n // Captions toggle\n this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\n\n // Download\n this.bind(elements.buttons.download, 'click', () => {\n triggerEvent.call(player, player.media, 'download');\n }, 'download');\n\n // Fullscreen toggle\n this.bind(elements.buttons.fullscreen, 'click', () => {\n player.fullscreen.toggle();\n }, 'fullscreen');\n\n // Picture-in-Picture\n this.bind(elements.buttons.pip, 'click', () => {\n player.pip = 'toggle';\n }, 'pip');\n\n // Airplay\n this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\n\n // Settings menu - click toggle\n this.bind(elements.buttons.settings, 'click', event => {\n // Prevent the document click listener closing the menu\n event.stopPropagation();\n event.preventDefault();\n controls.toggleMenu.call(player, event);\n }, null, false); // Can't be passive as we're preventing default\n\n // Settings menu - keyboard toggle\n // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\n // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\n this.bind(elements.buttons.settings, 'keyup', event => {\n if (![' ', 'Enter'].includes(event.key)) {\n return;\n }\n\n // Because return triggers a click anyway, all we need to do is set focus\n if (event.key === 'Enter') {\n controls.focusFirstMenuItem.call(player, null, true);\n return;\n }\n\n // Prevent scroll\n event.preventDefault();\n\n // Prevent playing video (Firefox)\n event.stopPropagation();\n\n // Toggle menu\n controls.toggleMenu.call(player, event);\n }, null, false // Can't be passive as we're preventing default\n );\n\n // Escape closes menu\n this.bind(elements.settings.menu, 'keydown', event => {\n if (event.key === 'Escape') {\n controls.toggleMenu.call(player, event);\n }\n });\n\n // Set range input alternative \"value\", which matches the tooltip time (#954)\n this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\n const rect = elements.progress.getBoundingClientRect();\n const percent = 100 / rect.width * (event.pageX - rect.left);\n event.currentTarget.setAttribute('seek-value', percent);\n });\n\n // Pause while seeking\n this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\n const seek = event.currentTarget;\n const attribute = 'play-on-seeked';\n if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\n return;\n }\n\n // Record seek time so we can prevent hiding controls for a few seconds after seek\n player.lastSeekTime = Date.now();\n\n // Was playing before?\n const play = seek.hasAttribute(attribute);\n // Done seeking\n const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\n\n // If we're done seeking and it was playing, resume playback\n if (play && done) {\n seek.removeAttribute(attribute);\n silencePromise(player.play());\n } else if (!done && player.playing) {\n seek.setAttribute(attribute, '');\n player.pause();\n }\n });\n\n // Fix range inputs on iOS\n // Super weird iOS bug where after you interact with an ,\n // it takes over further interactions on the page. This is a hack\n if (browser.isIos) {\n const inputs = getElements.call(player, 'input[type=\"range\"]');\n Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\n }\n\n // Seek\n this.bind(elements.inputs.seek, inputEvent, event => {\n const seek = event.currentTarget;\n // If it exists, use seek-value instead of \"value\" for consistency with tooltip time (#954)\n let seekTo = seek.getAttribute('seek-value');\n if (is.empty(seekTo)) {\n seekTo = seek.value;\n }\n seek.removeAttribute('seek-value');\n player.currentTime = seekTo / seek.max * player.duration;\n }, 'seek');\n\n // Seek tooltip\n this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\n\n // Preview thumbnails plugin\n // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\n this.bind(elements.progress, 'mousemove touchmove', event => {\n const {\n previewThumbnails\n } = player;\n if (previewThumbnails && previewThumbnails.loaded) {\n previewThumbnails.startMove(event);\n }\n });\n\n // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\n this.bind(elements.progress, 'mouseleave touchend click', () => {\n const {\n previewThumbnails\n } = player;\n if (previewThumbnails && previewThumbnails.loaded) {\n previewThumbnails.endMove(false, true);\n }\n });\n\n // Show scrubbing preview\n this.bind(elements.progress, 'mousedown touchstart', event => {\n const {\n previewThumbnails\n } = player;\n if (previewThumbnails && previewThumbnails.loaded) {\n previewThumbnails.startScrubbing(event);\n }\n });\n this.bind(elements.progress, 'mouseup touchend', event => {\n const {\n previewThumbnails\n } = player;\n if (previewThumbnails && previewThumbnails.loaded) {\n previewThumbnails.endScrubbing(event);\n }\n });\n\n // Polyfill for lower fill in for webkit\n if (browser.isWebKit) {\n Array.from(getElements.call(player, 'input[type=\"range\"]')).forEach(element => {\n this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\n });\n }\n\n // Current time invert\n // Only if one time element is used for both currentTime and duration\n if (player.config.toggleInvert && !is.element(elements.display.duration)) {\n this.bind(elements.display.currentTime, 'click', () => {\n // Do nothing if we're at the start\n if (player.currentTime === 0) {\n return;\n }\n player.config.invertTime = !player.config.invertTime;\n controls.timeUpdate.call(player);\n });\n }\n\n // Volume\n this.bind(elements.inputs.volume, inputEvent, event => {\n player.volume = event.target.value;\n }, 'volume');\n\n // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\n this.bind(elements.controls, 'mouseenter mouseleave', event => {\n elements.controls.hover = !player.touch && event.type === 'mouseenter';\n });\n\n // Also update controls.hover state for any non-player children of fullscreen element (as above)\n if (elements.fullscreen) {\n Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\n this.bind(child, 'mouseenter mouseleave', event => {\n if (elements.controls) {\n elements.controls.hover = !player.touch && event.type === 'mouseenter';\n }\n });\n });\n }\n\n // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\n this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\n elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\n });\n\n // Show controls when they receive focus (e.g., when using keyboard tab key)\n this.bind(elements.controls, 'focusin', () => {\n const {\n config,\n timers\n } = player;\n\n // Skip transition to prevent focus from scrolling the parent element\n toggleClass(elements.controls, config.classNames.noTransition, true);\n\n // Toggle\n ui.toggleControls.call(player, true);\n\n // Restore transition\n setTimeout(() => {\n toggleClass(elements.controls, config.classNames.noTransition, false);\n }, 0);\n\n // Delay a little more for mouse users\n const delay = this.touch ? 3000 : 4000;\n\n // Clear timer\n clearTimeout(timers.controls);\n\n // Hide again after delay\n timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\n });\n\n // Mouse wheel for volume\n this.bind(elements.inputs.volume, 'wheel', event => {\n // Detect \"natural\" scroll - supported on OS X Safari only\n // Other browsers on OS X will be inverted until support improves\n const inverted = event.webkitDirectionInvertedFromDevice;\n // Get delta from event. Invert if `inverted` is true\n const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\n // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\n const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\n\n // Change the volume by 2%\n player.increaseVolume(direction / 50);\n\n // Don't break page scrolling at max and min\n const {\n volume\n } = player.media;\n if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\n event.preventDefault();\n }\n }, 'volume', false);\n });\n this.player = _player;\n this.lastKey = null;\n this.focusTimer = null;\n this.lastKeyDown = null;\n this.handleKey = this.handleKey.bind(this);\n this.toggleMenu = this.toggleMenu.bind(this);\n this.firstTouch = this.firstTouch.bind(this);\n }\n\n // Handle key presses\n handleKey(event) {\n const {\n player\n } = this;\n const {\n elements\n } = player;\n const {\n key,\n type,\n altKey,\n ctrlKey,\n metaKey,\n shiftKey\n } = event;\n const pressed = type === 'keydown';\n const repeat = pressed && key === this.lastKey;\n\n // Bail if a modifier key is set\n if (altKey || ctrlKey || metaKey || shiftKey) {\n return;\n }\n\n // If the event is bubbled from the media element\n // Firefox doesn't get the key for whatever reason\n if (!key) {\n return;\n }\n\n // Seek by increment\n const seekByIncrement = increment => {\n // Divide the max duration into 10th's and times by the number value\n player.currentTime = player.duration / 10 * increment;\n };\n\n // Handle the key on keydown\n // Reset on keyup\n if (pressed) {\n // Check focused element\n // and if the focused element is not editable (e.g. text input)\n // and any that accept key input http://webaim.org/techniques/keyboard/\n const focused = document.activeElement;\n if (is.element(focused)) {\n const {\n editable\n } = player.config.selectors;\n const {\n seek\n } = elements.inputs;\n if (focused !== seek && matches(focused, editable)) {\n return;\n }\n if (event.key === ' ' && matches(focused, 'button, [role^=\"menuitem\"]')) {\n return;\n }\n }\n\n // Which keys should we prevent default\n const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\n\n // If the key is found prevent default (e.g. prevent scrolling for arrows)\n if (preventDefault.includes(key)) {\n event.preventDefault();\n event.stopPropagation();\n }\n switch (key) {\n case '0':\n case '1':\n case '2':\n case '3':\n case '4':\n case '5':\n case '6':\n case '7':\n case '8':\n case '9':\n if (!repeat) {\n seekByIncrement(parseInt(key, 10));\n }\n break;\n case ' ':\n case 'k':\n if (!repeat) {\n silencePromise(player.togglePlay());\n }\n break;\n case 'ArrowUp':\n player.increaseVolume(0.1);\n break;\n case 'ArrowDown':\n player.decreaseVolume(0.1);\n break;\n case 'm':\n if (!repeat) {\n player.muted = !player.muted;\n }\n break;\n case 'ArrowRight':\n player.forward();\n break;\n case 'ArrowLeft':\n player.rewind();\n break;\n case 'f':\n player.fullscreen.toggle();\n break;\n case 'c':\n if (!repeat) {\n player.toggleCaptions();\n }\n break;\n case 'l':\n player.loop = !player.loop;\n break;\n }\n\n // Escape is handle natively when in full screen\n // So we only need to worry about non native\n if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\n player.fullscreen.toggle();\n }\n\n // Store last key for next cycle\n this.lastKey = key;\n } else {\n this.lastKey = null;\n }\n }\n\n // Toggle menu\n toggleMenu(event) {\n controls.toggleMenu.call(this.player, event);\n }\n }\n\n var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\n\n function createCommonjsModule(fn, module) {\n \treturn module = { exports: {} }, fn(module, module.exports), module.exports;\n }\n\n var loadjs_umd = createCommonjsModule(function (module, exports) {\n (function (root, factory) {\n {\n module.exports = factory();\n }\n })(commonjsGlobal, function () {\n /**\n * Global dependencies.\n * @global {Object} document - DOM\n */\n\n var devnull = function () {},\n bundleIdCache = {},\n bundleResultCache = {},\n bundleCallbackQueue = {};\n\n /**\n * Subscribe to bundle load event.\n * @param {string[]} bundleIds - Bundle ids\n * @param {Function} callbackFn - The callback function\n */\n function subscribe(bundleIds, callbackFn) {\n // listify\n bundleIds = bundleIds.push ? bundleIds : [bundleIds];\n var depsNotFound = [],\n i = bundleIds.length,\n numWaiting = i,\n fn,\n bundleId,\n r,\n q;\n\n // define callback function\n fn = function (bundleId, pathsNotFound) {\n if (pathsNotFound.length) depsNotFound.push(bundleId);\n numWaiting--;\n if (!numWaiting) callbackFn(depsNotFound);\n };\n\n // register callback\n while (i--) {\n bundleId = bundleIds[i];\n\n // execute callback if in result cache\n r = bundleResultCache[bundleId];\n if (r) {\n fn(bundleId, r);\n continue;\n }\n\n // add to callback queue\n q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\n q.push(fn);\n }\n }\n\n /**\n * Publish bundle load event.\n * @param {string} bundleId - Bundle id\n * @param {string[]} pathsNotFound - List of files not found\n */\n function publish(bundleId, pathsNotFound) {\n // exit if id isn't defined\n if (!bundleId) return;\n var q = bundleCallbackQueue[bundleId];\n\n // cache result\n bundleResultCache[bundleId] = pathsNotFound;\n\n // exit if queue is empty\n if (!q) return;\n\n // empty callback queue\n while (q.length) {\n q[0](bundleId, pathsNotFound);\n q.splice(0, 1);\n }\n }\n\n /**\n * Execute callbacks.\n * @param {Object or Function} args - The callback args\n * @param {string[]} depsNotFound - List of dependencies not found\n */\n function executeCallbacks(args, depsNotFound) {\n // accept function as argument\n if (args.call) args = {\n success: args\n };\n\n // success and error callbacks\n if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\n }\n\n /**\n * Load individual file.\n * @param {string} path - The file path\n * @param {Function} callbackFn - The callback function\n */\n function loadFile(path, callbackFn, args, numTries) {\n var doc = document,\n async = args.async,\n maxTries = (args.numRetries || 0) + 1,\n beforeCallbackFn = args.before || devnull,\n pathname = path.replace(/[\\?|#].*$/, ''),\n pathStripped = path.replace(/^(css|img)!/, ''),\n isLegacyIECss,\n e;\n numTries = numTries || 0;\n if (/(^css!|\\.css$)/.test(pathname)) {\n // css\n e = doc.createElement('link');\n e.rel = 'stylesheet';\n e.href = pathStripped;\n\n // tag IE9+\n isLegacyIECss = 'hideFocus' in e;\n\n // use preload in IE Edge (to detect load errors)\n if (isLegacyIECss && e.relList) {\n isLegacyIECss = 0;\n e.rel = 'preload';\n e.as = 'style';\n }\n } else if (/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\n // image\n e = doc.createElement('img');\n e.src = pathStripped;\n } else {\n // javascript\n e = doc.createElement('script');\n e.src = path;\n e.async = async === undefined ? true : async;\n }\n e.onload = e.onerror = e.onbeforeload = function (ev) {\n var result = ev.type[0];\n\n // treat empty stylesheets as failures to get around lack of onerror\n // support in IE9-11\n if (isLegacyIECss) {\n try {\n if (!e.sheet.cssText.length) result = 'e';\n } catch (x) {\n // sheets objects created from load errors don't allow access to\n // `cssText` (unless error is Code:18 SecurityError)\n if (x.code != 18) result = 'e';\n }\n }\n\n // handle retries in case of load failure\n if (result == 'e') {\n // increment counter\n numTries += 1;\n\n // exit function and try again\n if (numTries < maxTries) {\n return loadFile(path, callbackFn, args, numTries);\n }\n } else if (e.rel == 'preload' && e.as == 'style') {\n // activate preloaded stylesheets\n return e.rel = 'stylesheet'; // jshint ignore:line\n }\n\n // execute callback\n callbackFn(path, result, ev.defaultPrevented);\n };\n\n // add to document (unless callback returns `false`)\n if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\n }\n\n /**\n * Load multiple files.\n * @param {string[]} paths - The file paths\n * @param {Function} callbackFn - The callback function\n */\n function loadFiles(paths, callbackFn, args) {\n // listify paths\n paths = paths.push ? paths : [paths];\n var numWaiting = paths.length,\n x = numWaiting,\n pathsNotFound = [],\n fn,\n i;\n\n // define callback function\n fn = function (path, result, defaultPrevented) {\n // handle error\n if (result == 'e') pathsNotFound.push(path);\n\n // handle beforeload event. If defaultPrevented then that means the load\n // will be blocked (ex. Ghostery/ABP on Safari)\n if (result == 'b') {\n if (defaultPrevented) pathsNotFound.push(path);else return;\n }\n numWaiting--;\n if (!numWaiting) callbackFn(pathsNotFound);\n };\n\n // load scripts\n for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\n }\n\n /**\n * Initiate script load and register bundle.\n * @param {(string|string[])} paths - The file paths\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\n * callback or (3) object literal with success/error arguments, numRetries,\n * etc.\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\n * literal with success/error arguments, numRetries, etc.\n */\n function loadjs(paths, arg1, arg2) {\n var bundleId, args;\n\n // bundleId (if string)\n if (arg1 && arg1.trim) bundleId = arg1;\n\n // args (default is {})\n args = (bundleId ? arg2 : arg1) || {};\n\n // throw error if bundle is already defined\n if (bundleId) {\n if (bundleId in bundleIdCache) {\n throw \"LoadJS\";\n } else {\n bundleIdCache[bundleId] = true;\n }\n }\n function loadFn(resolve, reject) {\n loadFiles(paths, function (pathsNotFound) {\n // execute callbacks\n executeCallbacks(args, pathsNotFound);\n\n // resolve Promise\n if (resolve) {\n executeCallbacks({\n success: resolve,\n error: reject\n }, pathsNotFound);\n }\n\n // publish bundle load event\n publish(bundleId, pathsNotFound);\n }, args);\n }\n if (args.returnPromise) return new Promise(loadFn);else loadFn();\n }\n\n /**\n * Execute callbacks when dependencies have been satisfied.\n * @param {(string|string[])} deps - List of bundle ids\n * @param {Object} args - success/error arguments\n */\n loadjs.ready = function ready(deps, args) {\n // subscribe to bundle load event\n subscribe(deps, function (depsNotFound) {\n // execute callbacks\n executeCallbacks(args, depsNotFound);\n });\n return loadjs;\n };\n\n /**\n * Manually satisfy bundle dependencies.\n * @param {string} bundleId - The bundle id\n */\n loadjs.done = function done(bundleId) {\n publish(bundleId, []);\n };\n\n /**\n * Reset loadjs dependencies statuses\n */\n loadjs.reset = function reset() {\n bundleIdCache = {};\n bundleResultCache = {};\n bundleCallbackQueue = {};\n };\n\n /**\n * Determine if bundle has already been defined\n * @param String} bundleId - The bundle id\n */\n loadjs.isDefined = function isDefined(bundleId) {\n return bundleId in bundleIdCache;\n };\n\n // export\n return loadjs;\n });\n });\n\n // ==========================================================================\n function loadScript(url) {\n return new Promise((resolve, reject) => {\n loadjs_umd(url, {\n success: resolve,\n error: reject\n });\n });\n }\n\n // ==========================================================================\n\n // Parse Vimeo ID from URL\n function parseId$1(url) {\n if (is.empty(url)) {\n return null;\n }\n if (is.number(Number(url))) {\n return url;\n }\n const regex = /^.*(vimeo.com\\/|video\\/)(\\d+).*/;\n return url.match(regex) ? RegExp.$2 : url;\n }\n\n // Try to extract a hash for private videos from the URL\n function parseHash(url) {\n /* This regex matches a hexadecimal hash if given in any of these forms:\n * - [https://player.]vimeo.com/video/{id}/{hash}[?params]\n * - [https://player.]vimeo.com/video/{id}?h={hash}[¶ms]\n * - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\n * - video/{id}/{hash}\n * If matched, the hash is available in capture group 4\n */\n const regex = /^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/;\n const found = url.match(regex);\n return found && found.length === 5 ? found[4] : null;\n }\n\n // Set playback state and trigger change (only on actual change)\n function assurePlaybackState$1(play) {\n if (play && !this.embed.hasPlayed) {\n this.embed.hasPlayed = true;\n }\n if (this.media.paused === play) {\n this.media.paused = !play;\n triggerEvent.call(this, this.media, play ? 'play' : 'pause');\n }\n }\n const vimeo = {\n setup() {\n const player = this;\n\n // Add embed class for responsive\n toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\n\n // Set speed options from config\n player.options.speed = player.config.speed.options;\n\n // Set intial ratio\n setAspectRatio.call(player);\n\n // Load the SDK if not already\n if (!is.object(window.Vimeo)) {\n loadScript(player.config.urls.vimeo.sdk).then(() => {\n vimeo.ready.call(player);\n }).catch(error => {\n player.debug.warn('Vimeo SDK (player.js) failed to load', error);\n });\n } else {\n vimeo.ready.call(player);\n }\n },\n // API Ready\n ready() {\n const player = this;\n const config = player.config.vimeo;\n const {\n premium,\n referrerPolicy,\n ...frameParams\n } = config;\n // Get the source URL or ID\n let source = player.media.getAttribute('src');\n let hash = '';\n // Get from
if needed\n if (is.empty(source)) {\n source = player.media.getAttribute(player.config.attributes.embed.id);\n // hash can also be set as attribute on the
\n hash = player.media.getAttribute(player.config.attributes.embed.hash);\n } else {\n hash = parseHash(source);\n }\n const hashParam = hash ? {\n h: hash\n } : {};\n\n // If the owner has a pro or premium account then we can hide controls etc\n if (premium) {\n Object.assign(frameParams, {\n controls: false,\n sidedock: false\n });\n }\n\n // Get Vimeo params for the iframe\n const params = buildUrlParams({\n loop: player.config.loop.active,\n autoplay: player.autoplay,\n muted: player.muted,\n gesture: 'media',\n playsinline: player.config.playsinline,\n // hash has to be added to iframe-URL\n ...hashParam,\n ...frameParams\n });\n const id = parseId$1(source);\n // Build an iframe\n const iframe = createElement('iframe');\n const src = format(player.config.urls.vimeo.iframe, id, params);\n iframe.setAttribute('src', src);\n iframe.setAttribute('allowfullscreen', '');\n iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\n\n // Set the referrer policy if required\n if (!is.empty(referrerPolicy)) {\n iframe.setAttribute('referrerPolicy', referrerPolicy);\n }\n\n // Inject the package\n if (premium || !config.customControls) {\n iframe.setAttribute('data-poster', player.poster);\n player.media = replaceElement(iframe, player.media);\n } else {\n const wrapper = createElement('div', {\n class: player.config.classNames.embedContainer,\n 'data-poster': player.poster\n });\n wrapper.appendChild(iframe);\n player.media = replaceElement(wrapper, player.media);\n }\n\n // Get poster image\n if (!config.customControls) {\n fetch(format(player.config.urls.vimeo.api, src)).then(response => {\n if (is.empty(response) || !response.thumbnail_url) {\n return;\n }\n\n // Set and show poster\n ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\n });\n }\n\n // Setup instance\n // https://github.com/vimeo/player.js\n player.embed = new window.Vimeo.Player(iframe, {\n autopause: player.config.autopause,\n muted: player.muted\n });\n player.media.paused = true;\n player.media.currentTime = 0;\n\n // Disable native text track rendering\n if (player.supported.ui) {\n player.embed.disableTextTrack();\n }\n\n // Create a faux HTML5 API using the Vimeo API\n player.media.play = () => {\n assurePlaybackState$1.call(player, true);\n return player.embed.play();\n };\n player.media.pause = () => {\n assurePlaybackState$1.call(player, false);\n return player.embed.pause();\n };\n player.media.stop = () => {\n player.pause();\n player.currentTime = 0;\n };\n\n // Seeking\n let {\n currentTime\n } = player.media;\n Object.defineProperty(player.media, 'currentTime', {\n get() {\n return currentTime;\n },\n set(time) {\n // Vimeo will automatically play on seek if the video hasn't been played before\n\n // Get current paused state and volume etc\n const {\n embed,\n media,\n paused,\n volume\n } = player;\n const restorePause = paused && !embed.hasPlayed;\n\n // Set seeking state and trigger event\n media.seeking = true;\n triggerEvent.call(player, media, 'seeking');\n\n // If paused, mute until seek is complete\n Promise.resolve(restorePause && embed.setVolume(0))\n // Seek\n .then(() => embed.setCurrentTime(time))\n // Restore paused\n .then(() => restorePause && embed.pause())\n // Restore volume\n .then(() => restorePause && embed.setVolume(volume)).catch(() => {\n // Do nothing\n });\n }\n });\n\n // Playback speed\n let speed = player.config.speed.selected;\n Object.defineProperty(player.media, 'playbackRate', {\n get() {\n return speed;\n },\n set(input) {\n player.embed.setPlaybackRate(input).then(() => {\n speed = input;\n triggerEvent.call(player, player.media, 'ratechange');\n }).catch(() => {\n // Cannot set Playback Rate, Video is probably not on Pro account\n player.options.speed = [1];\n });\n }\n });\n\n // Volume\n let {\n volume\n } = player.config;\n Object.defineProperty(player.media, 'volume', {\n get() {\n return volume;\n },\n set(input) {\n player.embed.setVolume(input).then(() => {\n volume = input;\n triggerEvent.call(player, player.media, 'volumechange');\n });\n }\n });\n\n // Muted\n let {\n muted\n } = player.config;\n Object.defineProperty(player.media, 'muted', {\n get() {\n return muted;\n },\n set(input) {\n const toggle = is.boolean(input) ? input : false;\n player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\n muted = toggle;\n triggerEvent.call(player, player.media, 'volumechange');\n });\n }\n });\n\n // Loop\n let {\n loop\n } = player.config;\n Object.defineProperty(player.media, 'loop', {\n get() {\n return loop;\n },\n set(input) {\n const toggle = is.boolean(input) ? input : player.config.loop.active;\n player.embed.setLoop(toggle).then(() => {\n loop = toggle;\n });\n }\n });\n\n // Source\n let currentSrc;\n player.embed.getVideoUrl().then(value => {\n currentSrc = value;\n controls.setDownloadUrl.call(player);\n }).catch(error => {\n this.debug.warn(error);\n });\n Object.defineProperty(player.media, 'currentSrc', {\n get() {\n return currentSrc;\n }\n });\n\n // Ended\n Object.defineProperty(player.media, 'ended', {\n get() {\n return player.currentTime === player.duration;\n }\n });\n\n // Set aspect ratio based on video size\n Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\n const [width, height] = dimensions;\n player.embed.ratio = roundAspectRatio(width, height);\n setAspectRatio.call(this);\n });\n\n // Set autopause\n player.embed.setAutopause(player.config.autopause).then(state => {\n player.config.autopause = state;\n });\n\n // Get title\n player.embed.getVideoTitle().then(title => {\n player.config.title = title;\n ui.setTitle.call(this);\n });\n\n // Get current time\n player.embed.getCurrentTime().then(value => {\n currentTime = value;\n triggerEvent.call(player, player.media, 'timeupdate');\n });\n\n // Get duration\n player.embed.getDuration().then(value => {\n player.media.duration = value;\n triggerEvent.call(player, player.media, 'durationchange');\n });\n\n // Get captions\n player.embed.getTextTracks().then(tracks => {\n player.media.textTracks = tracks;\n captions.setup.call(player);\n });\n player.embed.on('cuechange', ({\n cues = []\n }) => {\n const strippedCues = cues.map(cue => stripHTML(cue.text));\n captions.updateCues.call(player, strippedCues);\n });\n player.embed.on('loaded', () => {\n // Assure state and events are updated on autoplay\n player.embed.getPaused().then(paused => {\n assurePlaybackState$1.call(player, !paused);\n if (!paused) {\n triggerEvent.call(player, player.media, 'playing');\n }\n });\n if (is.element(player.embed.element) && player.supported.ui) {\n const frame = player.embed.element;\n\n // Fix keyboard focus issues\n // https://github.com/sampotts/plyr/issues/317\n frame.setAttribute('tabindex', -1);\n }\n });\n player.embed.on('bufferstart', () => {\n triggerEvent.call(player, player.media, 'waiting');\n });\n player.embed.on('bufferend', () => {\n triggerEvent.call(player, player.media, 'playing');\n });\n player.embed.on('play', () => {\n assurePlaybackState$1.call(player, true);\n triggerEvent.call(player, player.media, 'playing');\n });\n player.embed.on('pause', () => {\n assurePlaybackState$1.call(player, false);\n });\n player.embed.on('timeupdate', data => {\n player.media.seeking = false;\n currentTime = data.seconds;\n triggerEvent.call(player, player.media, 'timeupdate');\n });\n player.embed.on('progress', data => {\n player.media.buffered = data.percent;\n triggerEvent.call(player, player.media, 'progress');\n\n // Check all loaded\n if (parseInt(data.percent, 10) === 1) {\n triggerEvent.call(player, player.media, 'canplaythrough');\n }\n\n // Get duration as if we do it before load, it gives an incorrect value\n // https://github.com/sampotts/plyr/issues/891\n player.embed.getDuration().then(value => {\n if (value !== player.media.duration) {\n player.media.duration = value;\n triggerEvent.call(player, player.media, 'durationchange');\n }\n });\n });\n player.embed.on('seeked', () => {\n player.media.seeking = false;\n triggerEvent.call(player, player.media, 'seeked');\n });\n player.embed.on('ended', () => {\n player.media.paused = true;\n triggerEvent.call(player, player.media, 'ended');\n });\n player.embed.on('error', detail => {\n player.media.error = detail;\n triggerEvent.call(player, player.media, 'error');\n });\n\n // Rebuild UI\n if (config.customControls) {\n setTimeout(() => ui.build.call(player), 0);\n }\n }\n };\n\n // ==========================================================================\n\n // Parse YouTube ID from URL\n function parseId(url) {\n if (is.empty(url)) {\n return null;\n }\n const regex = /^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/;\n return url.match(regex) ? RegExp.$2 : url;\n }\n\n // Set playback state and trigger change (only on actual change)\n function assurePlaybackState(play) {\n if (play && !this.embed.hasPlayed) {\n this.embed.hasPlayed = true;\n }\n if (this.media.paused === play) {\n this.media.paused = !play;\n triggerEvent.call(this, this.media, play ? 'play' : 'pause');\n }\n }\n function getHost(config) {\n if (config.noCookie) {\n return 'https://www.youtube-nocookie.com';\n }\n if (window.location.protocol === 'http:') {\n return 'http://www.youtube.com';\n }\n\n // Use YouTube's default\n return undefined;\n }\n const youtube = {\n setup() {\n // Add embed class for responsive\n toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\n\n // Setup API\n if (is.object(window.YT) && is.function(window.YT.Player)) {\n youtube.ready.call(this);\n } else {\n // Reference current global callback\n const callback = window.onYouTubeIframeAPIReady;\n\n // Set callback to process queue\n window.onYouTubeIframeAPIReady = () => {\n // Call global callback if set\n if (is.function(callback)) {\n callback();\n }\n youtube.ready.call(this);\n };\n\n // Load the SDK\n loadScript(this.config.urls.youtube.sdk).catch(error => {\n this.debug.warn('YouTube API failed to load', error);\n });\n }\n },\n // Get the media title\n getTitle(videoId) {\n const url = format(this.config.urls.youtube.api, videoId);\n fetch(url).then(data => {\n if (is.object(data)) {\n const {\n title,\n height,\n width\n } = data;\n\n // Set title\n this.config.title = title;\n ui.setTitle.call(this);\n\n // Set aspect ratio\n this.embed.ratio = roundAspectRatio(width, height);\n }\n setAspectRatio.call(this);\n }).catch(() => {\n // Set aspect ratio\n setAspectRatio.call(this);\n });\n },\n // API ready\n ready() {\n const player = this;\n const config = player.config.youtube;\n // Ignore already setup (race condition)\n const currentId = player.media && player.media.getAttribute('id');\n if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\n return;\n }\n\n // Get the source URL or ID\n let source = player.media.getAttribute('src');\n\n // Get from
if needed\n if (is.empty(source)) {\n source = player.media.getAttribute(this.config.attributes.embed.id);\n }\n\n // Replace the ';return t.match(this.opts.regex.youtube)?(e=\"//www.youtube.com\",-1!==t.search(\"youtube-nocookie.com\")&&(e=\"//www.youtube-nocookie.com\"),i+t.replace(this.opts.regex.youtube,e+\"/embed/$1\")+s):t.match(this.opts.regex.vimeo)?i+t.replace(this.opts.regex.vimeo,\"//player.vimeo.com/video/$2\")+s:t},_isNeedToChange:function(t,e,i){return i.getEmbedCode()!==e.getEmbedCode()||t.responsive!==i.isResponsive()||t.caption!==i.getCaption()||void 0},_isHtmlString:function(t){return/^\\s*<(\\w+|!)[^>]*>/.test(t)},_isFigure:function(t){return/^
\"),s=this._createImageFromResponseItem(t[i]),n.append(s),s=this.app.create(\"block.image\",n),this.app.block.add({instance:s,type:\"image\"}),Object.prototype.hasOwnProperty.call(t[i],\"caption\")&&(n=this.dom(\"
\").html(t[i].caption),s.$block.append(n),this.app.create(\"block.figcaption\",n)),this.app.broadcast(\"image.\"+(e?\"select\":\"upload\"),{instance:s,data:t[i]}),this.$last=s.getBlock(),this.imageslen++)},remove:function(){this.app.popup.close(),this.app.block.remove()},error:function(t){this.app.broadcast(\"image.upload.error\",{response:t})},getStates:function(){var t,e,i,s=this._findImages();for(t in this.dataStates)this.dataStates.hasOwnProperty(t)&&(e=this.dataStates[t],i=s.is('[data-image=\"'+e.id+'\"]'),this._setImageState(e.id,i));return this.dataStates},createUploadBox:function(t,e){if(t)return t=this.dom(\"
\"),e.append(t),t},createSelectBox:function(t,e,i){t&&(this.$selectbox=this._createImagesBox(e),\"object\"==typeof t?this._parseList(t,i):(e=this.opts.editor.reloadmarker?{d:(new Date).getTime()}:{},this.ajax.get({url:t,data:e,success:function(t){this._parseList(t,i)}.bind(this)})))},_findImages:function(){return this.app.editor.getEditor().find(\"[data-image]\")},_addImageState:function(t){var e=t.attr(\"data-image\");this.dataStates[e]={type:\"image\",status:!0,url:t.attr(\"src\"),$img:t,id:e}},_setImageState:function(t,e){this.dataStates[t].status=e},_checkImageLoad:function(){this.imagescount++,this.imagescount===this.imageslen&&(this.app.block.unset(),this.app.block.set(this.$last))},_buildEditUpload:function(){var t,e,i;this.opts.image.upload&&(i=this.app.block.get(),t=this.app.popup.getBody(),(e=this._createFormItem()).addClass(this.prefix+\"-form-item-edit-image-box\"),this.$imageclone=i.getImage().clone(),this.$imageclone.removeAttr(\"width height style\"),(i=this.dom(\"
\").addClass(this.prefix+\"-form-item-image\")).append(this.$imageclone),e.append(i),this.$upload=this.dom(\"
\"),e.append(this.$upload),t.prepend(e),this._buildUpload(this.$upload,\"image.changeClone\"))},_buildUpload:function(t,e){this.opts.image.upload&&(e={box:!0,placeholder:this.lang.get(\"image.upload-new-placeholder\"),url:this.opts.image.upload,name:this.opts.image.name,data:this.opts.image.data,multiple:this.opts.image.multiple,success:e,error:\"image.error\"},this.app.create(\"upload\",t,e))},_createSelectBox:function(t){this.createSelectBox(this.opts.image.select,t,\"image.insertFromSelect\")},_createUploadBox:function(t){this.$upload=this.createUploadBox(this.opts.image.upload,t),this._buildUpload(this.$upload,\"image.insertByUpload\")},_createImageFromResponseItem:function(t){for(var e,i=this.dom(\"\").attr(\"src\",t.url).one(\"load\",this._checkImageLoad.bind(this)),s=i,n=[\"id\",\"alt\",\"width\",\"height\"],a=0;a\")).attr(\"href\",t.link),i.wrap(e),s=e),s},_createImagesBox:function(t){var e=this.dom(\"
\").addClass(this.prefix+\"-popup-images-box\");return t.append(e),e},_createOrSection:function(t){var e;this.opts.image.url&&(this.opts.image.upload||this.opts.image.select)&&((e=this.dom(\"
\").addClass(this.prefix+\"-popup-image-section-or\")).html(this.lang.get(\"image.or\")),t.append(e))},_createImageByUrl:function(t){var e;this.opts.image.url&&(e=this._createFormItem(),this.$urlinput=this._createUrlInput(),this.$urlbutton=this._createUrlButton(),e.append(this.$urlinput),e.append(this.$urlbutton),t.append(e),this.$urlinput.focus())},_createFormItem:function(){return this.dom(\"
\").addClass(this.prefix+\"-form-container-flex\")},_createUrlInput:function(){var t=this.dom(\"\").addClass(this.prefix+\"-form-input\");return t.attr(\"placeholder\",this.lang.get(\"image.url-placeholder\")),t},_createUrlButton:function(){var t=this.dom(\"