mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-10-09 12:50:50 +00:00
4.3.0
* Added: Setting to hide the "Not Live" bar beneath videos and clips that appears when the channel is currently live. * Fixed: Handling of `https://www.twitch.tv/<channel>/clip/<slug>` urls for rich chat embeds and rich link tool-tips. * Fixed: Lower the priority of custom highlight terms so they will not break links. * Fixed: Holding multiple modifier keys to display in-line chat actions. * Fixed: Clean up out-dated avatar display setting for the directory. * API Added: Allow add-ons to access the Popper JS library via `FrankerFaceZ.utilities.popper`. * API Added: `<icon-picker />` Vue component for selecting an icon. * API Added: `<react-link />` Vue component for creating links that cause the React app to navigate without a page load. * API Added: `<t-list />` Vue component for translating text including Vue elements. * API Added: `maybeLoad(icon)` function for font awesome to only load the font if the icon is from font awesome. * API Added: `generateUUID()` function to `FrankerFaceZ.utilities.object` * API Added: The `vue-observe-visibility` module is now loaded with Vue and made available in all Vue contexts.
This commit is contained in:
parent
3157aeb390
commit
aa25bff498
22 changed files with 210 additions and 218 deletions
|
@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 2, revision: 6,
|
||||
major: 4, minor: 3, revision: 0,
|
||||
commit: __git_commit__,
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
|
@ -174,7 +174,8 @@ FrankerFaceZ.utilities = {
|
|||
time: require('utilities/time'),
|
||||
tooltip: require('utilities/tooltip'),
|
||||
i18n: require('utilities/translation-core'),
|
||||
dayjs: require('dayjs')
|
||||
dayjs: require('dayjs'),
|
||||
popper: require('popper.js').default
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,140 +4,15 @@
|
|||
{{ t('setting.actions.icon', 'Icon') }}
|
||||
</label>
|
||||
|
||||
<div class="tw-full-width">
|
||||
<div class="tw-search-input">
|
||||
<label for="icon-search" class="tw-hide-accessible">
|
||||
{{ t('setting.actions.icon.search', 'Search for Icon') }}
|
||||
</label>
|
||||
<div class="tw-relative tw-mg-t-05">
|
||||
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
|
||||
<figure class="ffz-i-search" />
|
||||
</div>
|
||||
<input
|
||||
id="icon-search"
|
||||
:placeholder="t('setting.actions.icon.search', 'Search for Icon')"
|
||||
v-model="search"
|
||||
type="search"
|
||||
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<simplebar classes="tw-c-background-alt-2 tw-border-l tw-border-r tw-border-b ffz-icon-picker tw-mg-b-05">
|
||||
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap" >
|
||||
<div
|
||||
v-for="i of visible"
|
||||
:key="i[0]"
|
||||
:aria-checked="value.icon === i[0]"
|
||||
:class="{'tw-interactable--selected': value.icon === i[0]}"
|
||||
class="ffz-icon tw-interactable tw-interactable--inverted"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
@keydown.space.stop.prevent=""
|
||||
@keyup.space="change(i[0])"
|
||||
@keyup.enter="change(i[0])"
|
||||
@click="change(i[0])"
|
||||
>
|
||||
<figure :class="`tw-mg-y-05 tw-mg-x-1 ${i[0]}`" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tw-align-center tw-pd-1 tw-c-text-alt-2">
|
||||
{{ t('setting.actions.empty-search', 'no results') }}
|
||||
</div>
|
||||
</simplebar>
|
||||
</div>
|
||||
<icon-picker :value="value.icon" @input="change" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {escape_regex, deep_copy} from 'utilities/object';
|
||||
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
|
||||
|
||||
const FFZ_ICONS = [
|
||||
'zreknarf',
|
||||
'crown',
|
||||
'verified',
|
||||
'inventory',
|
||||
'ignore',
|
||||
'pin-outline',
|
||||
'pin',
|
||||
'block',
|
||||
'ok',
|
||||
'clock',
|
||||
'eye',
|
||||
'eye-off',
|
||||
'trash',
|
||||
'discord',
|
||||
'star',
|
||||
'star-empty',
|
||||
'twitch',
|
||||
'twitter',
|
||||
'download',
|
||||
'upload',
|
||||
'download-cloud',
|
||||
'upload-cloud',
|
||||
'tag',
|
||||
'tags',
|
||||
'retweet',
|
||||
'thumbs-up',
|
||||
'thumbs-down',
|
||||
'bell',
|
||||
'bell-off',
|
||||
'pencil',
|
||||
'info',
|
||||
'help',
|
||||
'calendar',
|
||||
'lock',
|
||||
'lock-open',
|
||||
'arrows-cw',
|
||||
'gift',
|
||||
'eyedropper',
|
||||
'github',
|
||||
'user-secret'
|
||||
];
|
||||
|
||||
const FFZ_ALIASES = {
|
||||
'block': ['ban', 'block'],
|
||||
'ok': ['ok', 'unban', 'untimeout', 'checkmark'],
|
||||
'clock': ['clock', 'clock-o', 'timeout']
|
||||
};
|
||||
|
||||
|
||||
const ICONS = FFZ_ICONS
|
||||
.map(x => [`ffz-i-${x}`, FFZ_ALIASES[x] ? FFZ_ALIASES[x].join(' ') : x])
|
||||
.concat(FA_ICONS.filter(x => ! FFZ_ICONS.includes(x)).map(x => [`ffz-fa fa-${x}`, FA_ALIASES[x] ? FA_ALIASES[x].join(' ') : x]));
|
||||
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
icons: deep_copy(ICONS)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
visible() {
|
||||
if ( ! this.search || ! this.search.length )
|
||||
return this.icons;
|
||||
|
||||
const search = this.search.toLowerCase().replace(' ', '-'),
|
||||
reg = new RegExp('(?:^|-| )' + escape_regex(search), 'i');
|
||||
|
||||
return this.icons.filter(x => reg.test(x[1]));
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
load();
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(val) {
|
||||
this.value.icon = val;
|
||||
|
@ -146,15 +21,4 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ffz-icon-picker {
|
||||
max-height: 15rem;
|
||||
font-size: 1.6rem;
|
||||
|
||||
.ffz-icon {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</script>
|
|
@ -5,6 +5,7 @@
|
|||
// ============================================================================
|
||||
|
||||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
|
||||
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||
|
||||
import GET_CLIP from './clip_info.gql';
|
||||
|
@ -74,13 +75,18 @@ export const Clips = {
|
|||
hide_token: true,
|
||||
|
||||
test(token) {
|
||||
return token.type === 'link' && CLIP_URL.test(token.url)
|
||||
if ( token.type !== 'link' )
|
||||
return false;
|
||||
|
||||
return CLIP_URL.test(token.url) || NEW_CLIP_URL.test(token.url);
|
||||
},
|
||||
|
||||
process(token) {
|
||||
const match = CLIP_URL.exec(token.url),
|
||||
apollo = this.resolve('site.apollo');
|
||||
let match = CLIP_URL.exec(token.url);
|
||||
if ( ! match )
|
||||
match = NEW_CLIP_URL.exec(token.url);
|
||||
|
||||
const apollo = this.resolve('site.apollo');
|
||||
if ( ! apollo || ! match || match[1] === 'create' )
|
||||
return;
|
||||
|
||||
|
|
|
@ -415,7 +415,7 @@ export const BlockedBadges = {
|
|||
|
||||
export const CustomHighlights = {
|
||||
type: 'highlight',
|
||||
priority: 100,
|
||||
priority: 35,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-highlight.vue'),
|
||||
|
||||
|
|
|
@ -319,18 +319,8 @@ export default class Scroller extends Module {
|
|||
}
|
||||
|
||||
inst.ffzHandleKey = inst.ffzHandleKey.bind(inst);
|
||||
|
||||
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keydown');
|
||||
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keyup');
|
||||
|
||||
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keydown');
|
||||
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keyup');
|
||||
|
||||
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keydown');
|
||||
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keyup');
|
||||
|
||||
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keydown');
|
||||
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keyup');
|
||||
window.addEventListener('keydown', inst.ffzHandleKey);
|
||||
window.addEventListener('keyup', inst.ffzHandleKey);
|
||||
|
||||
inst.hoverPause = inst.ffzMouseMove.bind(inst);
|
||||
inst.hoverResume = inst.ffzMouseLeave.bind(inst);
|
||||
|
@ -624,18 +614,8 @@ export default class Scroller extends Module {
|
|||
inst.ffzInstallHandler();
|
||||
}
|
||||
|
||||
onUnmount() { // eslint-disable-line class-methods-use-this
|
||||
const Mousetrap = this.web_munch.getModule('mousetrap') || window.Mousetrap;
|
||||
if ( Mousetrap != null ) {
|
||||
Mousetrap.unbind('alt', 'keydown');
|
||||
Mousetrap.unbind('alt', 'keyup');
|
||||
Mousetrap.unbind('shift', 'keydown');
|
||||
Mousetrap.unbind('shift', 'keyup');
|
||||
Mousetrap.unbind('ctrl', 'keydown');
|
||||
Mousetrap.unbind('ctrl', 'keyup');
|
||||
Mousetrap.unbind('command', 'keydown');
|
||||
Mousetrap.unbind('command', 'keyup');
|
||||
}
|
||||
|
||||
onUnmount(inst) { // eslint-disable-line class-methods-use-this
|
||||
window.removeEventListener('keydown', inst.ffzHandleKey);
|
||||
window.removeEventListener('keyup', inst.ffzHandleKey);
|
||||
}
|
||||
}
|
|
@ -230,14 +230,15 @@ export default class CSSTweaks extends Module {
|
|||
changed: val => this.toggle('square-avatars', !val)
|
||||
});
|
||||
|
||||
this.settings.add('channel.not-live-bar', {
|
||||
this.settings.add('channel.hide-not-live-bar', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Channel > Appearance >> General',
|
||||
title: 'Show notification below clips and videos if the streamer is live.',
|
||||
title: 'Hide the "Not Live" bar.',
|
||||
description: 'Hide the bar which appears beneath clips and videos when the streamer is live, telling you they are live.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggle('not-live-bar', !val)
|
||||
changed: val => this.toggleHide('not-live-bar', val)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -254,7 +255,7 @@ export default class CSSTweaks extends Module {
|
|||
this.toggleHide('top-discover', !this.settings.get('layout.discover'));
|
||||
|
||||
this.toggle('square-avatars', ! this.settings.get('channel.round-avatars'));
|
||||
this.toggleHide('not-live-bar', ! this.settings.get('channel.not-live-bar'));
|
||||
this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar'));
|
||||
|
||||
const recs = this.settings.get('layout.side-nav.show-rec-channels');
|
||||
this.toggleHide('side-rec-channels', recs === 0);
|
||||
|
|
|
@ -85,26 +85,15 @@ export default class Directory extends SiteModule {
|
|||
|
||||
|
||||
this.settings.add('directory.show-channel-avatars', {
|
||||
default: 1,
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Directory > Channels >> Appearance',
|
||||
title: 'Channel Avatars',
|
||||
description: 'Show channel avatars next to stream titles or directly on their thumbnails.',
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'By Title'},
|
||||
{value: 2, title: 'Over Thumbnail (Hidden on Hover)'},
|
||||
{value: 3, title: 'Over Thumbnail'}
|
||||
]
|
||||
title: 'Display channel avatars.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: value => {
|
||||
this.css_tweaks.toggleHide('profile-hover', value === 2);
|
||||
this.DirectoryCard.forceUpdate();
|
||||
}
|
||||
changed: () => this.DirectoryCard.forceUpdate()
|
||||
});
|
||||
|
||||
|
||||
|
@ -225,7 +214,7 @@ export default class Directory extends SiteModule {
|
|||
|
||||
cls.prototype.renderIconicImage = function() {
|
||||
if ( this.props.context !== CARD_CONTEXTS.SingleChannelList &&
|
||||
t.settings.get('directory.show-channel-avatars') !== 1 )
|
||||
! t.settings.get('directory.show-channel-avatars') )
|
||||
return;
|
||||
|
||||
return old_render_iconic.call(this);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="ffz--icon-picker">
|
||||
<div class="tw-full-width">
|
||||
<div class="tw-search-input">
|
||||
<label for="icon-search" class="tw-hide-accessible">
|
||||
<label :for="'icon-search$' + id" class="tw-hide-accessible">
|
||||
{{ t('setting.icon.search', 'Search for Icon') }}
|
||||
</label>
|
||||
<div class="tw-relative tw-mg-t-05">
|
||||
|
@ -10,7 +10,7 @@
|
|||
<figure class="ffz-i-search" />
|
||||
</div>
|
||||
<input
|
||||
id="icon-search"
|
||||
:id="'icon-search$' + id"
|
||||
:placeholder="t('setting.actions.icon.search', 'Search for Icon')"
|
||||
v-model="search"
|
||||
type="search"
|
||||
|
@ -23,13 +23,14 @@
|
|||
</div>
|
||||
|
||||
<simplebar classes="tw-c-background-alt-2 tw-border-l tw-border-r tw-border-b ffz--icon-picker__list tw-mg-b-05">
|
||||
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap" >
|
||||
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap tw-justify-content-between" >
|
||||
<div
|
||||
v-for="i of visible"
|
||||
:key="i[0]"
|
||||
:aria-checked="value.icon === i[0]"
|
||||
:class="{'tw-interactable--selected': value.icon === i[0]}"
|
||||
class="ffz-icon tw-interactable tw-interactable--inverted"
|
||||
:aria-checked="value === i[0]"
|
||||
:class="{'tw-interactable--selected': value === i[0]}"
|
||||
:data-title="i[1]"
|
||||
class="ffz-tooltip ffz-icon tw-interactable tw-interactable--inverted"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
@keydown.space.stop.prevent=""
|
||||
|
@ -50,50 +51,81 @@
|
|||
|
||||
<script>
|
||||
|
||||
let id = 0;
|
||||
|
||||
import {escape_regex, deep_copy} from 'utilities/object';
|
||||
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
|
||||
|
||||
const FFZ_ICONS = [
|
||||
'cancel',
|
||||
'zreknarf',
|
||||
'crown',
|
||||
'verified',
|
||||
'inventory',
|
||||
'ignore',
|
||||
'pin-outline',
|
||||
'pin',
|
||||
'block',
|
||||
'ok',
|
||||
'search',
|
||||
'clock',
|
||||
'eye',
|
||||
'eye-off',
|
||||
'trash',
|
||||
'discord',
|
||||
'star',
|
||||
'star-empty',
|
||||
'twitch',
|
||||
'twitter',
|
||||
'down-dir',
|
||||
'right-dir',
|
||||
'attention',
|
||||
'ok',
|
||||
'cog',
|
||||
'plus',
|
||||
'folder-open',
|
||||
'download',
|
||||
'upload',
|
||||
'download-cloud',
|
||||
'upload-cloud',
|
||||
'floppy',
|
||||
'crown',
|
||||
'verified',
|
||||
'heart',
|
||||
'heart-empty',
|
||||
'tag',
|
||||
'tags',
|
||||
'retweet',
|
||||
'thumbs-up',
|
||||
'thumbs-down',
|
||||
'bell',
|
||||
'bell-off',
|
||||
'pencil',
|
||||
'info',
|
||||
'help',
|
||||
'calendar',
|
||||
'left-dir',
|
||||
'inventory',
|
||||
'lock',
|
||||
'lock-open',
|
||||
'arrows-cw',
|
||||
'ignore',
|
||||
'block',
|
||||
'pin',
|
||||
'pin-outline',
|
||||
'gift',
|
||||
'eyedropper',
|
||||
'discord',
|
||||
'eye',
|
||||
'eye-off',
|
||||
'views',
|
||||
'conversations',
|
||||
'channels',
|
||||
'camera',
|
||||
'cw',
|
||||
'up-dir',
|
||||
'up-big',
|
||||
'link-ext',
|
||||
'twitter',
|
||||
'github',
|
||||
'user-secret'
|
||||
'gauge',
|
||||
'download-cloud',
|
||||
'upload-cloud',
|
||||
'smile',
|
||||
'keyboard',
|
||||
'calendar-empty',
|
||||
'ellipsis-vert',
|
||||
'twitch',
|
||||
'bell-off',
|
||||
'trash',
|
||||
'eyedropper',
|
||||
'user-secret',
|
||||
'window-maximize',
|
||||
'window-minimize',
|
||||
'window-restore',
|
||||
'window-close'
|
||||
];
|
||||
|
||||
const FFZ_ALIASES = {
|
||||
|
@ -112,6 +144,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
id: id++,
|
||||
search: '',
|
||||
icons: deep_copy(ICONS)
|
||||
}
|
||||
|
@ -131,11 +164,19 @@ export default {
|
|||
|
||||
mounted() {
|
||||
load();
|
||||
|
||||
this.$nextTick(() => {
|
||||
if ( this.value ) {
|
||||
const el = this.$el.querySelector('.tw-interactable--selected');
|
||||
if ( el )
|
||||
el.scrollIntoViewIfNeeded();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(val) {
|
||||
this.value.icon = val;
|
||||
this.value = val;
|
||||
this.$emit('input', this.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<a :href="href" @click="reactNavigate(href, $event)">
|
||||
<a :href="href" @click="onClick($event)">
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
@ -7,7 +7,17 @@
|
|||
<script>
|
||||
|
||||
export default {
|
||||
props: ['href']
|
||||
props: ['href', 'click'],
|
||||
|
||||
methods: {
|
||||
onClick(event) {
|
||||
this.$emit('click', event);
|
||||
|
||||
if ( ! event.defaultPrevented )
|
||||
this.reactNavigate(this.href, event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -237,3 +237,9 @@ export const load = () => {
|
|||
}));
|
||||
}
|
||||
|
||||
export const maybeLoad = icon => {
|
||||
if ( loaded || ! String(icon).startsWith('fa-') && ! String(icon).startsWith('ffz-fa') )
|
||||
return;
|
||||
|
||||
load();
|
||||
}
|
|
@ -2,6 +2,28 @@
|
|||
|
||||
const HOP = Object.prototype.hasOwnProperty;
|
||||
|
||||
// Source: https://gist.github.com/jed/982883 (WTFPL)
|
||||
export function generateUUID(input) {
|
||||
return input // if the placeholder was passed, return
|
||||
? ( // a random number from 0 to 15
|
||||
input ^ // unless b is 8,
|
||||
Math.random() // in which case
|
||||
* 16 // a random number from
|
||||
>> input/4 // 8 to 11
|
||||
).toString(16) // in hexadecimal
|
||||
: ( // or otherwise a concatenated string:
|
||||
[1e7] + // 10000000 +
|
||||
-1e3 + // -1000 +
|
||||
-4e3 + // -4000 +
|
||||
-8e3 + // -80000000 +
|
||||
-1e11 // -100000000000,
|
||||
).replace( // replacing
|
||||
/[018]/g, // zeroes, ones, and eights with
|
||||
generateUUID // random hex digits
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function has(object, key) {
|
||||
return object ? HOP.call(object, key) : false;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
import {DEBUG} from 'utilities/constants';
|
||||
|
||||
|
||||
export class Vue extends Module {
|
||||
|
@ -18,12 +19,15 @@ export class Vue extends Module {
|
|||
|
||||
async onLoad() {
|
||||
const Vue = window.ffzVue = this.Vue = (await import(/* webpackChunkName: "vue" */ 'vue')).default,
|
||||
ObserveVisibility = await import(/* webpackChunkName: "vue" */ 'vue-observe-visibility'),
|
||||
RavenVue = await import(/* webpackChunkName: "vue" */ 'raven-js/plugins/vue'),
|
||||
components = this._components;
|
||||
|
||||
this.component((await import(/* webpackChunkName: "vue" */ 'src/std-components/index.js')).default);
|
||||
|
||||
if ( this.root.raven )
|
||||
Vue.use(ObserveVisibility);
|
||||
|
||||
if ( ! DEBUG && this.root.raven )
|
||||
this.root.raven.addPlugin(RavenVue, Vue);
|
||||
|
||||
for(const key in components)
|
||||
|
@ -82,6 +86,11 @@ export class Vue extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.on('i18n:transform', () => {
|
||||
this._vue_i18n.locale = this.i18n.locale;
|
||||
this._vue_i18n.phrases = {};
|
||||
});
|
||||
|
||||
this.on('i18n:changed', () => {
|
||||
this._vue_i18n.locale = this.i18n.locale;
|
||||
this._vue_i18n.phrases = {};
|
||||
|
@ -97,6 +106,41 @@ export class Vue extends Module {
|
|||
vue.prototype.$i18n = this._vue_i18n;
|
||||
}
|
||||
|
||||
vue.component('t-list', {
|
||||
props: {
|
||||
tag: {
|
||||
required: false
|
||||
},
|
||||
phrase: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
default: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
return createElement(
|
||||
this.tag || 'span',
|
||||
this.$i18n.tList_(
|
||||
this.phrase,
|
||||
this.default,
|
||||
Object.assign({}, this.data, this.$scopedSlots)
|
||||
).map(out => {
|
||||
if ( typeof out === 'function' )
|
||||
return out();
|
||||
return out;
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
vue.mixin({
|
||||
methods: {
|
||||
reactNavigate(url, event) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue