Skip to content

Commit

Permalink
Merge pull request #8594 from nextcloud/enh/CKEditor-slash-commands
Browse files Browse the repository at this point in the history
Add slash command and emoji picker to mail composer
  • Loading branch information
hamza221 authored Oct 27, 2023
2 parents 21db406 + f34f69b commit 34d3f14
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 5 deletions.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@ckeditor/ckeditor5-image": "37.1.0",
"@ckeditor/ckeditor5-link": "37.1.0",
"@ckeditor/ckeditor5-list": "37.1.0",
"@ckeditor/ckeditor5-mention": "37.1.0",
"@ckeditor/ckeditor5-paragraph": "37.1.0",
"@ckeditor/ckeditor5-remove-format": "37.1.0",
"@ckeditor/ckeditor5-theme-lark": "37.1.0",
Expand Down
81 changes: 81 additions & 0 deletions src/ckeditor/smartpicker/InsertItemCommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @author Daniel Kesselberg <[email protected]>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

import Command from '@ckeditor/ckeditor5-core/src/command'
export default class InsertItemCommand extends Command {

/**
* @param {module:core/editor/editor~Editor} editor instance
* @param {module:engine/model/writer~Writer} writer instance
* @param {string} item smart picker or emoji picker
* @param {string} trigger the character to replace
*/
insertItem(editor, writer, item, trigger) {
const currentPosition = editor.model.document.selection.getLastPosition()
if (currentPosition === null) {
// null as current position is probably not possible
// @TODO Add error to handle such a situation in the callback
return
}

const range = editor.model.createRange(
currentPosition.getShiftedBy(-5),
currentPosition
)

// Iterate over all items in this range:
const walker = range.getWalker({ shallow: false, direction: 'backward' })

for (const value of walker) {
if (value.type === 'text' && value.item.data.includes(trigger)) {
writer.remove(value.item)

const text = value.item.data
const lastSlash = text.lastIndexOf(trigger)

const textElement = writer.createElement('paragraph')
writer.insertText(text.substring(0, lastSlash), textElement)
editor.model.insertContent(textElement)

const itemElement = writer.createElement('paragraph')
writer.insertText(item, itemElement)
editor.model.insertContent(itemElement)

return
}
}

// @TODO If we end up here, we did not find the slash. We should throw an error maybe.
}

/**
* @param {string} item link from smart picker or emoji from emoji picker
* @param {string} trigger the character to replace
*/
execute(item, trigger) {
this.editor.model.change(writer => {
this.insertItem(this.editor, writer, item, trigger)
})
}

refresh() {
this.isEnabled = true
}

}
31 changes: 31 additions & 0 deletions src/ckeditor/smartpicker/PickerPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @author Daniel Kesselberg <[email protected]>
*
* Mail
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
import InsertItemCommand from './InsertItemCommand'
export default class PickerPlugin extends Plugin {

init() {
this.editor.commands.add(
'insertItem',
new InsertItemCommand(this.editor)
)
}

}
123 changes: 118 additions & 5 deletions src/components/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<script>
import CKEditor from '@ckeditor/ckeditor5-vue2'
import AlignmentPlugin from '@ckeditor/ckeditor5-alignment/src/alignment'
import { Mention } from '@ckeditor/ckeditor5-mention'
import Editor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote'
Expand All @@ -53,10 +54,11 @@ import ImagePlugin from '@ckeditor/ckeditor5-image/src/image'
import ImageResizePlugin from '@ckeditor/ckeditor5-image/src/imageresize'
import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload'
import MailPlugin from '../ckeditor/mail/MailPlugin'
import { searchProvider, getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText'
import { emojiSearch, emojiAddRecent } from '@nextcloud/vue/dist/Functions/emoji'
import { getLanguage } from '@nextcloud/l10n'
import logger from '../logger'
import PickerPlugin from '../ckeditor/smartpicker/PickerPlugin'
export default {
name: 'TextEditor',
Expand Down Expand Up @@ -90,7 +92,14 @@ export default {
},
},
data() {
const plugins = [EssentialsPlugin, ParagraphPlugin, SignaturePlugin, QuotePlugin]
const plugins = [
EssentialsPlugin,
ParagraphPlugin,
SignaturePlugin,
QuotePlugin,
PickerPlugin,
Mention,
]
const toolbar = ['undo', 'redo']
if (this.html) {
Expand Down Expand Up @@ -131,6 +140,9 @@ export default {
}
return {
linkTribute: null,
emojiTribute: null,
textSmiles: [],
ready: false,
editor: Editor,
config: {
Expand All @@ -140,13 +152,86 @@ export default {
items: toolbar,
},
language: 'en',
mention: {
feeds: [
{
marker: ':',
feed: this.getEmoji,
itemRenderer: this.customEmojiRenderer,
},
{
marker: '/',
feed: this.getLink,
itemRenderer: this.customLinkRenderer,
},
],
},
},
}
},
beforeMount() {
this.loadEditorTranslations(getLanguage())
},
methods: {
getLink(text) {
const results = searchProvider(text)
if (results.length === 1 && !results[0].title.toLowerCase().includes(text.toLowerCase())) {
return []
}
return results
},
getEmoji(text) {
const emojiResults = emojiSearch(text)
if (this.textSmiles.includes(':' + text)) {
emojiResults.unshift(':' + text)
}
return emojiResults
},
customEmojiRenderer(item) {
const itemElement = document.createElement('span')
itemElement.classList.add('custom-item')
itemElement.id = `mention-list-item-id-${item.colons}`
itemElement.textContent = `${item.native} `
itemElement.style.width = '100%'
itemElement.style.borderRadius = '8px'
itemElement.style.padding = '4px 8px'
itemElement.style.display = 'block'
const usernameElement = document.createElement('span')
usernameElement.classList.add('custom-item-username')
usernameElement.textContent = item.colons
itemElement.appendChild(usernameElement)
return itemElement
},
customLinkRenderer(item) {
const itemElement = document.createElement('span')
itemElement.classList.add('link-container')
itemElement.style.width = '100%'
itemElement.style.borderRadius = '8px'
itemElement.style.padding = '4px 8px'
itemElement.style.display = 'block'
const icon = document.createElement('img')
icon.style.width = '20px'
icon.style.marginRight = '1em'
icon.style.filter = 'var(--background-invert-if-dark)'
icon.classList.add('link-icon')
icon.src = `${item.icon_url} `
const usernameElement = document.createElement('span')
usernameElement.classList.add('link-title')
usernameElement.textContent = `${item.title} `
itemElement.appendChild(icon)
itemElement.appendChild(usernameElement)
return itemElement
},
async loadEditorTranslations(language) {
if (language === 'en') {
// The default, nothing to fetch
Expand Down Expand Up @@ -178,6 +263,26 @@ export default {
*/
onEditorReady(editor) {
logger.debug('TextEditor is ready', { editor })
editor.commands.get('mention')?.on('execute', (event, data) => {
event.stop()
const eventData = data[0]
const item = eventData.mention
if (eventData.marker === ':') {
emojiAddRecent(item)
this.editorInstance.execute('insertItem', item.native, ':')
}
if (eventData.marker === '/') {
getLinkWithPicker(item.id)
.then((link) => {
this.editorInstance.execute('insertItem', link, '/')
this.editorInstance.editing.view.focus()
})
.catch((error) => {
console.debug('Smart picker promise rejected:', error)
})
}
}, { priority: 'high' })
this.editorInstance = editor
if (this.focus) {
Expand Down Expand Up @@ -215,14 +320,17 @@ export default {
</script>
<style lang="scss" scoped>
:deep(a) {
color: #07d;
}
:deep(p) {
cursor: text;
margin: 0 !important;
}
.link-title {
color: red;
}
</style>
<style>
Expand All @@ -232,5 +340,10 @@ https://github.com/ckeditor/ckeditor5/issues/1142
*/
:root {
--ck-z-default: 10000;
}
--ck-color-list-button-on-background: var(--color-primary-element-light);
--ck-color-list-background: var(--color-main-background);
--ck-color-list-button-hover-background: var(--color-primary-element-light);
--ck-balloon-border-width: 0;
--ck-color-text: var(--color-text-primary);
}
</style>
Loading

0 comments on commit 34d3f14

Please sign in to comment.