Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: Add table editor UI
Integrate table functionality into the whiteboard interface:

- Add "Insert table" menu item to Excalidraw menu with mdiTable icon
- Create TableEditorDialog using Nextcloud Text editor component

Requires Text app to be installed and enabled for table editing.

Signed-off-by: silver <[email protected]>
  • Loading branch information
silverkszlo authored and juliusknorr committed Dec 9, 2025
commit ba5c2d3497efd8c82fbe9e9ffa646a99cacd0868
10 changes: 9 additions & 1 deletion src/components/ExcalidrawMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { useCallback, useEffect, useRef, memo } from 'react'
import type { KeyboardEvent as ReactKeyboardEvent } from 'react'
import { Icon } from '@mdi/react'
import { mdiMonitorScreenshot, mdiImageMultiple, mdiTimerOutline } from '@mdi/js'
import { mdiMonitorScreenshot, mdiImageMultiple, mdiTimerOutline, mdiTable} from '@mdi/js'
import { MainMenu, CaptureUpdateAction } from '@nextcloud/excalidraw'
import { RecordingMenuItem } from './Recording'
import { PresentationMenuItem } from './Presentation'
Expand All @@ -17,6 +17,7 @@ import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types
import { useExcalidrawStore } from '../stores/useExcalidrawStore'
import type { RecordingHookState } from '../types/recording'
import type { PresentationState } from '../types/presentation'
import { useTableInsertion } from '../hooks/useTableInsertion'

interface ExcalidrawMenuProps {
fileNameWithoutExtension: string
Expand All @@ -31,6 +32,7 @@ export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExte
const { excalidrawAPI } = useExcalidrawStore(useShallow(state => ({
excalidrawAPI: state.excalidrawAPI,
})))
const { insertTable } = useTableInsertion()

const openExportDialog = useCallback(() => {
// Trigger export by dispatching the keyboard shortcut to the Excalidraw canvas
Expand Down Expand Up @@ -152,6 +154,12 @@ export const ExcalidrawMenu = memo(function ExcalidrawMenu({ fileNameWithoutExte
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.DefaultItems.ChangeCanvasBackground />
<MainMenu.Separator />
<MainMenu.Item
icon={<Icon path={mdiTable} size={0.75} />}
onSelect={() => insertTable()}>
{t('whiteboard', 'Insert table')}
</MainMenu.Item>
<MainMenu.Separator />
<MainMenu.Item
icon={<Icon path={mdiImageMultiple} size={0.75} />}
onSelect={openExportDialog}
Expand Down
222 changes: 222 additions & 0 deletions src/components/TableEditorDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script>
import { NcButton, NcModal, NcNoteCard } from '@nextcloud/vue'
import { defineComponent } from 'vue'
import { t } from '@nextcloud/l10n'

export default defineComponent({
name: 'TableEditorDialog',
components: {
NcButton,
NcModal,
NcNoteCard,
},
props: {
initialMarkdown: {
type: String,
default: '',
},
},
emits: ['cancel', 'submit'],
data() {
return {
show: true,
editor: null,
isLoading: true,
error: null,
currentMarkdown: this.initialMarkdown || this.getDefaultTable(),
}
},
computed: {
isEditing() {
return Boolean(this.initialMarkdown)
},
},
async mounted() {
await this.$nextTick()
await this.initializeEditor()
},
beforeUnmount() {
this.destroyEditor()
},
methods: {
t,
async initializeEditor() {
try {
// Check if Text app is available
if (!window.OCA?.Text) {
this.error = t('whiteboard', 'Nextcloud Text app is not available. Please install and enable it.')
this.isLoading = false
return
}

const editorContainer = this.$refs.editorContainer
if (!editorContainer) {
this.error = t('whiteboard', 'Editor container not found')
this.isLoading = false
return
}

// Create the Text editor instance with callbacks
this.editor = await window.OCA.Text.createEditor({
el: editorContainer,
content: this.currentMarkdown,
// Track content changes
onUpdate: ({ markdown }) => {
this.currentMarkdown = markdown
},
onCreate: ({ markdown }) => {
this.currentMarkdown = markdown
},
})

this.isLoading = false

// Focus the editor after a short delay
setTimeout(() => {
if (this.editor) {
this.editor.focus?.()
}
}, 100)
} catch (error) {
console.error('Failed to initialize Text editor:', error)
this.error = t('whiteboard', 'Failed to load the editor: {error}', { error: error.message })
this.isLoading = false
}
},

getDefaultTable() {
return `| Column 1 | Column 2 | Column 3 |
| -------- | -------- | -------- |
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |`
},

onCancel() {
this.show = false
this.$emit('cancel')
},

async onInsert() {
if (!this.editor) {
this.error = t('whiteboard', 'Editor not initialized')
return
}

try {
// Use the tracked markdown content
const markdown = this.currentMarkdown

if (!markdown || !markdown.trim()) {
this.error = t('whiteboard', 'Please enter some table content')
return
}

this.$emit('submit', {
markdown: markdown.trim(),
})

this.show = false
} catch (error) {
console.error('Failed to get editor content:', error)
this.error = t('whiteboard', 'Failed to get content: {error}', { error: error.message })
}
},

destroyEditor() {
if (this.editor) {
try {
this.editor.destroy()
} catch (error) {
console.error('Error destroying editor:', error)
}
this.editor = null
}
},
},
})
</script>

<template>
<NcModal v-if="show"
:can-close="true"
size="large"
@close="onCancel">
<div class="table-editor-dialog">
<div class="editor-header">
<h2>
{{ isEditing ? t('whiteboard', 'Edit Table') : t('whiteboard', 'Insert Table') }}
</h2>
<p>{{ t('whiteboard', 'Use the table feature in the editor to create or edit your table') }}</p>
</div>

<NcNoteCard v-if="error" type="error">
{{ error }}
</NcNoteCard>

<div v-if="isLoading" class="loading-message">
{{ t('whiteboard', 'Loading editor…') }}
</div>

<div ref="editorContainer" class="editor-container" />

<div class="dialog-buttons">
<NcButton @click="onCancel">
{{ t('whiteboard', 'Cancel') }}
</NcButton>
<NcButton type="primary" :disabled="isLoading || error" @click="onInsert">
{{ isEditing ? t('whiteboard', 'Update') : t('whiteboard', 'Insert') }}
</NcButton>
</div>
</div>
</NcModal>
</template>

<style scoped lang="scss">
.table-editor-dialog {
padding: 20px;
display: flex;
flex-direction: column;
min-height: 500px;
}

.editor-header {
margin-bottom: 16px;

h2 {
margin: 0 0 8px 0;
}

p {
margin: 0;
color: var(--color-text-maxcontrast);
font-size: 14px;
}
}

.loading-message {
padding: 40px;
text-align: center;
color: var(--color-text-maxcontrast);
}

.editor-container {
flex: 1;
min-height: 400px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
}

.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
</style>