From 3bc9e82770a3d6a7a230d13b46cd1ccb21a34171 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 1 Dec 2025 18:44:17 +0100 Subject: [PATCH 01/21] feat(outline): Store headings in `useEditorHeadings` composable Signed-off-by: Jonas --- src/components/Editor.vue | 11 +++++++++++ src/composables/useEditorHeadings.ts | 23 +++++++++++++++++++++++ src/{files.js => files.ts} | 7 ++++--- vite.config.ts | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/composables/useEditorHeadings.ts rename src/{files.js => files.ts} (86%) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index afa472f7db6..3dfad093105 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -99,6 +99,7 @@ import { generateRemoteUrl } from '@nextcloud/router' import { Awareness } from 'y-protocols/awareness.js' import { provideConnection } from '../composables/useConnection.ts' import { useDelayedFlag } from '../composables/useDelayedFlag.ts' +import { useEditorHeadings } from '../composables/useEditorHeadings.ts' import { useEditorMethods } from '../composables/useEditorMethods.ts' import { provideEditorWidth } from '../composables/useEditorWidth.ts' import { provideSaveService } from '../composables/useSaveService.ts' @@ -116,6 +117,7 @@ import { serializePlainText, } from './../EditorFactory.js' import { createMarkdownSerializer } from './../extensions/Markdown.js' +import { headingAnchorPluginKey } from '../plugins/headingAnchor.js' import markdownit from './../markdownit/index.js' import isMobile from './../mixins/isMobile.js' import AttachmentResolver from './../services/AttachmentResolver.js' @@ -273,6 +275,8 @@ export default defineComponent({ const syncProvider = shallowRef(null) + const { updateHeadings } = useEditorHeadings() + return { awareness, connection, @@ -291,6 +295,7 @@ export default defineComponent({ syncProvider, syncService, updateUser, + updateHeadings, width, ydoc, } @@ -522,6 +527,9 @@ export default defineComponent({ } }) this.updateUser(session) + this.updateHeadings( + [...headingAnchorPluginKey.getState(this.editor.state)?.headings] ?? [] + ) }, onChange({ document }) { @@ -544,6 +552,9 @@ export default defineComponent({ this.emit('update:content', { markdown: proseMirrorMarkdown, }) + this.updateHeadings( + [...headingAnchorPluginKey.getState(this.editor.state)?.headings] ?? [] + ) }, onSync({ steps, document }) { diff --git a/src/composables/useEditorHeadings.ts b/src/composables/useEditorHeadings.ts new file mode 100644 index 00000000000..f4774a23f55 --- /dev/null +++ b/src/composables/useEditorHeadings.ts @@ -0,0 +1,23 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { readonly, shallowRef } from 'vue' + +type Heading = { + id: string, + level: number, + offset: number, + text: string, +} + +const headings = shallowRef([]) + +export const useEditorHeadings = () => { + const updateHeadings = (newHeadings: Heading[]) => { + headings.value = newHeadings + } + + return { headings: readonly(headings), updateHeadings } +} diff --git a/src/files.js b/src/files.ts similarity index 86% rename from src/files.js rename to src/files.ts index ebb08156b64..c85c0d8d79f 100644 --- a/src/files.js +++ b/src/files.ts @@ -2,6 +2,7 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + import { loadState } from '@nextcloud/initial-state' // eslint-disable-next-line import/no-unresolved, n/no-missing-import @@ -12,7 +13,7 @@ const workspaceEnabled = loadState('text', 'workspace_enabled') const openReadOnlyEnabled = loadState('text', 'open_read_only_enabled') document.addEventListener('DOMContentLoaded', async () => { - if (workspaceAvailable && OCA && OCA?.Files?.Settings) { + if (workspaceAvailable && window.OCA.Files?.Settings) { const { default: Vue } = await import('vue') const { default: FilesSettings } = await import('./views/FilesSettings.vue') @@ -20,8 +21,8 @@ document.addEventListener('DOMContentLoaded', async () => { render: (h) => h(FilesSettings, {}), }) const el = vm.$mount().$el - OCA.Files.Settings.register( - new OCA.Files.Settings.Setting('text', { + window.OCA.Files.Settings.register( + new window.OCA.Files.Settings.Setting('text', { el: () => { return el }, diff --git a/vite.config.ts b/vite.config.ts index 6ea4f59dba5..362b67e5065 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,7 +32,7 @@ const rewriteMiddlewarePlugin = { const config = createAppConfig( { text: path.join(__dirname, 'src', 'main.js'), - files: path.join(__dirname, 'src', 'files.js'), + files: path.join(__dirname, 'src', 'files.ts'), public: path.join(__dirname, 'src', 'public.js'), viewer: path.join(__dirname, 'src', 'viewer.js'), editor: path.join(__dirname, 'src', 'editor.js'), From 72c5fa991d0053381d64a45092048af9d55eeda0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Dec 2025 10:25:30 +0100 Subject: [PATCH 02/21] feat(css): remove grid container in editor content wrapper Fixes: #7228 Signed-off-by: Jonas --- cypress/e2e/shareWithAttachments.spec.js | 4 +-- src/components/BaseReader.vue | 31 +++-------------- src/components/Editor.vue | 8 ++--- src/components/Editor/ContentContainer.vue | 40 ++-------------------- src/components/ViewerComponent.vue | 2 +- src/composables/useEditorWidth.ts | 5 ++- src/css/print.scss | 20 +---------- src/views/RichWorkspace.vue | 2 +- 8 files changed, 18 insertions(+), 94 deletions(-) diff --git a/cypress/e2e/shareWithAttachments.spec.js b/cypress/e2e/shareWithAttachments.spec.js index 5d5b13127cf..1bae56c9fae 100644 --- a/cypress/e2e/shareWithAttachments.spec.js +++ b/cypress/e2e/shareWithAttachments.spec.js @@ -92,8 +92,8 @@ describe('Public Share with attachments', () => { it('open attached files in folder description', function () { cy.visit(`/s/${this.token}`) - cy.get('.content-wrapper').should('exist') - cy.get('.content-wrapper .name', { timeout: 10_000 }).click() + cy.get('.editor__content-wrapper').should('exist') + cy.get('.editor__content-wrapper .name', { timeout: 10_000 }).click() cy.get('.viewer').should('exist') cy.get('.language-plaintext').should('contain', 'multiple lines') }) diff --git a/src/components/BaseReader.vue b/src/components/BaseReader.vue index e081af8d933..3a6f9bb3471 100644 --- a/src/components/BaseReader.vue +++ b/src/components/BaseReader.vue @@ -4,20 +4,11 @@ --> @@ -85,23 +76,9 @@ export default { diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 3dfad093105..9d2616bc641 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -905,10 +905,6 @@ export default defineComponent({ &.draggedOver { background-color: var(--color-primary-element-light); } - - .text-editor__content-wrapper { - position: relative; - } } } @@ -916,11 +912,11 @@ export default defineComponent({ width: 50%; } -.text-editor__wrapper.has-conflicts > .content-wrapper { +.text-editor__wrapper.has-conflicts > .editor__content-wrapper { width: 50%; #read-only-editor { - margin: 0px auto; + margin: 0 auto; // Add height of the menubar as padding-top padding-top: calc( var(--default-clickable-area) + 2 * var(--default-grid-baseline) diff --git a/src/components/Editor/ContentContainer.vue b/src/components/Editor/ContentContainer.vue index efbc083308a..cd5cdc3b80b 100644 --- a/src/components/Editor/ContentContainer.vue +++ b/src/components/Editor/ContentContainer.vue @@ -6,20 +6,13 @@ @@ -73,40 +66,13 @@ export default { diff --git a/src/components/ViewerComponent.vue b/src/components/ViewerComponent.vue index d7a9837d185..d331f868263 100644 --- a/src/components/ViewerComponent.vue +++ b/src/components/ViewerComponent.vue @@ -200,7 +200,7 @@ export default { &.source-viewer { display: block; - .text-editor__content-wrapper { + .editor__content-wrapper { margin-top: var(--header-height); } diff --git a/src/composables/useEditorWidth.ts b/src/composables/useEditorWidth.ts index 1d6f3d2f6e8..3ebb02adb08 100644 --- a/src/composables/useEditorWidth.ts +++ b/src/composables/useEditorWidth.ts @@ -44,6 +44,9 @@ export const editorWidthKey = Symbol('text:editor:width') as InjectionKey< Readonly | null> > +// wide screen: 80ch content, narrow screen: full width +const defaultEditorWidth = 'min(80ch, 100vw)' + /** * Detect if other apps (such as collectives) already configured texts max width. * @@ -73,7 +76,7 @@ export const provideEditorWidth = () => { valueSingleton = value isFullWidth.value = value }) - const width = computed(() => (isFullWidth.value ? '100%' : '80ch')) + const width = computed(() => (isFullWidth.value ? '100%' : defaultEditorWidth)) const applyEditorWidth = () => { document.documentElement.style.setProperty( '--text-editor-max-width', diff --git a/src/css/print.scss b/src/css/print.scss index 3bc92a2b8e5..0ee6cc908c6 100644 --- a/src/css/print.scss +++ b/src/css/print.scss @@ -61,9 +61,7 @@ // Hide table buttons display: none !important; } - .content-wrapper .editor__content { - // Prevent cut lines (due to `display: grid`) - display: block; + .editor__content-wrapper .editor__content { // Margins set by page rule max-width: 100%; } @@ -121,22 +119,6 @@ display: none; } - .text-editor__content-wrapper { - &.--show-outline { - display: block; - } - - .editor--outline { - width: auto; - height: auto; - overflow: unset; - position: relative; - } - .editor--outline__btn-close { - display: none; - } - } - .collaboration-carets__caret, .collaboration-carets__label { display: none; diff --git a/src/views/RichWorkspace.vue b/src/views/RichWorkspace.vue index 8053187caac..ad55a7f5753 100644 --- a/src/views/RichWorkspace.vue +++ b/src/views/RichWorkspace.vue @@ -292,7 +292,7 @@ export default { overflow: visible !important; } -#rich-workspace:deep(.content-wrapper) { +#rich-workspace:deep(.editor__content-wrapper) { overflow: scroll !important; max-height: calc(40vh - 50px); padding-left: 10px; From c97446e57e7887a4cdf496fb311479b95cdda486 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 8 Dec 2025 13:04:29 +0100 Subject: [PATCH 03/21] feat(outline): Add visible outline representation next to content Fixes: #3586 Fixes: #5379 Fixes: #6876 Fixes: #7522 Fixes: nextcloud/collectives#1624 Fixes: nextcloud/collectives#1715 Fixes: nextcloud/collectives#1777 Signed-off-by: Jonas --- src/components/BaseReader.vue | 23 +-- src/components/Editor.vue | 4 +- src/components/Editor/ContentContainer.vue | 16 +- src/components/Editor/EditorOutline.vue | 188 ++++++++++++++------- src/css/print.scss | 4 + 5 files changed, 158 insertions(+), 77 deletions(-) diff --git a/src/components/BaseReader.vue b/src/components/BaseReader.vue index 3a6f9bb3471..fc8f77569b5 100644 --- a/src/components/BaseReader.vue +++ b/src/components/BaseReader.vue @@ -9,30 +9,27 @@ id="read-only-editor" class="editor__content text-editor__content" :editor="editor" /> + diff --git a/src/css/print.scss b/src/css/print.scss index 0ee6cc908c6..af976287d29 100644 --- a/src/css/print.scss +++ b/src/css/print.scss @@ -65,6 +65,10 @@ // Margins set by page rule max-width: 100%; } + .editor__outline { + // Hide outline + display: none !important; + } .text-editor__wrapper { height: fit-content; position: unset; From 8e483820d282d7c91178e0a8dfa2e320c27f8ebb Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 9 Dec 2025 19:37:41 +0100 Subject: [PATCH 04/21] fix(editor): leave space for floating buttons and outline in full width Signed-off-by: Jonas --- src/components/Editor/ContentContainer.vue | 12 ++++++++---- src/composables/useEditorWidth.ts | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Editor/ContentContainer.vue b/src/components/Editor/ContentContainer.vue index 13c8f477bc6..d70e7cc320f 100644 --- a/src/components/Editor/ContentContainer.vue +++ b/src/components/Editor/ContentContainer.vue @@ -22,7 +22,6 @@ import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' import { EditorContent } from '@tiptap/vue-2' import { useEditor } from '../../composables/useEditor.ts' import { useEditorFlags } from '../../composables/useEditorFlags.ts' -import { useEditorWidth } from '../../composables/useEditorWidth.ts' import EditorOutline from './EditorOutline.vue' import FloatingButtons from './FloatingButtons.vue' @@ -43,15 +42,13 @@ export default { const isMobile = useIsMobile() const { editor } = useEditor() const { isRichEditor, isRichWorkspace } = useEditorFlags() - const { isFullWidth } = useEditorWidth() - return { editor, isMobile, isFullWidth, isRichEditor, isRichWorkspace } + return { editor, isMobile, isRichEditor, isRichWorkspace } }, computed: { showFloatingButtons() { return ( !this.readOnly && !this.isMobile - && !this.isFullWidth && this.isRichEditor && !this.isRichWorkspace ) @@ -68,6 +65,13 @@ export default { diff --git a/src/components/Editor/TableOfContents.vue b/src/components/Editor/TableOfContents.vue index 21901b40b26..3ec437d1c9c 100644 --- a/src/components/Editor/TableOfContents.vue +++ b/src/components/Editor/TableOfContents.vue @@ -4,24 +4,36 @@ --> diff --git a/src/components/Editor/TocContainer.vue b/src/components/Editor/TocContainer.vue new file mode 100644 index 00000000000..59b5c491016 --- /dev/null +++ b/src/components/Editor/TocContainer.vue @@ -0,0 +1,152 @@ + + + + + + + diff --git a/src/components/Editor/TocOutline.vue b/src/components/Editor/TocOutline.vue new file mode 100644 index 00000000000..786b3ebd942 --- /dev/null +++ b/src/components/Editor/TocOutline.vue @@ -0,0 +1,79 @@ + + + + + + + diff --git a/src/composables/useEditorFlags.ts b/src/composables/useEditorFlags.ts index ca080b344c6..e6098df875e 100644 --- a/src/composables/useEditorFlags.ts +++ b/src/composables/useEditorFlags.ts @@ -11,6 +11,7 @@ export interface EditorFlags { isPublic: boolean isRichEditor: boolean isRichWorkspace: boolean + useTableOfContents: boolean } interface Props { isDirectEditing: boolean @@ -24,14 +25,16 @@ export const provideEditorFlags = (props: Props) => { const isRichEditor = loadState('text', 'rich_editing_enabled', true) && props.mime === 'text/markdown' - provide(editorFlagsKey, { isPublic, isRichEditor, isRichWorkspace }) - return { isPublic, isRichEditor, isRichWorkspace } + const useTableOfContents = !isRichWorkspace && isRichEditor + provide(editorFlagsKey, { isPublic, isRichEditor, isRichWorkspace, useTableOfContents }) + return { isPublic, isRichEditor, isRichWorkspace, useTableOfContents } } export const useEditorFlags = () => { - const { isPublic, isRichEditor, isRichWorkspace } = inject(editorFlagsKey, { + const { isPublic, isRichEditor, isRichWorkspace, useTableOfContents } = inject(editorFlagsKey, { isPublic: false, isRichEditor: false, isRichWorkspace: false, + useTableOfContents: false, }) - return { isPublic, isRichEditor, isRichWorkspace } + return { isPublic, isRichEditor, isRichWorkspace, useTableOfContents } } diff --git a/src/composables/useEditorHeadings.ts b/src/composables/useEditorHeadings.ts index f4774a23f55..8521f631cdb 100644 --- a/src/composables/useEditorHeadings.ts +++ b/src/composables/useEditorHeadings.ts @@ -10,14 +10,25 @@ type Heading = { level: number, offset: number, text: string, + previous?: number, } const headings = shallowRef([]) +const displayToc = shallowRef(false) export const useEditorHeadings = () => { const updateHeadings = (newHeadings: Heading[]) => { headings.value = newHeadings } - return { headings: readonly(headings), updateHeadings } + const setDisplayToc = (display: boolean) => { + displayToc.value = display + } + + return { + displayToc: readonly(displayToc), + headings: readonly(headings), + setDisplayToc, + updateHeadings, + } } diff --git a/src/composables/useEditorWidth.ts b/src/composables/useEditorWidth.ts index ae55e2cfb60..063ca71b37d 100644 --- a/src/composables/useEditorWidth.ts +++ b/src/composables/useEditorWidth.ts @@ -44,9 +44,12 @@ export const editorWidthKey = Symbol('text:editor:width') as InjectionKey< Readonly | null> > +// desktop: leave space for floating buttons and outline (2 * 40px) // wide screen: 80ch content, narrow screen: full width -const defaultEditorWidth = 'min(80ch, 100vw)' -const fullEditorWidth = 'calc(100% - 2 * 48px)' +const defaultEditorWidthDesktop = 'min(80ch, (100% - 2 * 40px))' +const defaultEditorWidthMobile = 'min(80ch, 100%)' +const fullEditorWidthDesktop = 'calc(100% - 2 * 40px)' +const fullEditorWidthMobile = '100%' /** * Detect if other apps (such as collectives) already configured texts max width. @@ -66,6 +69,7 @@ function maxWidthSetOutsideOfText() { } export const provideEditorWidth = () => { + const isMobile = useIsMobile() // keep style that is already set - for example by collectives if (maxWidthSetOutsideOfText()) { provide(editorWidthKey, null) @@ -77,7 +81,9 @@ export const provideEditorWidth = () => { valueSingleton = value isFullWidth.value = value }) - const width = computed(() => (isFullWidth.value ? fullEditorWidth : defaultEditorWidth)) + const defaultEditorWidth = computed(() => (isMobile.value ? defaultEditorWidthMobile : defaultEditorWidthDesktop)) + const fullEditorWidth = computed(() => (isMobile.value ? fullEditorWidthMobile : fullEditorWidthDesktop)) + const width = computed(() => (isFullWidth.value ? fullEditorWidth.value : defaultEditorWidth.value)) const applyEditorWidth = () => { document.documentElement.style.setProperty( '--text-editor-max-width', From de6909623070b503fab04a9cc3ec59c0b7cb3b6e Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 11 Dec 2025 16:33:37 +0100 Subject: [PATCH 06/21] fix(outline/toc): migrate to `useEditorHeadings` in more places Signed-off-by: Jonas --- src/components/Editor.vue | 24 +- src/components/Editor/ContentContainer.vue | 17 +- .../Editor/MarkdownContentEditor.vue | 26 +- .../Editor/PlainTableContentEditor.vue | 3 +- src/components/Editor/TableOfContents.vue | 9 +- src/components/Editor/TocContainer.vue | 23 +- src/components/Editor/TocOutline.vue | 10 +- src/components/Editor/Wrapper.provider.js | 25 - src/components/Editor/Wrapper.vue | 66 +-- src/components/Menu/ActionList.vue | 3 +- src/components/Menu/BaseActionEntry.js | 13 +- src/components/Menu/MenuBar.vue | 22 +- src/components/Menu/ReadonlyBar.vue | 9 +- src/components/Menu/entries.js | 488 ------------------ .../Suggestion/LinkPicker/suggestions.js | 13 +- src/components/ViewerComponent.vue | 7 +- src/composables/useEditorFlags.ts | 22 +- src/composables/useEditorHeadings.ts | 72 ++- src/composables/useEditorWidth.ts | 12 +- src/composables/useMenuEntries.ts | 31 ++ src/editor.js | 27 +- src/extensions/Keymap.js | 2 +- 22 files changed, 207 insertions(+), 717 deletions(-) delete mode 100644 src/components/Menu/entries.js create mode 100644 src/composables/useMenuEntries.ts diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 99b9b8c6c80..ba22c38a799 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -18,9 +18,7 @@ :is-resolving-conflict="isResolvingConflict" :has-connection-issue="requireReconnect" :content-loaded="contentLoaded" - :show-outline-outside="showOutlineOutside" - @read-only-toggled="readOnlyToggled" - @outline-toggled="outlineToggled"> + @read-only-toggled="readOnlyToggled">