diff --git a/.babelrc b/.babelrc index 80ff6f824..46bd38bd6 100644 --- a/.babelrc +++ b/.babelrc @@ -7,5 +7,5 @@ } ] ], - "plugins": ["@babel/external-helpers", "@babel/transform-runtime"] + "plugins": ["@babel/plugin-external-helpers", "@babel/plugin-transform-runtime"] } diff --git a/.eslintrc b/.eslintrc index cc575c10f..6524fb017 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,6 +9,9 @@ "impliedStrict": true } }, + "globals": { + "expect": true + }, "env": { "es6": true, "amd": true, diff --git a/__test__/index.test.js b/__test__/index.test.js new file mode 100644 index 000000000..6b7f899af --- /dev/null +++ b/__test__/index.test.js @@ -0,0 +1,8 @@ +import Artplayer from '../index'; + +test('test', () => { + document.body.innerHTML = '
'; + const app = new Artplayer({ + container: '.artplayer-app' + }); +}); diff --git a/dist/artplayer.js b/dist/artplayer.js index a296d282b..be4262941 100644 --- a/dist/artplayer.js +++ b/dist/artplayer.js @@ -5,4 +5,4 @@ * Released under the MIT License. */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.artplayer={})}(this,function(t){"use strict";var i="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function e(t,e){return t(e={exports:{}},e.exports),e.exports}var r=e(function(n){function i(t,e){return n.exports=i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},i(t,e)}n.exports=i}),o=e(function(i){function o(t,e,n){return!function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}()?i.exports=o=function(t,e,n){var i=[null];i.push.apply(i,e);var o=new(Function.bind.apply(t,i));return n&&r(o,n.prototype),o}:i.exports=o=Reflect.construct,o.apply(null,arguments)}i.exports=o});var n=function(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e".concat(t,"
")}).join("")}i.art.emit("subtitle:update",a)})}})}},{key:"load",value:function(e){var n,i=this,o=this.art.notice;return fetch(e).then(function(t){return n=t.headers.get("Content-Type"),t.text()}).then(function(t){return/x-subrip/gi.test(n)?i.srtToVtt(t):e}).catch(function(t){throw o.show(t),t})}},{key:"srtToVtt",value:function(t){var e="WEBVTT \r\n\r\n".concat(t.replace(/\{\\([ibu])\}/g,"$1>").replace(/\{\\([ibu])1\}/g,"<$1>").replace(/\{([ibu])\}/g,"<$1>").replace(/\{\/([ibu])\}/g,"$1>").replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g,"$1.$2").concat("\r\n\r\n"));return URL.createObjectURL(new Blob([e],{type:"text/vtt"}))}},{key:"checkExt",value:function(t){var e=function t(e){return e.includes("?")?t(e.split("?")[0]):e.includes("#")?t(e.split("#")[0]):e.trim().toLowerCase().split(".").pop()}(t);w("vtt"===e||"srt"===e,"'subtitle.url' option require 'vtt' or 'srt' format, but got '".concat(e,"'."))}},{key:"show",value:function(){var t=this.art,e=t.refs.$subtitle,n=t.i18n,i=t.notice;Pt(e,"display","block"),this.isOpen=!0,i.show(n.get("Show subtitle")),this.art.emit("subtitle:show",e)}},{key:"hide",value:function(){var t=this.art,e=t.refs.$subtitle,n=t.i18n,i=t.notice;Pt(e,"display","none"),this.isOpen=!1,i.show(n.get("Hide subtitle")),this.art.emit("subtitle:hide",e)}},{key:"switch",value:function(e){var n=this,i=1".concat(t,"
")}).join("")}i.art.emit("subtitle:update",a)})}})}},{key:"load",value:function(t){var e,n=this,i=this.art.notice;return fetch(t).then(function(t){return e=t.headers.get("Content-Type"),t.text()}).then(function(t){return/x-subrip/gi.test(e)?n.vttText=n.srtToVtt(t):n.vttText=t,n.vttToBlob(n.vttText)}).catch(function(t){throw i.show(t),console.warn(t),t})}},{key:"srtToVtt",value:function(t){return"WEBVTT \r\n\r\n".concat(t.replace(/\{\\([ibu])\}/g,"$1>").replace(/\{\\([ibu])1\}/g,"<$1>").replace(/\{([ibu])\}/g,"<$1>").replace(/\{\/([ibu])\}/g,"$1>").replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g,"$1.$2").concat("\r\n\r\n"))}},{key:"vttToBlob",value:function(t){return URL.createObjectURL(new Blob([t],{type:"text/vtt"}))}},{key:"checkExt",value:function(t){var e=function t(e){return e.includes("?")?t(e.split("?")[0]):e.includes("#")?t(e.split("#")[0]):e.trim().toLowerCase().split(".").pop()}(t);w("vtt"===e||"srt"===e,"'subtitle.url' option require 'vtt' or 'srt' format, but got '".concat(e,"'."))}},{key:"show",value:function(){var t=this.art,e=t.refs.$subtitle,n=t.i18n,i=t.notice;Tt(e,"display","block"),this.isOpen=!0,i.show(n.get("Show subtitle")),this.art.emit("subtitle:show",e)}},{key:"hide",value:function(){var t=this.art,e=t.refs.$subtitle,n=t.i18n,i=t.notice;Tt(e,"display","none"),this.isOpen=!1,i.show(n.get("Hide subtitle")),this.art.emit("subtitle:hide",e)}},{key:"switch",value:function(e){var n=this,i=1${item}
`)\n .join('');\n }\n this.art.emit('subtitle:update', $subtitle);\n });\n }\n });\n }\n\n load(url) {\n const { notice } = this.art;\n let type;\n return fetch(url)\n .then(response => {\n type = response.headers.get('Content-Type');\n return response.text();\n })\n .then(text => {\n if ((/x-subrip/gi).test(type)) {\n this.vttText = this.srtToVtt(text);\n } else {\n this.vttText = text;\n }\n return this.vttToBlob(this.vttText);\n })\n .catch(err => {\n notice.show(err);\n console.warn(err);\n throw err;\n });\n }\n\n srtToVtt(text) {\n return 'WEBVTT \\r\\n\\r\\n'.concat(\n text\n .replace(/\\{\\\\([ibu])\\}/g, '$1>')\n .replace(/\\{\\\\([ibu])1\\}/g, '<$1>')\n .replace(/\\{([ibu])\\}/g, '<$1>')\n .replace(/\\{\\/([ibu])\\}/g, '$1>')\n .replace(/(\\d\\d:\\d\\d:\\d\\d),(\\d\\d\\d)/g, '$1.$2')\n .concat('\\r\\n\\r\\n')\n );\n }\n\n vttToBlob(vttText) {\n return URL.createObjectURL(\n new Blob([vttText], {\n type: 'text/vtt'\n })\n );\n }\n\n checkExt(url) {\n const ext = getExt(url);\n errorHandle(\n ext === 'vtt' || ext === 'srt',\n `'subtitle.url' option require 'vtt' or 'srt' format, but got '${ext}'.`\n );\n }\n\n show() {\n const { refs: { $subtitle }, i18n, notice } = this.art;\n setStyle($subtitle, 'display', 'block');\n this.isOpen = true;\n notice.show(i18n.get('Show subtitle'));\n this.art.emit('subtitle:show', $subtitle);\n }\n\n hide() {\n const { refs: { $subtitle }, i18n, notice } = this.art;\n setStyle($subtitle, 'display', 'none');\n this.isOpen = false;\n notice.show(i18n.get('Hide subtitle'));\n this.art.emit('subtitle:hide', $subtitle);\n }\n\n switch(url, name = 'unknown') {\n const { $track, i18n, notice } = this.art.refs;\n this.checkExt(url);\n errorHandle($track, 'You need to initialize the subtitle option first.');\n this.load(url).then(data => {\n if (url !== data) {\n $track.src = data;\n notice.show(`${i18n.get('Switch subtitle')}: ${name}`);\n this.art.emit('subtitle:switch', url);\n }\n });\n }\n}\n","import { debounce } from './utils';\n\nexport default class Events {\n constructor(art) {\n this.art = art;\n this.destroyEvents = [];\n this.proxy = this.proxy.bind(this);\n this.hover = this.hover.bind(this);\n this.init();\n }\n\n init() {\n const { refs: { $player } } = this.art;\n this.hover($player, () => {\n $player.classList.add('artplayer-hover');\n this.art.emit('hoverenter');\n }, () => {\n $player.classList.remove('artplayer-hover');\n $player.classList.remove('artplayer-hide-cursor');\n this.art.emit('hoverleave');\n });\n\n const hideCursor = debounce(() => {\n $player.classList.add('artplayer-hide-cursor');\n if (this.art.player.fullscreenState || this.art.player.fullscreenWebState) {\n $player.classList.remove('artplayer-hover');\n this.art.controls.hide();\n }\n }, 5000);\n this.proxy($player, 'mousemove', () => {\n $player.classList.remove('artplayer-hide-cursor');\n this.art.controls.show();\n if (!this.art.player.pipState) {\n hideCursor();\n }\n });\n }\n\n proxy(target, name, callback, option = {}) {\n if (Array.isArray(name)) {\n name.forEach(item => this.proxy(target, item, callback, option));\n return;\n }\n\n target.addEventListener(name, callback, option);\n this.destroyEvents.push(() => {\n target.removeEventListener(name, callback, option);\n });\n }\n\n hover(target, mouseenter, mouseleave) {\n this.proxy(target, 'mouseenter', mouseenter);\n this.proxy(target, 'mouseleave', mouseleave);\n }\n\n destroy() {\n this.destroyEvents.forEach(event => event());\n }\n}\n","export default class Hotkey {\n constructor(art) {\n this.art = art;\n if (this.art.option.hotkey) {\n this.init();\n }\n }\n\n init() {\n const { player, refs: { $player }, events: { proxy } } = this.art;\n\n proxy(document, ['click', 'contextmenu'], event => {\n this.art.isFocus = event.path.indexOf($player) > -1;\n });\n\n proxy(window, 'keydown', event => {\n if (this.art.isFocus) {\n const tag = document.activeElement.tagName.toUpperCase();\n const editable = document.activeElement.getAttribute('contenteditable');\n if (\n tag !== 'INPUT' &&\n tag !== 'TEXTAREA' &&\n editable !== '' &&\n editable !== 'true'\n ) {\n switch (event.keyCode) {\n case 39:\n event.preventDefault();\n player.seek(player.currentTime() + 10);\n break;\n case 37:\n event.preventDefault();\n player.seek(player.currentTime() - 10);\n break;\n case 38:\n event.preventDefault();\n player.volume(player.volume() + 0.05);\n break;\n case 40:\n event.preventDefault();\n player.volume(player.volume() - 0.05);\n break;\n case 32:\n event.preventDefault();\n player.toggle();\n break;\n default:\n break;\n }\n }\n }\n });\n }\n}\n","import { append, setStyles, setStyle } from './utils';\n\nlet id = 0;\nexport default class Layers {\n constructor(art) {\n this.art = art;\n this.add = this.add.bind(this);\n this.art.option.layers.filter(item => !item.disable).forEach(this.add);\n }\n\n add(option, callback) {\n const { refs: { $layers } } = this.art;\n id++;\n const $layer = document.createElement('div');\n $layer.setAttribute('data-art-layer-id', id);\n $layer.setAttribute('class', `art-layer art-layer-${option.name || id}`);\n setStyle($layer, 'z-index', option.index || id);\n append($layer, option.html);\n setStyles($layer, option.style || {});\n $layers.appendChild($layer);\n this.art.emit('layers:add', $layer);\n callback && callback($layer);\n }\n\n show() {\n const { $layers } = this.art.refs;\n setStyle($layers, 'display', 'block');\n this.art.emit('layers:show', $layers);\n }\n\n hide() {\n const { $layers } = this.art.refs;\n setStyle($layers, 'display', 'none');\n this.art.emit('layers:hide', $layers);\n }\n}\n","import icons from './icons';\nimport { append, setStyle } from './utils';\n\nexport default class Loading {\n constructor(art) {\n this.art = art;\n const { option, refs: { $loading } } = art;\n append($loading, option.loading || icons.loading);\n }\n\n hide() {\n const { $loading } = this.art.refs;\n setStyle($loading, 'display', 'none');\n this.art.emit('loading:hide', $loading);\n }\n\n show() {\n const { $loading } = this.art.refs;\n setStyle($loading, 'display', 'flex');\n this.art.emit('loading:show', $loading);\n }\n}\n","import { setStyle } from './utils';\n\nexport default class Notice {\n constructor(art) {\n this.art = art;\n this.timer = null;\n }\n\n show(msg, autoHide = true, time = 1000) {\n const { $notice, $noticeInner } = this.art.refs;\n setStyle($notice, 'display', 'block');\n $noticeInner.innerHTML = msg instanceof Error ? msg.message.trim() : msg;\n if (autoHide) {\n clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.hide();\n }, time);\n }\n this.art.emit('notice:show', $notice);\n }\n\n hide() {\n const { $notice } = this.art.refs;\n setStyle($notice, 'display', 'none');\n this.art.emit('notice:hide', $notice);\n }\n}\n","import icons from './icons';\nimport { append, setStyle } from './utils';\n\nexport default class Mask {\n constructor(art) {\n this.art = art;\n const { option, refs: { $mask } } = art;\n append($mask, option.play || icons.playBig);\n }\n\n show() {\n const { $mask } = this.art.refs;\n setStyle($mask, 'display', 'flex');\n this.art.emit('mask:show', $mask);\n }\n\n hide() {\n const { $mask } = this.art.refs;\n setStyle($mask, 'display', 'none');\n this.art.emit('mask:show', $mask);\n }\n}\n","import { append } from '../utils';\n\nexport default class Common {\n constructor(option) {\n this.option = option;\n }\n\n apply(art, $setting) {\n this.art = art;\n this.$header = $setting.querySelector('.art-setting-header');\n this.$body = $setting.querySelector('.art-setting-body');\n append(this.$body, '———— 先占坑,暂无设置 ————');\n }\n}\n","import { setStyle, append } from '../utils';\nimport Common from './common';\n\nlet id = 0;\nexport default class Setting {\n constructor(art) {\n this.art = art;\n this.isOpen = false;\n this.$settings = [];\n if (art.option.setting) {\n this.init();\n this.mount();\n }\n }\n\n init() {\n const { refs: { $settingClose }, events: { proxy } } = this.art;\n\n proxy($settingClose, 'click', () => {\n this.hide();\n });\n\n this.add(new Common({\n name: 'common',\n title: 'Common',\n disable: false,\n index: 10\n }));\n }\n\n add(setting) {\n const { option } = setting;\n if (option && !option.disable) {\n id++;\n const name = option.name || setting.constructor.name.toLowerCase() || `setting${id}`;\n const title = option.title || name;\n const $setting = document.createElement('div');\n $setting.setAttribute('class', `art-setting art-setting-${name}`);\n $setting.dataset.settingIndex = option.index || id;\n append($setting, `${item}
`)\n .join('');\n }\n this.art.emit('subtitle:update', $subtitle);\n });\n }\n });\n }\n\n load(url) {\n const { notice } = this.art;\n let type;\n return fetch(url)\n .then(response => {\n type = response.headers.get('Content-Type');\n return response.text();\n })\n .then(text => {\n if ((/x-subrip/gi).test(type)) {\n this.vttText = this.srtToVtt(text);\n } else {\n this.vttText = text;\n }\n return this.vttToBlob(this.vttText);\n })\n .catch(err => {\n notice.show(err);\n console.warn(err);\n throw err;\n });\n }\n\n srtToVtt(text) {\n return 'WEBVTT \\r\\n\\r\\n'.concat(\n text\n .replace(/\\{\\\\([ibu])\\}/g, '$1>')\n .replace(/\\{\\\\([ibu])1\\}/g, '<$1>')\n .replace(/\\{([ibu])\\}/g, '<$1>')\n .replace(/\\{\\/([ibu])\\}/g, '$1>')\n .replace(/(\\d\\d:\\d\\d:\\d\\d),(\\d\\d\\d)/g, '$1.$2')\n .concat('\\r\\n\\r\\n')\n );\n }\n\n vttToBlob(vttText) {\n return URL.createObjectURL(\n new Blob([vttText], {\n type: 'text/vtt'\n })\n );\n }\n\n checkExt(url) {\n const ext = getExt(url);\n errorHandle(\n ext === 'vtt' || ext === 'srt',\n `'subtitle.url' option require 'vtt' or 'srt' format, but got '${ext}'.`\n );\n }\n\n show() {\n const { refs: { $subtitle }, i18n, notice } = this.art;\n setStyle($subtitle, 'display', 'block');\n this.isOpen = true;\n notice.show(i18n.get('Show subtitle'));\n this.art.emit('subtitle:show', $subtitle);\n }\n\n hide() {\n const { refs: { $subtitle }, i18n, notice } = this.art;\n setStyle($subtitle, 'display', 'none');\n this.isOpen = false;\n notice.show(i18n.get('Hide subtitle'));\n this.art.emit('subtitle:hide', $subtitle);\n }\n\n switch(url, name = 'unknown') {\n const { $track, i18n, notice } = this.art.refs;\n this.checkExt(url);\n errorHandle($track, 'You need to initialize the subtitle option first.');\n this.load(url).then(data => {\n if (url !== data) {\n $track.src = data;\n notice.show(`${i18n.get('Switch subtitle')}: ${name}`);\n this.art.emit('subtitle:switch', url);\n }\n });\n }\n}\n","import { debounce } from './utils';\n\nexport default class Events {\n constructor(art) {\n this.art = art;\n this.destroyEvents = [];\n this.proxy = this.proxy.bind(this);\n this.hover = this.hover.bind(this);\n this.init();\n }\n\n init() {\n const { refs: { $player } } = this.art;\n this.hover($player, () => {\n $player.classList.add('artplayer-hover');\n this.art.emit('hoverenter');\n }, () => {\n $player.classList.remove('artplayer-hover');\n $player.classList.remove('artplayer-hide-cursor');\n this.art.emit('hoverleave');\n });\n\n const hideCursor = debounce(() => {\n $player.classList.add('artplayer-hide-cursor');\n if (this.art.player.fullscreenState || this.art.player.fullscreenWebState) {\n $player.classList.remove('artplayer-hover');\n this.art.controls.hide();\n }\n }, 5000);\n this.proxy($player, 'mousemove', () => {\n $player.classList.remove('artplayer-hide-cursor');\n this.art.controls.show();\n if (!this.art.player.pipState) {\n hideCursor();\n }\n });\n }\n\n proxy(target, name, callback, option = {}) {\n if (Array.isArray(name)) {\n name.forEach(item => this.proxy(target, item, callback, option));\n return;\n }\n\n target.addEventListener(name, callback, option);\n this.destroyEvents.push(() => {\n target.removeEventListener(name, callback, option);\n });\n }\n\n hover(target, mouseenter, mouseleave) {\n this.proxy(target, 'mouseenter', mouseenter);\n this.proxy(target, 'mouseleave', mouseleave);\n }\n\n destroy() {\n this.destroyEvents.forEach(event => event());\n }\n}\n","export default class Hotkey {\n constructor(art) {\n this.art = art;\n if (this.art.option.hotkey) {\n this.init();\n }\n }\n\n init() {\n const { player, refs: { $player }, events: { proxy } } = this.art;\n\n proxy(document, ['click', 'contextmenu'], event => {\n this.art.isFocus = event.path.indexOf($player) > -1;\n });\n\n proxy(window, 'keydown', event => {\n if (this.art.isFocus) {\n const tag = document.activeElement.tagName.toUpperCase();\n const editable = document.activeElement.getAttribute('contenteditable');\n if (\n tag !== 'INPUT' &&\n tag !== 'TEXTAREA' &&\n editable !== '' &&\n editable !== 'true'\n ) {\n switch (event.keyCode) {\n case 39:\n event.preventDefault();\n player.seek(player.currentTime() + 10);\n break;\n case 37:\n event.preventDefault();\n player.seek(player.currentTime() - 10);\n break;\n case 38:\n event.preventDefault();\n player.volume(player.volume() + 0.05);\n break;\n case 40:\n event.preventDefault();\n player.volume(player.volume() - 0.05);\n break;\n case 32:\n event.preventDefault();\n player.toggle();\n break;\n default:\n break;\n }\n }\n }\n });\n }\n}\n","import { append, setStyles, setStyle } from './utils';\n\nlet id = 0;\nexport default class Layers {\n constructor(art) {\n this.art = art;\n this.add = this.add.bind(this);\n this.art.option.layers.filter(item => !item.disable).forEach(this.add);\n }\n\n add(option, callback) {\n const { refs: { $layers } } = this.art;\n id++;\n const $layer = document.createElement('div');\n $layer.setAttribute('data-art-layer-id', id);\n $layer.setAttribute('class', `art-layer art-layer-${option.name || id}`);\n setStyle($layer, 'z-index', option.index || id);\n append($layer, option.html);\n setStyles($layer, option.style || {});\n $layers.appendChild($layer);\n this.art.emit('layers:add', $layer);\n callback && callback($layer);\n }\n\n show() {\n const { $layers } = this.art.refs;\n setStyle($layers, 'display', 'block');\n this.art.emit('layers:show', $layers);\n }\n\n hide() {\n const { $layers } = this.art.refs;\n setStyle($layers, 'display', 'none');\n this.art.emit('layers:hide', $layers);\n }\n}\n","import icons from './icons';\nimport { append, setStyle } from './utils';\n\nexport default class Loading {\n constructor(art) {\n this.art = art;\n const { option, refs: { $loading } } = art;\n append($loading, option.loading || icons.loading);\n }\n\n hide() {\n const { $loading } = this.art.refs;\n setStyle($loading, 'display', 'none');\n this.art.emit('loading:hide', $loading);\n }\n\n show() {\n const { $loading } = this.art.refs;\n setStyle($loading, 'display', 'flex');\n this.art.emit('loading:show', $loading);\n }\n}\n","import { setStyle } from './utils';\n\nexport default class Notice {\n constructor(art) {\n this.art = art;\n this.timer = null;\n }\n\n show(msg, autoHide = true, time = 1000) {\n const { $notice, $noticeInner } = this.art.refs;\n setStyle($notice, 'display', 'block');\n $noticeInner.innerHTML = msg instanceof Error ? msg.message.trim() : msg;\n if (autoHide) {\n clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.hide();\n }, time);\n }\n this.art.emit('notice:show', $notice);\n }\n\n hide() {\n const { $notice } = this.art.refs;\n setStyle($notice, 'display', 'none');\n this.art.emit('notice:hide', $notice);\n }\n}\n","import icons from './icons';\nimport { append, setStyle } from './utils';\n\nexport default class Mask {\n constructor(art) {\n this.art = art;\n const { option, refs: { $mask } } = art;\n append($mask, option.play || icons.playBig);\n }\n\n show() {\n const { $mask } = this.art.refs;\n setStyle($mask, 'display', 'flex');\n this.art.emit('mask:show', $mask);\n }\n\n hide() {\n const { $mask } = this.art.refs;\n setStyle($mask, 'display', 'none');\n this.art.emit('mask:show', $mask);\n }\n}\n","import { append } from '../utils';\n\nexport default class Common {\n constructor(option) {\n this.option = option;\n }\n\n apply(art, $setting) {\n this.art = art;\n this.$header = $setting.querySelector('.art-setting-header');\n this.$body = $setting.querySelector('.art-setting-body');\n append(this.$body, '———— 先占坑,暂无设置 ————');\n }\n}\n","import { setStyle, append } from '../utils';\nimport Common from './common';\n\nlet id = 0;\nexport default class Setting {\n constructor(art) {\n this.art = art;\n this.isOpen = false;\n this.$settings = [];\n if (art.option.setting) {\n this.init();\n this.mount();\n }\n }\n\n init() {\n const { refs: { $settingClose }, events: { proxy } } = this.art;\n\n proxy($settingClose, 'click', () => {\n this.hide();\n });\n\n this.add(new Common({\n name: 'common',\n title: 'Common',\n disable: false,\n index: 10\n }));\n }\n\n add(setting) {\n const { option } = setting;\n if (option && !option.disable) {\n id++;\n const name = option.name || setting.constructor.name.toLowerCase() || `setting${id}`;\n const title = option.title || name;\n const $setting = document.createElement('div');\n $setting.setAttribute('class', `art-setting art-setting-${name}`);\n $setting.dataset.settingIndex = option.index || id;\n append($setting, `