Merge pull request #415 from directus/tweak-shortcuts

Tweak shortcuts
This commit is contained in:
Rijk van Zanten
2020-09-29 18:29:04 -04:00
committed by GitHub
13 changed files with 200 additions and 75 deletions

View File

@@ -14,6 +14,7 @@
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { nanoid } from 'nanoid';
import useShortcut from '@/composables/use-shortcut';
export default defineComponent({
model: {
@@ -31,6 +32,14 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const dialog = ref<HTMLElement | null>(null);
useShortcut('escape', (event, cancelNext) => {
if (_active.value) {
emitToggle();
cancelNext();
}
});
const localActive = ref(false);
const className = ref<string | null>(null);

View File

@@ -15,7 +15,9 @@
1
</v-button>
<span v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > (totalVisible + 1)" class="gap">...</span>
<span v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > totalVisible + 1" class="gap">
...
</span>
<v-button
v-for="page in visiblePages"
@@ -30,7 +32,10 @@
{{ page }}
</v-button>
<span v-if="showFirstLast && value < length - Math.ceil(totalVisible / 2) && length > (totalVisible + 1)" class="gap">
<span
v-if="showFirstLast && value < length - Math.ceil(totalVisible / 2) && length > totalVisible + 1"
class="gap"
>
...
</span>
@@ -139,9 +144,9 @@ body {
display: flex;
.gap {
display: none;
margin: 0 4px;
color: var(--foreground-subdued);
display: none;
line-height: 2em;
@include breakpoint(small) {

View File

@@ -78,7 +78,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref, PropType } from '@vue/composition-api';
import { defineComponent, computed, ref, PropType, onMounted, watch } from '@vue/composition-api';
import { Header, HeaderRaw, Item, ItemSelectEvent, Sort } from './types';
import TableHeader from './table-header/';
import TableRow from './table-row/';
@@ -86,7 +86,6 @@ import { sortBy, clone, forEach, pick } from 'lodash';
import { i18n } from '@/lang/';
import draggable from 'vuedraggable';
import hideDragImage from '@/utils/hide-drag-image';
import useShortcut from '@/composables/use-shortcut';
const HeaderDefaults: Header = {
text: '',
@@ -292,10 +291,6 @@ export default defineComponent({
return gridTemplateColumns;
});
useShortcut('mod+a', () => {
onToggleSelectAll(!allItemsSelected.value);
});
return {
_headers,
_items,

View File

@@ -17,7 +17,7 @@ import { useShortcut } from '@/composables/use-shortcut';
export default defineComponent({
setup(props) {
useShortcut('mod+s', save);
useShortcut('meta+s', save);
function save() {
// ...

View File

@@ -1,27 +1,104 @@
import { onMounted, onUnmounted } from '@vue/composition-api';
import Mousetrap, { ExtendedKeyboardEvent } from 'mousetrap';
import { onMounted, onUnmounted, Ref, ref } from '@vue/composition-api';
import Vue from 'vue';
const mousetrap = new Mousetrap();
mousetrap.stopCallback = function (e: Event, element: Element) {
// if the element has the class "mousetrap" then no need to stop
if (element.hasAttribute('data-disable-mousetrap')) {
return true;
}
type ShortcutHandler = (event: KeyboardEvent, cancelNext: () => void) => void | any | boolean;
return false;
};
const keysdown: Set<string> = new Set([]);
const handlers: Record<string, ShortcutHandler[]> = {};
document.body.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.repeat) return;
keysdown.add(mapKeys(event.key));
callHandlers(event);
});
document.body.addEventListener('keyup', (event: KeyboardEvent) => {
const key = mapKeys(event.key);
keysdown.delete(key.toLowerCase());
keysdown.delete(key.toUpperCase());
});
export default function useShortcut(
shortcut: string | string[],
handler: (evt?: ExtendedKeyboardEvent, combo?: string) => void
shortcuts: string | string[],
handler: ShortcutHandler,
reference: Ref<HTMLElement | undefined> | Ref<Vue | undefined> = ref(document.body)
) {
const callback: ShortcutHandler = (event, cancelNext) => {
if (!reference.value) return;
const ref = reference.value instanceof HTMLElement ? reference.value : (reference.value.$el as HTMLElement);
if (
document.activeElement === ref ||
ref.contains(document.activeElement) ||
document.activeElement === document.body
) {
event.preventDefault();
return handler(event, cancelNext);
}
return false;
};
onMounted(() => {
mousetrap.bind(shortcut, (e, combo) => {
e.preventDefault();
handler(e, combo);
[shortcuts].flat().forEach((shortcut) => {
if (handlers.hasOwnProperty(shortcut)) {
handlers[shortcut].unshift(callback);
} else {
handlers[shortcut] = [callback];
}
});
});
onUnmounted(() => {
mousetrap.unbind(shortcut);
[shortcuts].flat().forEach((shortcut) => {
if (handlers.hasOwnProperty(shortcut)) {
handlers[shortcut] = handlers[shortcut].filter((f) => f !== callback);
if (handlers[shortcut].length === 0) {
delete handlers[shortcut];
}
}
});
});
}
function mapKeys(key: string) {
const map: Record<string, string> = {
Control: 'meta',
Command: 'meta',
};
key = map.hasOwnProperty(key) ? map[key] : key;
if (key.match(/^[a-z]$/) !== null) {
if (keysdown.has('shift')) key = key.toUpperCase();
} else if (key.match(/^[A-Z]$/) !== null) {
if (keysdown.has('shift')) key = key.toLowerCase();
} else {
key = key.toLowerCase();
}
return key;
}
function callHandlers(event: KeyboardEvent) {
Object.entries(handlers).forEach(([key, value]) => {
const rest = key.split('+').filter((keySegment) => keysdown.has(keySegment) === false);
if (rest.length > 0) return;
for (let i = 0; i < value.length; i++) {
let cancel = false;
value[i](event, cancelNext);
// if cancelNext is called, discontinue going through the queue.
if (typeof cancel === 'boolean' && cancel) break;
function cancelNext() {
cancel = true;
}
}
});
}

View File

@@ -41,7 +41,7 @@
<modal-detail
v-if="!disabled"
:active="showDetailModal"
:active.sync="showDetailModal"
:collection="junctionCollection"
:primary-key="junctionRowPrimaryKey"
:edits="editsAtStart"

View File

@@ -149,13 +149,14 @@ import { HeaderRaw, Item } from '@/components/v-table/types';
import { Field, Filter } from '@/types';
import router from '@/router';
import useSync from '@/composables/use-sync';
import { debounce } from 'lodash';
import { debounce, clone } from 'lodash';
import Draggable from 'vuedraggable';
import useCollection from '@/composables/use-collection';
import useItems from '@/composables/use-items';
import i18n from '@/lang';
import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
import hideDragImage from '@/utils/hide-drag-image';
import useShortcut from '@/composables/use-shortcut';
type layoutOptions = {
widths?: {
@@ -211,7 +212,7 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const table = ref<Vue | null>(null);
const table = ref<Vue>();
const mainElement = inject('main-element', ref<Element | null>(null));
const _selection = useSync(props, 'selection', emit);
@@ -260,6 +261,14 @@ export default defineComponent({
return count;
});
useShortcut(
'meta+a',
() => {
_selection.value = clone(items.value).map((item: any) => item[primaryKeyField.value.field]);
},
table
);
return {
_selection,
table,

View File

@@ -137,6 +137,7 @@
</template>
<v-form
ref="form"
:disabled="isNew ? false : updateAllowed === false"
:loading="loading"
:initial-values="item"
@@ -182,6 +183,7 @@
<script lang="ts">
import { defineComponent, computed, toRefs, ref } from '@vue/composition-api';
import Vue from 'vue';
import CollectionsNavigation from '../components/navigation.vue';
import router from '@/router';
@@ -223,6 +225,7 @@ export default defineComponent({
},
},
setup(props) {
const form = ref<HTMLElement>();
const userStore = useUserStore();
const { collection, primaryKey } = toRefs(props);
@@ -283,8 +286,8 @@ export default defineComponent({
return i18n.t('archive');
});
useShortcut('mod+s', saveAndStay);
useShortcut('mod+shift+s', saveAndAddNew);
useShortcut('meta+s', saveAndStay, form);
useShortcut('meta+shift+s', saveAndAddNew, form);
const navigationGuard: NavigationGuard = (to, from, next) => {
const hasEdits = Object.keys(edits.value).length > 0;
@@ -337,6 +340,7 @@ export default defineComponent({
updateAllowed,
toggleArchive,
validationErrors,
form,
};
function useBreadcrumb() {

View File

@@ -132,6 +132,7 @@
/>
<v-form
ref="form"
:fields="formFields"
:loading="loading"
:initial-values="item"
@@ -231,6 +232,7 @@ export default defineComponent({
},
},
setup(props) {
const form = ref<HTMLElement>();
const { primaryKey } = toRefs(props);
const { breadcrumb } = useBreadcrumb();
const fieldsStore = useFieldsStore();
@@ -291,7 +293,7 @@ export default defineComponent({
const { moveToDialogActive, moveToFolder, moving, selectedFolder } = useMovetoFolder();
useShortcut('mod+s', saveAndStay);
useShortcut('meta+s', saveAndStay, form);
return {
item,
@@ -324,6 +326,7 @@ export default defineComponent({
moving,
selectedFolder,
fileSrc,
form,
};
function changeCacheBuster() {

View File

@@ -30,6 +30,7 @@
<v-modal
v-else
:active="true"
@toggle="cancelField"
:title="
field === '+'
? $t('creating_new_field', { collection: collectionInfo.name })

View File

@@ -110,17 +110,26 @@
<v-divider />
<v-list-item @click="setWidth('half')" :disabled="field.meta && field.meta.width === 'half'">
<v-list-item
@click="setWidth('half')"
:disabled="field.meta && field.meta.width === 'half'"
>
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
<v-list-item-content>{{ $t('half_width') }}</v-list-item-content>
</v-list-item>
<v-list-item @click="setWidth('full')" :disabled="field.meta && field.meta.width === 'full'">
<v-list-item
@click="setWidth('full')"
:disabled="field.meta && field.meta.width === 'full'"
>
<v-list-item-icon><v-icon name="border_right" /></v-list-item-icon>
<v-list-item-content>{{ $t('full_width') }}</v-list-item-content>
</v-list-item>
<v-list-item @click="setWidth('fill')" :disabled="field.meta && field.meta.width === 'fill'">
<v-list-item
@click="setWidth('fill')"
:disabled="field.meta && field.meta.width === 'fill'"
>
<v-list-item-icon><v-icon name="aspect_ratio" /></v-list-item-icon>
<v-list-item-content>{{ $t('fill_width') }}</v-list-item-content>
</v-list-item>
@@ -402,6 +411,41 @@ export default defineComponent({
}
}
.group {
position: relative;
padding: var(--input-padding);
background-color: var(--background-subdued);
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
.header {
display: flex;
align-items: center;
margin-bottom: var(--input-padding);
}
.name {
font-family: var(--family-monospace);
}
.drag-handle {
margin-right: 8px;
transition: color var(--fast) var(--transition);
&:hover {
color: var(--foreground);
}
}
.group-options {
cursor: pointer;
}
.v-notice {
cursor: pointer;
}
}
.field {
.label {
flex-grow: 1;
@@ -447,39 +491,4 @@ export default defineComponent({
.spacer {
flex-grow: 1;
}
.group {
position: relative;
padding: var(--input-padding);
background-color: var(--background-subdued);
border-radius: var(--border-radius);
border: 2px solid var(--border-normal);
.header {
margin-bottom: var(--input-padding);
display: flex;
align-items: center;
}
.name {
font-family: var(--family-monospace);
}
.drag-handle {
margin-right: 8px;
transition: color var(--fast) var(--transition);
&:hover {
color: var(--foreground);
}
}
.group-options {
cursor: pointer;
}
.v-notice {
cursor: pointer;
}
}
</style>

View File

@@ -119,6 +119,7 @@
</div>
<v-form
ref="form"
:fields="formFields"
:loading="loading"
:initial-values="item"
@@ -208,6 +209,7 @@ export default defineComponent({
},
},
setup(props) {
const form = ref<HTMLElement>();
const fieldsStore = useFieldsStore();
const userStore = useUserStore();
@@ -291,8 +293,8 @@ export default defineComponent({
return i18n.t('archive');
});
useShortcut('mod+s', saveAndStay);
useShortcut('mod+shift+s', saveAndAddNew);
useShortcut('meta+s', saveAndStay, form);
useShortcut('meta+shift+s', saveAndAddNew, form);
return {
title,
@@ -330,6 +332,7 @@ export default defineComponent({
collectionInfo,
archiving,
archiveTooltip,
form,
};
function useBreadcrumb() {

View File

@@ -1,5 +1,11 @@
<template>
<v-textarea class="new-comment" :placeholder="$t('leave_comment')" v-model="newCommentContent" expand-on-focus>
<v-textarea
class="new-comment"
:placeholder="$t('leave_comment')"
v-model="newCommentContent"
expand-on-focus
ref="textarea"
>
<template #append>
<v-icon name="alternate_email" class="add-mention" />
<v-icon name="insert_emoticon" class="add-emoji" />
@@ -22,6 +28,7 @@ import { defineComponent, ref, PropType } from '@vue/composition-api';
import notify from '@/utils/notify';
import api from '@/api';
import i18n from '@/lang';
import useShortcut from '@/composables/use-shortcut';
export default defineComponent({
props: {
@@ -39,12 +46,15 @@ export default defineComponent({
},
},
setup(props) {
const newCommentContent = ref(null);
const textarea = ref<HTMLElement>();
useShortcut('meta+enter', postComment, textarea);
const newCommentContent = ref<string | null>(null);
const saving = ref(false);
return { newCommentContent, postComment, saving };
return { newCommentContent, postComment, saving, textarea };
async function postComment() {
if (newCommentContent.value === null || newCommentContent.value.length === 0) return;
saving.value = true;
try {