/**
* 播放器組件 - 所有 UI 對象的基類
*
* @file component.js
*/
從“全局/窗口”導入窗口;
從'./mixins/evented'導入事件;
從“./mixins/stateful”導入有狀態;
import * as Dom from './utils/dom.js';
從 './utils/fn.js' 導入 * 作為 Fn;
從 './utils/guid.js' 導入 * 作為 Guid;
從 './utils/string-cases.js' 導入 {toTitleCase, toLowerCase};
從 './utils/merge-options.js' 導入 mergeOptions;
從'./utils/computed-style'導入計算樣式;
從“./utils/map.js”導入地圖;
從'./utils/set.js'導入集合;
從“鍵碼”導入鍵碼;
/**
* 所有 UI 組件的基類。
* 組件是代表 javascript 對象和元素的 UI 對象
* 在 DOM 中。它們可以是其他組件的子組件,並且可以有
* 孩子們自己。
*
* 組件也可以使用來自 {@link EventTarget} 的方法
*/
類組件{
/**
* 組件準備就緒時調用的回調。沒有任何
* 參數和任何回調值將被忽略。
*
* @callback 組件~ReadyCallback
* @this 組件
*/
/**
* 創建此類的一個實例。
*
* @param {Player} 播放器
* 此類應附加到的 `Player`。
*
* @param {對象} [選項]
* 組件選項的鍵/值存儲。
*
* @param {Object[]} [options.children]
* 用於初始化此組件的子對像數組。兒童對像有
* 如果需要多個相同類型的組件,將使用名稱屬性
* 添加。
*
* @param {string} [options.className]
* 類或空格分隔的類列表以添加組件
*
* @param {Component~ReadyCallback} [就緒]
* `Component` 準備就緒時調用的函數。
*/
構造函數(播放器,選項,準備就緒){
// 組件可能是播放器本身,我們不能將 `this` 傳遞給 super
如果(!播放器 && 這個。播放){
this.player_ = player = this; // eslint-disable-line
}其他{
this.player_ = 玩家;
}
this.isDisposed_ = false;
// 通過 `addChild` 方法保存對父組件的引用
this.parentComponent_ = null;
// 複製 prototype.options_ 以防止覆蓋默認值
this.options_ = mergeOptions({}, this.options_);
// 使用提供的選項更新選項
options = this.options_ = mergeOptions(this.options_, options);
// 如果提供了選項或選項元素,則從中獲取 ID
this.id_ = options.id || (options.el && options.el.id);
// 如果選項中沒有 ID,則生成一個
如果(!this.id_){
// 在模擬玩家的情況下不需要玩家 ID 函數
const id = player && player.id && player.id() || 'no_player';
this.id_ = `${id}_component_${Guid.newGUID()}`;
}
this.name_ = options.name ||無效的;
// 如果選項中沒有提供,則創建元素
如果(選項.el){
this.el_ = options.el;
} 否則,如果(選項。創建!== 假){
this.el_ = this.createEl();
}
如果(options.className && this.el_){
options.className.split(' ').forEach(c => this.addClass(c));
}
// 如果 evented 是除 false 之外的任何值,我們想在 evented 中混合
如果(選項。事件!==假){
// 使它成為事件對象並使用 `el_`(如果可用)作為其事件總線
事件(這,{eventBusKey:this.el_?'el_':null});
this.handleLanguagechange = this.handleLanguagechange.bind(this);
this.on(this.player_, 'languagechange', this.handleLanguagechange);
}
有狀態的(這,this.constructor.defaultState);
這個.children_ = [];
this.childIndex_ = {};
this.childNameIndex_ = {};
this.setTimeoutIds_ = new Set();
this.setIntervalIds_ = new Set();
this.rafIds_ = new Set();
this.namedRafs_ = new Map();
this.clearingTimersOnDispose_ = false;
// 在選項中添加任何子組件
如果(選項。initChildren !== false){
這個.initChildren();
}
// 不想在這裡觸發就緒,否則它會在 init 實際啟動之前觸發
// 為運行此構造函數的所有子級完成
this.ready(準備就緒);
如果(選項。報告活動!== 假){
this.enableTouchActivity();
}
}
/**
* 處理 `Component` 和所有子組件。
*
* @fires 組件#dispose
*
* @param {Object} 選項
* @param {Element} options.originalEl 用來替換播放器元素的元素
*/
處置(選項={}){
// 如果組件已經被處置,則退出。
如果(this.isDisposed_){
返回;
}
如果(this.readyQueue_){
this.readyQueue_.length = 0;
}
/**
* 當 `Component` 被釋放時觸發。
*
* @event 組件#dispose
* @type {EventTarget~Event}
*
* @property {boolean} [氣泡=假]
* 設置為 false 這樣 dispose 事件就不會
* 冒泡
*/
this.trigger({type: 'dispose', bubbles: false});
this.isDisposed_ = true;
// 處置所有孩子。
如果(this.children_){
for (let i = this.children_.length - 1; i >= 0; i--) {
如果(this.children_[i].dispose){
this.children_[i].dispose();
}
}
}
// 刪除子引用
this.children_ = null;
this.childIndex_ = null;
this.childNameIndex_ = null;
this.parentComponent_ = null;
如果(this.el_){
// 從 DOM 中移除元素
如果(this.el_.parentNode){
如果(選項。restoreEl){
this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
}其他{
this.el_.parentNode.removeChild(this.el_);
}
}
this.el_ = null;
}
// 在處理元素後刪除對播放器的引用
this.player_ = null;
}
/**
* 判斷這個組件是否已經被釋放。
*
* @return {布爾值}
* 如果組件已被處置,則為“true”。否則為“假”。
*/
isDisposed() {
返回布爾值(this.isDisposed_);
}
/**
* 返回 `Component` 附加到的 {@link Player}。
*
* @return {玩家}
* 這個 `Component` 附加到的播放器。
*/
玩家(){
返回這個.player_;
}
/**
* 選項對象與新選項的深度合併。
* > 注意:當 `obj` 和 `options` 都包含值為對象的屬性時。
* 這兩個屬性得到合併使用 {@link 模塊:合併選項}
*
* @param {對象}對象
* 包含新選項的對象。
*
* @return {對象}
* `this.options_` 和 `obj` 合併在一起的新對象。
*/
選項(對象){
如果(!對象){
返回 this.options_;
}
this.options_ = mergeOptions(this.options_, obj);
返回 this.options_;
}
/**
* 獲取 `Component` 的 DOM 元素
*
* @return {元素}
* 此 `Component` 的 DOM 元素。
*/
埃爾(){
返回這個。el_;
}
/**
* 創建 `Component` 的 DOM 元素。
*
* @param {string} [標籤名]
* 元素的 DOM 節點類型。例如'div'
*
* @param {對象} [屬性]
* 應設置的屬性對象。
*
* @param {對象} [屬性]
* 應設置的屬性對象。
*
* @return {元素}
* 被創建的元素。
*/
createEl(tagName, properties, attributes) {
返回 Dom.createEl(tagName, properties, attributes);
}
/**
* 本地化給定字符串的英文字符串。
*
* 如果提供了令牌,它將嘗試對提供的字符串運行簡單的令牌替換。
* 它查找的標記看起來像 `{1}`,索引為 1-indexed 到標記數組中。
*
* 如果提供了 `defaultValue`,它將在 `string` 上使用它,
* 如果在提供的語言文件中找不到值。
* 如果你想有一個用於令牌替換的描述性密鑰,這很有用
* 但是有一個簡潔的本地化字符串,不需要包含 `en.json`。
*
* 目前用於進度條計時。
*```js
* {
* "進度條計時:currentTime={1} duration={2}": "{1} of {2}"
* }
*```
* 然後像這樣使用:
*```js
* this.localize('進度條計時:currentTime={1} duration{2}',
* [this.player_.currentTime(), this.player_.duration()],
* '{1} of {2}');
*```
*
* 其中輸出如下內容:
*
*
* @param {string} 字符串
* 要本地化的字符串和要在語言文件中查找的鍵。
* @param {string[]} [令牌]
* 如果當前項目有代幣替換,請在此處提供代幣。
* @param {string} [默認值]
* 默認為“字符串”。可以是用於令牌替換的默認值
* 如果查找鍵需要分開。
*
* @return {字符串}
* 本地化字符串,如果沒有本地化,則為英文字符串。
*/
本地化(字符串,標記,默認值=字符串){
const code = this.player_.language && this.player_.language();
const languages = this.player_.languages && this.player_.languages();
const 語言 = 語言 && 語言[代碼];
const primaryCode = code && code.split('-')[0];
const primaryLang = 語言 && 語言[primaryCode];
讓 localizedString = defaultValue;
如果(語言&&語言[字符串]){
localizedString = 語言[字符串];
} else if (primaryLang && primaryLang[string]) {
localizedString = primaryLang[字符串];
}
如果(代幣){
localizedString = localizedString.replace(/\\{(\\d+)\\}/g, function(match, index) {
const value = tokens[index - 1];
讓 ret = 價值;
如果(類型值==='未定義'){
ret = 匹配;
}
返還;
});
}
返回本地化字符串;
}
/**
* 處理組件中播放器的語言更改。應該被子組件覆蓋。
*
* @抽象的
*/
handleLanguagechange() {}
/**
* 返回 `Component` 的 DOM 元素。這是孩子們被插入的地方。
* 這通常與 {@link Component#el} 中返回的元素相同。
*
* @return {元素}
* 此 `Component` 的內容元素。
*/
contentEl() {
返回 this.contentEl_ ||這個.el_;
}
/**
* 獲取這個 `Component` 的 ID
*
* @return {字符串}
* 這個 `Component` 的 id
*/
身份證(){
返回這個.id_;
}
/**
* 獲取 `Component` 的名稱。該名稱用於引用“組件”
* 並在註冊期間設置。
*
* @return {字符串}
* 此 `Component` 的名稱。
*/
姓名() {
返回此名稱_;
}
/**
* 獲取所有子組件的數組
*
* @return {數組}
* 這些孩子
*/
孩子們() {
返回這個。children_;
}
/**
* 返回具有給定 `id` 的子 `Component`。
*
* @param {string} id
* 要獲取的子 `Component` 的 id。
*
* @return {組件|未定義}
* 具有給定 id 或未定義的子 Component 。
*/
getChildById(id) {
返回 this.childIndex_[id];
}
/**
* 返回具有給定“名稱”的子“組件”。
*
* @param {string} 名稱
* 要獲取的子組件的名稱。
*
* @return {組件|未定義}
* 具有給定“名稱”或未定義的子“組件”。
*/
得到孩子(名字){
如果(!名稱){
返回;
}
返回 this.childNameIndex_[name];
}
/**
* 返回給定後的後代 `Component`
* 後代 `names`。例如 ['foo', 'bar', 'baz'] 會
* 嘗試在當前組件上獲取“foo”,在“foo”上獲取“bar”
* component 和 'bar' 組件上的 'baz' 並返回 undefined
* 如果其中任何一個不存在。
*
* @param {...string[]|...string} 名稱
* 要獲取的子組件的名稱。
*
* @return {組件|未定義}
* 給定後代之後的後代 `Component`
* `names` 或未定義。
*/
getDescendant(...名字){
// 將數組參數展平到主數組中
names = names.reduce((acc, n) => acc.concat(n), []);
讓 currentChild = 這個;
對於(讓我 = 0; 我的 < 名字。長度; 我 ++){
currentChild = currentChild.getChild(名字[i]);
如果(!currentChild || !currentChild.getChild){
返回;
}
}
返回當前子;
}
/**
* 在當前 `Component` 中添加一個子 `Component`。
*
*
* @param {string|Component} 孩子
* 要添加的子項的名稱或實例。
*
* @param {對象} [選項={}]
* 將傳遞給子項的選項的鍵/值存儲
* 孩子。
*
* @param {number} [index=this.children_.length]
* 嘗試將孩子添加到的索引。
*
* @return {組件}
* 作為子組件添加的 `Component`。當使用字符串時
* `Component` 將由此過程創建。
*/
addChild(child, options = {}, index = this.children_.length) {
讓組件;
讓組件名稱;
// 如果 child 是一個字符串,創建帶有選項的組件
如果 (typeof child === 'string') {
componentName = toTitleCase(child);
const componentClassName = options.componentClass ||組件名稱;
// 通過選項設置名稱
options.name = 組件名稱;
// 為此控件集創建一個新的對象和元素
// 如果沒有.player_,這是一個玩家
const ComponentClass = Component.getComponent(componentClassName);
如果(!組件類){
throw new Error(`組件 ${componentClassName} 不存在`);
}
// 直接存儲在 videojs 對像上的數據可能是
// 錯誤識別為要保留的組件
//與 4.x 向後兼容性。檢查以確保
// 組件類可以被實例化。
如果(類型組件類!== '功能') {
返回空值;
}
component = new ComponentClass(this.player_ || this, options);
// child 是一個組件實例
}其他{
組件=孩子;
}
如果(component.parentComponent_){
component.parentComponent_.removeChild(component);
}
this.children_.splice(index, 0, component);
component.parentComponent_ = this;
if (typeof component.id === 'function') {
this.childIndex_[component.id()] = component;
}
// 如果名稱未用於創建組件,請檢查我們是否可以使用
// 組件的名稱函數
組件名 = 組件名 || (component.name && toTitleCase(component.name()));
如果(組件名稱){
this.childNameIndex_[componentName] = component;
this.childNameIndex_[toLowerCase(componentName)] = component;
}
// 將 UI 對象的元素添加到容器 div (box)
// 不需要有一個元素
if (typeof component.el === 'function' && component.el()) {
// 如果在組件之前插入,則在該組件的元素之前插入
讓 refNode = null;
如果 (this.children_[index + 1]) {
// 大多數孩子都是組件,但視頻技術是一個 HTML 元素
如果 (this.children_[index + 1].el_) {
refNode = this.children_[index + 1].el_;
} else if (Dom.isEl(this.children_[index + 1])) {
refNode = this.children_[index + 1];
}
}
this.contentEl().insertBefore(component.el(), refNode);
}
// 如果需要,返回以便它可以存儲在父對像上。
返回組件;
}
/**
* 從此 `Component` 的子列表中刪除一個子 `Component`。同時刪除
* 來自此 `Component` 元素的子 `Component` 元素。
*
* @param {Component} 組件
* 要刪除的子 `Component`。
*/
removeChild(組件){
if (typeof component === 'string') {
組件 = this.getChild(組件);
}
如果(!組件||!this.children_){
返回;
}
讓 childFound = false;
for (let i = this.children_.length - 1; i >= 0; i--) {
如果(this.children_[i] === 組件){
找到孩子=真;
this.children_.splice(i, 1);
休息;
}
}
如果(!childFound){
返回;
}
component.parentComponent_ = null;
this.childIndex_[component.id()] = null;
this.childNameIndex_[toTitleCase(component.name())] = null;
this.childNameIndex_[toLowerCase(component.name())] = null;
const compEl = component.el();
if (compEl && compEl.parentNode === this.contentEl()) {
this.contentEl().removeChild(component.el());
}
}
/**
* 根據選項添加和初始化默認子組件。
*/
初始化兒童(){
const children = this.options_.children;
如果(孩子們){
// `this` 是 `parent`
const parentOptions = this.options_;
const handleAdd = (child) => {
const name = child.name;
讓 opts = child.opts;
// 允許在父選項中設置子選項
// 例如 videojs(id, { controlBar: false });
// 而不是 videojs(id, { children: { controlBar: false });
如果(父選項[名稱]!==未定義){
opts = parentOptions[名稱];
}
// 允許禁用默認組件
// 例如 options['children']['posterImage'] = false
如果(選擇===假){
返回;
}
// 如果沒有配置,允許將選項作為簡單的布爾值傳遞
// 是必要的。
如果(選擇===真){
選擇={};
}
// 我們還想傳遞原始播放器選項
// 對每個組件也是如此,所以他們不需要
// 稍後返回播放器以獲取選項。
opts.playerOptions = this.options_.playerOptions;
// 創建並添加子組件。
// 在父實例上按名稱添加對子實例的直接引用。
// 如果使用了兩個相同的組件,則應提供不同的名稱
// 對於每個
const newChild = this.addChild(name, opts);
如果(新孩子){
這個[名字] = newChild;
}
};
// 允許在選項中傳遞一組子項詳細信息
讓工作的孩子;
const Tech = Component.getComponent('Tech');
如果(Array.isArray(孩子)){
workingChildren = 孩子們;
}其他{
workingChildren = Object.keys(children);
}
童工
// 在 this.options_ 和 workingChildren 中的孩子會
// 給我們額外的我們不想要的孩子。所以,我們想過濾掉它們。
.concat(Object.keys(this.options_)
.filter(函數(子){
返回!workingChildren.some(函數(wchild){
如果 (typeof wchild === 'string') {
返回孩子 === wchild;
}
返回孩子 === wchild.name;
});
}))
.map((子) => {
讓名字;
讓選擇;
如果 (typeof child === 'string') {
名字=孩子;
opts = children[名字] || this.options_[名稱] || {};
}其他{
名字=孩子的名字;
選擇=孩子;
}
返回{名稱,選擇};
})
.filter((子) => {
// 我們必須確保 child.name 不在 techOrder 中,因為
// 技術被註冊為組件但不能不兼容
// 請參閱 https://github.com/videojs/video.js/issues/2772
const c = Component.getComponent(child.opts.componentClass ||
toTitleCase(child.name));
返回 c &&!科技科技 (c);
})
.forEach(句柄添加);
}
}
/**
* 構建默認的 DOM 類名。應該被子組件覆蓋。
*
* @return {字符串}
* 此對象的 DOM 類名稱。
*
* @抽象的
*/
buildCSSClass() {
// 子類可以包含一個函數:
// 返回 '類名' + this._super();
返回 '';
}
/**
* 將偵聽器綁定到組件的就緒狀態。
* 與事件監聽器的不同之處在於,如果就緒事件已經發生
* 它會立即觸發該功能。
*
* @return {組件}
* 返回自身;方法可以鏈接。
*/
就緒(fn,sync = false){
如果(!fn){
返回;
}
如果(!this.isReady_){
this.readyQueue_ = this.readyQueue_ || [];
this.readyQueue_.push(fn);
返回;
}
如果(同步){
fn.call(this);
}其他{
// 為了保持一致性,默認異步調用函數
this.setTimeout(fn, 1);
}
}
/**
* 為這個 `Component` 觸發所有準備好的監聽器。
*
* @fires 組件#ready
*/
觸發就緒(){
this.isReady_ = true;
// 確保 ready 被異步觸發
this.setTimeout(函數() {
const readyQueue = this.readyQueue_;
// 重置就緒隊列
this.readyQueue_ = [];
如果 (readyQueue && readyQueue.length > 0) {
readyQueue.forEach(函數(fn){
fn.call(this);
}, 這);
}
// 也允許使用事件監聽器
/**
* 當 `Component` 就緒時觸發。
*
* @event 組件#ready
* @type {EventTarget~Event}
*/
this.trigger('準備好');
}, 1);
}
/**
* 查找與 `selector` 匹配的單個 DOM 元素。這可以在“組件”中
* `contentEl()` 或其他自定義上下文。
*
* @param {string} 選擇器
* 一個有效的 CSS 選擇器,將傳遞給 querySelector。
*
* @param {Element|string} [context=this.contentEl()]
* 在其中查詢的 DOM 元素。也可以是一個選擇器字符串
* 在這種情況下,第一個匹配元素將用作上下文。如果
* 使用缺少的 `this.contentEl()`。如果 `this.contentEl()` 返回
* 什麼都沒有,它會返回到 `document`。
*
* @return {元素|null}
* 找到的 dom 元素,或者為 null
*
* @see [關於 CSS 選擇器的信息](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
*/
$(選擇器,上下文){
返回 Dom.$(選擇器,上下文 || this.contentEl());
}
/**
* 查找所有與 `selector` 匹配的 DOM 元素。這可以在“組件”中
* `contentEl()` 或其他自定義上下文。
*
* @param {string} 選擇器
* 一個有效的 CSS 選擇器,將被傳遞給 querySelectorAll 。
*
* @param {Element|string} [context=this.contentEl()]
* 在其中查詢的 DOM 元素。也可以是一個選擇器字符串
* 在這種情況下,第一個匹配元素將用作上下文。如果
* 使用缺少的 `this.contentEl()`。如果 `this.contentEl()` 返回
* 什麼都沒有,它會返回到 `document`。
*
* @return {節點列表}
* 找到的 dom 元素列表
*
* @see [關於 CSS 選擇器的信息](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
*/
$$(選擇器,上下文){
返回 Dom.$$(選擇器,上下文 || this.contentEl());
}
/**
* 檢查組件的元素是否有 CSS 類名。
*
* @param {string} classToCheck
* 要檢查的 CSS 類名。
*
* @return {布爾值}
* - 如果 `Component` 有類則為真。
* - 如果 `Component` 沒有類,則為假
*/
hasClass(classToCheck){
返回 Dom.hasClass(this.el_, classToCheck);
}
/**
* 將 CSS 類名稱添加到 `Component` 的元素。
*
* @param {string} classToAdd
* 要添加的 CSS 類名
*/
添加類(類添加){
Dom.addClass(this.el_, classToAdd);
}
/**
* 從 `Component` 的元素中移除一個 CSS 類名。
*
* @param {string} classToRemove
* 要刪除的 CSS 類名
*/
removeClass(classToRemove) {
Dom.removeClass(this.el_, classToRemove);
}
/**
* 在組件的元素中添加或刪除 CSS 類名。
* - 當 {@link Component#hasClass} 返回 false 時添加 `classToToggle`。
* - 當 {@link Component#hasClass} 返回 true 時,`classToToggle` 被移除。
*
* @param {string} classToToggle
* 添加或刪除的類基於 (@link Component#hasClass}
*
* @param {boolean|Dom~predicate} [謂詞]
* {@link Dom~predicate} 函數或布爾值
*/
toggleClass(classToToggle,謂詞){
Dom.toggleClass(this.el_, classToToggle, predicate);
}
/**
* 如果 `Component` 的元素被隱藏,則顯示它
* 來自它的“vjs-hidden”類名。
*/
展示() {
this.removeClass('vjs-hidden');
}
/**
* 如果 `Component` 的元素當前正在顯示,則隱藏它,方法是添加
* 'vjs-hidden` 類名。
*/
隱藏() {
this.addClass('vjs-hidden');
}
/**
* 通過添加 'vjs-lock-showing' 將 `Component` 的元素鎖定在其可見狀態
* 它的類名。在 fadeIn/fadeOut 期間使用。
*
* @私人的
*/
鎖定顯示(){
this.addClass('vjs-lock-showing');
}
/**
* 通過刪除 'vjs-lock-showing' 將 `Component` 的元素從其可見狀態解鎖
* 來自它的類名。在 fadeIn/fadeOut 期間使用。
*
* @私人的
*/
解鎖顯示(){
this.removeClass('vjs-lock-showing');
}
/**
* 獲取 `Component` 元素的屬性值。
*
* @param {string} 屬性
* 要從中獲取值的屬性的名稱。
*
* @return {字符串|空}
* - 請求的屬性值。
* - 如果屬性不存在,在某些瀏覽器上可以是空字符串
* 或沒有價值
* - 如果屬性不存在或已經存在,大多數瀏覽器將返回 null
* 沒有價值。
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
*/
getAttribute(屬性){
返回 Dom.getAttribute(this.el_, attribute);
}
/**
* 設置 `Component` 元素的屬性值
*
* @param {string} 屬性
* 要設置的屬性的名稱。
*
* @param {string} 值
* 將屬性設置為的值。
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
*/
setAttribute(屬性,值){
Dom.setAttribute(this.el_, attribute, value);
}
/**
* 從 `Component` 的元素中刪除一個屬性。
*
* @param {string} 屬性
* 要刪除的屬性的名稱。
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
*/
刪除屬性(屬性){
Dom.removeAttribute(this.el_, attribute);
}
/**
* 根據 CSS 樣式獲取或設置組件的寬度。
* 有關更多詳細信息,請參閱{@link Component#dimension}。
*
* @param {數字|字符串} [數字]
* 您要設置的寬度後綴為“%”、“px”或什麼都不加。
*
* @param {boolean} [skipListeners]
* 跳過componentresize事件觸發
*
* @return {數字|字符串}
* 獲取時的寬度,沒有寬度則為零。可以是字符串
* 用 '%' 或 'px' 後綴。
*/
寬度(num,skipListeners){
返回 this.dimension('width', num, skipListeners);
}
/**
* 根據 CSS 樣式獲取或設置組件的高度。
* 有關更多詳細信息,請參閱{@link Component#dimension}。
*
* @param {數字|字符串} [數字]
* 您要設置的高度後綴為“%”、“px”或什麼都不加。
*
* @param {boolean} [skipListeners]
* 跳過componentresize事件觸發
*
* @return {數字|字符串}
* 獲取時的寬度,沒有寬度則為零。可以是字符串
* 用 '%' 或 'px' 後綴。
*/
高度(數量,skipListeners){
返回 this.dimension('height', num, skipListeners);
}
/**
* 同時設置 `Component` 元素的寬度和高度。
*
* @param {number|string} 寬度
* 設置 `Component` 元素的寬度。
*
* @param {number|string} 高度
* 將 `Component` 的元素設置為的高度。
*/
尺寸(寬度,高度){
// 跳過寬度上的 componentresize 監聽器以進行優化
這個。寬度(寬度,真);
這個。高度(高度);
}
/**
* 獲取或設置 `Component` 元素的寬度或高度。這是共享代碼
* 對於 {@link Component#width} 和 {@link Component#height}。
*
* 須知:
* - 如果數字中的寬度或高度,這將返回帶有“px”後綴的數字。
* - 如果寬度/高度是百分比,這將返回後綴為“%”的百分比
* - 隱藏元素的寬度為 0,帶有 `window.getComputedStyle`。這個功能
* 默認為 `Component` 的 `style.width` 並退回到 `window.getComputedStyle`。
* 請參見 [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
* 了解更多信息
* - 如果您想要組件的計算樣式,請使用 {@link Component#currentWidth}
* 和 {@link {Component#currentHeight}
*
* @fires 組件#componentresize
*
* @param {string} widthOrHeight
8 '寬度'或'高度'
*
* @param {數字|字符串} [數字]
8 新維度
*
* @param {boolean} [skipListeners]
* 跳過componentresize事件觸發
*
* @return {數字}
* 獲取時的維度,未設置時為0
*/
維度(widthOrHeight,num,skipListeners){
如果(數字!==未定義){
//設置為零,如果為空或字面上 NaN(NaN!== NaN)
如果(數字 === 空 || 數!== 數字){
數 = 0;
}
// 檢查是否使用 css 寬度/高度(% 或 px)並調整
如果(「+ 號)。指數('%')!== -1 || (」+ 數字). 索引 ('像素')!-1) {
this.el_.style[widthOrHeight] = num;
} else if (num === 'auto') {
this.el_.style[widthOrHeight] = '';
}其他{
this.el_.style[widthOrHeight] = num + 'px';
}
// skipListeners 允許我們在設置寬度和高度時避免觸發調整大小事件
如果(!skipListeners){
/**
* 調整組件大小時觸發。
*
* @event 組件#componentresize
* @type {EventTarget~Event}
*/
this.trigger('componentresize');
}
返回;
}
// 沒有設置值,所以獲取它
// 確保元素存在
如果(!this.el_){
返回 0;
}
// 從樣式中獲取維度值
const val = this.el_.style[widthOrHeight];
const pxIndex = val.indexOf('px');
如果(pX 指數!-1) {
// 返回沒有'px'的像素值
返回 parseInt(val.slice(0, pxIndex), 10);
}
// 沒有 px 所以使用 % 或沒有設置樣式,所以退回到 offsetWidth/height
//如果組件有顯示:無,偏移量將返回 0
// TODO: 使用 px 處理顯示:無和無尺寸樣式
返回 parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
}
/**
* 獲取組件元素的計算寬度或高度。
*
* 使用“window.getComputedStyle”。
*
* @param {string} widthOrHeight
* 包含“寬度”或“高度”的字符串。想要哪個就拿哪個。
*
* @return {數字}
* 被要求的維度,如果沒有設置則為 0
* 對於那個維度。
*/
當前尺寸(寬度或高度){
讓 computedWidthOrHeight = 0;
如果(寬度或高度!=='寬度'&&寬度或高度!=='高度'){
throw new Error('currentDimension 只接受寬度或高度值');
}
computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
// 從變量中移除 'px' 並解析為整數
computedWidthOrHeight = parseFloat(computedWidthOrHeight);
// 如果計算值仍然為 0,則可能是瀏覽器在撒謊
// 我們要檢查偏移值。
// 此代碼也會在 getComputedStyle 不存在的地方運行。
如果(計算寬度或高度=== 0 || isNaN(計算寬度或高度)){
const rule = `offset${toTitleCase(widthOrHeight)}`;
computedWidthOrHeight = this.el_[rule];
}
返回計算寬度或高度;
}
/**
* 包含 `Component` 的寬度和高度值的對象
* 計算風格。使用“window.getComputedStyle”。
*
* @typedef {Object} 組件~DimensionObject
*
* @property {number} 寬度
* `Component` 的計算樣式的寬度。
*
* @property {number} 高度
* `Component` 的計算樣式的高度。
*/
/**
* 獲取包含計算出的寬度和高度值的對象
* 組件的元素。
*
* 使用“window.getComputedStyle”。
*
* @return {組件~維度對象}
* 組件元素的計算尺寸。
*/
當前尺寸(){
返回 {
寬度:this.currentDimension('width'),
高度:this.currentDimension('height')
};
}
/**
* 獲取組件元素的計算寬度。
*
* 使用“window.getComputedStyle”。
*
* @return {數字}
* 組件元素的計算寬度。
*/
當前寬度(){
返回 this.currentDimension('width');
}
/**
* 獲取組件元素的計算高度。
*
* 使用“window.getComputedStyle”。
*
* @return {數字}
* 組件元素的計算高度。
*/
當前高度(){
返回 this.currentDimension('height');
}
/**
* 將焦點設置到該組件
*/
重點() {
這個.el_.focus();
}
/**
* 從此組件中移除焦點
*/
模糊(){
這個.el_.blur();
}
/**
* 當此組件收到它不處理的 `keydown` 事件時,
* 它將事件傳遞給 Player 進行處理。
*
* @param {EventTarget~Event} 事件
* 導致調用此函數的 `keydown` 事件。
*/
handleKeyDown(事件){
如果(this.player_){
// 我們只是在這裡停止傳播,因為我們希望未處理的事件落下
// 返回瀏覽器。排除選項卡以進行焦點捕獲。
如果(!密鑰代碼. 一鍵 (事件, '標籤')) {
事件.stopPropagation();
}
this.player_.handleKeyDown(事件);
}
}
/**
* 許多組件曾經有一個 `handleKeyPress` 方法,這是很糟糕的
* 之所以命名是因為它監聽了 `keydown` 事件。現在這個方法名
* 委託給 `handleKeyDown`。這意味著任何調用“handleKeyPress”的人
* 不會看到他們的方法調用停止工作。
*
* @param {EventTarget~Event} 事件
* 導致調用此函數的事件。
*/
handleKeyPress(事件){
這個.handleKeyDown(事件);
}
/**
* 當檢測到觸摸事件支持時發出“點擊”事件。這習慣了
* 支持通過點擊視頻來切換控件。他們得到啟用
* 因為否則每個子組件都會有額外的開銷。
*
* @私人的
* @fires 組件#tap
* @listens 組件#touchstart
* @listens 組件#touchmove
* @listens 組件#touchleave
* @listens 組件#touchcancel
* @listens 組件#touchend
*/
emitTapEvents() {
// 跟踪開始時間,以便我們可以確定觸摸持續了多長時間
讓 touchStart = 0;
讓 firstTouch = null;
// 觸摸事件期間允許的最大移動仍被視為點擊
// 其他流行的庫使用從 2 (hammer.js) 到 15 的任何地方,
// 所以 10 看起來是一個不錯的整數。
const tapMovementThreshold = 10;
// 在仍被視為點擊的情況下觸摸的最大長度
const touchTimeThreshold = 200;
讓 couldBeTap;
this.on('touchstart', 函數(事件) {
// 如果多於一根手指,不要考慮將其視為點擊
如果 (event.touches.length === 1) {
// 從對像中複製 pageX/pageY
第一次接觸 = {
pageX: event.touches[0].pageX,
pageY: event.touches[0].pageY
};
// 記錄開始時間,以便我們可以檢測到點擊與“觸摸並按住”
touchStart = window.performance.now();
// 重置 couldBeTap 跟踪
couldBeTap = 真;
}
});
this.on('touchmove', 函數(事件) {
// 如果多於一根手指,不要考慮將其視為點擊
如果 (event.touches.length > 1) {
couldBeTap = 假;
} else if (firstTouch) {
// 除了最輕微的點擊,有些設備會拋出觸摸動作。
// 所以,如果我們只移動了一小段距離,這仍然可以是一個水龍頭
const xdiff = event.touches[0].pageX - firstTouch.pageX;
const ydiff = event.touches[0].pageY - firstTouch.pageY;
const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
如果(touchDistance > tapMovementThreshold){
couldBeTap = 假;
}
}
});
const noTap = function() {
couldBeTap = 假;
};
// 去做:聽原來的目標。 http://youtu.be/DujfpXOKUp8?t=13m8s
this.on('touchleave', noTap);
this.on('touchcancel', noTap);
// 當觸摸結束時,測量花費了多長時間並觸發適當的
// 事件
this.on('touchend', function(event) {
firstTouch = null;
// 只有當 touchmove/leave/cancel 事件沒有發生時才繼續
如果(couldBeTap === 真){
// 測量觸摸持續了多長時間
const touchTime = window.performance.now() - touchStart;
// 確保觸摸小於被視為點擊的閾值
如果(觸摸時間<觸摸時間閾值){
// 不要讓瀏覽器把它變成點擊
事件.preventDefault();
/**
* 當一個 `Component` 被點擊時觸發。
*
* @event 組件#tap
* @type {EventTarget~Event}
*/
this.trigger('tap');
// 複製 touchend 事件對象並更改
// 如果其他事件屬性不准確,則鍵入要點擊的內容
// Events.fixEvent 運行(例如 event.target)
}
}
});
}
/**
* 此函數會在觸摸事件發生時報告用戶活動。這可以得到
* 被任何希望觸摸事件以另一種方式起作用的子組件關閉。
*
* 發生觸摸事件時報告用戶觸摸活動。用戶活動習慣
* 確定控件何時顯示/隱藏。說到鼠標就簡單了
* 事件,因為任何鼠標事件都應該顯示控件。所以我們捕獲鼠標
* 向玩家冒泡並在發生時報告活動的事件。
* 對於觸摸事件,它不像 `touchstart` 和 `touchend` 切換播放器那麼容易
* 控制。所以觸摸事件也不能在玩家層面幫助我們。
*
* 異步檢查用戶活動。所以可能發生的是點擊事件
* 在視頻中關閉控件。然後 `touchend` 事件冒泡到
* 播放器。其中,如果它報告用戶活動,將把控件右
* 重新開始。我們也不希望完全阻止觸摸事件冒泡。
* 此外,“touchmove”事件和點擊以外的任何事件都不應轉動
* 控件重新打開。
*
* @listens 組件#touchstart
* @listens 組件#touchmove
* @listens 組件#touchend
* @listens 組件#touchcancel
*/
enableTouchActivity() {
// 如果根播放器不支持報告用戶活動,則不要繼續
如果(!這個。播放器()||!這個. 播放器 (). 報告用戶活動) {
返回;
}
// 用於報告用戶處於活動狀態的偵聽器
const report = Fn.bind(this.player(), this.player().reportUserActivity);
讓觸摸保持;
this.on('touchstart', function() {
報告();
// 只要他們正在觸摸設備或按下鼠標,
// 即使他們沒有移動手指或鼠標,我們也認為他們是活躍的。
// 所以我們想繼續更新他們是活躍的
this.clearInterval(touchHolding);
// 以與 activityCheck 相同的時間間隔報告
touchHolding = this.setInterval(報告, 250);
});
const touchEnd = 函數(事件){
報告();
// 如果觸摸保持,則停止保持活動的間隔
this.clearInterval(touchHolding);
};
this.on('touchmove', 報告);
this.on('touchend', touchEnd);
this.on('touchcancel', touchEnd);
}
/**
* 沒有參數並綁定到“Component”的上下文中的回調。
*
* @callback 組件~GenericCallback
* @this 組件
*/
/**
* 創建一個在 `x` 毫秒超時後運行的函數。這個函數是一個
* `window.setTimeout` 的包裝器。使用這個有幾個原因
* 相反:
* 1。它通過 {@link Component#clearTimeout} 清除
* {@link Component#dispose} 被調用。
* 2。函數回調將變成 {@link Component~GenericCallback}
*
* > 注意:您不能在此函數返回的 id 上使用 `window.clearTimeout`。這個
* 將導致它的處置偵聽器不被清理!請用
* {@link Component#clearTimeout} 或 {@link Component#dispose} 代替。
*
* @param {Component~GenericCallback} fn
* 將在 `timeout` 後運行的函數。
*
* @param {number} 超時
* 在執行指定函數之前延遲的超時時間(以毫秒為單位)。
*
* @return {數字}
* 返回用於標識超時的超時 ID。它還可以
* 在 {@link Component#clearTimeout} 中使用以清除超時
* 已設置。
*
* @listens 組件#dispose
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
*/
設置超時(fn,超時){
// 聲明為變量,以便它們在超時函數中正確可用
// eslint-disable-next-line
var timeoutId,disposeFn;
fn = Fn.bind(this, fn);
this.clearTimersOnDispose_();
timeoutId = window.setTimeout(() => {
如果(this.setTimeoutIds_.has(timeoutId)){
this.setTimeoutIds_.delete(timeoutId);
}
fn();
}, 暫停);
this.setTimeoutIds_.add(timeoutId);
返回超時標識;
}
/**
* 清除通過“window.setTimeout”或創建的超時
* {@link 組件#setTimeout}。如果您通過 {@link Component#setTimeout} 設置超時
* 使用此函數代替 `window.clearTimout`。如果你不處理
* 直到 {@link Component#dispose} 才會清理監聽器!
*
* @param {number} timeoutId
* 要清除的超時 ID。的返回值
* {@link Component#setTimeout} 或 `window.setTimeout`。
*
* @return {數字}
* 返回已清除的超時 ID。
*
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
*/
clearTimeout(timeoutId) {
如果(this.setTimeoutIds_.has(timeoutId)){
this.setTimeoutIds_.delete(timeoutId);
window.clearTimeout(timeoutId);
}
返回超時標識;
}
/**
* 創建一個每“x”毫秒運行一次的函數。這個函數是一個包裝器
* 圍繞 `window.setInterval`。不過,有幾個理由改用這個。
* 1。它通過 {@link Component#clearInterval} 清除,當
* {@link Component#dispose} 被調用。
* 2。函數回調將是一個 {@link Component~GenericCallback}
*
* @param {Component~GenericCallback} fn
* 每 `x` 秒運行一次的函數。
*
* @param {number} 間隔
* 每 `x` 毫秒執行指定的函數。
*
* @return {數字}
* 返回可用於標識間隔的 id。它也可以用於
* {@link Component#clearInterval} 清除間隔。
*
* @listens 組件#dispose
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
*/
設置間隔(fn,間隔){
fn = Fn.bind(this, fn);
this.clearTimersOnDispose_();
const intervalId = window.setInterval(fn, interval);
this.setIntervalIds_.add(intervalId);
返回間隔ID;
}
/**
* 清除通過“window.setInterval”或創建的間隔
* {@link 組件#setInterval}。如果您通過 {@link Component#setInterval} 設置間隔
* 使用此函數代替 `window.clearInterval`。如果你不處理
* 直到 {@link Component#dispose} 才會清理監聽器!
*
* @param {number} intervalId
* 要清除的間隔的 id。的返回值
* {@link Component#setInterval} 或 `window.setInterval`。
*
* @return {數字}
* 返回被清除的間隔 id。
*
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
*/
clearInterval(intervalId) {
如果(this.setIntervalIds_.has(intervalId)){
this.setIntervalIds_.delete(intervalId);
window.clearInterval(intervalId);
}
返回間隔ID;
}
/**
* 排隊要傳遞給 requestAnimationFrame (rAF) 的回調,但是
* 有一些額外的獎勵:
*
* - 通過回退到不支持 rAF 的瀏覽器來支持
* {@link 組件#setTimeout}。
*
* - 回調變成了 {@link Component~GenericCallback}(即
* 綁定到組件)。
*
* - 如果組件處理自動取消 rAF 回調
* 在被調用之前被釋放。
*
* @param {Component~GenericCallback} fn
* 將綁定到此組件並僅執行的函數
* 在瀏覽器的下一次重繪之前。
*
* @return {數字}
* 返回用於識別超時的 rAF ID。它可以
* 也可以用在 {@link Component#cancelAnimationFrame} 中取消
* 動畫幀回調。
*
* @listens 組件#dispose
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
*/
requestAnimationFrame(fn) {
// 退回到使用定時器。
如果(!this.supportsRaf_){
返回 this.setTimeout(fn, 1000 / 60);
}
this.clearTimersOnDispose_();
// 聲明為變量,以便它們在 rAF 函數中正確可用
// eslint-disable-next-line
變量標識;
fn = Fn.bind(this, fn);
id = window.requestAnimationFrame(() => {
如果(this.rafIds_.has(id)){
this.rafIds_.delete(id);
}
fn();
});
this.rafIds_.add(id);
返回ID;
}
/**
* 請求一個動畫幀,但只有一個命名動畫
* 框架將排隊。另一個永遠不會添加,直到
* 前一個結束。
*
* @param {string} 名稱
* 給這個requestAnimationFrame的名字
*
* @param {Component~GenericCallback} fn
* 將綁定到此組件並僅執行的函數
* 在瀏覽器的下一次重繪之前。
*/
requestNamedAnimationFrame(名稱, fn) {
如果(this.namedRafs_.has(名稱)){
返回;
}
this.clearTimersOnDispose_();
fn = Fn.bind(this, fn);
const id = this.requestAnimationFrame(() => {
fn();
如果(this.namedRafs_.has(名稱)){
this.namedRafs_.delete(名字);
}
});
this.namedRafs_.set(name, id);
返回名稱;
}
/**
* 取消當前命名的動畫幀(如果存在)。
*
* @param {string} 名稱
* 要取消的 requestAnimationFrame 的名稱。
*/
cancelNamedAnimationFrame(名稱){
如果(!this.namedRafs_.has(名稱)){
返回;
}
this.cancelAnimationFrame(this.namedRafs_.get(name));
this.namedRafs_.delete(名字);
}
/**
* 取消傳遞給 {@link Component#requestAnimationFrame} 的排隊回調
* (rAF)。
*
* 如果您通過 {@link Component#requestAnimationFrame} 對 rAF 回調進行排隊,
* 使用此函數代替 `window.cancelAnimationFrame`。如果你不這樣做,
* 在 {@link Component#dispose} 之前,你的 dispose 偵聽器不會被清理!
*
* @param {number} id
* 要清除的 rAF ID。{@link Component#requestAnimationFrame} 的返回值。
*
* @return {數字}
* 返回被清除的 rAF ID。
*
* @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
*/
取消動畫框架(id){
// 退回到使用定時器。
如果(!this.supportsRaf_){
返回 this.clearTimeout(id);
}
如果(this.rafIds_.has(id)){
this.rafIds_.delete(id);
window.cancelAnimationFrame(id);
}
返回ID;
}
/**
* 設置`requestAnimationFrame`、`setTimeout`、
* 和 `setInterval`,清除處置。
*
* > 以前每個計時器都自己添加和刪除處理偵聽器。
* 為了更好的性能,決定對它們進行批處理,並使用 `Set`s
* 跟踪未完成的計時器 ID。
*
* @私人的
*/
clearTimersOnDispose_() {
如果(this.clearingTimersOnDispose_){
返回;
}
this.clearingTimersOnDispose_ = true;
this.one('處置', () => {
[
['namedRafs_', 'cancelNamedAnimationFrame'],
['rafIds_', 'cancelAnimationFrame'],
['setTimeoutIds_', 'clearTimeout'],
['setIntervalIds_', 'clearInterval']
].forEach(([idName, cancelName]) => {
// 對於 `Set` 鍵,實際上又是值
// 所以 forEach((val, val) =>` 但是對於我們想要使用的地圖
// 鑰匙。
this[idName].forEach((val, key) => this[cancelName](key));
});
this.clearingTimersOnDispose_ = false;
});
}
/**
* 給定名稱和組件,用 `videojs` 註冊一個 `Component`。
*
* > 注意:{@link Tech} 不應註冊為“組件”。 {@link 技術}
* 應使用 {@link Tech.registerTech} 或
* {@link 視頻:視頻. 註冊技術}.
*
* > 注意:這個函數也可以在videojs上看到為
* {@link 視頻:視頻. 註冊組件}。
*
* @param {string} 名稱
* 要註冊的 `Component` 的名稱。
*
* @param {Component} ComponentToRegister
* 要註冊的 `Component` 類。
*
* @return {組件}
* 已註冊的 `Component`。
*/
靜態註冊組件(名稱,ComponentToRegister){
如果(名稱類型!=='字符串'||!名稱){
throw new Error(`非法組件名稱,“${name}”;必須是非空字符串。`);
}
const Tech = Component.getComponent('Tech');
// 我們需要確保僅在 Tech 已註冊時才進行此檢查。
const isTech = Tech && Tech.isTech(ComponentToRegister);
const isComp = Component === ComponentToRegister ||
Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
如果(isTech || !isComp){
讓理由;
如果(是科技){
reason = '技術人員必須使用 Tech.registerTech() 進行註冊';
}其他{
reason = '必須是 Component 的子類';
}
throw new Error(`非法組件, "${name}"; ${reason}.`);
}
名稱 = toTitleCase(名稱);
如果(!Component.components_){
組件.components_ = {};
}
const Player = Component.getComponent('Player');
if (name === 'Player' && Player && Player.players) {
const players = Player.players;
const playerNames = Object.keys(玩家);
// 如果我們有被處理掉的玩家,那麼他們的名字仍然是
// 在 Players.players.所以,我們必須遍歷並驗證該值
// 每個項目都不為空。這允許註冊播放器組件
// 在處理完所有玩家之後或在創建任何玩家之前。
如果(玩家&&
playerNames.length > 0 &&
playerNames.map((pname) => players[pname]).every(Boolean)) {
throw new Error('創建播放器後無法註冊播放器組件。');
}
}
Component.components_[name] = ComponentToRegister;
Component.components_[toLowerCase(name)] = ComponentToRegister;
返回組件註冊;
}
/**
* 根據註冊的名稱獲取一個 `Component`。
*
* @param {string} 名稱
* 要獲取的組件的名稱。
*
* @return {組件}
* 以給定名稱註冊的 `Component`。
*/
靜態獲取組件(名稱){
如果(!名稱||!Component.components_){
返回;
}
返回 Component.components_[name];
}
}
/**
* 該組件是否支持`requestAnimationFrame`。
*
* 這主要是為了測試目的而公開的。
*
* @私人的
* @type {布爾}
*/
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === '函數' &&
typeof window.cancelAnimationFrame === '函數';
Component.registerComponent('組件', 組件);
導出默認組件;