Skip to content
Merged
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: 6 additions & 2 deletions packages/web-app-mail/src/components/MailComposeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
<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">
<TextEditorToolbar :teleport="toolbarDropTeleport" />
<TextEditorToolbar
:actions-to-display="actionsToDisplay"
:teleport="toolbarDropTeleport"
/>
<TextEditorContent />
</TextEditorProvider>
</div>
Expand Down Expand Up @@ -108,8 +111,9 @@ export type ComposeFormState = {
attachments?: MailComposeAttachment[]
}

const { modelValue, toolbarDropTeleport = undefined } = defineProps<{
const { modelValue, actionsToDisplay } = defineProps<{
modelValue: ComposeFormState
actionsToDisplay?: string[]
toolbarDropTeleport?: string
}>()

Expand Down
38 changes: 38 additions & 0 deletions packages/web-app-mail/src/components/MailWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<div class="flex-1 min-h-0 overflow-auto">
<MailComposeForm
v-model="composeState"
:actions-to-display="composeActionsToDisplay"
:toolbar-drop-teleport="composeToolbarDropTeleport"
/>
</div>
Expand Down Expand Up @@ -88,6 +89,7 @@
<div class="flex-1 min-h-0 overflow-auto">
<MailComposeForm
v-model="composeState"
:actions-to-display="expandedComposeActionsToDisplay"
:toolbar-drop-teleport="composeToolbarDropTeleport"
/>
</div>
Expand Down Expand Up @@ -121,6 +123,7 @@ 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 MailComposeForm, { type ComposeFormState } from './MailComposeForm.vue'
import MailComposeAttachmentButton from './MailComposeAttachmentButton.vue'
Expand All @@ -145,6 +148,32 @@ const appliedDraftId = ref<string | null>(null)
const SAVED_HINT_DURATION_MS = 2000
const AUTO_SAVE_INTERVAL_MS = 120000 // 2(min) * 60 * 1000

const COMPACT_COMPOSE_ACTIONS = [
'undo',
'redo',
'heading',
'font-size',
'bold',
'italic',
'underline',
'strikethrough',
'bullet-list',
'ordered-list',
'task-list',
'blockquote',
'code-block',
'link'
]

const MOBILE_COMPOSE_ACTIONS = [
'bold',
'italic',
'underline',
'bullet-list',
'ordered-list',
'link'
]

const props = defineProps<{
draftMail?: Mail | null
}>()
Expand All @@ -156,6 +185,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 @@ -176,6 +206,14 @@ const selectedIdentityId = computed(() => {

const isExpanded = ref(false)

const composeActionsToDisplay = computed(() => {
return unref(isMobile) ? MOBILE_COMPOSE_ACTIONS : COMPACT_COMPOSE_ACTIONS
})

const expandedComposeActionsToDisplay = computed(() => {
return unref(isMobile) ? MOBILE_COMPOSE_ACTIONS : undefined
})

const composeToolbarDropId = 'mail-compose-toolbar-drop'
const composeToolbarDropTeleport = `#${composeToolbarDropId}`

Expand Down
4 changes: 2 additions & 2 deletions packages/web-pkg/src/editor/components/TextEditorProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import { provide } from 'vue'
import type { TextEditorInstance } from '../types'

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

provide('textEditor', editor)
provide('textEditor', props.editor)
</script>
41 changes: 33 additions & 8 deletions packages/web-pkg/src/editor/components/TextEditorToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
@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 || item.menuComponent">
<oc-button
:id="`toolbar-dropdown-trigger-${item.id}`"
Expand All @@ -27,6 +24,8 @@
}"
:aria-label="item.title"
:disabled="!isItemEnabled(item)"
@mousedown.prevent
@click.stop
>
<oc-icon
:name="getActiveIcon(item).icon"
Expand Down Expand Up @@ -65,6 +64,7 @@
justify-content="space-between"
class="p-1"
:disabled="!isItemEnabled(child)"
@mousedown.prevent
@click="child.toolbarAction?.(textEditor.editor.value!)"
>
<span class="inline-flex items-center gap-2">
Expand Down Expand Up @@ -124,13 +124,14 @@
</template>

<script setup lang="ts">
import { computed, inject, nextTick, onMounted, ref, unref, useTemplateRef } from 'vue'
import { computed, inject, nextTick, onMounted, ref, unref, useTemplateRef, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import type { TextEditorInstance } from '../types'
import { EditorAction } from '../composables'
import type { EditorAction, EditorActionGroup } from '../composables'
import { OcDrop } from '@opencloud-eu/design-system/components'

defineProps<{
const { actionsToDisplay = undefined, teleport = undefined } = defineProps<{
actionsToDisplay?: string[]
teleport?: string
}>()

Expand All @@ -140,6 +141,24 @@ const scrollContainerRef = useTemplateRef('scrollContainer')
const canScrollLeft = ref(false)
const canScrollRight = ref(false)

const isToolbarItemVisible = (item: EditorAction) => {
if (!actionsToDisplay) {
return item.showInToolbar !== false
}

return actionsToDisplay.includes(item.id)
}

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

const dropRefs = ref<Record<string, ComponentPublicInstance<typeof OcDrop>>>({})

function setDropRef(itemId: string, el: ComponentPublicInstance<typeof OcDrop> | null) {
Expand All @@ -164,6 +183,11 @@ onMounted(async () => {
updateScrollState()
})

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

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

.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,11 +19,18 @@ import {
FontSize,
LineHeight
} from '@tiptap/extension-text-style'
import { EditorActionGroup, useEditorActions } from '../useEditorActions'
import {
EditorActionGroup,
useEditorActions,
type UseEditorActionsOptions
} from '../useEditorActions'
import { TextEditorState } from '../../types'
import { imageFileHandlerExtension } from './imageFileHandler'

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

const editorContentType = () => {
Expand Down Expand Up @@ -106,6 +113,7 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate
orderedList,
taskList,
horizontalRule,
link,
image,
imageUrl,
imageUpload,
Expand All @@ -118,7 +126,13 @@ export const useStrategyHtml = (editorState: TextEditorState): ContentTypeStrate
addColumnBefore,
addColumnAfter,
deleteColumn
} = useEditorActions(editorState)
} = useEditorActions(editorState, editorActionOptions)

const toolbarLink = () => ({
...link(),
showInToolbar: true
})
Comment thread
tammi-23 marked this conversation as resolved.
Resolved

const editorActionGroups = (): EditorActionGroup[] => {
return [
{
Expand Down Expand Up @@ -179,7 +193,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
4 changes: 2 additions & 2 deletions packages/web-pkg/src/editor/composables/useEditorActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { markRaw, unref } from 'vue'
import type { MaybeRef } from 'vue'
import type { Component, MaybeRef } from 'vue'
import type { Editor, Range } from '@tiptap/core'
import type { Component } from 'vue'
import type {} from '@tiptap/extension-text-align'
import { useGettext } from 'vue3-gettext'
import { storeToRefs } from 'pinia'
import { OcEmojiPicker } from '@opencloud-eu/design-system/components'
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 @@ -14,9 +15,21 @@ export function useTextEditor(options: TextEditorOptions): TextEditorInstance {
editorZoom: ref(100)
}

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
4 changes: 2 additions & 2 deletions packages/web-pkg/src/editor/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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'

Expand Down