quill icon indicating copy to clipboard operation
quill copied to clipboard

Shift + Enter Keyboard Shortcut Not Working in Quill 2.0.2

Open unknown-user-github opened this issue 1 year ago • 7 comments

Hi Quill Team,

I am currently using Quill 2.0.2 on Windows 10 with Firefox 126.0.1. I encountered an issue with the "Shift + Enter" keyboard shortcut: it does not seem to work as expected. Specifically, when I press "Shift + Enter" while writing text in the editor, it always creates a new paragraph (

) instead of inserting a line break (
).

Here is a snippet of my current implementation:

var quill = new Quill('#editor', {
  theme: 'snow'
});

quill.keyboard.addBinding({
  key: 13,  // Enter key
  shiftKey: true
}, function(range, context) {
  quill.insertText(range.index, '\n');
  quill.setSelection(range.index + 1);
});

Could you help me understand what might be going wrong?

unknown-user-github avatar Jun 09 '24 15:06 unknown-user-github

grafik https://quilljs.com/docs/modules/keyboard#configuration

kozi avatar Jun 13 '24 06:06 kozi

Isn’t this more about the fact that Quill doesn’t support line breaks (<br> tags) as any \n will be recognized as a new ‘row’ in Parchment?

lutzissler avatar Jun 19 '24 07:06 lutzissler

I found #2872. This appears to be a long standing issue. Quite unfortunate, that was the last piece of the puzzle to migrate away from CKEditor5. :/

dstj avatar Sep 13 '24 17:09 dstj

Try CodeMirror…

lutzissler avatar Sep 13 '24 18:09 lutzissler

@lutzissler Thanks, but CodeMirror doesn't appear to have a toolbar for Bold, List, Headers, etc. It's specialized for code output. That's not my use case.

dstj avatar Sep 13 '24 18:09 dstj

Sorry, meant the sister project, ProseMirror.

lutzissler avatar Sep 13 '24 18:09 lutzissler

Is there any similar ProseMirror (or tiptap) based OpenSource solution?

kozi avatar Sep 13 '24 19:09 kozi

We use an editor to manage our email templates. I resolved the visual alignment issue in the application by removing the top padding from the following CSS rule:

css

.quill-editor .ql-editor p {
    font-size: 14px;
}

This adjustment corrects the display issue. When rendering or sending the content, I apply the following transformations to format the output as desired:

php
// Replace <p><br></p> with <br> for standalone line breaks
$body = preg_replace('/<p><br><\/p>/', '<br>', $body);

// Replace </p><p> with <br> to combine paragraphs on the same line
$body = preg_replace('/<\/p>\s*<p>/', '<br>', $body);

This approach visually aligns the editor content while also formatting the HTML appropriately for rendering and sending.

Diego-Franke avatar Nov 04 '24 11:11 Diego-Franke

Thanks for the workaround, but it is horrible that we have to do such a thing!

AlexanderWeckmer avatar Jan 04 '25 15:01 AlexanderWeckmer

Following @Diego-Franke solution, please see here our poor man's approach:

We wanted to keep some of the <p> tags and change the </p><p> to <br>s.


// Replace <p><br></p> with <br> for standalone line breaks
$regexText = preg_replace('/<p><br><\/p>/', '<br />', $textFromQuill);

// Replace empty <p></p> tags with a placeholder
$regexText = preg_replace('/<p>\s*<\/p>/', '<!--EMPTYBYQUILL-->', $regexText);

// Replace </p><p> with <br />
$regexText = preg_replace('/<\/p>\s*<p>/', '<br />', $regexText);

// Remove the placeholder
$regexText = preg_replace('/<!--EMPTYBYQUILL-->/', '', $regexText);

Why This Approach?

We needed something quick and dirty to process our HTML without pulling in a full parser. While regex for HTML is less than ideal, it works in our specific use case.

Disclaimer:

We know it’s not elegant, but maybe it’ll help someone or inspire a better solution. Suggestions are more than welcome! Or Quill will accept <br>, as it should!!!

AlexanderWeckmer avatar Jan 04 '25 18:01 AlexanderWeckmer

Yeah, I had shift+enter working with Quill v1.3, but the upgrade to Quill v2 and Angular 19 broke it. The fix was to change the keyboard binding as follows:

  onEditorCreated(editor: Quill) {
    editor.scroll.register(SoftLineBreakBlot);
    editor.keyboard.addBinding({ key: 'Enter', shiftKey: true }, shiftEnterHandler);    <<<<<
    editor.clipboard.addMatcher('BR', brMatcher);
  }

Using 'Enter' instead of 13.

hhubik avatar Jan 31 '25 16:01 hhubik

@hhubik Really? Can you share more of your code, maybe your whole editor configuration? Because I cannot get the handler to fire with Shift + Enter.

quill.keyboard.addBinding({ key: 'Enter', shiftKey: true }, () => { console.log('shift enter handler'); });

This never fires for me.

Update

Maybe it's because I am using ngx-quill with Angular that quill.keyboard.addBinding() does not work, but I have found a potential solution from here and here.

Here is the relevant code:

	public modules = {
		uploader: false,
		toolbar: [
			...
		],
		clipboard: {
			matchers: [
				['BR', lineBreakMatcher]
			]
		},
		keyboard: {
			bindings: {
				"shift enter": {
					key: 'Enter',
					shiftKey: true,
					handler: function (range) {
						shiftEnterHandler(this.quill, range);
					}
				}
			}
		}
	}
export function shiftEnterHandler(quill: Quill, range) {
	const currentLeaf = quill.getLeaf(range.index)[0];
	const nextLeaf = quill.getLeaf(range.index + 1)[0];
	quill.insertEmbed(range.index, "sbreak", true, Quill.sources.USER);
	// Insert a second break if:
	// At the end of the editor, OR next leaf has a different parent (<p>)
	if (nextLeaf === null || currentLeaf.parent !== nextLeaf.parent) {
		quill.insertEmbed(range.index, "sbreak", true, Quill.sources.USER);
	}
	// Now that we've inserted a line break, move the cursor forward
	quill.setSelection(range.index + 1, Quill.sources.SILENT);
}

function lineBreakMatcher(): Delta {
	const newDelta = new Delta();
	newDelta.insert({'sbreak': ''});
	return newDelta;
}
export class SoftLineBreakBlot extends Embed {
	static blotName = 'sbreak';
	static tagName = 'br';
}
Quill.register(SoftLineBreakBlot);

dstj avatar Feb 04 '25 19:02 dstj

@dstj looks like you have found another way of doing this in the meantime, which looks a bit different from what I did. I am hosting the editor in a standalone Angular 19 component and do all the editor initialization in that component like this:

import { Component, ChangeDetectionStrategy, OnDestroy, ElementRef, Input, HostBinding, LOCALE_ID, inject } from '@angular/core';
...
import Quill, { Delta } from 'quill';
import Embed from 'quill/blots/embed';

const SOFTBREAK_KEYWORD = 'softbreak';

export class SoftLineBreakBlot extends Embed {
  static override blotName = SOFTBREAK_KEYWORD;
  static override tagName = 'br';
  static override className = SOFTBREAK_KEYWORD;
}

export function shiftEnterHandler(this: any, range: { index: number }) {
  // console.log('keybinding enter + shift', range, this);
  const currentLeaf = this.quill.getLeaf(range.index)[0];
  const nextLeaf = this.quill.getLeaf(range.index + 1)[0];
  this.quill.insertEmbed(range.index, SOFTBREAK_KEYWORD, true, Quill.sources.USER);
  // Insert a second break if:
  // At the end of the editor, OR next leaf has a different parent (<p>)
  if (nextLeaf === null || currentLeaf.parent !== nextLeaf.parent) {
    this.quill.insertEmbed(range.index, SOFTBREAK_KEYWORD, true, Quill.sources.USER);
  }
  // // Now that we've inserted a line break, move the cursor forward
  this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
}

/**
 * Converts HTML <br> elements to softbreak embeds when pasting content
 * @param node The DOM node being matched
 * @returns Delta containing a softbreak embed
 */
export function brMatcher(node: HTMLElement): Delta {
  if (!(node instanceof HTMLBRElement)) {
    return new Delta();
  }
  return new Delta().insert({ [SOFTBREAK_KEYWORD]: true });
}
@Component({
  selector: 'my-comp',
  templateUrl: './my-comp.html',
  styleUrls: ['./my-comp.scss'],
  providers: [{ provide: MatFormFieldControl, useExisting: InfoDocumentFormControlComponent }],
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
...
    QuillEditorComponent,
...
  ]
})
export class MyComponent implements ControlValueAccessor, MatFormFieldControl<xxxx>, OnDestroy {
...
  quillModules: QuillModules = {};
  quillCustomOptions: CustomOption[] = [];


  constructor() {
...
    this.quillModules = {
      keyboard: {
        bindings: {
          'shift+enter': {
            key: ['Enter'],
            shiftKey: true,
            handler: shiftEnterHandler
          }
        }
      },
      // see https://quilljs.com/docs/modules/toolbar/ for more info
      toolbar: [
        [{ header: [1, 2, 3, false] }],
        ['bold', 'italic', 'underline', 'strike'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        [{ align: [] }],
        ['image']
      ]
    };

    this.quillCustomOptions = [
      {
        import: 'formats/font',
        whitelist: ['mirza', 'roboto', 'aref', 'serif', 'sansserif', 'monospace']
      }
    ];
  }

...
  onEditorCreated(editor: Quill) {
    editor.scroll.register(SoftLineBreakBlot);
    editor.keyboard.addBinding({ key: 'Enter', shiftKey: true }, shiftEnterHandler);
    editor.clipboard.addMatcher('BR', brMatcher);

    // set editor content if a value is provided
    if (this.editorContent) {
      editor.clipboard.dangerouslyPasteHTML(this.editorContent);
    }
    console.log('editor created', editor);
  }

Here is the relevant part of the component template:

      <quill-editor
...
        [sanitize]="true"
        formControlName="xxx"
        [modules]="quillModules"
        (onEditorCreated)="onEditorCreated($event)"
      >
      </quill-editor>

hhubik avatar Feb 04 '25 22:02 hhubik