Browse Source

Fixes: Mobile - Add actual line breaks on "Enter"

Vladimir Sheremet 2 years ago
parent
commit
1e1af933d4

+ 25 - 9
app/frontend/cypress/shared/components/Form/fields/FieldEditor/editor-actions.cy.ts

@@ -2,24 +2,27 @@
 
 import { mountEditor } from './utils'
 
-const testAction = (action: string, expected: (text: string) => string) => {
+const testAction = (
+  action: string,
+  expected: (text: string) => string,
+  typeText = 'Something',
+  hint = ' ',
+) => {
   describe(`testing action - ${action}`, { retries: 2 }, () => {
-    it(`${action} - enabled, text after is affected`, () => {
+    it(`${action}${hint} - enabled, text after is affected`, () => {
       mountEditor()
       cy.findByRole('textbox').click()
       cy.findByTestId('action-bar').findByLabelText(action).click()
       cy.findByRole('textbox')
-        .type('Something')
-        .should('contain.html', expected('Something'))
+        .type(typeText)
+        .should('contain.html', expected(typeText))
     })
 
-    it(`${action} - toggle text`, () => {
+    it(`${action}${hint} - toggle text`, () => {
       mountEditor()
-      cy.findByRole('textbox')
-        .type('Something{selectall}')
-        .should('have.html', '<p>Something</p>')
+      cy.findByRole('textbox').type(`${typeText}{selectall}`)
       cy.findByTestId('action-bar').findByLabelText(action).click()
-      cy.findByRole('textbox').should('contain.html', expected('Something'))
+      cy.findByRole('textbox').should('contain.html', expected(typeText))
     })
   })
 }
@@ -35,6 +38,19 @@ describe('testing actions', () => {
   testAction('Add ordered list', (text) => `<ol><li><p>${text}</p></li></ol>`)
   testAction('Add bullet list', (text) => `<ul><li><p>${text}</p></li></ul>`)
 
+  testAction(
+    'Add ordered list',
+    () => `<ol><li><p>Something1</p></li><li><p>Something2</p></li></ol>`,
+    'Something1{enter}Something2',
+    ' (multiline)',
+  )
+  testAction(
+    'Add bullet list',
+    () => `<ul><li><p>Something1</p></li><li><p>Something2</p></li></ul>`,
+    'Something1{enter}Something2',
+    ' (multiline)',
+  )
+
   describe('testing action - remove formatting', () => {
     it('removes formatting', () => {
       mountEditor()

+ 4 - 1
app/frontend/cypress/shared/components/Form/fields/FieldEditor/editor-footer.cy.ts

@@ -11,6 +11,7 @@ describe('displays footer information', () => {
 
   it("doesn't display footer, if footer if disabled", () => {
     mountEditor({
+      contentType: 'text/plain',
       meta: {
         footer: {
           disabled: true,
@@ -32,6 +33,7 @@ describe('displays footer information', () => {
 
   it('displays footer, if footer text is provided', () => {
     mountEditor({
+      contentType: 'text/plain',
       meta: {
         footer: {
           text: '/AB',
@@ -52,6 +54,7 @@ describe('displays footer information', () => {
 
   it('renders counter that decrements', () => {
     mountEditor({
+      contentType: 'text/plain',
       meta: {
         footer: {
           text: '/AB',
@@ -79,7 +82,7 @@ describe('displays footer information', () => {
           .and('have.class', 'text-orange')
       })
     cy.findByRole('textbox')
-      .type('1234567')
+      .type('\n\n\n4567')
       .then(() => {
         cy.findByTitle('Available characters')
           .should('have.text', '-6')

+ 9 - 0
app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorInput.vue

@@ -75,6 +75,9 @@ const editor = useEditor({
     },
     // add inlined files
     handlePaste(view, event) {
+      if (!editorExtensions.some((n) => n.name === 'image')) {
+        return
+      }
       const files = event.clipboardData?.files || null
       convertFileList(files).then((urls) => {
         editor.value?.commands.setImages(urls)
@@ -88,6 +91,9 @@ const editor = useEditor({
       return false
     },
     handleDrop(view, event) {
+      if (!editorExtensions.some((n) => n.name === 'image')) {
+        return
+      }
       const e = event as unknown as InputEvent
       const files = e.dataTransfer?.files || null
       convertFileList(files).then((urls) => {
@@ -230,6 +236,9 @@ const removeSignature = () => {
 }
 
 const characters = computed(() => {
+  if (contentType.value === 'text/plain') {
+    return currentValue.value?.length || 0
+  }
   if (!editor.value) return 0
   return editor.value.storage.characterCount.characters({
     node: editor.value.state.doc,

+ 2 - 2
app/frontend/shared/components/Form/fields/FieldEditor/FieldEditorWrapper.vue

@@ -18,7 +18,7 @@ interface Props {
 const props = defineProps<Props>()
 
 const editorRerenderKey = computed(() => {
-  // when plain changes, we need to rerender the editor
+  // when content-type changes, we need to rerender the editor
   const type = props.context.contentType === 'text/plain' ? 'plain' : 'html'
   return `${type}-${props.context.id}`
 })
@@ -49,7 +49,7 @@ Object.assign(props.context, preContext)
         <FieldEditorFooter
           v-if="context.meta?.footer && !context.meta.footer.disabled"
           :footer="context.meta.footer"
-          :characters="0"
+          :characters="context._value?.length || 0"
         />
       </div>
     </template>

+ 5 - 0
app/frontend/shared/components/Form/fields/FieldEditor/extensions/HardBreakParagraph.ts

@@ -0,0 +1,5 @@
+// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+import HardBreak from '@tiptap/extension-hard-break'
+
+export default HardBreak

+ 15 - 0
app/frontend/shared/components/Form/fields/FieldEditor/extensions/HardBreakSimple.ts

@@ -0,0 +1,15 @@
+// Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+import HardBreak from '@tiptap/extension-hard-break'
+
+export default HardBreak.extend({
+  addKeyboardShortcuts() {
+    return {
+      // by default "Enter" doesn't add an actual line break, only "visible" break.
+      // actual line break is added with "Shift+Enter", which is not very intuitive,
+      // so we are rewriting it for out implementation to also use "Enter" for line break
+      // WARNING: this should not be used for HTML editor as it causes bugs with other extensions
+      Enter: () => this.editor.commands.setHardBreak(),
+    }
+  },
+})

+ 6 - 0
app/frontend/shared/components/Form/fields/FieldEditor/extensions/list.ts

@@ -17,6 +17,8 @@ import UserMention, { UserLink } from '../suggestions/UserMention'
 import KnowledgeBaseSuggestion from '../suggestions/KnowledgeBaseSuggestion'
 import TextModuleSuggestion from '../suggestions/TextModuleSuggestion'
 import Image from './Image'
+import HardBreakSimple from './HardBreakSimple'
+import HardBreakParagraph from './HardBreakParagraph'
 import Signature from './Signature'
 import type { FieldEditorProps } from '../types'
 
@@ -34,9 +36,11 @@ export const getPlainExtensions = (): Extensions => [
     horizontalRule: false,
     italic: false,
     listItem: false,
+    hardBreak: false,
     orderedList: false,
     strike: false,
   }),
+  HardBreakSimple,
   CharacterCount,
 ]
 
@@ -46,6 +50,7 @@ export const getHtmlExtensions = (): Extensions => [
     listItem: false,
     blockquote: false,
     paragraph: false,
+    hardBreak: false,
   }),
   Paragraph.extend({
     addAttributes() {
@@ -61,6 +66,7 @@ export const getHtmlExtensions = (): Extensions => [
   Underline,
   OrderedList,
   ListItem,
+  HardBreakParagraph,
   Blockquote.extend({
     addAttributes() {
       return {

+ 2 - 2
app/frontend/shared/components/Form/fields/FieldEditor/useEditorActions.ts

@@ -41,7 +41,7 @@ export default function useEditorActions(
     return editor.value?.isActive(type, attributes) ?? false
   }
 
-  // this is primarily used by cypress tests, where it requires actual input in dom
+  // this is primarily used by cypress tests, where it requires an actual input in the DOM tree
   let fileInput: HTMLInputElement | null = null
 
   const getInputForImage = () => {
@@ -51,7 +51,7 @@ export default function useEditorActions(
     fileInput.type = 'file'
     fileInput.multiple = true
     fileInput.accept = 'image/*'
-    fileInput.style.display = 'hidden'
+    fileInput.style.display = 'none'
     if (import.meta.env.DEV || VITE_TEST_MODE) {
       fileInput.dataset.testId = 'editor-image-input'
     }

+ 7 - 6
app/frontend/shared/components/Form/fields/FieldEditor/useNavigateOptions.ts

@@ -22,6 +22,7 @@ export default function useNavigateOptions(
       selectedIndex.value += 1
     }
     focus(selectedIndex.value)
+    return selectedIndex.value in items.value
   }
 
   const goPrevious = () => {
@@ -31,27 +32,27 @@ export default function useNavigateOptions(
       selectedIndex.value -= 1
     }
     focus(selectedIndex.value)
+    return selectedIndex.value in items.value
   }
 
   const selectItem = (index?: number) => {
     const item = items.value[index || selectedIndex.value]
     if (item) {
       onSelect(item)
+      return true
     }
+    return false
   }
 
   const onKeyDown = (event: KeyboardEvent) => {
     if (event.key === 'ArrowDown') {
-      goNext()
-      return true
+      return goNext()
     }
     if (event.key === 'ArrowUp') {
-      goPrevious()
-      return true
+      return goPrevious()
     }
     if (event.key === 'Enter' || event.key === 'Tab') {
-      selectItem()
-      return true
+      return selectItem()
     }
     return false
   }

+ 1 - 0
package.json

@@ -103,6 +103,7 @@
     "@tiptap/core": "^2.0.0-beta.220",
     "@tiptap/extension-blockquote": "^2.0.0-beta.220",
     "@tiptap/extension-character-count": "^2.0.0-beta.220",
+    "@tiptap/extension-hard-break": "^2.0.0-beta.220",
     "@tiptap/extension-image": "^2.0.0-beta.220",
     "@tiptap/extension-link": "^2.0.0-beta.220",
     "@tiptap/extension-list-item": "^2.0.0-beta.220",