Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/web-app-mail/src/components/MailComposeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<div class="flex-1 flex flex-col min-h-0">
<div class="mail-body-editor flex flex-col gap-2 h-full min-h-0 flex-1">
<div class="mail-body-editor-wrapper flex-1 min-h-0" @click="onWrapperClick">
<TextEditorProvider :editor="textEditor">
<TextEditorProvider :editor="textEditor" :toolbar-variant="toolbarVariant">
<TextEditorToolbar />
<TextEditorContent />
</TextEditorProvider>
Expand All @@ -74,7 +74,8 @@ import {
useTextEditor,
TextEditorProvider,
TextEditorContent,
TextEditorToolbar
TextEditorToolbar,
type TextEditorToolbarVariant
} from '@opencloud-eu/web-pkg/editor'
import { storeToRefs } from 'pinia'
import DOMPurify from 'dompurify'
Expand Down Expand Up @@ -108,8 +109,9 @@ export type ComposeFormState = {
attachments?: MailComposeAttachment[]
}

const { modelValue } = defineProps<{
const { modelValue, toolbarVariant = 'default' } = defineProps<{
modelValue: ComposeFormState
toolbarVariant?: TextEditorToolbarVariant
}>()

const emit = defineEmits<{
Expand Down
18 changes: 16 additions & 2 deletions packages/web-app-mail/src/components/MailWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<div class="flex flex-col flex-1 min-h-0">
<div class="flex-1 min-h-0 overflow-auto">
<MailComposeForm v-model="composeState" />
<MailComposeForm v-model="composeState" :toolbar-variant="composeToolbarVariant" />
</div>

<div class="px-4 pt-3 pb-2">
Expand Down Expand Up @@ -83,7 +83,10 @@
<template #content>
<div class="flex flex-col flex-1 min-h-0">
<div class="flex-1 min-h-0 overflow-auto">
<MailComposeForm v-model="composeState" />
<MailComposeForm
v-model="composeState"
:toolbar-variant="expandedComposeToolbarVariant"
/>
</div>

<div class="px-4 pt-3">
Expand Down Expand Up @@ -115,7 +118,9 @@ import { ref, computed, unref, watch, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { storeToRefs } from 'pinia'
import { useIsMobile } from '@opencloud-eu/design-system/composables'
import { useGroupwareAccountsStore, useModals } from '@opencloud-eu/web-pkg'
import type { TextEditorToolbarVariant } from '@opencloud-eu/web-pkg/editor'
import MailComposeForm, { type ComposeFormState } from './MailComposeForm.vue'
import MailComposeAttachmentButton from './MailComposeAttachmentButton.vue'
import MailSavedHint from './MailSavedHint.vue'
Expand Down Expand Up @@ -150,6 +155,7 @@ const emit = defineEmits<{
const accountsStore = useGroupwareAccountsStore()
const mailboxesStore = useMailboxesStore()
const connector = useMailDraftConnector()
const { isMobile } = useIsMobile()

const { currentAccount } = storeToRefs(accountsStore)
const { mailboxes } = storeToRefs(mailboxesStore)
Expand All @@ -170,6 +176,14 @@ const selectedIdentityId = computed(() => {

const isExpanded = ref(false)

const composeToolbarVariant = computed<TextEditorToolbarVariant>(() => {
return unref(isMobile) ? 'mobile' : 'expanded-compose'
})

const expandedComposeToolbarVariant = computed<TextEditorToolbarVariant>(() => {
return unref(isMobile) ? 'mobile' : 'default'
})

const { showSavedHint, flashSavedHint, clearSavedHint } = useSavedHint(SAVED_HINT_DURATION_MS)

const canSaveDraft = computed(() => {
Expand Down
10 changes: 6 additions & 4 deletions packages/web-pkg/src/editor/components/TextEditorProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
</template>

<script setup lang="ts">
import { provide } from 'vue'
import type { TextEditorInstance } from '../types'
import { provide, toRef } from 'vue'
import type { TextEditorInstance, TextEditorToolbarVariant } from '../types'

const { editor } = defineProps<{
const props = defineProps<{
editor: TextEditorInstance
toolbarVariant?: TextEditorToolbarVariant
}>()

provide('textEditor', editor)
provide('textEditor', props.editor)
provide('textEditorToolbarVariant', toRef(props, 'toolbarVariant'))
</script>
81 changes: 72 additions & 9 deletions packages/web-pkg/src/editor/components/TextEditorToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<template>
<div v-if="visible" class="text-editor-toolbar relative border-b border-b-role-border py-1">
<div
v-if="visible"
class="text-editor-toolbar relative border-b border-b-role-border py-1"
:class="{ 'text-editor-toolbar--compact': isCompactToolbar }"
>
<div
ref="scrollContainer"
class="flex items-center gap-1 overflow-x-auto before:grow after:grow"
@scroll="updateScrollState"
>
<div
v-for="(group, groupIndex) in textEditor.actionGroups()"
v-for="(group, groupIndex) in toolbarGroups"
:key="`toolbar-group-${group.id}`"
class="text-editor-toolbar-group inline-flex items-stretch"
:class="{ 'border-l border-l-role-border pl-1': groupIndex > 0 }"
>
<template
v-for="item in group.actions.filter((a) => a.showInToolbar !== false)"
:key="`toolbar-item-${item.id}`"
>
<template v-for="item in group.actions" :key="`toolbar-item-${item.id}`">
<template v-if="item.childActions">
<oc-button
:id="`toolbar-dropdown-trigger-${item.id}`"
Expand Down Expand Up @@ -111,16 +112,68 @@
</template>

<script setup lang="ts">
import { computed, inject, nextTick, onMounted, ref, unref, useTemplateRef } from 'vue'
import type { TextEditorInstance } from '../types'
import { EditorAction } from '../composables'
import { computed, inject, nextTick, onMounted, ref, unref, useTemplateRef, watch } from 'vue'
import type { Ref } from 'vue'
import type { TextEditorInstance, TextEditorToolbarVariant } from '../types'
import type { EditorAction, EditorActionGroup } from '../composables'

const textEditor = inject<TextEditorInstance>('textEditor')!
const providedToolbarVariant = inject<Ref<TextEditorToolbarVariant | undefined>>(
'textEditorToolbarVariant',
ref<TextEditorToolbarVariant | undefined>()
)

const toolbarVariant = computed<TextEditorToolbarVariant>(() => {
return unref(providedToolbarVariant) ?? 'default'
})

const scrollContainerRef = useTemplateRef('scrollContainer')
const canScrollLeft = ref(false)
const canScrollRight = ref(false)

const toolbarActionsByVariant: Record<TextEditorToolbarVariant, string[] | null> = {
default: null,
'expanded-compose': [
'undo',
'redo',
'heading',
'font-size',
'bold',
'italic',
'underline',
'strikethrough',
'bullet-list',
'ordered-list',
'task-list',
'blockquote',
'code-block',
'link'
],
mobile: ['bold', 'italic', 'underline', 'bullet-list', 'ordered-list', 'link']
}

const isCompactToolbar = computed(() => unref(toolbarVariant) !== 'default')

const isToolbarItemVisible = (item: EditorAction) => {
const actionIds = toolbarActionsByVariant[unref(toolbarVariant)]

if (!actionIds) {
return item.showInToolbar !== false
}

return actionIds.includes(item.id)
}

const toolbarGroups = computed<EditorActionGroup[]>(() => {
return textEditor
.actionGroups()
.map((group) => ({
...group,
actions: group.actions.filter(isToolbarItemVisible)
}))
.filter((group) => group.actions.length)
})

const updateScrollState = () => {
const el = scrollContainerRef.value
if (!el) {
Expand All @@ -137,6 +190,11 @@ onMounted(async () => {
updateScrollState()
})

watch(toolbarGroups, async () => {
await nextTick()
updateScrollState()
})

const visible = computed(() => {
if (unref(textEditor.readonly)) {
return false
Expand Down Expand Up @@ -202,6 +260,11 @@ const getActiveIcon = (item: EditorAction) => {
.text-editor-toolbar-btn {
gap: 0 !important;
}

.text-editor-toolbar--compact .text-editor-toolbar-btn {
@apply min-w-9 p-1;
}

.text-editor-toolbar-btn--active {
@apply bg-role-secondary-container;
}
Expand Down
23 changes: 19 additions & 4 deletions packages/web-pkg/src/editor/composables/strategies/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ import {
FontSize,
LineHeight
} from '@tiptap/extension-text-style'
import { EditorActionGroup, useEditorActions } from '../useEditorActions'
import {
useEditorActions,
type EditorActionGroup,
type UseEditorActionsOptions
} from '../useEditorActions'
import { TextEditorState } from '../../types'

export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrategy => {
export const useStrategyHtml = (
editorState: TextEditorState,
editorActionOptions: UseEditorActionsOptions = {}
): ContentTypeStrategy => {
const { $gettext } = useGettext()

const editorContentType = () => {
Expand Down Expand Up @@ -93,6 +100,7 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate
blockquote,
codeBlock,
horizontalRule,
link,
tableMenu,
createTable,
addRowBefore,
Expand All @@ -101,7 +109,13 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate
addColumnBefore,
addColumnAfter,
deleteColumn
} = useEditorActions(editorState)
} = useEditorActions(editorState, editorActionOptions)

const toolbarLink = () => ({
...link(),
showInToolbar: false
})

const editorActionGroups = (): EditorActionGroup[] => {
return [
{
Expand Down Expand Up @@ -160,7 +174,8 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate
addRowBefore(),
deleteColumn(),
deleteRow(),
horizontalRule()
horizontalRule(),
toolbarLink()
]
}
]
Expand Down
6 changes: 4 additions & 2 deletions packages/web-pkg/src/editor/composables/useContentStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ContentType, TextEditorState } from '../types'
import type { UseEditorActionsOptions } from './useEditorActions'
import {
ContentTypeStrategy,
useStrategyHtml,
Expand All @@ -10,15 +11,16 @@ import {
export const useContentStrategy = () => {
const resolveStrategy = (
contentType: ContentType,
editorState: TextEditorState
editorState: TextEditorState,
editorActionOptions: UseEditorActionsOptions = {}
): ContentTypeStrategy => {
switch (contentType) {
case 'plain-text':
return useStrategyPlainText(editorState)
case 'markdown':
return useStrategyMarkdown(editorState)
case 'html':
return useStrategyHtml(editorState)
return useStrategyHtml(editorState, editorActionOptions)
case 'tiptap-json':
return useStrategyTiptapJson(editorState)
default:
Expand Down
15 changes: 14 additions & 1 deletion packages/web-pkg/src/editor/composables/useTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Placeholder } from '@tiptap/extension-placeholder'
import type { ShallowRef } from 'vue'
import type { Editor } from '@tiptap/vue-3'
import type { TextEditorOptions, TextEditorInstance, TextEditorState } from '../types'
import type { UseEditorActionsOptions } from './useEditorActions'
import { SlashCommands } from '../extensions'
import { useContentStrategy } from './useContentStrategy'

Expand All @@ -13,9 +14,21 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance {
sourceMode: ref(false)
}

const editorActionOptions: UseEditorActionsOptions = {}
if (options.onRequestLinkUrl) {
editorActionOptions.onRequestLinkUrl = async (editor, currentUrl) => {
const href = await options.onRequestLinkUrl?.(currentUrl)
if (!href) {
return
}

editor.chain().focus().extendMarkRange('link').setLink({ href }).run()
}
}

const contentType = ref(options.contentType)
const readonly = ref(options.readonly ?? false)
const strategy = resolveStrategy(options.contentType, state)
const strategy = resolveStrategy(options.contentType, state, editorActionOptions)

// Debounce onUpdate to avoid firing on every keystroke while typing.
let debounceTimer: ReturnType<typeof setTimeout> | null = null
Expand Down
7 changes: 6 additions & 1 deletion packages/web-pkg/src/editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export type { ContentType, TextEditorOptions, TextEditorInstance } from './types'
export type {
ContentType,
TextEditorOptions,
TextEditorInstance,
TextEditorToolbarVariant
} from './types'
export { useTextEditor } from './composables/useTextEditor'
export { default as TextEditorProvider } from './components/TextEditorProvider.vue'
export { default as TextEditorContent } from './components/TextEditorContent.vue'
Expand Down
5 changes: 3 additions & 2 deletions packages/web-pkg/src/editor/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ShallowRef, Ref, ComputedRef } from 'vue'
import { Editor } from '@tiptap/vue-3'
import { EditorActionGroup } from './composables'
import type { Editor } from '@tiptap/vue-3'
import type { EditorActionGroup } from './composables'

export type ContentType = 'plain-text' | 'markdown' | 'html' | 'tiptap-json'
export type TextEditorToolbarVariant = 'default' | 'expanded-compose' | 'mobile'

export interface TextEditorOptions {
contentType: ContentType
Expand Down