mirror of
https://github.com/directus/directus.git
synced 2026-01-30 08:47:57 -05:00
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
// ...
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<modal-detail
|
||||
v-if="!disabled"
|
||||
:active="showDetailModal"
|
||||
:active.sync="showDetailModal"
|
||||
:collection="junctionCollection"
|
||||
:primary-key="junctionRowPrimaryKey"
|
||||
:edits="editsAtStart"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<v-modal
|
||||
v-else
|
||||
:active="true"
|
||||
@toggle="cancelField"
|
||||
:title="
|
||||
field === '+'
|
||||
? $t('creating_new_field', { collection: collectionInfo.name })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user