1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-07 03:28:31 +00:00
* Fixed: Unable to see in-line chat action context menus in theater mode.
* Changed: Add a new socket server to the list to take pressure off the others.
* API Added: `debounce` method in `FrankerFaceZ.utilities.object`.
* API Added: `<autocomplete>` Vue component for implementing text fields with auto-completion.
* API Changed: Update localized Vue strings immediately when the i18n debug transformation changes.
* API Changed: `<icon-picker />` now has a closed and open state. It doesn't always show the drawer of icons.
* API Changed: Include the `vue-clickaway` mixin in everything.
This commit is contained in:
SirStendec 2019-06-09 19:48:26 -04:00
parent aa25bff498
commit 21ee6fcfb7
23 changed files with 667 additions and 100 deletions

View file

@ -1,51 +1,72 @@
<template lang="html">
<div class="ffz--icon-picker">
<div class="tw-full-width">
<div class="tw-search-input">
<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">
<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$' + id"
: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 v-on-clickaway="close" class="ffz--icon-picker tw-relative">
<div class="tw-search-input tw-full-width">
<label v-if="open" :for="'icon-search$' + id" class="tw-hide-accessible">{{ t('setting.icon.search', 'Search for Icon') }}</label>
<div class="tw-relative">
<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="[(isOpen || ! val || ! val.length) ? 'ffz-i-search' : val]" />
</div>
<input
ref="input"
:id="'icon-search$' + id"
:placeholder="('setting.icon.search', 'Search for Icon')"
:value="isOpen ? search : val"
:class="[clearable ? 'tw-pd-r-5' : 'tw-pd-r-1']"
type="text"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-y-05"
autocapitalize="off"
autocorrect="off"
autocomplete="off"
spellcheck="false"
@input="update"
@focus="onFocus"
@blur="onBlur"
@keydown.escape="open = false"
>
<button
v-if="clearable"
class="tw-absolute tw-right-0 tw-top-0 tw-button tw-button--text tw-tooltip-wrapper"
@click="change('', false)"
@keydown.escape="open = false"
@focus="onFocus(false)"
@blur="onBlur"
>
<span class="tw-button__text ffz-i-trash" />
<div class="tw-tooltip tw-tooltip--up tw-tooltip--align-right">
{{ t('setting.icon.clear', 'Clear') }}
</div>
</button>
</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 tw-justify-content-between" >
<div
v-for="i of visible"
:key="i[0]"
: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=""
@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>
<balloon v-if="open" :dir="direction" color="background-base">
<div ref="list">
<simplebar classes="scrollable-area--suppress-scroll-x ffz--icon-picker__list">
<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="val === i[0]"
:class="{'tw-interactable--selected': val === i[0]}"
:data-title="i[1]"
class="ffz-tooltip 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])"
@focus="onFocus(false)"
@blur="onBlur"
>
<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>
</balloon>
</div>
</template>
@ -53,8 +74,9 @@
let id = 0;
import {escape_regex, deep_copy} from 'utilities/object';
import {escape_regex, deep_copy, debounce} from 'utilities/object';
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
import { maybeLoad } from '../utilities/font-awesome';
const FFZ_ICONS = [
'cancel',
@ -107,6 +129,7 @@ const FFZ_ICONS = [
'cw',
'up-dir',
'up-big',
'play',
'link-ext',
'twitter',
'github',
@ -140,11 +163,30 @@ const ICONS = FFZ_ICONS
.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'],
props: {
value: String,
alwaysOpen: {
type: Boolean,
required: false,
default: false
},
clearable: {
type: Boolean,
required: false,
default: false
},
direction: {
type: String,
required: false,
default: 'down'
}
},
data() {
return {
id: id++,
open: false,
val: this.value,
search: '',
icons: deep_copy(ICONS)
}
@ -159,25 +201,81 @@ export default {
reg = new RegExp('(?:^|-| )' + escape_regex(search), 'i');
return this.icons.filter(x => reg.test(x[1]));
},
isOpen() {
return this.alwaysOpen || this.open
}
},
mounted() {
load();
watch: {
value() {
this.val = this.value;
},
this.$nextTick(() => {
if ( this.value ) {
const el = this.$el.querySelector('.tw-interactable--selected');
if ( el )
el.scrollIntoViewIfNeeded();
isOpen() {
if ( ! this.isOpen ) {
requestAnimationFrame(() => {
const ffz = FrankerFaceZ.get();
if ( ffz )
ffz.emit('tooltips:cleanup');
});
return;
}
});
load();
this.$nextTick(() => {
if ( this.val ) {
const root = this.$refs.list,
el = root && root.querySelector('.tw-interactable--selected');
if ( el )
el.scrollIntoViewIfNeeded();
}
});
}
},
created() {
this.maybeClose = debounce(this.maybeClose, 10);
},
mounted() {
maybeLoad(this.val);
},
methods: {
change(val) {
this.value = val;
this.$emit('input', this.value);
update() {
if ( this.open )
this.search = this.$refs.input.value;
},
close() {
this.open = false;
},
change(val, close = true) {
this.val = val;
this.$emit('input', this.val);
if ( close )
this.open = false;
},
onFocus(open = true) {
this.focused = true;
if ( open )
this.open = true;
},
onBlur() {
this.focused = false;
this.maybeClose();
},
maybeClose() {
if ( ! this.focused )
this.open = false;
}
}
}