Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add soft line breaks #4565

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
46 changes: 37 additions & 9 deletions packages/quill/src/blots/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import Delta from 'quill-delta';
import Break from './break.js';
import Inline from './inline.js';
import TextBlot from './text.js';
import SoftBreak, { SOFT_BREAK_CHARACTER } from './soft-break.js';

const NEWLINE_LENGTH = 1;
const softBreakRegex = new RegExp(`(${SOFT_BREAK_CHARACTER})`, 'g');

class Block extends BlockBlot {
cache: { delta?: Delta | null; length?: number } = {};
Expand All @@ -25,6 +27,11 @@ class Block extends BlockBlot {

deleteAt(index: number, length: number) {
super.deleteAt(index, length);
this.children.forEach((child) => {
if (child instanceof Break) {
child.optimize();
}
});
this.cache = {};
}

Expand All @@ -42,6 +49,11 @@ class Block extends BlockBlot {
value,
);
}
this.children.forEach((child) => {
if (child instanceof Break) {
child.optimize();
}
});
this.cache = {};
}

Expand All @@ -55,11 +67,17 @@ class Block extends BlockBlot {
const lines = value.split('\n');
const text = lines.shift() as string;
if (text.length > 0) {
if (index < this.length() - 1 || this.children.tail == null) {
super.insertAt(Math.min(index, this.length() - 1), text);
} else {
this.children.tail.insertAt(this.children.tail.length(), text);
}
const softLines = text.split(softBreakRegex);
let i = index;
softLines.forEach((str) => {
if (str === SOFT_BREAK_CHARACTER) {
super.insertAt(i, SoftBreak.blotName, SOFT_BREAK_CHARACTER);
} else {
super.insertAt(Math.min(i, this.length() - 1), str);
}
i += str.length;
});

this.cache = {};
}
// TODO: Fix this next time the file is edited.
Expand All @@ -74,11 +92,12 @@ class Block extends BlockBlot {
}

insertBefore(blot: Blot, ref?: Blot | null) {
const { head } = this.children;
super.insertBefore(blot, ref);
if (head instanceof Break) {
head.remove();
}
this.children.forEach((child) => {
if (child instanceof Break) {
child.optimize();
}
});
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it was necessary to add these calls to optimize here since it seems that optimize is not called when using the editor methods to modify text (i.e. applyDelta, insertText, etc.). This appears to be the case because the existing call to optimize in ScrollBlot depends on there being 1 or more mutations causing the change, which only happens when the editor contents is changed through some user action in the browser.

If there is some better way of accomplishing this than what I've done here, please let me know.

this.cache = {};
}

Expand All @@ -96,6 +115,15 @@ class Block extends BlockBlot {

optimize(context: { [key: string]: any }) {
super.optimize(context);

// in order for an end-of-block soft break to be rendered properly by the browser, we need a trailing break
if (
this.children.length > 0 &&
this.children.tail?.statics.blotName === SoftBreak.blotName
) {
const breakBlot = this.scroll.create(Break.blotName);
super.insertBefore(breakBlot, null);
}
this.cache = {};
}

Expand Down
11 changes: 9 additions & 2 deletions packages/quill/src/blots/break.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { EmbedBlot } from 'parchment';
import SoftBreak from './soft-break.js';

class Break extends EmbedBlot {
static value() {
return undefined;
}

optimize() {
if (this.prev || this.next) {
optimize(): void {
const thisIsLastBlotInParent = this.next == null;
const noPrevBlots = this.prev == null;
const prevBlotIsSoftBreak =
this.prev != null && this.prev.statics.blotName == SoftBreak.blotName;
const shouldRender =
thisIsLastBlotInParent && (noPrevBlots || prevBlotIsSoftBreak);
if (!shouldRender) {
this.remove();
}
}
Expand Down
21 changes: 21 additions & 0 deletions packages/quill/src/blots/soft-break.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EmbedBlot } from 'parchment';

export const SOFT_BREAK_CHARACTER = '\u2028';

export default class SoftBreak extends EmbedBlot {
static tagName = 'BR';
static blotName: string = 'soft-break';
static className: string = 'soft-break';

length(): number {
return 1;
}

value(): string {
return SOFT_BREAK_CHARACTER;
}

optimize(): void {
return;
}
}
2 changes: 2 additions & 0 deletions packages/quill/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Embed from './blots/embed.js';
import Inline from './blots/inline.js';
import Scroll from './blots/scroll.js';
import TextBlot from './blots/text.js';
import SoftBreak from './blots/soft-break.js';

import Clipboard from './modules/clipboard.js';
import History from './modules/history.js';
Expand All @@ -38,6 +39,7 @@ Quill.register({
'blots/block': Block,
'blots/block/embed': BlockEmbed,
'blots/break': Break,
'blots/soft-break': SoftBreak,
'blots/container': Container,
'blots/cursor': Cursor,
'blots/embed': Embed,
Expand Down
1 change: 1 addition & 0 deletions packages/quill/src/core/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class Editor {
const normalizedDelta = normalizeDelta(contents);
const change = new Delta().retain(index).concat(normalizedDelta);
this.scroll.insertContents(index, normalizedDelta);
this.scroll.optimize();
return this.update(change);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/quill/src/modules/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { FontStyle } from '../formats/font.js';
import { SizeStyle } from '../formats/size.js';
import { deleteRange } from './keyboard.js';
import normalizeExternalHTML from './normalizeExternalHTML/index.js';
import { SOFT_BREAK_CHARACTER } from '../blots/soft-break.js';

const debug = logger('quill:clipboard');

Expand All @@ -33,6 +34,7 @@ const CLIPBOARD_CONFIG: [Selector, Matcher][] = [
[Node.TEXT_NODE, matchText],
[Node.TEXT_NODE, matchNewline],
['br', matchBreak],
['br.soft-break', matchSoftBreak],
[Node.ELEMENT_NODE, matchNewline],
[Node.ELEMENT_NODE, matchBlot],
[Node.ELEMENT_NODE, matchAttributor],
Expand Down Expand Up @@ -496,6 +498,10 @@ function matchBreak(node: Node, delta: Delta) {
return delta;
}

function matchSoftBreak() {
return new Delta().insert(SOFT_BREAK_CHARACTER);
}

function matchCodeBlock(node: Node, delta: Delta, scroll: ScrollBlot) {
const match = scroll.query('code-block');
const language =
Expand Down
17 changes: 17 additions & 0 deletions packages/quill/src/modules/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import logger from '../core/logger.js';
import Module from '../core/module.js';
import type { BlockEmbed } from '../blots/block.js';
import type { Range } from '../core/selection.js';
import SoftBreak, { SOFT_BREAK_CHARACTER } from '../blots/soft-break.js';

const debug = logger('quill:keyboard');

Expand Down Expand Up @@ -84,6 +85,13 @@ class Keyboard extends Module<KeyboardOptions> {
this.addBinding(this.options.bindings[name]);
}
});
this.addBinding(
{
key: 'Enter',
shiftKey: true,
},
this.handleShiftEnter,
);
this.addBinding({ key: 'Enter', shiftKey: null }, this.handleEnter);
this.addBinding(
{ key: 'Enter', metaKey: null, ctrlKey: null, altKey: null },
Expand Down Expand Up @@ -352,6 +360,15 @@ class Keyboard extends Module<KeyboardOptions> {
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.focus();
}

handleShiftEnter(range: Range) {
this.quill.insertEmbed(
range.index,
SoftBreak.blotName,
SOFT_BREAK_CHARACTER,
);
this.quill.setSelection(range.index + 1);
}
}

const defaultOptions: KeyboardOptions = {
Expand Down
141 changes: 71 additions & 70 deletions packages/quill/test/e2e/__dev_server__/index.html
Original file line number Diff line number Diff line change
@@ -1,74 +1,75 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>Quill E2E Tests</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<link href="/quill.core.css" rel="stylesheet" />
<link href="/quill.snow.css" rel="stylesheet" />
</head>

<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Quill E2E Tests</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<link href="/quill.core.css" rel="stylesheet">
<link href="/quill.snow.css" rel="stylesheet">
</head>

<body>
<div id="root">
<div id="standalone-container">
<div id="toolbar-container">
<span class="ql-formats">
<select class="ql-font"></select>
<select class="ql-size"></select>
</span>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-script" value="sub"></button>
<button class="ql-script" value="super"></button>
</span>
<span class="ql-formats">
<button class="ql-header" value="1"></button>
<button class="ql-header" value="2"></button>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
<button class="ql-indent" value="-1"></button>
<button class="ql-indent" value="+1"></button>
</span>
<span class="ql-formats">
<button class="ql-direction" value="rtl"></button>
<select class="ql-align"></select>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image"></button>
<button class="ql-video"></button>
<button class="ql-formula"></button>
</span>
<span class="ql-formats">
<button class="ql-clean"></button>
</span>
</div>
<div id="editor" style="height: 350px;">
<body>
<div id="root">
<div id="standalone-container">
<div id="toolbar-container">
<span class="ql-formats">
<select class="ql-font"></select>
<select class="ql-size"></select>
</span>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-script" value="sub"></button>
<button class="ql-script" value="super"></button>
</span>
<span class="ql-formats">
<button class="ql-header" value="1"></button>
<button class="ql-header" value="2"></button>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
<button class="ql-indent" value="-1"></button>
<button class="ql-indent" value="+1"></button>
</span>
<span class="ql-formats">
<button class="ql-direction" value="rtl"></button>
<select class="ql-align"></select>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image"></button>
<button class="ql-video"></button>
<button class="ql-formula"></button>
</span>
<span class="ql-formats">
<button class="ql-clean"></button>
</span>
</div>
<div id="editor" style="height: 350px"></div>
<script>
window.quill = new Quill(document.getElementById('editor'), {
modules: { syntax: true, toolbar: '#toolbar-container' },
placeholder: 'Compose an epic...',
theme: 'snow',
});
</script>
</div>
<script>
window.quill = new Quill(document.getElementById('editor'), {
modules: { syntax: true, toolbar: '#toolbar-container', },
placeholder: 'Compose an epic...', theme: 'snow',
})
</script>
</div>
</div>
</body>

</html>
</body>
</html>
Loading