|
|
@@ -8,43 +8,49 @@
|
|
|
:class="{active: menuNewActive}"
|
|
|
@stateChange="onStateChange">
|
|
|
<tooltip :content="i18n('buttonNew')" placement="bottom" align="start" slot="toggle">
|
|
|
- <span class="btn-ghost">
|
|
|
+ <a class="btn-ghost" tabindex="0">
|
|
|
<icon name="plus"></icon>
|
|
|
- </span>
|
|
|
+ </a>
|
|
|
</tooltip>
|
|
|
- <div
|
|
|
+ <a
|
|
|
class="dropdown-menu-item"
|
|
|
v-text="i18n('buttonNew')"
|
|
|
- @click.prevent="onEditScript('_new')"
|
|
|
+ tabindex="0"
|
|
|
+ @click.prevent="editScript('_new')"
|
|
|
/>
|
|
|
<a class="dropdown-menu-item" v-text="i18n('installFrom', 'OpenUserJS')" href="https://openuserjs.org/" target="_blank" rel="noopener noreferrer"></a>
|
|
|
<a class="dropdown-menu-item" v-text="i18n('installFrom', 'GreasyFork')" href="https://greasyfork.org/scripts" target="_blank" rel="noopener noreferrer"></a>
|
|
|
- <div
|
|
|
+ <a
|
|
|
class="dropdown-menu-item"
|
|
|
v-text="i18n('buttonInstallFromURL')"
|
|
|
+ tabindex="0"
|
|
|
@click.prevent="installFromURL"
|
|
|
/>
|
|
|
</dropdown>
|
|
|
<tooltip :content="i18n('buttonUpdateAll')" placement="bottom" align="start">
|
|
|
- <span class="btn-ghost" @click="updateAll">
|
|
|
+ <a class="btn-ghost" tabindex="0" @click="updateAll">
|
|
|
<icon name="refresh"></icon>
|
|
|
- </span>
|
|
|
+ </a>
|
|
|
</tooltip>
|
|
|
</div>
|
|
|
<div class="flex-auto" v-else
|
|
|
v-text="`${i18n('headerRecycleBin')}${trash.length ? ` (${trash.length})` : ''}`" />
|
|
|
<tooltip :content="i18n('buttonRecycleBin')" placement="bottom">
|
|
|
- <span class="btn-ghost trash-button" @click="toggleRecycle" ref="trash"
|
|
|
- :class="{ active: showRecycle, filled: trash.length }">
|
|
|
- <icon name="trash"></icon>
|
|
|
+ <a
|
|
|
+ class="btn-ghost trash-button"
|
|
|
+ :class="{ active: showRecycle, filled: trash.length }"
|
|
|
+ @click="showRecycle = !showRecycle"
|
|
|
+ tabindex="0"
|
|
|
+ >
|
|
|
+ <icon name="trash" :class="{ 'trash-animate': removing }"></icon>
|
|
|
<b v-if="trash.length" v-text="trash.length"/>
|
|
|
- </span>
|
|
|
+ </a>
|
|
|
</tooltip>
|
|
|
<dropdown align="right" class="filter-sort">
|
|
|
<tooltip :content="i18n('labelSettings')" placement="bottom" slot="toggle">
|
|
|
- <span class="btn-ghost">
|
|
|
+ <a class="btn-ghost" tabindex="0">
|
|
|
<icon name="cog"/>
|
|
|
- </span>
|
|
|
+ </a>
|
|
|
</tooltip>
|
|
|
<div>
|
|
|
<locale-group i18n-key="labelFilterSort">
|
|
|
@@ -76,6 +82,7 @@
|
|
|
:class="{'has-error': searchError}"
|
|
|
:placeholder="i18n('labelSearchScript')"
|
|
|
v-model="search"
|
|
|
+ ref="search"
|
|
|
id="installed-search">
|
|
|
<icon name="search"></icon>
|
|
|
</label>
|
|
|
@@ -99,6 +106,7 @@
|
|
|
</div>
|
|
|
<div class="flex-auto pos-rel">
|
|
|
<div class="scripts abs-full"
|
|
|
+ ref="scriptList"
|
|
|
:style="`--num-columns:${numColumns}`"
|
|
|
:data-columns="numColumns"
|
|
|
:data-table="filters.viewTable">
|
|
|
@@ -106,14 +114,21 @@
|
|
|
v-for="(script, index) in sortedScripts"
|
|
|
v-show="!search || script.$cache.show !== false"
|
|
|
:key="script.props.id"
|
|
|
- :class="{ removing: removing && removing.id === script.props.id }"
|
|
|
+ :focused="selectedScript === script"
|
|
|
+ :showHotkeys="showHotkeys"
|
|
|
:script="script"
|
|
|
:draggable="filters.sort.value === 'exec' && !script.config.removed"
|
|
|
:visible="index < batchRender.limit"
|
|
|
- :name-clickable="filters.viewTable"
|
|
|
- @edit="onEditScript"
|
|
|
+ :nameClickable="filters.viewTable"
|
|
|
+ :hotkeys="scriptHotkeys"
|
|
|
+ @edit="handleActionEdit"
|
|
|
+ @remove="handleActionRemove"
|
|
|
+ @restore="handleActionRestore"
|
|
|
+ @toggle="handleActionToggle"
|
|
|
+ @update="handleActionUpdate"
|
|
|
@move="moveScript"
|
|
|
- @remove="onRemove"
|
|
|
+ @scrollDelta="handleSmoothScroll"
|
|
|
+ @tiptoggle.native="showHotkeys = !showHotkeys"
|
|
|
/>
|
|
|
</div>
|
|
|
<div
|
|
|
@@ -124,8 +139,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <edit v-if="script" :initial="script" @close="onEditScript()"></edit>
|
|
|
- <div class="trash-animate" v-if="removing" :style="removing.animation" />
|
|
|
+ <edit v-if="script" :initial="script" @close="editScript()"></edit>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -144,6 +158,8 @@ import LocaleGroup from '#/common/ui/locale-group';
|
|
|
import { forEachKey } from '#/common/object';
|
|
|
import { setRoute, lastRoute } from '#/common/router';
|
|
|
import storage from '#/common/storage';
|
|
|
+import { keyboardService, handleTabNavigation } from '#/common/keyboard';
|
|
|
+import ua from '#/common/ua';
|
|
|
import { loadData } from '#/options';
|
|
|
import ScriptItem from './script-item';
|
|
|
import Edit from './edit';
|
|
|
@@ -209,6 +225,23 @@ let step = 0;
|
|
|
let columnsForTableMode = [];
|
|
|
let columnsForCardsMode = [];
|
|
|
|
|
|
+const conditionAll = 'tabScripts';
|
|
|
+const conditionSearch = `${conditionAll} && inputFocus`;
|
|
|
+const conditionNotSearch = `${conditionAll} && !inputFocus`;
|
|
|
+const conditionScriptFocused = `${conditionNotSearch} && selectedScript && !showRecycle`;
|
|
|
+const conditionScriptFocusedRecycle = `${conditionNotSearch} && selectedScript && showRecycle`;
|
|
|
+const conditionHotkeys = `${conditionNotSearch} && selectedScript && showHotkeys`;
|
|
|
+const scriptHotkeys = {
|
|
|
+ edit: 'e',
|
|
|
+ toggle: 'space',
|
|
|
+ update: 'r',
|
|
|
+ restore: 'r',
|
|
|
+ remove: 'x',
|
|
|
+};
|
|
|
+const registerHotkey = (callback, items) => items.map(([key, condition, caseSensitive]) => (
|
|
|
+ keyboardService.register(key, callback, { condition, caseSensitive })
|
|
|
+));
|
|
|
+
|
|
|
export default {
|
|
|
components: {
|
|
|
ScriptItem,
|
|
|
@@ -221,9 +254,12 @@ export default {
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
+ scriptHotkeys,
|
|
|
store,
|
|
|
filterOptions,
|
|
|
filters,
|
|
|
+ filteredScripts: [],
|
|
|
+ focusedIndex: -1,
|
|
|
script: null,
|
|
|
search: null,
|
|
|
searchError: null,
|
|
|
@@ -231,7 +267,8 @@ export default {
|
|
|
menuNewActive: false,
|
|
|
showRecycle: false,
|
|
|
sortedScripts: [],
|
|
|
- removing: null,
|
|
|
+ removing: false,
|
|
|
+ showHotkeys: false,
|
|
|
// Speedup and deflicker for initial page load:
|
|
|
// skip rendering the script list when starting in the editor.
|
|
|
canRenderScripts: !store.route.paths[1],
|
|
|
@@ -247,14 +284,27 @@ export default {
|
|
|
'filters.showEnabledFirst': 'updateLater',
|
|
|
'filters.viewSingleColumn': 'adjustScriptWidth',
|
|
|
'filters.viewTable': 'adjustScriptWidth',
|
|
|
- showRecycle: 'onUpdate',
|
|
|
+ showRecycle(value) {
|
|
|
+ keyboardService.setContext('showRecycle', value);
|
|
|
+ this.focusedIndex = -1;
|
|
|
+ this.onUpdate();
|
|
|
+ },
|
|
|
scripts: 'refreshUI',
|
|
|
'store.route.paths.1': 'onHashChange',
|
|
|
+ selectedScript(script) {
|
|
|
+ keyboardService.setContext('selectedScript', script);
|
|
|
+ },
|
|
|
+ showHotkeys(value) {
|
|
|
+ keyboardService.setContext('showHotkeys', value);
|
|
|
+ },
|
|
|
},
|
|
|
computed: {
|
|
|
currentSortCompare() {
|
|
|
return filterOptions.sort[filters.sort.value]?.compare;
|
|
|
},
|
|
|
+ selectedScript() {
|
|
|
+ return this.filteredScripts[this.focusedIndex];
|
|
|
+ },
|
|
|
message() {
|
|
|
if (this.store.loading) {
|
|
|
return null;
|
|
|
@@ -290,6 +340,8 @@ export default {
|
|
|
const cmp = this.currentSortCompare;
|
|
|
if (cmp) scripts.sort(combinedCompare(cmp));
|
|
|
this.sortedScripts = scripts;
|
|
|
+ this.filteredScripts = this.search ? scripts.filter(({ $cache }) => $cache.show) : scripts;
|
|
|
+ this.selectScript(this.focusedIndex);
|
|
|
if (!step || numFound < step) this.renderScripts();
|
|
|
else this.debouncedRender();
|
|
|
},
|
|
|
@@ -347,7 +399,7 @@ export default {
|
|
|
onStateChange(active) {
|
|
|
this.menuNewActive = active;
|
|
|
},
|
|
|
- onEditScript(id) {
|
|
|
+ editScript(id) {
|
|
|
const pathname = ['scripts', id].filter(Boolean).join('/');
|
|
|
if (!id && pathname === lastRoute().pathname) {
|
|
|
window.history.back();
|
|
|
@@ -375,31 +427,6 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- toggleRecycle() {
|
|
|
- this.showRecycle = !this.showRecycle;
|
|
|
- },
|
|
|
- onRemove(id, rect) {
|
|
|
- const { trash } = this.$refs;
|
|
|
- if (!trash || this.removing) return;
|
|
|
- const trashRect = trash.getBoundingClientRect();
|
|
|
- this.removing = {
|
|
|
- id,
|
|
|
- animation: {
|
|
|
- width: `${trashRect.width}px`,
|
|
|
- height: `${trashRect.height}px`,
|
|
|
- top: `${trashRect.top}px`,
|
|
|
- left: `${trashRect.left}px`,
|
|
|
- transform: `translate(${rect.left - trashRect.left}px,${rect.top - trashRect.top}px) scale(${rect.width / trashRect.width},${rect.height / trashRect.height})`,
|
|
|
- transition: 'transform .3s',
|
|
|
- },
|
|
|
- };
|
|
|
- setTimeout(() => {
|
|
|
- this.removing.animation.transform = 'translate(0,0) scale(1,1)';
|
|
|
- setTimeout(() => {
|
|
|
- this.removing = null;
|
|
|
- }, 300);
|
|
|
- });
|
|
|
- },
|
|
|
async renderScripts() {
|
|
|
if (!this.canRenderScripts) return;
|
|
|
const { length } = this.sortedScripts;
|
|
|
@@ -474,6 +501,51 @@ export default {
|
|
|
this.numColumns = filters.viewSingleColumn ? 1
|
|
|
: widths.findIndex(w => window.innerWidth < w) + 1 || widths.length + 1;
|
|
|
},
|
|
|
+ selectScript(index) {
|
|
|
+ index = Math.min(index, this.filteredScripts.length - 1);
|
|
|
+ index = Math.max(index, -1);
|
|
|
+ if (index !== this.focusedIndex) {
|
|
|
+ this.focusedIndex = index;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ markRemove(script, removed) {
|
|
|
+ sendCmd('MarkRemoved', {
|
|
|
+ id: script.props.id,
|
|
|
+ removed,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleActionEdit(script) {
|
|
|
+ this.editScript(script.props.id);
|
|
|
+ },
|
|
|
+ handleActionRemove(script) {
|
|
|
+ this.markRemove(script, 1);
|
|
|
+ this.removing = true;
|
|
|
+ setTimeout(() => {
|
|
|
+ this.removing = false;
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+ handleActionRestore(script) {
|
|
|
+ this.markRemove(script, 0);
|
|
|
+ },
|
|
|
+ handleActionToggle(script) {
|
|
|
+ sendCmd('UpdateScriptInfo', {
|
|
|
+ id: script.props.id,
|
|
|
+ config: {
|
|
|
+ enabled: script.config.enabled ? 0 : 1,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleActionUpdate(script) {
|
|
|
+ sendCmd('CheckUpdate', script.props.id);
|
|
|
+ },
|
|
|
+ handleSmoothScroll(delta) {
|
|
|
+ if (!delta) return;
|
|
|
+ const el = this.$refs.scriptList;
|
|
|
+ el.scroll({
|
|
|
+ top: el.scrollTop + delta,
|
|
|
+ behavior: 'smooth',
|
|
|
+ });
|
|
|
+ },
|
|
|
},
|
|
|
created() {
|
|
|
this.debouncedUpdate = debounce(this.onUpdate, 100);
|
|
|
@@ -492,6 +564,117 @@ export default {
|
|
|
global.addEventListener('resize', this.adjustScriptWidth);
|
|
|
}
|
|
|
this.adjustScriptWidth();
|
|
|
+ this.disposeList = [
|
|
|
+ ...ua.isFirefox ? [
|
|
|
+ keyboardService.register('tab', () => {
|
|
|
+ handleTabNavigation(1);
|
|
|
+ }),
|
|
|
+ keyboardService.register('s-tab', () => {
|
|
|
+ handleTabNavigation(-1);
|
|
|
+ }),
|
|
|
+ ] : [],
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.$refs.search?.focus();
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-f', conditionAll],
|
|
|
+ ['/', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.$refs.search?.blur();
|
|
|
+ }, [
|
|
|
+ ['enter', conditionSearch],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ if (this.selectedScript) this.showHotkeys = !this.showHotkeys;
|
|
|
+ }, [
|
|
|
+ ['enter', `${conditionAll} && scriptFocus`],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.showHotkeys = false;
|
|
|
+ }, [
|
|
|
+ ['escape', conditionHotkeys],
|
|
|
+ ['q', conditionHotkeys, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ let index = this.focusedIndex;
|
|
|
+ if (index < 0) index = 0;
|
|
|
+ else index += this.numColumns;
|
|
|
+ if (index < this.filteredScripts.length) {
|
|
|
+ this.selectScript(index);
|
|
|
+ }
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-down', conditionAll],
|
|
|
+ ['down', conditionAll],
|
|
|
+ ['j', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ const index = this.focusedIndex - this.numColumns;
|
|
|
+ if (index >= 0) {
|
|
|
+ this.selectScript(index);
|
|
|
+ }
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-up', conditionAll],
|
|
|
+ ['up', conditionAll],
|
|
|
+ ['k', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.selectScript(this.focusedIndex - 1);
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-left', conditionAll],
|
|
|
+ ['left', conditionNotSearch],
|
|
|
+ ['h', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.selectScript(this.focusedIndex + 1);
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-right', conditionAll],
|
|
|
+ ['right', conditionNotSearch],
|
|
|
+ ['l', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.selectScript(0);
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-home', conditionAll],
|
|
|
+ ['g g', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.selectScript(this.filteredScripts.length - 1);
|
|
|
+ }, [
|
|
|
+ ['ctrlcmd-end', conditionAll],
|
|
|
+ ['G', conditionNotSearch, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.handleActionEdit(this.selectedScript);
|
|
|
+ }, [
|
|
|
+ [scriptHotkeys.edit, conditionScriptFocused, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.handleActionRemove(this.selectedScript);
|
|
|
+ }, [
|
|
|
+ ['delete', conditionScriptFocused],
|
|
|
+ [scriptHotkeys.remove, conditionScriptFocused, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.handleActionUpdate(this.selectedScript);
|
|
|
+ }, [
|
|
|
+ [scriptHotkeys.update, conditionScriptFocused, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.handleActionToggle(this.selectedScript);
|
|
|
+ }, [
|
|
|
+ [scriptHotkeys.toggle, conditionScriptFocused, true],
|
|
|
+ ]),
|
|
|
+ ...registerHotkey(() => {
|
|
|
+ this.handleActionRestore(this.selectedScript);
|
|
|
+ }, [
|
|
|
+ [scriptHotkeys.restore, conditionScriptFocusedRecycle, true],
|
|
|
+ ]),
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.disposeList?.forEach(dispose => {
|
|
|
+ dispose();
|
|
|
+ });
|
|
|
},
|
|
|
};
|
|
|
</script>
|
|
|
@@ -554,6 +737,7 @@ export default {
|
|
|
text-decoration: none;
|
|
|
color: var(--fill-9);
|
|
|
cursor: pointer;
|
|
|
+ &:focus,
|
|
|
&:hover {
|
|
|
color: inherit;
|
|
|
background: var(--fill-0-5);
|
|
|
@@ -580,13 +764,6 @@ export default {
|
|
|
&-tooltip {
|
|
|
white-space: pre-wrap;
|
|
|
}
|
|
|
- select {
|
|
|
- /* borders are copied from inputs in common/ui/style */
|
|
|
- border: 1px solid var(--fill-3);
|
|
|
- &:focus {
|
|
|
- border-color: var(--fill-7);
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
.filter-sort {
|
|
|
.vl-dropdown-menu {
|
|
|
@@ -620,12 +797,15 @@ export default {
|
|
|
}
|
|
|
|
|
|
.trash-animate {
|
|
|
- position: fixed;
|
|
|
- background: rgba(0,0,0,.1);
|
|
|
- transform-origin: top left;
|
|
|
+ animation: .5s linear rotate;
|
|
|
}
|
|
|
|
|
|
-.script.removing {
|
|
|
- opacity: .2;
|
|
|
+@keyframes rotate {
|
|
|
+ 0% {
|
|
|
+ transform: scale(1.2) rotate(0);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: scale(1.2) rotate(360deg);
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|