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
3 changes: 3 additions & 0 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
</template>
<ContentContainer v-show="contentLoaded"
ref="contentWrapper" />
<SuggestionsBar v-if="isRichEditor && contentLoaded" />
</MainContainer>
<Reader v-if="isResolvingConflict"
:content="syncError.data.outsideChange"
Expand Down Expand Up @@ -125,6 +126,7 @@ import Translate from './Modal/Translate.vue'
import CollisionResolveDialog from './CollisionResolveDialog.vue'
import { generateRemoteUrl } from '@nextcloud/router'
import { fetchNode } from '../services/WebdavClient.ts'
import SuggestionsBar from './SuggestionsBar.vue'

export default {
name: 'Editor',
Expand All @@ -141,6 +143,7 @@ export default {
Status,
Assistant,
Translate,
SuggestionsBar,
},
mixins: [
isMobile,
Expand Down
9 changes: 2 additions & 7 deletions src/components/Menu/ActionInsertLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
<script>
import { NcActions, NcActionButton, NcActionInput } from '@nextcloud/vue'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'

Expand All @@ -76,6 +75,7 @@
import { BaseActionEntry } from './BaseActionEntry.js'
import { useFileMixin } from '../Editor.provider.js'
import { useMenuIDMixin } from './MenuBar.provider.js'
import { buildFilePicker } from '../../helpers/filePicker.js'

export default {
name: 'ActionInsertLink',
Expand Down Expand Up @@ -122,12 +122,7 @@
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
}

const filePicker = getFilePickerBuilder(t('text', 'Select file or folder to link to'))
.startAt(this.startPath)
.allowDirectories(true)
.setMultiSelect(false)
.setType(FilePickerType.Choose)
.build()
const filePicker = buildFilePicker(this.startPath)

Check warning on line 125 in src/components/Menu/ActionInsertLink.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Menu/ActionInsertLink.vue#L125

Added line #L125 was not covered by tests

filePicker.pick()
.then((file) => {
Expand Down
200 changes: 200 additions & 0 deletions src/components/SuggestionsBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div v-if="isEmptyContent" class="container-suggestions">
<NcButton ref="linkFileOrFolder"
type="secondary"
size="normal"
class="suggestions--button"
@click="linkFile">
<template #icon>
<Document :size="20" />
</template>
<template v-if="!isMobile" #default>
{{ t('text', 'Link to file or folder') }}
</template>
</NcButton>

<NcButton type="secondary"
size="normal"
class="suggestions--button"
@click="$callChooseLocalAttachment">
<template #icon>
<Upload :size="20" />
</template>
<template v-if="!isMobile" #default>
{{ t('text', 'Upload') }}
</template>
</NcButton>

<NcButton type="secondary"
size="normal"
class="suggestions--button"
@click="insertTable">
<template #icon>
<TableIcon :size="20" />
</template>
<template v-if="!isMobile" #default>
{{ t('text', 'Insert Table') }}
</template>
</NcButton>

<NcButton type="secondary"
size="normal"
class="suggestions--button"
@click="linkPicker">
<template #icon>
<Shape :size="20" />
</template>
<template v-if="!isMobile" #default>
{{ t('text', 'Smart Picker') }}
</template>
</NcButton>
</div>
</template>

<script>
import { NcButton } from '@nextcloud/vue'
import { Document, Shape, Upload, Table as TableIcon } from '../components/icons.js'
import { useActionChooseLocalAttachmentMixin } from './Editor/MediaHandler.provider.js'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { useEditorMixin, useFileMixin } from './Editor.provider.js'
import { generateUrl } from '@nextcloud/router'
import { buildFilePicker } from '../helpers/filePicker.js'
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'

export default {
name: 'SuggestionsBar',
components: {
TableIcon,
Document,
NcButton,
Shape,
Upload,
},
mixins: [
useActionChooseLocalAttachmentMixin,
useEditorMixin,
useFileMixin,
],

setup() {
const isMobile = useIsMobile()
return {
isMobile,
}
},

data: () => {
return {
startPath: null,
isEmptyContent: false,
}
},

computed: {
relativePath() {
return this.$file?.relativePath ?? '/'
},
},

mounted() {
this.$editor.on('update', this.onUpdate)
this.onUpdate({ editor: this.$editor })
},

beforeDestroy() {
this.$editor.off('update', this.onUpdate)
},

methods: {
/**
* Open smart picker dialog
* Triggered by the "Smart Picker" button
*/
linkPicker() {
getLinkWithPicker(null, true)
.then(link => {
const chain = this.$editor.chain()
if (this.$editor.view.state?.selection.empty) {
chain.focus().insertPreview(link).run()
} else {
chain.setLink({ href: link }).focus().run()
}
})
.catch(error => {
console.error('Smart picker promise rejected', error)
})
},

/**
* Insert table
* Triggered by the "Insert table" button
*/
insertTable() {
this.$editor.chain().focus().insertTable()?.run()
},

/**
* Open dialog and ask user which file to link to
* Triggered by the "link to file or folder" button
*/
linkFile() {
if (this.startPath === null) {
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
}

const filePicker = buildFilePicker(this.startPath)

filePicker.pick()
.then((file) => {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
const url = new URL(generateUrl(`/f/${fileInfo.id}`), window.origin)
this.setLink(url.href, fileInfo.name)
this.startPath = fileInfo.path + (fileInfo.type === 'dir' ? `/${fileInfo.name}/` : '')
})
})
.catch(() => {
// do not close menu but keep focus
this.$refs.linkFileOrFolder.$el.focus()
})
},

/**
* Save user entered URL as a link markup
* Triggered when the user submits the ActionInput
*
* @param {string} url href attribute of the link
* @param {string} text Text part of the link
*/
setLink(url, text) {
this.$editor.chain().insertOrSetLink(text, { href: url }).focus().run()
},

onUpdate({ editor }) {
/**
* Empty document has an empty document and an empty paragraph (open and close blocks)
*/
const EMPTY_DOCUMENT_SIZE = 4
this.isEmptyContent = editor.state.doc.nodeSize <= EMPTY_DOCUMENT_SIZE
},
},
}
</script>

<style scoped lang="scss">

.container-suggestions {
display: flex;
flex-grow: 1;
margin-left: max(0px, (100% - var(--text-editor-max-width)) / 2);
}

.suggestions--button {
margin: 5px;
}
</style>
4 changes: 4 additions & 0 deletions src/css/print.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,13 @@
border: none!important;
}
}
.container-suggestions {
display: none;
}
}
}


.menubar-placeholder, .text-editor--readonly-bar {
display: none;
}
Expand Down
2 changes: 1 addition & 1 deletion src/css/prosemirror.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ div.ProseMirror {
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
padding: 4px 8px 200px 14px;
padding: 4px 8px 50px 14px;
line-height: 150%;
font-size: var(--default-font-size);
outline: none;
Expand Down
15 changes: 15 additions & 0 deletions src/helpers/filePicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'

export const buildFilePicker = (startPath) => {
return getFilePickerBuilder(t('text', 'Select file or folder to link to'))
.startAt(startPath)
.allowDirectories(true)
.setMultiSelect(false)
.setType(FilePickerType.Choose)
.build()
}

Check warning on line 15 in src/helpers/filePicker.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/filePicker.js#L9-L15

Added lines #L9 - L15 were not covered by tests