1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 15:27:43 +00:00

Make JS X Again

* Configure the project to allow the use of JSX in .jsx files.
* Add linting for JSX.
* Rewrite most existing code that uses `createElement` to use JSX syntax.
* Stop importing `createElement as e`. That's what the minifier is for. And we don't have to write it manually so much now because of JSX syntax.
This commit is contained in:
SirStendec 2018-04-01 18:24:08 -04:00
parent 57152e8ed5
commit f506b512b4
31 changed files with 514 additions and 322 deletions

View file

@ -1,3 +1,8 @@
{ {
"plugins": ["syntax-dynamic-import"] "plugins": [
"syntax-dynamic-import",
["transform-react-jsx", {
"pragma": "createElement"
}]
]
} }

View file

@ -7,11 +7,22 @@ module.exports = {
"eslint:recommended", "eslint:recommended",
"plugin:vue/recommended" "plugin:vue/recommended"
], ],
"plugins": ["vue"], "plugins": [
"vue",
"react"
],
"parserOptions": { "parserOptions": {
"parser": "babel-eslint", "parser": "babel-eslint",
"ecmaVersion": 8, "ecmaVersion": 8,
"sourceType": "module" "sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"pragma": "createElement"
}
}, },
"globals": { "globals": {
"import": false, "import": false,
@ -95,6 +106,7 @@ module.exports = {
"allowTemplateLiterals": true "allowTemplateLiterals": true
} }
], ],
"vue/html-indent": [ "vue/html-indent": [
"warn", "warn",
"tab" "tab"
@ -108,6 +120,32 @@ module.exports = {
"singleline": "never", "singleline": "never",
"multiline": "always" "multiline": "always"
} }
] ],
"jsx-quotes": ["error", "prefer-double"],
"react/jsx-boolean-value": "error",
"react/jsx-closing-bracket-location": ["error", "line-aligned"],
//"react/jsx-closing-tag-location": "error" -- stupid rule that doesn't allow line-aligned
"react/jsx-equals-spacing": "error",
"react/jsx-filename-extension": "error",
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
"react/jsx-indent": ["warn", "tab"],
"react/jsx-indent-props": ["warn", "tab"],
"react/jsx-key": "warn",
"react/jsx-no-bind": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-literals": ["warn"],
"react/jsx-no-target-blank": "error",
"react/jsx-sort-props": ["error", {
"callbacksLast": true,
"reservedFirst": true,
"noSortAlphabetically": true
}],
"react/jsx-tag-spacing": ["error", {
"beforeClosing": "never"
}],
"react/jsx-uses-react": "error",
"react/jsx-wrap-multilines": "error"
} }
}; };

View file

@ -47,6 +47,7 @@ and add the following to your workspace settings:
{ {
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"javascriptreact",
"vue" "vue"
] ]
}``` }```

128
package-lock.json generated
View file

@ -431,8 +431,7 @@
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true, "dev": true
"optional": true
}, },
"asn1": { "asn1": {
"version": "0.2.3", "version": "0.2.3",
@ -637,6 +636,17 @@
"babel-types": "6.26.0" "babel-types": "6.26.0"
} }
}, },
"babel-helper-builder-react-jsx": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz",
"integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=",
"dev": true,
"requires": {
"babel-runtime": "6.26.0",
"babel-types": "6.26.0",
"esutils": "2.0.2"
}
},
"babel-helper-call-delegate": { "babel-helper-call-delegate": {
"version": "6.24.1", "version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
@ -823,6 +833,12 @@
"integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=",
"dev": true "dev": true
}, },
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=",
"dev": true
},
"babel-plugin-syntax-trailing-function-commas": { "babel-plugin-syntax-trailing-function-commas": {
"version": "6.22.0", "version": "6.22.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz",
@ -1098,6 +1114,17 @@
"babel-runtime": "6.26.0" "babel-runtime": "6.26.0"
} }
}, },
"babel-plugin-transform-react-jsx": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz",
"integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=",
"dev": true,
"requires": {
"babel-helper-builder-react-jsx": "6.26.0",
"babel-plugin-syntax-jsx": "6.18.0",
"babel-runtime": "6.26.0"
}
},
"babel-plugin-transform-regenerator": { "babel-plugin-transform-regenerator": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
@ -2755,6 +2782,15 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"dev": true "dev": true
}, },
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "0.4.19"
}
},
"end-of-stream": { "end-of-stream": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@ -3037,6 +3073,18 @@
} }
} }
}, },
"eslint-plugin-react": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.7.0.tgz",
"integrity": "sha512-KC7Snr4YsWZD5flu6A5c0AcIZidzW3Exbqp7OT67OaD2AppJtlBr/GuPrW/vaQM/yfZotEvKAdrxrO+v8vwYJA==",
"dev": true,
"requires": {
"doctrine": "2.1.0",
"has": "1.0.1",
"jsx-ast-utils": "2.0.1",
"prop-types": "15.6.1"
}
},
"eslint-plugin-vue": { "eslint-plugin-vue": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-4.4.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-4.4.0.tgz",
@ -3525,6 +3573,29 @@
"websocket-driver": "0.7.0" "websocket-driver": "0.7.0"
} }
}, },
"fbjs": {
"version": "0.8.16",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
"integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
"dev": true,
"requires": {
"core-js": "1.2.7",
"isomorphic-fetch": "2.2.1",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"promise": "7.3.1",
"setimmediate": "1.0.5",
"ua-parser-js": "0.7.17"
},
"dependencies": {
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
"dev": true
}
}
},
"figures": { "figures": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@ -5004,6 +5075,16 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true "dev": true
}, },
"isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"dev": true,
"requires": {
"node-fetch": "1.7.3",
"whatwg-fetch": "2.0.4"
}
},
"isstream": { "isstream": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -5133,6 +5214,15 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"jsx-ast-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz",
"integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=",
"dev": true,
"requires": {
"array-includes": "3.0.3"
}
},
"killable": { "killable": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz",
@ -5866,6 +5956,16 @@
"integrity": "sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g==", "integrity": "sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g==",
"dev": true "dev": true
}, },
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
}
},
"node-forge": { "node-forge": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
@ -7184,7 +7284,6 @@
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"asap": "2.0.6" "asap": "2.0.6"
} }
@ -7195,6 +7294,17 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true "dev": true
}, },
"prop-types": {
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
"integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
"dev": true,
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1"
}
},
"proxy-addr": { "proxy-addr": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
@ -9018,6 +9128,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "dev": true
}, },
"ua-parser-js": {
"version": "0.7.17",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==",
"dev": true
},
"uglify-es": { "uglify-es": {
"version": "3.3.9", "version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
@ -10067,6 +10183,12 @@
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"dev": true "dev": true
}, },
"whatwg-fetch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==",
"dev": true
},
"whet.extend": { "whet.extend": {
"version": "0.9.9", "version": "0.9.9",
"resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz",

View file

@ -6,7 +6,7 @@
"main": "script.js", "main": "script.js",
"scripts": { "scripts": {
"start": "webpack-dev-server --config webpack.web.dev.js", "start": "webpack-dev-server --config webpack.web.dev.js",
"eslint": "eslint \"src/**/*.{js,vue}\"", "eslint": "eslint \"src/**/*.{js,jsx,vue}\"",
"build": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'", "build": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
"build:babel": "webpack --config webpack.web.babel.js --define process.env.NODE_ENV='production'", "build:babel": "webpack --config webpack.web.babel.js --define process.env.NODE_ENV='production'",
"build:prod": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'", "build:prod": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
@ -17,12 +17,14 @@
"babel-eslint": "^8.2.2", "babel-eslint": "^8.2.2",
"babel-loader": "^7.1.4", "babel-loader": "^7.1.4",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1", "copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.10", "css-loader": "^0.28.10",
"eslint": "^4.18.2", "eslint": "^4.18.2",
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-vue": "^4.4.0", "eslint-plugin-vue": "^4.4.0",
"extract-loader": "^1.0.2", "extract-loader": "^1.0.2",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",

View file

@ -66,7 +66,7 @@ class FrankerFaceZ extends Module {
// ======================================================================== // ========================================================================
discoverModules() { discoverModules() {
const ctx = require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.js$/), const ctx = require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/),
modules = this.populate(ctx, this.core_log); modules = this.populate(ctx, this.core_log);
this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`); this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);

View file

@ -6,7 +6,7 @@
import {API_SERVER, IS_WEBKIT, WEBKIT_CSS as WEBKIT} from 'utilities/constants'; import {API_SERVER, IS_WEBKIT, WEBKIT_CSS as WEBKIT} from 'utilities/constants';
import {createElement as e, ManagedStyle} from 'utilities/dom'; import {createElement, ManagedStyle} from 'utilities/dom';
import {has} from 'utilities/object'; import {has} from 'utilities/object';
import Module from 'utilities/module'; import Module from 'utilities/module';
@ -270,16 +270,32 @@ export default class Badges extends Module {
if ( ! bd ) if ( ! bd )
continue; continue;
out.push(e('div', {className: 'ffz-badge-tip'}, [ out.push(<div class="ffz-badge-tip">
{show_previews && <img class="preview-image ffz-badge" src={bd.image4x} />}
{bd.title}
</div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [
show_previews && e('img', { show_previews && e('img', {
className: 'preview-image ffz-badge', className: 'preview-image ffz-badge',
src: bd.image4x src: bd.image4x
}), }),
bd.title bd.title
])); ]));*/
} else if ( p === 'ffz' ) { } else if ( p === 'ffz' ) {
out.push(e('div', {className: 'ffz-badge-tip'}, [ out.push(<div class="ffz-badge-tip">
{show_previews && <div
class="preview-image ffz-badge"
style={{
backgroundColor: d.color,
backgroundImage: `url("${d.image}")`
}}
/>}
{d.title}
</div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [
show_previews && e('div', { show_previews && e('div', {
className: 'preview-image ffz-badge', className: 'preview-image ffz-badge',
style: { style: {
@ -288,7 +304,7 @@ export default class Badges extends Module {
} }
}), }),
d.title d.title
])); ]));*/
} }
} }
@ -297,7 +313,7 @@ export default class Badges extends Module {
} }
render(msg, e) { // eslint-disable-line class-methods-use-this render(msg, createElement) { // eslint-disable-line class-methods-use-this
const hidden_badges = this.parent.context.get('chat.badges.hidden') || {}, const hidden_badges = this.parent.context.get('chat.badges.hidden') || {},
badge_style = this.parent.context.get('chat.badges.style'), badge_style = this.parent.context.get('chat.badges.style'),
custom_mod = this.parent.context.get('chat.badges.custom-mod'), custom_mod = this.parent.context.get('chat.badges.custom-mod'),
@ -444,7 +460,7 @@ export default class Badges extends Module {
if ( data.replaced ) if ( data.replaced )
props['data-replaced'] = data.replaced; props['data-replaced'] = data.replaced;
out.push(e('span', props)); out.push(createElement('span', props));
} }
return out; return out;

View file

@ -171,6 +171,7 @@ export default class Emotes extends Module {
// ======================================================================== // ========================================================================
addDefaultSet(provider, set_id, data) { addDefaultSet(provider, set_id, data) {
const had_set = this.default_sets.includes(set_id);
if ( ! this.default_sets.sourceIncludes(provider, set_id) ) { if ( ! this.default_sets.sourceIncludes(provider, set_id) ) {
this.default_sets.push(provider, set_id); this.default_sets.push(provider, set_id);
this.refSet(set_id); this.refSet(set_id);
@ -178,13 +179,20 @@ export default class Emotes extends Module {
if ( data ) if ( data )
this.loadSetData(set_id, data); this.loadSetData(set_id, data);
if ( ! had_set )
this.emit(':update-default-sets');
} }
removeDefaultSet(provider, set_id) { removeDefaultSet(provider, set_id) {
const had_set = this.default_sets.includes(set_id);
if ( this.default_sets.sourceIncludes(provider, set_id) ) { if ( this.default_sets.sourceIncludes(provider, set_id) ) {
this.default_sets.remove(provider, set_id); this.default_sets.remove(provider, set_id);
this.unrefSet(set_id); this.unrefSet(set_id);
} }
if ( had_set && ! this.default_sets.includes(set_id) )
this.emit(':update-default-sets');
} }
refSet(set_id) { refSet(set_id) {

View file

@ -4,7 +4,7 @@
// Default Tokenizers // Default Tokenizers
// ============================================================================ // ============================================================================
import {sanitize, createElement as e} from 'utilities/dom'; import {sanitize, createElement} from 'utilities/dom';
import {has, split_chars} from 'utilities/object'; import {has, split_chars} from 'utilities/object';
const EMOTE_CLASS = 'chat-line__message--emote', const EMOTE_CLASS = 'chat-line__message--emote',
@ -40,17 +40,16 @@ export const Links = {
type: 'link', type: 'link',
priority: 50, priority: 50,
render(token, e) { render(token, createElement) {
return e('a', { return (<a
className: 'ffz-tooltip', class="ffz-tooltip"
'data-tooltip-type': 'link', data-tooltip-type="link"
'data-url': token.url, data-url={token.url}
'data-is-mail': token.is_mail, data-is-mail={token.is_mail}
rel="noopener noreferrer"
rel: 'noopener', target="_blank"
target: '_blank', href={token.url}
href: token.url >{token.text}</a>);
}, token.text);
}, },
tooltip(target, tip) { tooltip(target, tip) {
@ -209,10 +208,10 @@ export const Mentions = {
type: 'mention', type: 'mention',
priority: 40, priority: 40,
render(token, e) { render(token, createElement) {
return e('strong', { return (<strong class={`chat-line__message-mention${token.me ? ' ffz--mention-me' : ''}`}>
className: `chat-line__message-mention${token.me ? ' ffz--mention-me' : ''}` {token.text}
}, `${token.text}`); </strong>);
}, },
process(tokens, msg, user) { process(tokens, msg, user) {
@ -279,16 +278,16 @@ export const Mentions = {
export const CheerEmotes = { export const CheerEmotes = {
type: 'cheer', type: 'cheer',
render(token, e) { render(token, createElement) {
return e('span', { return (<span
className: `ffz-cheer ffz-tooltip`, class="ffz-cheer ffz-tooltip"
'data-tooltip-type': 'cheer', data-tooltip-type="cheer"
'data-prefix': token.prefix, data-prefix={token.prefix}
'data-amount': this.i18n.formatNumber(token.amount), data-amount={this.i18n.formatNumber(token.amount)}
'data-tier': token.tier, data-tier={token.tier}
'data-individuals': token.individuals ? JSON.stringify(token.individuals) : 'null', data-individuals={JSON.stringify(token.individuals || null)}
alt: token.text alt={token.text}
}); />);
}, },
tooltip(target) { tooltip(target) {
@ -300,16 +299,16 @@ export const CheerEmotes = {
length = individuals && individuals.length; length = individuals && individuals.length;
const out = [ const out = [
this.context.get('tooltip.emote-images') && e('div', { this.context.get('tooltip.emote-images') && (<div
className: 'preview-image ffz-cheer-preview', class="preview-image ffz-cheer-preview"
'data-prefix': prefix, data-prefix={prefix}
'data-tier': tier data-tier={tier}
}), />),
this.i18n.t('tooltip.bits', '%{count|number} Bits', amount), this.i18n.t('tooltip.bits', '%{count|number} Bits', amount),
]; ];
if ( length > 1 ) { if ( length > 1 ) {
out.push(e('br')); out.push(<br />);
individuals.sort(i => -i[0]); individuals.sort(i => -i[0]);
@ -319,11 +318,11 @@ export const CheerEmotes = {
amount, amount,
prefix, prefix,
tier tier
}, e)); }, createElement));
} }
if ( length > 12 ) { if ( length > 12 ) {
out.push(e('br')); out.push(<br />);
out.push(this.i18n.t('tooltip.bits.more', '(and %{count} more)', length-12)); out.push(this.i18n.t('tooltip.bits.more', '(and %{count} more)', length-12));
} }
} }
@ -448,33 +447,33 @@ export const CheerEmotes = {
export const AddonEmotes = { export const AddonEmotes = {
type: 'emote', type: 'emote',
render(token, e) { render(token, createElement) {
const mods = token.modifiers || [], ml = mods.length, const mods = token.modifiers || [], ml = mods.length,
emote = e('img', { emote = (<img
className: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : ''}`, class={`${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : ''}`}
src: token.src, src={token.src}
srcSet: token.srcSet, srcSet={token.srcSet}
alt: token.text, alt={token.text}
'data-tooltip-type': 'emote', data-tooltip-type="emote"
'data-provider': token.provider, data-provider={token.provider}
'data-id': token.id, data-id={token.id}
'data-set': token.set, data-set={token.set}
'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null, data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
}); />);
if ( ! ml ) if ( ! ml )
return emote; return emote;
return e('span', { return (<span
className: `${EMOTE_CLASS} modified-emote`, class={`${EMOTE_CLASS} modified-emote`}
'data-provider': token.provider, data-provider={token.provider}
'data-id': token.id, data-id={token.id}
'data-set': token.set data-set={token.set}
}, [ >
emote, {emote}
mods.map(t => e('span', null, this.tokenizers.emote.render(t, e))) {mods.map(t => <span>{this.tokenizers.emote.render(t, createElement)}</span>)}
]); </span>);
}, },
tooltip(target, tip) { tooltip(target, tip) {
@ -489,10 +488,10 @@ export const AddonEmotes = {
emote = emote_set && emote_set.emotes[emote_id]; emote = emote_set && emote_set.emotes[emote_id];
if ( emote ) if ( emote )
return e('span', null, [ return (<span>
this.tokenizers.emote.render(emote.token, e), {this.tokenizers.emote.render(emote.token, createElement)}
` - ${emote.hidden ? '???' : emote.name}` {` - ${emote.hidden ? '???' : emote.name}`}
]); </span>);
}) })
} }
@ -540,25 +539,23 @@ export const AddonEmotes = {
} }
return [ return [
preview && this.context.get('tooltip.emote-images') && e('img', { preview && this.context.get('tooltip.emote-images') && (<img
className: 'preview-image', class="preview-image"
src: preview, src={preview}
onLoad: tip.update onLoad={tip.update}
}), />),
this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: target.alt}), this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: target.alt}),
source && this.context.get('tooltip.emote-sources') && e('div', { source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
className: 'pd-t-05', {source}
}, source), </div>),
owner && this.context.get('tooltip.emote-sources') && e('div', { owner && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
className: 'pd-t-05' {owner}
}, owner), </div>),
mods && e('div', { mods && (<div class="tw-pd-t-1">{mods}</div>)
className: 'pd-t-1'
}, mods)
]; ];
}, },

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import {has, deep_copy} from 'utilities/object'; import {has, deep_copy} from 'utilities/object';
import {parse_path} from 'src/settings'; import {parse_path} from 'src/settings';
@ -547,7 +547,7 @@ export default class MainMenu extends Module {
return; return;
this._vue = new this.vue.Vue({ this._vue = new this.vue.Vue({
el: e('div'), el: createElement('div'),
render: h => h('main-menu', this.getData()) render: h => h('main-menu', this.getData())
}); });

View file

@ -4,7 +4,7 @@
// Channel Metadata // Channel Metadata
// ============================================================================ // ============================================================================
import {createElement as e, ClickOutside} from 'utilities/dom'; import {createElement, ClickOutside} from 'utilities/dom';
import {maybe_call} from 'utilities/object'; import {maybe_call} from 'utilities/object';
import {duration_to_string} from 'utilities/time'; import {duration_to_string} from 'utilities/time';
@ -277,35 +277,37 @@ export default class Metadata extends Module {
const fix = cls === 'tw-button--text'; const fix = cls === 'tw-button--text';
if ( typeof icon === 'string' ) if ( typeof icon === 'string' )
icon = e('span', 'tw-button__icon tw-button__icon--left', e('figure', icon)); icon = (<span class="tw-button__icon tw-button__icon--left"><figure class={icon} /></span>);
if ( def.popup && def.click ) { if ( def.popup && def.click ) {
el = e('span', { el = (<span
className: `ffz-stat${fix ? ' ffz-fix-padding--left' : ''}`, class={`ffz-stat${fix ? ' ffz-fix-padding--left' : ''}`}
'data-key': key, data-key={key}
tip_content: tooltip tip_content={tooltip}
}, [ >
btn = e('button', `tw-button ${cls}`, [ {btn = (<button class={`tw-button ${cls}`}>
icon, {icon}
stat = e('span', 'ffz-stat-text tw-button__text') {stat = (<span class="ffz-stat-text tw-button__text" />)}
]), </button>)}
popup = e('button', `tw-button ${cls} ffz-stat-arrow`, {popup = (<button class={`tw-button ${cls} ffz-stat-arrow`}>
e('span', 'tw-button__icon tw-pd-x-0', <span class="tw-button__icon tw-pd-x-0">
e('figure', 'ffz-i-down-dir') <figure class="ffz-i-down-dir" />
) </span>
) </button>)}
]); </span>);
} else } else
btn = popup = el = e('button', { btn = popup = el = (<button
className: `ffz-stat${fix ? ' ffz-fix-padding' : ''} tw-button ${cls}`, class={`ffz-stat${fix ? ' ffz-fix-padding' : ''} tw-button ${cls}`}
'data-key': key, data-key={key}
tip_content: tooltip tip_content={tooltip}
}, [ >
icon, {icon}
stat = e('span', 'ffz-stat-text tw-button__text'), {stat = <span class="ffz-stat-text tw-button__text" />}
def.popup && e('span', 'tw-button__icon tw-button__icon--right', e('figure', 'ffz-i-down-dir')) {def.popup && <span class="tw-button__icon tw-button__icon--right">
]); <figure class="ffz-i-down-dir" />
</span>}
</button>);
if ( def.click ) if ( def.click )
btn.addEventListener('click', e => { btn.addEventListener('click', e => {
@ -370,16 +372,16 @@ export default class Metadata extends Module {
} else { } else {
if ( typeof icon === 'string' ) if ( typeof icon === 'string' )
icon = e('span', 'tw-stat__icon', e('figure', icon)); icon = (<span class="tw-stat__icon"><figure class={icon} /></span>);
el = e('div', { el = (<div
className: 'ffz-stat tw-stat', class="ffz-stat tw-stat"
'data-key': key, data-key={key}
tip_content: tooltip tip_content={tooltip}
}, [ >
icon, {icon}
stat = e('span', 'ffz-stat-text tw-stat__value') {stat = <span class="ffz-stat-text tw-stat__value" />}
]); </div>)
} }
el._ffz_order = order; el._ffz_order = order;

View file

@ -4,7 +4,7 @@
// Tooltip Handling // Tooltip Handling
// ============================================================================ // ============================================================================
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import {has, maybe_call} from 'utilities/object'; import {has, maybe_call} from 'utilities/object';
import Tooltip from 'utilities/tooltip'; import Tooltip from 'utilities/tooltip';
@ -20,8 +20,8 @@ export default class TooltipProvider extends Module {
this.types.json = target => { this.types.json = target => {
const title = target.dataset.title; const title = target.dataset.title;
return [ return [
title && e('strong', null, title), title && createElement('strong', null, title),
e('code', { createElement('code', {
className: `block${title ? ' pd-t-05 border-t mg-t-05' : ''}`, className: `block${title ? ' pd-t-05 border-t mg-t-05' : ''}`,
style: { style: {
fontFamily: 'monospace', fontFamily: 'monospace',
@ -96,8 +96,8 @@ export default class TooltipProvider extends Module {
if ( ! handler ) if ( ! handler )
return [ return [
e('strong', null, 'Unhandled Tooltip Type'), createElement('strong', null, 'Unhandled Tooltip Type'),
e('code', { createElement('code', {
className: 'block pd-t-05 border-t mg-t-05', className: 'block pd-t-05 border-t mg-t-05',
style: { style: {
fontFamily: 'monospace', fontFamily: 'monospace',

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
export default class TranslationUI extends Module { export default class TranslationUI extends Module {
constructor(...args) { constructor(...args) {

View file

@ -16,7 +16,7 @@ export default class BaseSite extends Module {
} }
populateModules() { populateModules() {
const ctx = require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.js$/); const ctx = require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/);
const modules = this.populate(ctx, this.log); const modules = this.populate(ctx, this.log);
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`); this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
} }

View file

@ -11,7 +11,7 @@ import Fine from 'utilities/compat/fine';
import FineRouter from 'utilities/compat/fine-router'; import FineRouter from 'utilities/compat/fine-router';
import Apollo from 'utilities/compat/apollo'; import Apollo from 'utilities/compat/apollo';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import MAIN_URL from 'site/styles/main.scss'; import MAIN_URL from 'site/styles/main.scss';
@ -56,7 +56,7 @@ export default class Twilight extends BaseSite {
const current = this.router.current; const current = this.router.current;
this.fine.route(current && current.name); this.fine.route(current && current.name);
document.head.appendChild(e('link', { document.head.appendChild(createElement('link', {
href: MAIN_URL, href: MAIN_URL,
rel: 'stylesheet', rel: 'stylesheet',
type: 'text/css' type: 'text/css'

View file

@ -376,12 +376,12 @@ export default class ChatHook extends Module {
cls.prototype.render = function() { cls.prototype.render = function() {
if ( this.state.ffz_errors > 0 ) { if ( this.state.ffz_errors > 0 ) {
const React = t.web_munch.getModule('react'), const React = t.web_munch.getModule('react'),
e = React && React.createElement; createElement = React && React.createElement;
if ( ! e ) if ( ! createElement )
return null; return null;
return e('div', { return createElement('div', {
className: 'tw-border-l tw-c-background-alt-2 tw-c-text tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative' className: 'tw-border-l tw-c-background-alt-2 tw-c-text tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative'
}, 'There was an error displaying chat.'); }, 'There was an error displaying chat.');

View file

@ -4,7 +4,7 @@
// Chat Scroller // Chat Scroller
// ============================================================================ // ============================================================================
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import Twilight from 'site'; import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
@ -95,7 +95,7 @@ export default class Scroller extends Module {
let timer; let timer;
const auto = this.state.ffz_total_errors < 10, const auto = this.state.ffz_total_errors < 10,
React = t.web_munch.getModule('react'), React = t.web_munch.getModule('react'),
e = React && React.createElement, createElement = React && React.createElement,
handler = () => { handler = () => {
clearTimeout(timer); clearTimeout(timer);
this.ffzZeroErrors(); this.ffzZeroErrors();
@ -104,17 +104,17 @@ export default class Scroller extends Module {
if ( auto ) if ( auto )
timer = setTimeout(handler, 250); timer = setTimeout(handler, 250);
if ( ! e ) if ( ! createElement )
return null; return null;
return e('div', { return createElement('div', {
className: 'tw-border-l tw-c-background-alt-2 tw-c-text tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative' className: 'tw-border-l tw-c-background-alt-2 tw-c-text tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative'
}, [ }, [
e('div', {className: 'tw-mg-b-1'}, 'There was an error displaying chat.'), createElement('div', {className: 'tw-mg-b-1'}, 'There was an error displaying chat.'),
! auto && e('button', { ! auto && createElement('button', {
className: 'tw-button', className: 'tw-button',
onClick: handler onClick: handler
}, e('span', {className: 'tw-button__text'}, 'Try Again')) }, createElement('span', {className: 'tw-button__text'}, 'Try Again'))
]); ]);
} else } else
@ -176,9 +176,9 @@ export default class Scroller extends Module {
node.classList.add('tw-full-height'); node.classList.add('tw-full-height');
el = this._ffz_freeze_indicator = e('div', { el = this._ffz_freeze_indicator = createElement('div', {
className: 'ffz--freeze-indicator chat-list__more-messages-placeholder tw-relative tw-mg-x-2' className: 'ffz--freeze-indicator chat-list__more-messages-placeholder tw-relative tw-mg-x-2'
}, e('div', { }, createElement('div', {
className: 'chat-list__more-messages tw-bottom-0 tw-full-width tw-align-items-center tw-flex tw-justify-content-center tw-absolute tw-pd-05' className: 'chat-list__more-messages tw-bottom-0 tw-full-width tw-align-items-center tw-flex tw-justify-content-center tw-absolute tw-pd-05'
})); }));

View file

@ -32,27 +32,36 @@ export default class SettingsMenu extends Module {
if ( ! React ) if ( ! React )
return; return;
const e = React.createElement; const createElement = React.createElement;
this.SettingsMenu.ready(cls => { this.SettingsMenu.ready((cls, instances) => {
const old_universal = cls.prototype.renderUniversalOptions; const old_universal = cls.prototype.renderUniversalOptions;
cls.prototype.renderUniversalOptions = function() { cls.prototype.renderUniversalOptions = function() {
const val = old_universal.call(this); const val = old_universal.call(this);
val.props.children.push(e('div', {
className: 'tw-mg-t-1'
}, e('button', {
onClick: () => t.click(this)
}, t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center'))
));
window.menu = this; val.props.children.push(<div class="tw-mg-t-1">
<button onClick={this.ffzSettingsClick}>
{t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
</button>
</div>);
return val; return val;
} }
for(const inst of instances)
inst.ffzSettingsClick = e => t.click(inst, e);
this.SettingsMenu.forceUpdate(); this.SettingsMenu.forceUpdate();
}); });
this.SettingsMenu.on('mount', inst => {
inst.ffzSettingsClick = e => t.click(inst, e)
});
this.SettingsMenu.on('unmount', inst => {
inst.ffzSettingsClick = null;
});
} }
click(inst) { click(inst) {
@ -68,6 +77,7 @@ export default class SettingsMenu extends Module {
} else { } else {
this.emit('site.menu_button:clicked'); this.emit('site.menu_button:clicked');
} }
const parent = this.fine.searchParent(inst, n => n.toggleBalloonId); const parent = this.fine.searchParent(inst, n => n.toggleBalloonId);
parent && parent.handleButtonClick(); parent && parent.handleButtonClick();
} }

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import {get} from 'utilities/object'; import {get} from 'utilities/object';
import Popper from 'popper.js'; import Popper from 'popper.js';
@ -236,83 +236,61 @@ export default class Following extends SiteModule {
const simplebarContentChildren = []; const simplebarContentChildren = [];
// Hosted Channel Header // Hosted Channel Header
simplebarContentChildren.push( simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
e('p', { {this.i18n.t('directory.hosted', 'Hosted Channel')}
className: 'tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2', </p>);
textContent: this.i18n.t('directory.hosted', 'Hosted Channel')
})
);
// Hosted Channel Content // Hosted Channel Content
simplebarContentChildren.push( simplebarContentChildren.push(<a
e('a', { class="tw-interactable"
className: 'tw-interactable', href={`/${hostData.channel}`}
href: `/${hostData.channel}`, onClick={e => this.parent.hijackUserClick(e, hostData.channel, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
onclick: event => >
this.parent.hijackUserClick( <div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
event, <div class="ffz-channel-avatar">
hostData.channel, <img src={inst.props.viewerCount.profileImageURL} alt={inst.props.channelName} />
this.destroyHostMenu.bind(this) </div>
) <p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
}, e('div', 'tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05', {inst.props.channelName}
[ </p>
e('div', { </div>
className: 'ffz-channel-avatar', </a>);
}, e('img', {
src: inst.props.viewerCount.profileImageURL,
alt: inst.props.channelName
})),
e('p', {
className: 'tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5',
textContent: inst.props.channelName
})
]
))
);
// Hosting Channels Header // Hosting Channels Header
simplebarContentChildren.push( simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
e('p', { {this.i18n.t('directory.hosting', 'Hosting Channels')}
className: 'tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2', </p>);
textContent: this.i18n.t('directory.hosting', 'Hosting Channels')
})
);
// Hosting Channels Content // Hosting Channels Content
for (let i = 0; i < hostData.nodes.length; i++) { for (let i = 0; i < hostData.nodes.length; i++) {
const node = hostData.nodes[i]; const node = hostData.nodes[i];
simplebarContentChildren.push( simplebarContentChildren.push(<a
e('a', { class="tw-interactable"
className: 'tw-interactable', href={`/${node.login}`}
href: `/${node.login}`, onClick={e => this.parent.hijackUserClick(e, node.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
onclick: event => this.parent.hijackUserClick(event, node.login, this.destroyHostMenu.bind(this)) >
}, e('div', 'tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05', <div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
[ <div class="ffz-channel-avatar">
e('div', { <img src={node.profileImageURL} alt={node.displayName} />
className: 'ffz-channel-avatar', </div>
}, e('img', { <p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
src: node.profileImageURL, {node.displayName}
alt: node.displayName </p>
})), </div>
e('p', { </a>);
className: 'tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5',
textContent: node.displayName
})
]
))
);
} }
this.hostMenu = e('div', 'ffz-host-menu tw-balloon tw-block', this.hostMenu = (<div class="ffz-host-menu tw-balloon tw-block">
e('div', 'tw-border tw-elevation-1 tw-border-radius-small tw-c-background', <div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background">
e('div', { <div class="scrollable-area" data-simplebar>
className: 'scrollable-area', <div class="simplebar-scroll-content">
'data-simplebar': true, <div class="simplebar-content">
}, e('div', 'simplebar-scroll-content', {simplebarContentChildren}
e('div', 'simplebar-content', simplebarContentChildren) </div>
)) </div>
) </div>
); </div>
</div>);
const root = (document.body.querySelector('.twilight-root') || document.body); const root = (document.body.querySelector('.twilight-root') || document.body);
root.appendChild(this.hostMenu); root.appendChild(this.hostMenu);

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import GAME_QUERY from './game.gql'; import GAME_QUERY from './game.gql';
@ -66,10 +66,12 @@ export default class Game extends SiteModule {
} }
block_btn = e('button', { block_btn = (<button
className: 'tw-mg-l-1 tw-button ffz-directory-toggle-block', class="tw-mg-l-1 tw-button ffz-directory-toggle-block"
onClick: this.generateClickHandler('directory.game.blocked-games', game, update_block) onClick={this.generateClickHandler('directory.game.blocked-games', game, update_block)}
}, block_label = e('span', 'tw-button__text')); >
{block_label = <span class="tw-button__text" />}
</button>);
update_block(); update_block();
@ -86,17 +88,19 @@ export default class Game extends SiteModule {
this.parent.ChannelCard.forceUpdate(); this.parent.ChannelCard.forceUpdate();
} }
hidden_btn = e('button', { hidden_btn = (<button
className: 'tw-mg-l-1 tw-button ffz-directory-toggle-thumbnail', class="tw-mg-l-1 tw-button ffz-directory-toggle-thumbnail"
onClick: this.generateClickHandler('directory.game.hidden-thumbnails', game, update_hidden) onClick={this.generateClickHandler('directory.game.hidden-thumbnails', game, update_hidden)}
}, hidden_label = e('span', 'tw-button__text')); >
{hidden_label = <span class="tw-button__text" />}
</button>)
update_hidden(); update_hidden();
buttons.appendChild(e('div', 'ffz-buttons', [ buttons.appendChild(<div class="ffz-buttons">
block_btn, {block_btn}
hidden_btn {hidden_btn}
])); </div>);
} }
generateClickHandler(setting, game, update_func) { generateClickHandler(setting, game, update_func) {

View file

@ -6,7 +6,7 @@
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {duration_to_string} from 'utilities/time'; import {duration_to_string} from 'utilities/time';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import {get} from 'utilities/object'; import {get} from 'utilities/object';
import Following from './following'; import Following from './following';
@ -244,15 +244,17 @@ export default class Directory extends SiteModule {
const up_text = duration_to_string(uptime, false, false, false, setting === 1); const up_text = duration_to_string(uptime, false, false, false, setting === 1);
if ( ! inst.ffz_uptime_el || card.querySelector('.ffz-uptime-element') === undefined ) { if ( ! inst.ffz_uptime_el || card.querySelector('.ffz-uptime-element') === undefined ) {
card.appendChild(inst.ffz_uptime_el = e('div', card.appendChild(inst.ffz_uptime_el = (<div class="video-preview-card__preview-overlay-stat tw-c-background-overlay tw-c-text-overlay tw-font-size-6 tw-top-0 tw-right-0 tw-z-default tw-inline-flex tw-absolute tw-mg-05 ffz-uptime-element">
'video-preview-card__preview-overlay-stat tw-c-background-overlay tw-c-text-overlay tw-font-size-6 tw-top-0 tw-right-0 tw-z-default tw-inline-flex tw-absolute tw-mg-05 ffz-uptime-element', <div class="tw-tooltip-wrapper tw-inline-flex">
e('div', 'tw-tooltip-wrapper tw-inline-flex', [ <div class="tw-stat">
e('div', 'tw-stat', [ <span class="tw-c-text-live tw-stat__icon">
e('span', 'tw-c-text-live tw-stat__icon', e('figure', 'ffz-i-clock')), <figure class="ffz-i-clock" />
inst.ffz_uptime_span = e('span', 'tw-stat__value') </span>
]), {inst.ffz_uptime_span = <span class="tw-stat__value" />}
inst.ffz_uptime_tt = e('div', 'tw-tooltip tw-tooltip--down tw-tooltip--align-center') </div>
]))); {inst.ffz_uptime_tt = <div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center" />}
</div>
</div>));
} }
if ( ! inst.ffz_update_timer ) if ( ! inst.ffz_update_timer )
@ -306,29 +308,29 @@ export default class Directory extends SiteModule {
if ( setting === 1 ) { if ( setting === 1 ) {
const body = card.querySelector('.tw-card-body .tw-flex'), const body = card.querySelector('.tw-card-body .tw-flex'),
avatar = e('a', { avatar = (<a
className: 'ffz-channel-avatar tw-mg-r-05 tw-mg-t-05', class="ffz-channel-avatar tw-mg-r-05 tw-mg-t-05"
href: `/${data.login}`, href={`/${data.login}`}
title: data.displayName, title={data.displayName}
onclick: event => this.hijackUserClick(event, data.login) onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
}, e('img', { >
src: data.profileImageURL <img src={data.profileImageURL} />
})); </a>);
body.insertBefore(avatar, body.firstElementChild); body.insertBefore(avatar, body.firstElementChild);
} else if ( setting === 2 || setting === 3 ) { } else if ( setting === 2 || setting === 3 ) {
const avatar_el = e('a', { const avatar_el = (<a
className: 'ffz-channel-avatar', class="ffz-channel-avatar"
href: `/${data.login}`, href={`/${data.login}`}
onclick: event => this.hijackUserClick(event, data.login) onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
}, e('div', 'live-channel-card__boxart tw-bottom-0 tw-absolute', >
e('figure', 'tw-aspect tw-aspect--align-top', <div class="live-channel-card__boxart tw-bottom-0 tw-absolute">
e('img', { <figure class="tw-aspect tw-aspect--align-top">
title: data.displayName, <img src={data.profileImageURL} title={data.displayName} />
src: data.profileImageURL </figure>
}))) </div>
); </a>);
const cont = card.querySelector('figure.tw-aspect > div'); const cont = card.querySelector('figure.tw-aspect > div');
if ( cont ) if ( cont )

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
const HOST_ERRORS = { const HOST_ERRORS = {
COMMAND_EXECUTION: { COMMAND_EXECUTION: {
@ -212,7 +212,7 @@ export default class HostButton extends Module {
this.activeTab = this.activeTab || 'auto-host'; this.activeTab = this.activeTab || 'auto-host';
this.vueEl = new vue.Vue({ this.vueEl = new vue.Vue({
el: e('div'), el: createElement('div'),
render: h => h('host-options', { render: h => h('host-options', {
hosts, hosts,
autoHostSettings, autoHostSettings,

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement, setChildren} from 'utilities/dom';
export default class MenuButton extends SiteModule { export default class MenuButton extends SiteModule {
constructor(...args) { constructor(...args) {
@ -29,7 +29,7 @@ export default class MenuButton extends SiteModule {
set pill(val) { set pill(val) {
this._pill_content = val; this._pill_content = val;
if ( this._pill ) if ( this._pill )
this._pill.innerHTML = this.formatPill(); setChildren(this._pill, this.formatPill(), true);
} }
formatPill() { formatPill() {
@ -46,28 +46,26 @@ export default class MenuButton extends SiteModule {
user_menu = container.querySelector('.top-nav__nav-items-container:last-child'); user_menu = container.querySelector('.top-nav__nav-items-container:last-child');
this._el = e('div', this._el = (<div class="ffz-top-nav tw-align-self-center tw-flex-grow-0 tw-flex-shrink-0 tw-flex-nowrap tw-pd-r-1 tw-pd-l-05">
'ffz-top-nav tw-align-self-center tw-flex-grow-0 tw-flex-shrink-0 tw-flex-nowrap tw-pd-r-1 tw-pd-l-05', {this._btn = (<button
this._btn = e('button', class="tw-button-icon tw-button-icon--overlay tw-button-icon--large"
{ onClick={e => this.emit(':clicked', e)} //eslint-disable-line react/jsx-no-bind
className: 'tw-button-icon tw-button-icon--overlay tw-button-icon--large', >
onClick: e => this.emit(':clicked', e) <div class="tw-tooltip-wrapper">
}, <span class="tw-button-icon__icon">
e('div', 'tw-tooltip-wrapper', [ <figure class="ffz-i-zreknarf" />
e('span', 'tw-button-icon__icon', </span>
e('figure', 'ffz-i-zreknarf') <div class="ffz-menu__pill absolute">
), <div class="tw-animation tw-animation--animate tw-animation--duration-medium tw-animation--timing-ease-in tw-animation--bounce-in">
{this._pill = (<div class="tw-pill tw-pill--notification" />)}
</div>
</div>
{this._tip = (<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center" />)}
</div>
</button>)}
</div>);
e('div', 'ffz-menu__pill absolute', setChildren(this._pill, this.formatPill(), true);
e('div', 'tw-animation tw-animation--animate tw-animation--duration-medium tw-animation--timing-ease-in tw-animation--bounce-in',
this._pill = e('span', 'tw-pill tw-pill--notification', this.formatPill(), true)
)
),
this._tip = e('div', 'tw-tooltip tw-tooltip--down tw-tooltip--align-center')
])
)
);
this.onTranslate(); this.onTranslate();

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
export default class Player extends Module { export default class Player extends Module {
@ -348,14 +348,13 @@ export default class Player extends Module {
let tip = container.querySelector('.ffz--player-reset .player-tip'); let tip = container.querySelector('.ffz--player-reset .player-tip');
if ( ! tip ) if ( ! tip )
container.insertBefore( container.insertBefore(<button
e('button', { class="player-button player-button--reset ffz--player-reset ffz-i-cancel"
className: 'player-button player-button--reset ffz--player-reset ffz-i-cancel', type="button"
type: 'button', onDblClick={t.resetPlayer.bind(t, inst)} // eslint-disable-line react/jsx-no-bind
onDblClick: t.resetPlayer.bind(t, inst) >
}, tip = e('span', 'player-tip js-control-tip')), {tip = <span class="player-tip js-control-tip" />}
el.nextSibling </button>, el.nextSibling);
);
tip.dataset.tip = this.i18n.t('player.reset_button', 'Double-Click to Reset Player'); tip.dataset.tip = this.i18n.t('player.reset_button', 'Double-Click to Reset Player');
} }

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
export default class SubButton extends Module { export default class SubButton extends Module {
constructor(...args) { constructor(...args) {
@ -59,16 +59,13 @@ export default class SubButton extends Module {
icon = btn.querySelector('.ffz--can-prime'); icon = btn.querySelector('.ffz--can-prime');
if ( should_show && ! icon ) { if ( should_show && ! icon ) {
btn.insertBefore( btn.insertBefore(<span class="tw-button__icon tw-button__icon--left ffz--can-prime">
e('span', 'tw-button__icon tw-button__icon--left ffz--can-prime', <figure
e('figure', { class="ffz-i-crown ffz-tooltip"
className: 'ffz-i-crown ffz-tooltip', data-tooltip-type="html"
'data-tooltip-type': 'html', data-title={this.i18n.t('sub-button.prime', 'Your free channel sub with Prime is available.')}
'data-title': this.i18n.t('sub-button.prime', 'Your free channel sub with Prime is available.') />
}) </span>, btn.firstElementChild);
),
btn.firstElementChild
);
} else if ( ! should_show && icon ) } else if ( ! should_show && icon )
icon.remove(); icon.remove();

View file

@ -5,7 +5,7 @@
// ============================================================================ // ============================================================================
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import THEME_CSS_URL from 'site/styles/theme.scss'; import THEME_CSS_URL from 'site/styles/theme.scss';
@ -58,7 +58,7 @@ export default class ThemeEngine extends Module {
if ( ! enable ) if ( ! enable )
return; return;
this._style = e('link', { this._style = createElement('link', {
rel: 'stylesheet', rel: 'stylesheet',
type: 'text/css', type: 'text/css',
href: THEME_CSS_URL href: THEME_CSS_URL

View file

@ -30,9 +30,12 @@ function camelCase(name) {
} }
export function createElement(tag, props, children, no_sanitize) { export function createElement(tag, props, ...children) {
const el = document.createElement(tag); const el = document.createElement(tag);
if ( children.length === 1)
children = children[0];
if ( typeof props === 'string' ) if ( typeof props === 'string' )
el.className = props; el.className = props;
else if ( props ) else if ( props )
@ -73,7 +76,7 @@ export function createElement(tag, props, children, no_sanitize) {
} }
if ( children ) if ( children )
setChildren(el, children, no_sanitize); setChildren(el, children);
return el; return el;
} }

View file

@ -521,7 +521,9 @@ export class Module extends EventEmitter {
for(const raw_path of ctx.keys()) { for(const raw_path of ctx.keys()) {
const raw_module = ctx(raw_path), const raw_module = ctx(raw_path),
module = raw_module.module || raw_module.default, module = raw_module.module || raw_module.default,
name = raw_path.slice(2, raw_path.length - (raw_path.endsWith('/index.js') ? 9 : 3)); name = raw_path.slice(2, raw_path.length - (raw_path.endsWith('/index.jsx') ? 10 : raw_path.endsWith('/index.js') ? 9 : raw_path.endsWith('.jsx') ? 4 : 3));
// TODO: rewrite the name code to not have like 4 endsWith in it.
try { try {
added[name] = this.register(name, module); added[name] = this.register(name, module);

View file

@ -8,7 +8,7 @@
// Better because they aren't hidden by parents with overflow: hidden; // Better because they aren't hidden by parents with overflow: hidden;
// ============================================================================ // ============================================================================
import {createElement as e, setChildren} from 'utilities/dom'; import {createElement, setChildren} from 'utilities/dom';
import {maybe_call} from 'utilities/object'; import {maybe_call} from 'utilities/object';
import Popper from 'popper.js'; import Popper from 'popper.js';
@ -208,17 +208,17 @@ export class Tooltip {
return; return;
// Build the DOM. // Build the DOM.
const arrow = e('div', opts.arrowClass), const arrow = createElement('div', opts.arrowClass),
inner = tip.element = e('div', opts.innerClass), inner = tip.element = createElement('div', opts.innerClass),
el = tip.outer = e('div', { el = tip.outer = createElement('div', {
className: opts.tooltipClass className: opts.tooltipClass
}, [inner, arrow]); }, [inner, arrow]);
arrow.setAttribute('x-arrow', true); arrow.setAttribute('x-arrow', true);
if ( opts.arrowInner ) if ( opts.arrowInner )
arrow.appendChild(e('div', opts.arrowInner)); arrow.appendChild(createElement('div', opts.arrowInner));
if ( tip.add_class ) { if ( tip.add_class ) {
inner.classList.add(tip.add_class); inner.classList.add(tip.add_class);

View file

@ -46,7 +46,9 @@
} }
.ffz--chat-actions,
.ffz--profile-manager { .ffz--profile-manager {
.ffz--action,
.ffz--profile { .ffz--profile {
outline: none; outline: none;

View file

@ -8,6 +8,7 @@ module.exports = {
avalon: './src/main.js' avalon: './src/main.js'
}, },
resolve: { resolve: {
extensions: ['.js', '.jsx'],
alias: { alias: {
res: path.resolve(__dirname, 'res/'), res: path.resolve(__dirname, 'res/'),
styles: path.resolve(__dirname, 'styles/'), styles: path.resolve(__dirname, 'styles/'),
@ -45,6 +46,11 @@ module.exports = {
} }
}] }]
}, },
{
test: /\.jsx$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{ {
test: /\.(graphql|gql)$/, test: /\.(graphql|gql)$/,
exclude: /node_modules/, exclude: /node_modules/,