Skip to content

Commit

Permalink
Merge pull request #878 from nextcloud-libraries/feat/vue-dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv committed Aug 10, 2023
2 parents f67c6b5 + 87f8c27 commit 31ab5b0
Show file tree
Hide file tree
Showing 26 changed files with 13,317 additions and 5,952 deletions.
15 changes: 15 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,15 @@
{
"extends": ["@nextcloud/eslint-config/typescript"],
"overrides": [
// https://github.com/mysticatea/eslint-plugin-node/issues/248#issuecomment-1052550467
{
"files": ["**/*.vue"],
"rules": {
"n/no-missing-import": "off",
// Note: you must disable the base rule as it can report incorrect errors
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": "error"
}
}
]
}
14 changes: 0 additions & 14 deletions babel.config.js

This file was deleted.

64 changes: 63 additions & 1 deletion l10n/messages.pot
Expand Up @@ -2,6 +2,68 @@ msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"

#: lib/toast.ts:223
#: lib/components/FilePicker/FilePickerBreadcrumbs.vue:89
msgid "\"{name}\" is an invalid file name."
msgstr ""

#: lib/components/FilePicker/FilePickerBreadcrumbs.vue:91
msgid "\"{name}\" is not an allowed filetype"
msgstr ""

#: lib/components/FilePicker/FilePickerBreadcrumbs.vue:87
msgid "\"/\" is not allowed inside a file name."
msgstr ""

#: lib/components/FilePicker/FilePickerNavigation.vue:57
msgid "All files"
msgstr ""

#: lib/legacy.ts:135
msgid "Choose"
msgstr ""

#: lib/legacy.ts:141
msgid "Copy"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:212
msgid "Could not create the new folder"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:135
#: lib/components/FilePicker/FilePickerNavigation.vue:65
msgid "Favorites"
msgstr ""

#: lib/components/FilePicker/FilePickerBreadcrumbs.vue:85
msgid "File name cannot be empty."
msgstr ""

#: lib/components/FilePicker/FileList.vue:39
msgid "Modified"
msgstr ""

#: lib/legacy.ts:147
msgid "Move"
msgstr ""

#: lib/components/FilePicker/FileList.vue:19
msgid "Name"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:135
#: lib/components/FilePicker/FilePickerNavigation.vue:61
msgid "Recent"
msgstr ""

#: lib/components/FilePicker/FileList.vue:8
msgid "Select entry"
msgstr ""

#: lib/components/FilePicker/FileList.vue:29
msgid "Size"
msgstr ""

#: lib/toast.ts:228
msgid "Undo"
msgstr ""
174 changes: 174 additions & 0 deletions lib/components/DialogBase.vue
@@ -0,0 +1,174 @@
<template>
<NcModal v-if="open" v-bind="modalProps" @close="handleClose">
<Fragment>
<div ref="wrapper" :class="['dialog__wrapper', { 'dialog__wrapper--collapsed': isNavigationCollapsed }]">
<!-- If the navigation is shown on top of the content, the header should be above the navigation -->
<h2 v-if="isNavigationCollapsed" class="dialog__name">
{{ props.name }}
</h2>
<!-- When the navigation is collapsed (too small dialog) it is displayed above the main content, otherwise on the inline start -->
<nav v-if="hasNavigation" class="dialog__navigation">
<slot name="navigation" :is-collapsed="isNavigationCollapsed" />
</nav>
<!-- Man dialog content -->
<div class="dialog__content">
<!-- If the navigation is shown on the side the header should be directly aligned with the content -->
<h2 v-if="!isNavigationCollapsed" class="dialog__name">
{{ props.name }}
</h2>
<slot>
<p>{{ props.message }}</p>
</slot>
</div>
</div>
<!-- The dialog actions aka the buttons -->
<div class="dialog__actions">
<slot name="actions">
<DialogButton v-for="(button, idx) in props.buttons"
:key="idx"
v-bind="button"
@click="handleClose" />
</slot>
</div>
</Fragment>
</NcModal>
</template>

<script setup lang="ts">
import { NcModal } from '@nextcloud/vue'
import { computed, ref, useSlots } from 'vue'
import DialogButton, { type IDialogButton } from './DialogButton.vue'
import { Fragment } from 'vue-frag'
import { useElementSize } from '@vueuse/core'
const props = withDefaults(defineProps<{
/** Name of the dialog (the heading) */
name: string
/** Text of the dialog */
message?: string
/**
* The element here to mount the dialog
* @default 'body'
*/
container?: string
/**
* Size of the underlying NcModal
* @default 'normal'
*/
size?: 'small' | 'normal' | 'large' | 'full'
/**
* Buttons to display
* @default []
*/
buttons?: readonly IDialogButton[]
}>(), {
size: 'normal',
container: 'body',
message: '',
buttons: () => [],
})
const emit = defineEmits<{
(e: 'close'): void
}>()
const slots = useSlots()
/**
* The dialog wrapper element
*/
const wrapper = ref<HTMLDivElement>()
/**
* We use the dialog width to decide if we collapse the navigation (flex direction row)
*/
const { width: dialogWidth } = useElementSize(wrapper)
/**
* Whether the navigation is collapsed due to dialog and window size
* (collapses when modal is below: 900px modal width - 2x 12px margin)
*/
const isNavigationCollapsed = computed(() => dialogWidth.value < 876)
/**
* Whether a navigation was passed and the element should be displayed
*/
const hasNavigation = computed(() => slots?.navigation !== undefined)
/**
* Whether the dialog is currently open
*/
const open = ref(true)
/**
* Handle closing the dialog, will emit the `close` event
*/
const handleClose = () => {
open.value = false
emit('close')
}
const modalProps = computed(() => ({
container: props.container,
name: props.name,
size: props.size,
}))
</script>

<style lang="scss" scoped>
.dialog {
&__wrapper {
margin-inline: 12px;
margin-block: 0 12px; // remove margin to align header with close button
display: flex;
flex-direction: row;
&--collapsed {
flex-direction: column;
}
}
&__navigation {
display: flex;
}
// Navigation styling when side-by-side with content
&__wrapper:not(&__wrapper--collapsed) &__navigation {
margin-block-start: calc(var(--default-clickable-area) + 16px); // align with __name (4px top & 12px bottom margin)
flex-direction: column;
overflow: hidden auto;
height: 100%;
min-width: 200px;
margin-inline-end: 20px;
}
// Navigation styling when on top of content
&__wrapper#{&}__wrapper--collapsed &__navigation {
flex-direction: row;
justify-content: space-between;
overflow: auto hidden;
width: 100%;
min-width: 100%;
}
&__name {
// Same as the NcAppSettingsDialog
text-align: start;
height: var(--default-clickable-area);
min-height: var(--default-clickable-area);
line-height: var(--default-clickable-area);
margin-block: 4px 12px; // start = 4px to align with close button
}
&__actions {
display: flex;
gap: 6px;
align-content: center;
width: fit-content;
margin-inline: auto 12px;
margin-block: 0 12px;
}
}
</style>
29 changes: 29 additions & 0 deletions lib/components/DialogButton.vue
@@ -0,0 +1,29 @@
<template>
<NcButton :aria-label="props.label" :type="props.type" @click="handleClick">
{{ props.label }}
<template v-if="props.icon !== undefined" #icon>
<component :is="props.icon" :size="20" />
</template>
</NcButton>
</template>

<script setup lang="ts">
import type { AsyncComponent, Component } from 'vue'
import { NcButton } from '@nextcloud/vue'
export interface IDialogButton {
label: string,
icon?: Component | AsyncComponent,
callback: () => void,
type?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'
}
const props = defineProps<IDialogButton>()
const emit = defineEmits<(e: 'click', event: MouseEvent) => void>()
const handleClick = (e: MouseEvent) => {
props.callback?.()
emit('click', e)
}
</script>
17 changes: 17 additions & 0 deletions lib/components/FilePicker/FileList.scss
@@ -0,0 +1,17 @@
tr.file-picker__row {
height: var(--row-height, 50px);

td {
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
padding-inline: 14px 0;
border-bottom: none; // remove lines between elements

// Make "size" and "modified" rows end aligned
&:not(:nth-of-type(2)) {
text-align: end;
padding-inline: 0 14px; // align with header
}
}
}

0 comments on commit 31ab5b0

Please sign in to comment.