Skip to content

Commit ac615d8

Browse files
authored
Merge pull request #7242 from nextcloud/backport/7204/stable31
[stable31] Bring back and refactor keymap, add focus trap to plain text editor
2 parents 15f2e7f + 514b4f8 commit ac615d8

21 files changed

Lines changed: 198 additions & 85 deletions

cypress/e2e/viewer.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ describe('Open test.md in viewer', function() {
8080

8181
cy.insertLine('- test')
8282

83+
// Ignore TypeError thrown by cypress keydown + keyup
84+
cy.on('uncaught:exception', (err) => {
85+
return !err.message.includes('Cannot read properties of undefined (reading \'toLowerCase\')')
86+
})
87+
8388
// Cypress does not have native tab key support, though this seems to work
8489
// for triggering the key handler of tiptap
8590
// https://github.com/cypress-io/cypress/issues/311

src/EditorFactory.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const createPlainEditor = ({ language, extensions = [] } = {}) => {
7171
defaultLanguage: language,
7272
exitOnTripleEnter: false,
7373
}),
74+
FocusTrap,
7475
...extensions,
7576
],
7677
})

src/components/Editor.vue

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
data-text-el="editor-container"
1010
class="text-editor"
1111
:class="{ 'is-mobile': isMobile }"
12-
tabindex="-1"
13-
@keydown.stop="onKeyDown">
12+
tabindex="-1">
1413
<SkeletonLoading v-if="showLoadingSkeleton" />
1514
<CollisionResolveDialog v-if="isResolvingConflict" :sync-error="syncError" />
1615
<Wrapper v-if="displayed"
@@ -374,6 +373,7 @@ export default {
374373
window.addEventListener('beforeprint', this.preparePrinting)
375374
window.addEventListener('afterprint', this.preparePrinting)
376375
}
376+
subscribe('text:keyboard:save', this.onKeyboardSave)
377377
subscribe('text:image-node:add', this.onAddImageNode)
378378
subscribe('text:image-node:delete', this.onDeleteImageNode)
379379
this.emit('update:loaded', true)
@@ -398,6 +398,7 @@ export default {
398398
window.removeEventListener('beforeprint', this.preparePrinting)
399399
window.removeEventListener('afterprint', this.preparePrinting)
400400
}
401+
unsubscribe('text:keyboard:save', this.onKeyboardSave)
401402
unsubscribe('text:image-node:add', this.onAddImageNode)
402403
unsubscribe('text:image-node:delete', this.onDeleteImageNode)
403404
unsubscribe('text:translate-modal:show', this.showTranslateModal)
@@ -728,6 +729,10 @@ export default {
728729
this.emit('blur')
729730
},
730731
732+
onKeyboardSave() {
733+
this.$syncService.save()
734+
},
735+
731736
onAddImageNode() {
732737
this.emit('add-image-node')
733738
},
@@ -844,26 +849,6 @@ export default {
844849
this.$editor.setEditable(this.editMode)
845850
},
846851
847-
onKeyDown(event) {
848-
if (event.key === 'Escape') {
849-
event.preventDefault()
850-
return
851-
}
852-
853-
if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.metaKey && this.$editor.isActive('codeBlock')) {
854-
this.$editor.commands.insertContent('\t')
855-
this.$editor.commands.focus()
856-
event.preventDefault()
857-
event.stopPropagation()
858-
return
859-
}
860-
861-
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
862-
this.$syncService.save()
863-
event.preventDefault()
864-
}
865-
},
866-
867852
showTranslateModal(e) {
868853
this.translateContent = e.content
869854
this.translateModal = true

src/components/Editor/Wrapper.vue

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</template>
1616

1717
<script>
18+
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
1819
import { useIsRichEditorMixin, useIsRichWorkspaceMixin } from './../Editor.provider.js'
1920
import { OUTLINE_STATE, OUTLINE_ACTIONS, READ_ONLY_ACTIONS } from './Wrapper.provider.js'
2021
import useStore from '../../mixins/store.js'
@@ -96,6 +97,7 @@ export default {
9697
},
9798
9899
mounted() {
100+
subscribe('text:keyboard:outline', this.outlineToggle)
99101
this.outline.enable = this.isAbleToShowOutline
100102
101103
this.$watch(
@@ -105,22 +107,17 @@ export default {
105107
Object.assign(this.outline, { enable })
106108
},
107109
)
108-
109-
document.addEventListener('keydown', this.handleKeyDown)
110110
},
111+
111112
beforeDestroy() {
112-
document.removeEventListener('keydown', this.handleKeyDown)
113+
unsubscribe('text:keyboard:outline', this.outlineToggle)
113114
},
115+
114116
methods: {
115117
outlineToggle() {
116118
this.outline.visible = !this.outline.visible
117119
this.$emit('outline-toggled', this.outline.visible)
118120
},
119-
handleKeyDown(event) {
120-
if (event.ctrlKey && event.altKey && event.key === 'h') {
121-
this.outlineToggle()
122-
}
123-
},
124121
readOnlyToggle() {
125122
this.$emit('read-only-toggled')
126123
},

src/components/Link/LinkBubbleView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
name="newHref"
7272
:label="t('text', 'URL')"
7373
:value.sync="newHref"
74-
@keypress.enter.prevent="updateLink" />
74+
@keyup.enter.prevent="updateLink" />
7575
</div>
7676

7777
<!-- link preview -->

src/components/Menu/MenuBar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
role="toolbar"
2323
class="text-menubar__entries"
2424
:aria-label="t('text', 'Formatting menu bar')"
25-
@keydown.left.stop="handleToolbarNavigation"
26-
@keydown.right.stop="handleToolbarNavigation">
25+
@keyup.left.stop="handleToolbarNavigation"
26+
@keyup.right.stop="handleToolbarNavigation">
2727
<!-- The visible inline actions -->
2828
<component :is="actionEntry.component ? actionEntry.component : (actionEntry.children ? 'ActionList' : 'ActionSingle')"
2929
v-for="(actionEntry, index) in visibleEntries"

src/components/Menu/ToolBarLogic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default defineComponent({
6666

6767
/**
6868
* Handle navigation in toolbar
69-
* @param {KeyboardEvent} event The keydown event
69+
* @param {KeyboardEvent} event The keyup event
7070
*/
7171
handleToolbarNavigation(event) {
7272
if (event.key === 'ArrowRight') {

src/components/PlainTextReader.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
</template>
99

1010
<script>
11-
/* eslint-disable import/no-named-as-default */
12-
import CodeBlock from '@tiptap/extension-code-block'
1311
import escapeHtml from 'escape-html'
1412
import BaseReader from './BaseReader.vue'
1513
import { PlainText } from './../extensions/index.js'
@@ -22,7 +20,7 @@ export default {
2220
renderHtml(content) {
2321
return '<pre>' + escapeHtml(content) + '</pre>'
2422
},
25-
extensions: () => [PlainText, CodeBlock],
23+
extensions: () => [PlainText],
2624
},
2725
2826
props: {

src/extensions/FocusTrap.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,29 @@ import { Extension } from '@tiptap/core'
77

88
let ownPaused = false
99

10+
const checkHasExtension = (editor, extensionName) => {
11+
return editor.extensionManager.extensions.some(
12+
(extension) => extension.name === extensionName,
13+
)
14+
}
15+
16+
const checkHasListExtension = (editor) => {
17+
return (
18+
checkHasExtension(editor, 'bulletList')
19+
|| checkHasExtension(editor, 'orderedList')
20+
|| checkHasExtension(editor, 'taskList')
21+
)
22+
}
23+
1024
const toggleFocusTrap = ({ editor }) => {
1125
const trapStack = window._nc_focus_trap ?? []
1226
const activeTrap = trapStack[trapStack.length - 1]
1327

14-
const possibleEditorTabCommand = editor.can().sinkListItem('listItem')
15-
|| editor.can().goToNextCell()
16-
|| editor.can().goToPreviousCell()
28+
const possibleEditorTabCommand
29+
= (checkHasListExtension(editor) && editor.can().sinkListItem('listItem'))
30+
|| (checkHasExtension(editor, 'table') && editor.can().goToNextCell())
31+
|| (checkHasExtension(editor, 'table') && editor.can().goToPreviousCell())
32+
|| (checkHasExtension(editor, 'codeBlock') && editor.isActive('codeBlock'))
1733

1834
if (possibleEditorTabCommand) {
1935
activeTrap?.pause()

src/extensions/Keymap.js

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,72 @@
55

66
import { Extension } from '@tiptap/core'
77
import { Plugin } from '@tiptap/pm/state'
8+
import { emit } from '@nextcloud/event-bus'
89

910
const Keymap = Extension.create({
10-
11-
name: 'customkeymap',
11+
name: 'CustomKeymap',
1212

1313
addKeyboardShortcuts() {
14-
return this.options
14+
return {
15+
/**
16+
* <Mod>-<Alt>-<H>
17+
* Toggle editor outline
18+
*/
19+
'Mod-Alt-h': () => {
20+
emit('text:keyboard:outline')
21+
return true
22+
},
23+
}
1524
},
1625

1726
addProseMirrorPlugins() {
1827
return [
1928
new Plugin({
2029
props: {
2130
handleKeyDown(view, event) {
22-
const key = event.key || event.keyCode
23-
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && (key === 'f' || key === 70)) {
24-
// We need to stop propagation and dispatch the event on the window
25-
// in order to force triggering the browser native search in the text editor
31+
/**
32+
* <Mod>-<S>
33+
* Save editor content
34+
*/
35+
if (
36+
(event.ctrlKey || event.metaKey)
37+
&& !event.altKey
38+
&& !event.shiftKey
39+
&& event.key === 's'
40+
) {
41+
event.preventDefault()
42+
event.stopPropagation()
43+
emit('text:keyboard:save')
44+
return true
45+
}
46+
47+
/**
48+
* <Esc>
49+
* Overwrite Viewer keybinding to close viewer
50+
*/
51+
if (
52+
!event.ctrlKey
53+
&& !event.metaKey
54+
&& !event.altKey
55+
&& !event.shiftKey
56+
&& event.key === 'Escape'
57+
) {
58+
event.preventDefault()
2659
event.stopPropagation()
27-
window.dispatchEvent(event)
2860
return true
2961
}
30-
if (event.key === 'Delete' && event.ctrlKey === true) {
31-
// Prevent deleting the file, by core Viewer.vue
62+
63+
/**
64+
* <Mod>-<Del>
65+
* Overwrite Viewer keybinding to delete the file
66+
*/
67+
if (
68+
(event.ctrlKey || event.metaKey)
69+
&& !event.altKey
70+
&& !event.shiftKey
71+
&& event.key === 'Delete'
72+
) {
3273
event.stopPropagation()
33-
window.dispatchEvent(event)
3474
return true
3575
}
3676
},

0 commit comments

Comments
 (0)