mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-31 15:08:31 +00:00
4.0.0 Beta 1
This commit is contained in:
parent
c2688646af
commit
262757a20d
187 changed files with 22878 additions and 38882 deletions
50
src/modules/main_menu/components/changelog.vue
Normal file
50
src/modules/main_menu/components/changelog.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--changelog border-t pd-t-1">
|
||||
<div class="align-center">
|
||||
<h2>{{ t('home.changelog', 'Changelog') }}</h2>
|
||||
</div>
|
||||
|
||||
<div ref="changes" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
fetch(url, container) {
|
||||
const done = data => {
|
||||
if ( ! data )
|
||||
data = 'There was an error loading this page from the server.';
|
||||
|
||||
container.innerHTML = data;
|
||||
|
||||
const btn = container.querySelector('#ffz-old-news-button');
|
||||
if ( btn )
|
||||
btn.addEventListener('click', () => {
|
||||
btn.parentElement.removeChild(btn);
|
||||
const old_news = container.querySelector('#ffz-old-news');
|
||||
if ( old_news )
|
||||
this.fetch(`${SERVER}/script/old_changes.html`, old_news);
|
||||
});
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(resp => resp.ok ? resp.text() : null)
|
||||
.then(done)
|
||||
.catch(err => done(null));
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetch(`${SERVER}/script/changelog.html`, this.$refs.changes);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
35
src/modules/main_menu/components/feedback-page.vue
Normal file
35
src/modules/main_menu/components/feedback-page.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--home border-t pd-t-1">
|
||||
<h2>Feedback</h2>
|
||||
|
||||
<div class="mg-y-1 c-background-accent c-text-overlay pd-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
Please keep in mind that FrankerFaceZ v4 is under heavy development.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Okay, still here? Great! You can provide feedback and bug reports by
|
||||
<a href="https://github.com/FrankerFaceZ/FrankerFaceZ/issues" target="_blank" rel="noopener">
|
||||
opening an issue at our GitHub repository</a>.
|
||||
|
||||
You can also <a href="https://twitter.com/FrankerFaceZ" target="_blank" rel="noopener">
|
||||
tweet at us</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When creating a GitHub issue, please check that someone else hasn't
|
||||
already created one for what you'd like to discuss or report.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
}
|
||||
</script>
|
67
src/modules/main_menu/components/filter-editor.vue
Normal file
67
src/modules/main_menu/components/filter-editor.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--filter-editor">
|
||||
<div ref="list" class="ffz--rule-list">
|
||||
<section v-for="(rule, idx) in rules">
|
||||
<div
|
||||
class="ffz--rule elevation-1 c-background border mg-b-05 pd-y-05 pd-r-1 flex flex--nowrap align-items-start"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-y-1">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 pd-y-05">
|
||||
Channel
|
||||
</div>
|
||||
|
||||
<div class="mg-x-1 flex flex-grow-1">
|
||||
<div class="flex-shrink-0 mg-r-1">
|
||||
<select class="tw-select">
|
||||
<option>is one of</option>
|
||||
<option>is not one of</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<input
|
||||
type="text"
|
||||
class="tw-input"
|
||||
value="SirStendec"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center">
|
||||
<button class="tw-button tw-button--text" @click="del(idx)">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.filters.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-button tw-button--hollow mg-y-1 full-width"
|
||||
@click="newRule"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('', 'Add New Rule') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['filters', 'rules', 'context'],
|
||||
|
||||
methods: {
|
||||
newRule() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
100
src/modules/main_menu/components/home-page.vue
Normal file
100
src/modules/main_menu/components/home-page.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--home flex flex--nowrap">
|
||||
<div class="flex-grow-1">
|
||||
<div class="align-center">
|
||||
<h1 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h1>
|
||||
<span class="c-text-alt">
|
||||
{{ t('home.tag-line', 'The Twitch Enhancement Suite') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section class="pd-t-1 border-t mg-t-1">
|
||||
<h2>Welcome to the v4.0 Beta</h2>
|
||||
|
||||
<p>
|
||||
This is the initial, beta release of FrankerFaceZ v4.0 with support
|
||||
for the Twitch website rewrite.
|
||||
|
||||
As you'll notice, this release is <strong>not</strong> complete.
|
||||
There are missing features. There are bugs. If you are a moderator,
|
||||
you will want to just keep opening a Legacy Chat Popout for now.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
FrankerFaceZ v4.0 is still under heavy development and there will
|
||||
be significant changes and improvements in the coming weeks. For
|
||||
now, here are some of the bigger issues:
|
||||
</p>
|
||||
|
||||
<ul class="mg-b-2">
|
||||
<li>Settings from the old version are not being imported.</li>
|
||||
<li>Settings cannot be searched.</li>
|
||||
<li>FFZ badges do not display.</li>
|
||||
<li>Oh god everything is missing.</li>
|
||||
<li>FFZ:AP is broken.</li>
|
||||
<li>Uptime breaks occasionally.</li>
|
||||
</ul>
|
||||
|
||||
<p>And the biggest features still under development:</p>
|
||||
|
||||
<ul class="mg-b-2">
|
||||
<li>Dark Theme (Pls No Purple)</li>
|
||||
<li>Chat Pause on Hover</li>
|
||||
<li>Badge Customization</li>
|
||||
<li>Emoji Rendering</li>
|
||||
<li>Emotes Menu</li>
|
||||
<li>Chat Filtering (Highlighted Words, etc.)</li>
|
||||
<li>Room Status Indicators</li>
|
||||
<li>Custom Mod Cards</li>
|
||||
<li>Custom Mod Actions</li>
|
||||
<li>Chat Room Tabs</li>
|
||||
<li>Recent Highlights</li>
|
||||
<li>More Channel Metadata</li>
|
||||
<li>Disable Hosting</li>
|
||||
<li>Portrait Mode</li>
|
||||
<li>Hiding stuff in the directory</li>
|
||||
<li>Directory Host Stacking</li>
|
||||
<li>Basically anything to do with the directory</li>
|
||||
<li>Importing and exporting settings</li>
|
||||
<li>User Aliases</li>
|
||||
<li>Rich Content in Chat (aka Clip Embeds)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
For a possibly more up-to-date list of what I'm working on,
|
||||
please consult <a href="https://trello.com/b/LGcYPFwi/frankerfacez-v4" target="_blank">this Trello board</a>.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="mg-l-1 flex-shrink-0 tweet-column">
|
||||
<a class="twitter-timeline" data-width="300" data-theme="dark" href="https://twitter.com/FrankerFaceZ?ref_src=twsrc%5Etfw">
|
||||
Tweets by FrankerFaceZ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
mounted() {
|
||||
let el;
|
||||
document.head.appendChild(el = e('script', {
|
||||
id: 'ffz--twitter-widget-script',
|
||||
async: true,
|
||||
charset: 'utf-8',
|
||||
src: 'https://platform.twitter.com/widgets.js',
|
||||
onLoad: () => el.parentElement.removeChild(el)
|
||||
}));
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
180
src/modules/main_menu/components/main-menu.vue
Normal file
180
src/modules/main_menu/components/main-menu.vue
Normal file
|
@ -0,0 +1,180 @@
|
|||
<template lang="html">
|
||||
<div class="ffz-main-menu elevation-3 c-background-alt border flex flex--nowrap flex-column" :class="{ maximized }">
|
||||
<header class="c-background pd-1 pd-l-2 full-width align-items-center flex flex-nowrap" @dblclick="resize">
|
||||
<h3 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h3>
|
||||
<div class="flex-grow-1 pd-x-2">
|
||||
<!--div class="tw-search-input">
|
||||
<label for="ffz-main-menu.search" class="hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label>
|
||||
<div class="relative">
|
||||
<div class="tw-input__icon-group">
|
||||
<div class="tw-input__icon">
|
||||
<figure class="ffz-i-search" />
|
||||
</div>
|
||||
</div>
|
||||
<input type="search" class="tw-input tw-input--icon-left" :placeholder="t('main-menu.search', 'Search Settings')" autocapitalize="off" autocorrect="off" autocomplete="off" id="ffz-main-menu.search">
|
||||
</div>
|
||||
</div-->
|
||||
</div>
|
||||
<button class="tw-button-icon mg-x-05" @click="resize">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure :class="{'ffz-i-window-maximize': !maximized, 'ffz-i-window-restore': maximized}" />
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button-icon mg-x-05" @click="close">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-window-close" />
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
<section class="border-t full-height full-width flex flex--nowrap">
|
||||
<nav class="ffz-vertical-nav c-background-alt-2 border-r full-height flex flex-column flex-shrink-0 flex-nowrap">
|
||||
<header class="border-b pd-1">
|
||||
<profile-selector
|
||||
:context="context"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</header>
|
||||
<div class="full-width full-height overflow-hidden flex flex-nowrap relative">
|
||||
<div class="ffz-vertical-nav__items full-width flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-tree
|
||||
:currentItem="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="c-text-alt border-t pd-1">
|
||||
<div>
|
||||
{{ t('main-menu.version', 'Version %{version}', {version: version.toString()}) }}
|
||||
</div>
|
||||
<div class="c-text-alt-2">
|
||||
{{version.build}}
|
||||
</div>
|
||||
</footer>
|
||||
</nav>
|
||||
<main class="flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-page
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
v-if="currentItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import displace from 'displacejs';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return this.$vnode.data;
|
||||
},
|
||||
|
||||
created() {
|
||||
this.context.context._add_user();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.context.context._remove_user();
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeProfile() {
|
||||
const new_id = this.$refs.profiles.value,
|
||||
new_profile = this.context.profiles[new_id];
|
||||
|
||||
if ( new_profile )
|
||||
this.context.currentProfile = new_profile;
|
||||
},
|
||||
|
||||
changeItem(item) {
|
||||
if ( this.$refs.page && this.$refs.page.onBeforeChange ) {
|
||||
if ( this.$refs.page.onBeforeChange(this.currentItem, item) === false )
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentItem = item;
|
||||
let current = item;
|
||||
while(current = current.parent)
|
||||
current.expanded = true;
|
||||
},
|
||||
|
||||
updateDrag() {
|
||||
if ( this.maximized )
|
||||
this.destroyDrag();
|
||||
else
|
||||
this.createDrag();
|
||||
},
|
||||
|
||||
destroyDrag() {
|
||||
if ( this.displace ) {
|
||||
this.displace.destroy();
|
||||
this.displace = null;
|
||||
}
|
||||
},
|
||||
|
||||
createDrag() {
|
||||
this.$nextTick(() => {
|
||||
if ( ! this.maximized )
|
||||
this.displace = displace(this.$el, {
|
||||
handle: this.$el.querySelector('header'),
|
||||
highlightInputs: true,
|
||||
constrain: true
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if ( this.displace )
|
||||
this.displace.reinit();
|
||||
},
|
||||
|
||||
navigate(key) {
|
||||
let item = this.nav_keys[key];
|
||||
while(item && item.page)
|
||||
item = item.parent;
|
||||
|
||||
if ( ! item )
|
||||
return;
|
||||
|
||||
this.changeItem(item);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
maximized() {
|
||||
this.updateDrag();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateDrag();
|
||||
|
||||
this._on_resize = this.handleResize.bind(this);
|
||||
window.addEventListener('resize', this._on_resize);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.destroyDrag();
|
||||
|
||||
if ( this._on_resize ) {
|
||||
window.removeEventListener('resize', this._on_resize);
|
||||
this._on_resize = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
34
src/modules/main_menu/components/menu-container.vue
Normal file
34
src/modules/main_menu/components/menu-container.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template lang="html">
|
||||
<div v-bind:class="classes" v-if="item.contents">
|
||||
<header v-if="! item.no_header">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</header>
|
||||
<section
|
||||
v-if="item.description"
|
||||
v-html="t(item.desc_i18n_key, item.description, item)"
|
||||
class="pd-b-1"
|
||||
/>
|
||||
<component
|
||||
v-for="i in item.contents"
|
||||
v-bind:is="i.component"
|
||||
:context="context"
|
||||
:item="i"
|
||||
:key="i.full_key"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
computed: {
|
||||
classes() {
|
||||
return [
|
||||
'ffz--menu-container',
|
||||
this.item.full_box ? 'border' : 'border-t'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
89
src/modules/main_menu/components/menu-page.vue
Normal file
89
src/modules/main_menu/components/menu-page.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--menu-page">
|
||||
<header class="mg-b-1">
|
||||
<template v-for="i in breadcrumbs">
|
||||
<a v-if="i !== item" href="#" @click="$emit('change-item', i, false)">{{ t(i.i18n_key, i.title, i) }}</a>
|
||||
<strong v-if="i === item">{{ t(i.i18n_key, i.title, i) }}</strong>
|
||||
<template v-if="i !== item">» </template>
|
||||
</template>
|
||||
</header>
|
||||
<section v-if="! context.currentProfile.live && item.profile_warning !== false" class="border-t pd-t-1 pd-b-2">
|
||||
<div class="c-background-accent c-text-overlay pd-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.profiles.inactive', "This profile isn't active.") }}
|
||||
</h3>
|
||||
|
||||
{{ t(
|
||||
'setting.profiles.inactive.description',
|
||||
"This profile's rules don't match the current context and it therefore isn't currently active, so you " +
|
||||
"won't see changes you make here reflected on Twitch."
|
||||
) }}
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="border-t pd-y-1"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</section>
|
||||
<template v-if="! item.contents">
|
||||
<ul class="border-t pd-y-1">
|
||||
<li class="pd-x-1" v-for="i in item.items">
|
||||
<a href="#" @click="$emit('change-item', i, false)">
|
||||
{{ t(i.i18n_key, i.title, i) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<component
|
||||
v-for="i in item.contents"
|
||||
v-bind:is="i.component"
|
||||
ref="children"
|
||||
:context="context"
|
||||
:item="i"
|
||||
:key="i.full_key"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
const out = [];
|
||||
let current = this.item;
|
||||
while(current) {
|
||||
out.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeItem(item) {
|
||||
this.$emit('change-item', item);
|
||||
},
|
||||
|
||||
navigate(...args) {
|
||||
this.$emit('navigate', ...args);
|
||||
},
|
||||
|
||||
onBeforeChange(current, new_item) {
|
||||
for(const child of this.$refs.children)
|
||||
if ( child && child.onBeforeChange ) {
|
||||
const res = child.onBeforeChange(current, new_item);
|
||||
if ( res !== undefined )
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
165
src/modules/main_menu/components/menu-tree.vue
Normal file
165
src/modules/main_menu/components/menu-tree.vue
Normal file
|
@ -0,0 +1,165 @@
|
|||
<template lang="html">
|
||||
<ul
|
||||
v-if="modal"
|
||||
class="ffz--menu-tree"
|
||||
:role="[root ? 'group' : 'tree']"
|
||||
:tabindex="tabIndex"
|
||||
@keyup.up="prevItem"
|
||||
@keyup.down="nextItem"
|
||||
@keyup.left="prevLevel"
|
||||
@keyup.right="nextLevel"
|
||||
@keyup.*="expandAll"
|
||||
>
|
||||
<li
|
||||
v-for="item in modal"
|
||||
:key="item.full_key"
|
||||
:class="[currentItem === item ? 'active' : '']"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="flex__item flex flex--nowrap align-items-center pd-y-05 pd-r-05"
|
||||
|
||||
role="treeitem"
|
||||
:aria-expanded="item.expanded"
|
||||
:aria-selected="currentItem === item"
|
||||
@click="clickItem(item)"
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
class="arrow"
|
||||
:class="[
|
||||
item.items ? '' : 'ffz--invisible',
|
||||
item.expanded ? 'ffz-i-down-dir' : 'ffz-i-right-dir'
|
||||
]"
|
||||
/>
|
||||
<span class="flex-grow-1">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</span>
|
||||
<span v-if="item.pill" class="pill">
|
||||
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill, item) : item.pill }}
|
||||
</span>
|
||||
</div>
|
||||
<menu-tree
|
||||
:root="item"
|
||||
:currentItem="currentItem"
|
||||
:modal="item.items"
|
||||
v-if="item.items && item.expanded"
|
||||
@change-item="i => $emit('change-item', i)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
function findLastVisible(node) {
|
||||
if ( node.expanded && node.items )
|
||||
return findLastVisible(node.items[node.items.length - 1]);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
function findNextVisible(node, modal) {
|
||||
const items = node.parent ? node.parent.items : modal,
|
||||
idx = items.indexOf(node);
|
||||
|
||||
if ( items[idx + 1] )
|
||||
return items[idx+1];
|
||||
|
||||
if ( node.parent )
|
||||
return findNextVisible(node.parent, modal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function recursiveExpand(node) {
|
||||
node.expanded = true;
|
||||
if ( node.items )
|
||||
for(const item of node.items)
|
||||
recursiveExpand(item);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
props: ['root', 'modal', 'currentItem'],
|
||||
|
||||
computed: {
|
||||
tabIndex() {
|
||||
return this.root ? undefined : 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickItem(item) {
|
||||
if ( ! item.expanded )
|
||||
item.expanded = true;
|
||||
else if ( this.currentItem === item )
|
||||
item.expanded = false;
|
||||
|
||||
this.$emit('change-item', item);
|
||||
},
|
||||
|
||||
expandAll() {
|
||||
for(const item of this.modal)
|
||||
recursiveExpand(item);
|
||||
},
|
||||
|
||||
prevItem() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem,
|
||||
items = i.parent ? i.parent.items : this.modal,
|
||||
idx = items.indexOf(i);
|
||||
|
||||
if ( idx > 0 )
|
||||
this.$emit('change-item', findLastVisible(items[idx-1]));
|
||||
|
||||
else if ( i.parent )
|
||||
this.$emit('change-item', i.parent);
|
||||
},
|
||||
|
||||
nextItem(e) {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
let target;
|
||||
|
||||
if ( i.expanded && i.items )
|
||||
target = i.items[0];
|
||||
|
||||
else
|
||||
target = findNextVisible(i, this.modal);
|
||||
|
||||
if ( target )
|
||||
this.$emit('change-item', target);
|
||||
},
|
||||
|
||||
prevLevel() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
|
||||
if ( i.expanded && i.items )
|
||||
i.expanded = false;
|
||||
else if ( i.parent )
|
||||
this.$emit('change-item', i.parent);
|
||||
},
|
||||
|
||||
nextLevel() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
if ( i.expanded && i.items )
|
||||
this.$emit('change-item', i.items[0]);
|
||||
else
|
||||
i.expanded = true;
|
||||
|
||||
if ( event.ctrlKey )
|
||||
recursiveExpand(this.currentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
205
src/modules/main_menu/components/profile-editor.vue
Normal file
205
src/modules/main_menu/components/profile-editor.vue
Normal file
|
@ -0,0 +1,205 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--profile-editor">
|
||||
<div class="flex align-items-center border-t pd-1">
|
||||
<div class="flex-grow-1"></div>
|
||||
<button
|
||||
class="tw-button tw-button--text"
|
||||
@click="save"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('settings.profiles.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="mg-l-1 tw-button tw-button--text"
|
||||
:disabled="item.profile && context.profiles.length < 2"
|
||||
@click="del"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.profiles.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-download">
|
||||
{{ t('setting.profiles.export', 'Export') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.general', 'General') }}
|
||||
</header>
|
||||
|
||||
<div class="ffz--widget flex flex--nowrap">
|
||||
<label for="ffz:editor:name">
|
||||
{{ t('settings.data_management.profiles.edit.name', 'Name') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="tw-input"
|
||||
ref="name"
|
||||
id="ffz:editor:name"
|
||||
v-model="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ffz--widget flex flex--nowrap">
|
||||
<label for="ffz:editor:description">
|
||||
{{ t('settings.data_management.profiles.edit.desc', 'Description') }}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
class="tw-input"
|
||||
ref="desc"
|
||||
id="ffz:editor:description"
|
||||
v-model="desc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.rules', 'Rules') }}
|
||||
</header>
|
||||
<section class="pd-b-1">
|
||||
{{ t(
|
||||
'settings.data_management.profiles.edit.rules.description',
|
||||
'Rules allows you to define a series of conditions under which this profile will be active.'
|
||||
) }}
|
||||
</section>
|
||||
|
||||
<filter-editor
|
||||
:filters="filters"
|
||||
:rules="rules"
|
||||
:context="test_context"
|
||||
@change="unsaved = true"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
old_name: null,
|
||||
old_desc: null,
|
||||
|
||||
name: null,
|
||||
desc: null,
|
||||
unsaved: false,
|
||||
|
||||
filters: null,
|
||||
rules: null,
|
||||
test_context: null
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.context.context.on('context_changed', this.updateContext, this);
|
||||
this.updateContext();
|
||||
this.revert();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.context.context.off('context_changed', this.updateContext, this);
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
name() {
|
||||
if ( this.name !== this.old_name )
|
||||
this.unsaved = true;
|
||||
},
|
||||
|
||||
desc() {
|
||||
if ( this.desc !== this.old_desc )
|
||||
this.unsaved = true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
revert() {
|
||||
const profile = this.item.profile;
|
||||
|
||||
this.old_name = this.name = profile ?
|
||||
profile.i18n_key ?
|
||||
this.t(profile.i18n_key, profile.title, profile) :
|
||||
profile.title :
|
||||
'Unnamed Profile',
|
||||
|
||||
this.old_desc = this.desc = profile ?
|
||||
profile.desc_i18n_key ?
|
||||
this.t(profile.desc_i18n_key, profile.description, profile) :
|
||||
profile.description :
|
||||
'';
|
||||
|
||||
this.rules = profile ? profile.context : {};
|
||||
this.unsaved = ! profile;
|
||||
},
|
||||
|
||||
del() {
|
||||
if ( this.item.profile || this.unsaved ) {
|
||||
if ( ! confirm(this.t(
|
||||
'settings.profiles.warn-delete',
|
||||
'Are you sure you wish to delete this profile? It cannot be undone.'
|
||||
)) )
|
||||
return
|
||||
|
||||
if ( this.item.profile )
|
||||
this.context.deleteProfile(this.item.profile);
|
||||
}
|
||||
|
||||
this.unsaved = false;
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
save() {
|
||||
if ( ! this.item.profile ) {
|
||||
this.item.profile = this.context.createProfile({
|
||||
name: this.name,
|
||||
description: this.desc
|
||||
});
|
||||
|
||||
} else if ( this.unsaved ) {
|
||||
const changes = {
|
||||
name: this.name,
|
||||
description: this.desc
|
||||
};
|
||||
|
||||
// Disable i18n if required.
|
||||
if ( this.name !== this.old_name )
|
||||
changes.i18n_key = undefined;
|
||||
|
||||
if ( this.desc !== this.old_desc )
|
||||
changes.desc_i18n_key = undefined;
|
||||
|
||||
this.item.profile.update(changes);
|
||||
}
|
||||
|
||||
this.unsaved = false;
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
updateContext() {
|
||||
this.test_context = this.context.context.context;
|
||||
},
|
||||
|
||||
onBeforeChange() {
|
||||
if ( this.unsaved )
|
||||
return confirm(
|
||||
this.t(
|
||||
'settings.warn-unsaved',
|
||||
'You have unsaved changes. Are you sure you want to leave the editor?'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
129
src/modules/main_menu/components/profile-manager.vue
Normal file
129
src/modules/main_menu/components/profile-manager.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--profile-manager border-t pd-y-1">
|
||||
<div class="c-background-accent c-text-overlay pd-1 mg-b-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
This feature is not yet finished.
|
||||
</h3>
|
||||
|
||||
Creating and editing profiles is disabled until the rule editor is finished.
|
||||
</div>
|
||||
<div class="flex align-items-center pd-b-05">
|
||||
<div class="flex-grow-1">
|
||||
{{ t('setting.profiles.drag', 'Drag profiles to change their priority.') }}
|
||||
</div>
|
||||
<button class="mg-l-1 tw-button tw-button--text" disabled @notclick="edit()">
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('setting.profiles.new', 'New Profile') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.profiles.import', 'Import…') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div ref="list" class="ffz--profile-list">
|
||||
<section
|
||||
v-for="p in context.profiles"
|
||||
:key="p.id"
|
||||
:data-profile="p.id"
|
||||
>
|
||||
<div
|
||||
class="ffz--profile elevation-1 c-background border pd-y-05 pd-r-1 mg-y-05 flex flex--nowrap"
|
||||
:class="{live: p.live}"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-t-1 pd-b-05">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
|
||||
<div v-if="p.description" class="description">
|
||||
{{ t(p.desc_i18n_key, p.description, p) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center">
|
||||
<button class="tw-button tw-button--text" disabled @notclick="edit(p)">
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.profiles.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center border-l mg-l-1 pd-l-1">
|
||||
<div v-if="p.live" class="ffz--profile__icon ffz-i-ok tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is active.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="! p.live" class="ffz--profile__icon ffz-i-cancel tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.inactive', 'This profile is not active.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
edit(profile) {
|
||||
const item = {
|
||||
full_key: 'data_management.profiles.edit_profile',
|
||||
key: 'edit_profile',
|
||||
|
||||
profile_warning: false,
|
||||
|
||||
title: `Edit Profile`,
|
||||
i18n_key: 'setting.data_management.profiles.edit_profile',
|
||||
parent: this.item.parent,
|
||||
|
||||
contents: [{
|
||||
page: true,
|
||||
profile,
|
||||
component: 'profile-editor'
|
||||
}]
|
||||
};
|
||||
|
||||
item.contents[0].parent = item;
|
||||
this.$emit('change-item', item);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this._sortable = Sortable.create(this.$refs.list, {
|
||||
draggable: 'section',
|
||||
filter: 'button',
|
||||
|
||||
onUpdate: (event) => {
|
||||
const id = event.item.dataset.profile,
|
||||
profile = this.context.profile_keys[id];
|
||||
|
||||
if ( profile )
|
||||
profile.move(event.newIndex);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if ( this._sortable )
|
||||
this._sortable.destroy();
|
||||
|
||||
this._sortable = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
218
src/modules/main_menu/components/profile-selector.vue
Normal file
218
src/modules/main_menu/components/profile-selector.vue
Normal file
|
@ -0,0 +1,218 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--profile-selector">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="tw-select"
|
||||
:class="{active: opened}"
|
||||
ref="button"
|
||||
@keyup.up.stop.prevent="focusShow"
|
||||
@keyup.left.stop.prevent="focusShow"
|
||||
@keyup.down.stop.prevent="focusShow"
|
||||
@keyup.right.stop.prevent="focusShow"
|
||||
@keyup.enter="focusShow"
|
||||
@keyup.space="focusShow"
|
||||
@click="togglePopup"
|
||||
>
|
||||
{{ t(context.currentProfile.i18n_key, context.currentProfile.title, context.currentProfile) }}
|
||||
</div>
|
||||
<div v-if="opened" v-on-clickaway="hide" class="tw-balloon block tw-balloon--lg tw-balloon--down tw-balloon--left">
|
||||
<div
|
||||
class="ffz--profile-list elevation-2 c-background-alt"
|
||||
@keyup.escape="focusHide"
|
||||
@focusin="focus"
|
||||
@focusout="blur"
|
||||
>
|
||||
<div class="scrollable-area border-b" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content" ref="popup">
|
||||
<div
|
||||
v-for="(p, idx) in context.profiles"
|
||||
tabindex="0"
|
||||
class="ffz--profile-row relative border-b pd-y-05 pd-r-3 pd-l-1"
|
||||
:class="{
|
||||
live: p.live,
|
||||
current: p === context.currentProfile
|
||||
}"
|
||||
@keydown.up.stop.prevent=""
|
||||
@keydown.down.stop.prevent=""
|
||||
@keydown.page-up.stop.prevent=""
|
||||
@keydown.page-down.stop.prevent=""
|
||||
@keyup.up.stop="prevItem"
|
||||
@keyup.down.stop="nextItem"
|
||||
@keyup.home="firstItem"
|
||||
@keyup.end="lastItem"
|
||||
@keyup.page-up.stop="prevPage"
|
||||
@keyup.page-down.stop="nextPage"
|
||||
@keyup.enter="changeProfile(p)"
|
||||
@click="changeProfile(p)"
|
||||
>
|
||||
<div
|
||||
v-if="p.live"
|
||||
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok absolute"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is active.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
|
||||
<div v-if="p.description" class="description">
|
||||
{{ t(p.desc_i18n_key, p.description, p) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pd-y-05 pd-x-05 align-right">
|
||||
<button class="tw-button tw-button--text" @click="openConfigure">
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.profiles.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
const indexOf = Array.prototype.indexOf;
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
props: ['context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
opened: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openConfigure() {
|
||||
this.hide();
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
focus() {
|
||||
this._focused = true;
|
||||
},
|
||||
|
||||
blur() {
|
||||
this._focused = false;
|
||||
if ( ! this._blur_timer )
|
||||
this._blur_timer = setTimeout(() => {
|
||||
this._blur_timer = null;
|
||||
if ( ! this._focused && document.hasFocus() )
|
||||
this.hide();
|
||||
}, 10);
|
||||
},
|
||||
|
||||
|
||||
hide() {
|
||||
this.opened = false;
|
||||
},
|
||||
|
||||
show() {
|
||||
if ( ! this.opened )
|
||||
this.opened = true;
|
||||
},
|
||||
|
||||
togglePopup() {
|
||||
if ( this.opened )
|
||||
this.hide();
|
||||
else
|
||||
this.show();
|
||||
},
|
||||
|
||||
|
||||
focusHide() {
|
||||
this.hide();
|
||||
this.$refs.button.focus();
|
||||
},
|
||||
|
||||
focusShow() {
|
||||
this.show();
|
||||
this.$nextTick(() => this.$refs.popup.querySelector('.current').focus());
|
||||
},
|
||||
|
||||
prevItem(e) {
|
||||
const el = e.target.previousSibling;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
nextItem(e) {
|
||||
const el = e.target.nextSibling;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
firstItem() {
|
||||
const el = this.$refs.popup.firstElementChild;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
prevPage(e) {
|
||||
this.select(indexOf.call(this.$refs.popup.children, e.target) - 5);
|
||||
},
|
||||
|
||||
nextPage(e) {
|
||||
this.select(indexOf.call(this.$refs.popup.children, e.target) + 5);
|
||||
},
|
||||
|
||||
select(idx) {
|
||||
const kids = this.$refs.popup.children,
|
||||
el = kids[idx <= 0 ? 0 : Math.min(idx, kids.length - 1)];
|
||||
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
lastItem() {
|
||||
const el = this.$refs.popup.lastElementChild;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
scroll(el) {
|
||||
const scroller = this.$refs.popup.parentElement,
|
||||
|
||||
top = el.offsetTop,
|
||||
bottom = el.offsetHeight + top,
|
||||
|
||||
// We need to use the margin-bottom because of the scrollbar library.
|
||||
// In fact, the scrollbar library is why any of this function exists.
|
||||
scroll_top = scroller.scrollTop,
|
||||
scroll_bottom = scroller.offsetHeight + parseInt(scroller.style.marginBottom || 0, 10) + scroll_top;
|
||||
|
||||
if ( top < scroll_top )
|
||||
scroller.scrollBy(0, top - scroll_top);
|
||||
|
||||
else if ( bottom > scroll_bottom )
|
||||
scroller.scrollBy(0, bottom - scroll_bottom);
|
||||
},
|
||||
|
||||
changeProfile(profile) {
|
||||
this.context.currentProfile = profile;
|
||||
this.focusHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
58
src/modules/main_menu/components/setting-check-box.vue
Normal file
58
src/modules/main_menu/components/setting-check-box.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--checkbox" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
:checked="value"
|
||||
@change="onChange"
|
||||
>
|
||||
|
||||
<label class="tw-checkbox__label" :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
style="padding-left:2.2rem"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
this.set(this.$refs.control.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
44
src/modules/main_menu/components/setting-hotkey.vue
Normal file
44
src/modules/main_menu/components/setting-hotkey.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--hotkey-input">
|
||||
<label
|
||||
:for="item.full_key"
|
||||
v-html="t(item.i18n_key, item.title, item)"
|
||||
/>
|
||||
<div class="relative">
|
||||
<div class="tw-input__icon-group tw-input__icon-group--right">
|
||||
<div class="tw-input__icon">
|
||||
<figure class="ffz-i-keyboard" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
type="text"
|
||||
class="mg-05 tw-input tw-input--icon-right"
|
||||
ref="display"
|
||||
:id="item.full_key"
|
||||
tabindex="0"
|
||||
@keyup="onKey"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
v-if="item.description"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onKey(e) {
|
||||
const name = `${e.ctrlKey ? 'Ctrl-' : ''}${e.shiftKey ? 'Shift-' : ''}${e.altKey ? 'Alt-' : ''}${e.code}`;
|
||||
this.$refs.display.innerText = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
27
src/modules/main_menu/components/setting-radio-buttons.vue
Normal file
27
src/modules/main_menu/components/setting-radio-buttons.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template lang="html">
|
||||
<div class="atw-input">
|
||||
<header>
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</header>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
<div v-for="(i, idx) in data" class="mg-l-1">
|
||||
<input type="radio" :name="item.full_key" :id="item.full_key + idx" :value="i.value" class="tw-radio__input">
|
||||
<label :for="item.full_key + idx" class="pd-y-05 tw-radio__label">{{ t(i.i18n_key, i.title, i) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context']
|
||||
}
|
||||
|
||||
</script>
|
64
src/modules/main_menu/components/setting-select-box.vue
Normal file
64
src/modules/main_menu/components/setting-select-box.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--select-box" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<label :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
class="mg-05 tw-select display-inline width-auto"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
@change="onChange"
|
||||
>
|
||||
<option v-for="i in data" :selected="i.value === value">
|
||||
{{ i.i18n_key ? t(i.i18n_key, i.title, i) : i.title }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
const idx = this.$refs.control.selectedIndex,
|
||||
raw_value = this.data[idx];
|
||||
|
||||
if ( raw_value )
|
||||
this.set(raw_value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
59
src/modules/main_menu/components/setting-text-box.vue
Normal file
59
src/modules/main_menu/components/setting-text-box.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--text-box" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<label :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="mg-05 tw-input display-inline width-auto"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
@change="onChange"
|
||||
:value="value"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
const value = this.$refs.control.value;
|
||||
if ( value != null )
|
||||
this.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue