Skip to content

Commit

Permalink
Improve list handling
Browse files Browse the repository at this point in the history
* Fix lists of different types that are next to each other (previously unrendered)
* Make Backspace no longer unindent but just delete newline
* Make Enter no longer make list paragraph but outdent
* Allow smart entry to keep the indentation of its line
* Make a. A. I. i. all behave like 1. (don't set "start" attribute)
* Add "dash" type to bullets created with a dash so an application may style it with a dash (it is not a supported type in browsers, but should be in my opinion)
  • Loading branch information
jacwright committed Nov 7, 2022
1 parent b3434ce commit 5523b07
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 58 deletions.
3 changes: 3 additions & 0 deletions examples/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import InlineMenu from './InlineMenu.svelte';
import Virtualized from './Virtualized.svelte';
import Placeholder from './Placeholder.svelte';
import MediumImages from './MediumImages.svelte';
import SmartEntry from './SmartEntry.svelte';
let url = globalHistory.location.pathname;
const fullScreenRoutes = new Set(['/medium-images']);
Expand All @@ -35,6 +36,7 @@ globalHistory.listen(() => url = globalHistory.location.pathname);
<a href="/bubble-menu" class="menu-item" class:current={url === '/bubble-menu'} use:link>Bubble Menu</a>
<a href="/inline-menu" class="menu-item" class:current={url === '/inline-menu'} use:link>Inline Menu</a>
<a href="/virtualized" class="menu-item" class:current={url === '/virtualized'} use:link>Virtualized Rendering</a>
<a href="/smart-entry" class="menu-item" class:current={url === '/smart-entry'} use:link>Smart Text</a>
<a href="/placeholder" class="menu-item" class:current={url === '/placeholder'} use:link>Placeholders</a>
<a href="/medium-images" class="menu-item" class:current={url === '/medium-images'} use:link>Medium-like Images</a>
</div>
Expand All @@ -47,6 +49,7 @@ globalHistory.listen(() => url = globalHistory.location.pathname);
<Route path="/bubble-menu" component={BubbleMenu}/>
<Route path="/inline-menu" component={InlineMenu}/>
<Route path="/virtualized" component={Virtualized}/>
<Route path="/smart-entry" component={SmartEntry}/>
<Route path="/placeholder" component={Placeholder}/>
<Route path="/medium-images" component={MediumImages}/>
</div>
Expand Down
28 changes: 28 additions & 0 deletions examples/SmartEntry.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script>
import { Editor, smartEntry, smartQuotes } from 'typewriter-editor';
import Root from 'typewriter-editor/lib/Root.svelte';
const editor = window.editor = new Editor({
modules: { smartEntry: smartEntry(), smartQuotes }
});
</script>
<style>
:global(ul[type="dash"]) {
list-style-type: "";
}
</style>

<div class="description">
<h1>Smart Entry</h1>
<p>
Try typing <code>* </code> (an asterisk and a space), a double dash, or quotes!
</p>
</div>

<Root {editor} class="text-content">
<h1>Smart Entry</h1>
<p>
Try typing <code>* </code> (an asterisk and a space) or a double dash!
</p>
<p><br></p>
</Root>
12 changes: 6 additions & 6 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"dependencies": {
"@popperjs/core": "^2.11.6",
"@typewriter/document": "^0.7.3"
"@typewriter/document": "^0.7.4"
},
"peerDependencies": {
"svelte": "3.x"
Expand Down
70 changes: 34 additions & 36 deletions src/modules/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Editor from '../Editor';
import { addShortcutsToEvent, KeyboardEventWithShortcut, ShortcutEvent } from './shortcutFromEvent';
import { Line, normalizeRange } from '@typewriter/document';
import { Source } from '../Source';
import { LineType, Types } from '../typesetting';


// A list of bad characters that we don't want coming in from pasted content (e.g. "\f" aka line feed)
Expand Down Expand Up @@ -34,27 +35,27 @@ export function keyboard(editor: Editor) {

if (isEmpty(line) && type !== lines.default && !type.contained && !type.defaultFollows && !type.frozen && isCollapsed) {
// Convert a bullet point into a paragraph
editor.formatLine(EMPTY_OBJ);
} else {
if (at === start && to === end && type.frozen) {
options = { dontFixNewline: true };
if (at === 0) {
// if single selection and line element (hr, image etc) insert new line before
selection = [ at, at ];
} else {
selection = [ to, to ];
}
attributes = type.nextLineAttributes ? type.nextLineAttributes(attributes) : EMPTY_OBJ;
} else if (atEnd && (type.nextLineAttributes || type.defaultFollows || type.frozen)) {
attributes = type.nextLineAttributes ? type.nextLineAttributes(attributes) : EMPTY_OBJ;
} else if (atStart && !atEnd) {
if (type.defaultFollows) attributes = EMPTY_OBJ;
options = { dontFixNewline: true };
}
editor.insert('\n', attributes, selection, options);
if (at === start && to === end && type.frozen) {
editor.select(at === 0 ? 0 : to);
if (unindent(lines, doc.getLineAt(at))) return;
}

if (at === start && to === end && type.frozen) {
options = { dontFixNewline: true };
if (at === 0) {
// if single selection and line element (hr, image etc) insert new line before
selection = [ at, at ];
} else {
selection = [ to, to ];
}
attributes = type.nextLineAttributes ? type.nextLineAttributes(attributes) : EMPTY_OBJ;
} else if (atEnd && (type.nextLineAttributes || type.defaultFollows || type.frozen)) {
attributes = type.nextLineAttributes ? type.nextLineAttributes(attributes) : EMPTY_OBJ;
} else if (atStart && !atEnd) {
if (type.defaultFollows) attributes = EMPTY_OBJ;
options = { dontFixNewline: true };
}
editor.insert('\n', attributes, selection, options);
if (at === start && to === end && type.frozen) {
editor.select(at === 0 ? 0 : to);
}
}

Expand Down Expand Up @@ -99,7 +100,7 @@ export function keyboard(editor: Editor) {

if (direction === -1 && selection[0] + selection[1] === 0) {
// At the beginning of the document
unindent(doc.getLineAt(at), true);
unindent(lines, doc.getLineAt(at), true);
} else {
const range = normalizeRange(selection);
const line = doc.getLineAt(range[0]);
Expand All @@ -108,9 +109,6 @@ export function keyboard(editor: Editor) {
const outside = isCollapsed && ((direction === -1 && at === start) || (direction === 1 && at === end - 1));

if (outside && !type.contained) {
// At the beginning of a line
if (direction === -1 && unindent(doc.getLineAt(at))) return;

// Delete the next line if it is empty
const mergingLine = doc.lines[doc.lines.indexOf(line) + direction];
const [ first, second ] = direction === 1 ? [ line, mergingLine] : [ mergingLine, line ];
Expand All @@ -124,20 +122,20 @@ export function keyboard(editor: Editor) {

editor.delete(direction, { dontFixNewline: type.frozen });
}
}


function unindent(line: Line, force?: boolean) {
if (!line) return;
const type = lines.findByAttributes(line.attributes, true);
if (!type) return;
if (type.indentable && line.attributes.indent) {
editor.outdent();
return true;
}
if (force || type !== lines.default && !type.defaultFollows) {
editor.formatLine(EMPTY_OBJ);
return true;
}
function unindent(lines: Types<LineType>, line: Line, force?: boolean) {
if (!line) return;
const type = lines.findByAttributes(line.attributes, true);
if (!type) return;
if (type.indentable && line.attributes.indent) {
editor.outdent();
return true;
}
if (force || type !== lines.default && !type.defaultFollows) {
editor.formatLine(EMPTY_OBJ);
return true;
}
}

Expand Down
23 changes: 12 additions & 11 deletions src/modules/smartEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AttributeMap, Delta } from '@typewriter/document';
import Editor, { EditorChangeEvent } from '../Editor';


export type Replacement = [RegExp, (captured: string) => AttributeMap];
export type Replacement = [RegExp, (captured: string, attr: AttributeMap) => AttributeMap];
export type TextReplacement = [RegExp, (captured: string) => string];
const httpExpr = /(https?:\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&/=]*\s$/s;
const wwwExpr = /(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&/=]*\s$/s;
Expand All @@ -16,13 +16,14 @@ export type Handler = (editor?: Editor, index?: number, prefix?: string, wholeTe
*/
export const lineReplacements: Replacement[] = [
[ /^(#{1,6}) $/, capture => ({ header: capture.length }) ],
[ /^[-*] $/, () => ({ list: 'bullet' }) ],
[ /^1\. $/, () => ({ list: 'ordered' }) ],
[ /^([AaIi])\. $/, type => ({ list: 'ordered', type }) ],
[ /^(-?\d+)\. $/, start => ({ list: 'ordered', start }) ], // Use /^(-?\d+)\. $/ to support lists starting at something other than 1.
[ /^([A-Z])\. $/, char => ({ list: 'ordered', type: 'A', start: char.charCodeAt(0) - 'A'.charCodeAt(0) + 1 }) ],
[ /^([a-z])\. $/, char => ({ list: 'ordered', type: 'a', start: char.charCodeAt(0) - 'a'.charCodeAt(0) + 1 }) ],
[ /^([IVXLCDM]+)\. $/i, chars => ({ list: 'ordered', type: chars[0].toUpperCase() === chars[0] ? 'I' : 'i', start: fromRomanNumeral(chars) }) ],
[ /^\* $/, (_, { indent }) => ({ list: 'bullet', indent }) ],
[ /^- $/, (_, { indent }) => ({ list: 'bullet', type: 'dash', indent }) ], // set the type to dash to allow for styling in-app (e.g. `list-style-type: "- ";`)
[ /^1\. $/, (_, { indent }) => ({ list: 'ordered', indent }) ],
[ /^([AaIi])\. $/, (type, { indent }) => ({ list: 'ordered', type, indent }) ],
[ /^(-?\d+)\. $/, (start, { indent }) => ({ list: 'ordered', start, indent }) ], // Use /^(-?\d+)\. $/ to support lists starting at something other than 1.
[ /^([A-Z])\. $/, (char, { indent }) => ({ list: 'ordered', type: 'A', indent, start: char === 'A' ? undefined : char.charCodeAt(0) - 'A'.charCodeAt(0) + 1 }) ],
[ /^([a-z])\. $/, (char, { indent }) => ({ list: 'ordered', type: 'a', indent, start: char === 'a' ? undefined : char.charCodeAt(0) - 'a'.charCodeAt(0) + 1 }) ],
[ /^([IVXLCDM]+)\. $/i, (chars, { indent }) => ({ list: 'ordered', type: chars[0].toUpperCase() === chars[0] ? 'I' : 'i', indent, start: chars.toUpperCase() === 'I' ? undefined : fromRomanNumeral(chars) }) ],
[ /^> $/, () => ({ blockquote: true }) ],
];

Expand Down Expand Up @@ -58,7 +59,7 @@ export function lineReplace(editor: Editor, index: number, prefix: string) {
return lineReplacements.some(([ regexp, getAttributes ]) => {
const match = prefix.match(regexp);
if (match) {
const attributes = getAttributes(match[1]);
const attributes = getAttributes(match[1], editor.doc.getLineFormat(index));
if (!editor.typeset.lines.findByAttributes(attributes)) {
return false;
}
Expand All @@ -82,7 +83,7 @@ export function linkReplace(editor: Editor, index: number, prefix: string) {
let text = match[0].slice(0, -1);
if (text[text.length - 1] === '.') text = text.slice(0, -1);
const end = index - (match[0].length - text.length);
const attributes = getAttributes(text);
const attributes = getAttributes(text, editor.doc.getTextFormat(index));
if (!editor.typeset.formats.findByAttributes(attributes)) {
return false;
}
Expand All @@ -99,7 +100,7 @@ export function markReplace(editor: Editor, index: number, prefix: string, whole
const match = prefix.match(regexp);
if (match) {
let [ text, _, matched, last ] = match;
const attributes = getAttributes(matched);
const attributes = getAttributes(matched, editor.doc.getTextFormat(index));
if (!editor.typeset.formats.findByAttributes(attributes)) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/rendering/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function combineLines(editor: Editor, lines: Line[]): CombinedData {
if (type.shouldCombine) {
collect.push(line);
const next = lines[i + 1];
if (!next || getLineType(editor, next) !== type || !type.shouldCombine(line.attributes, next.attributes)) {
if (!next || getLineType(editor, next) !== type || !type.shouldCombine(collect[0].attributes, next.attributes)) {
// By keeping the last array reference we can optimize updates
const last = linesMultiples.get(collect[0]);
if (last && last.length === collect.length && collect.every((v, i) => last[i] === v)) {
Expand Down
7 changes: 4 additions & 3 deletions src/typesetting/lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const list = line({
'Mod+Space': 'toggleCheck',
},
fromDom(node: HTMLElement) {
let indent = -1, parent = node.parentNode;
let indent = -1, parent = node.parentNode, type = parent && (parent as Element).getAttribute('type');
const list = node.hasAttribute('data-checked') ? 'check' : parent && parent.nodeName === 'OL' ? 'ordered' : 'bullet';
while (parent) {
if (/^UL|OL$/.test(parent.nodeName)) indent++;
Expand All @@ -75,16 +75,17 @@ export const list = line({
// Support pasting from quilljs content
indent = parseInt(node.className.replace('ql-indent-', ''));
}
const attr: { list: string, checked?: boolean, indent?: number } = { list };
const attr: { list: string, type?: string, checked?: boolean, indent?: number } = { list };
if (indent) attr.indent = indent;
if (type) attr.type = type;
if (node.getAttribute('data-checked') === 'true') attr.checked = true;
return attr;
},
nextLineAttributes(attributes) {
const { start, ...rest } = attributes;
return rest;
},
shouldCombine: (prev, next) => prev.list === next.list || next.indent,
shouldCombine: (prev, next) => (prev.list === next.list && !next.start && prev.type === next.type) || next.indent,
renderMultiple: (lists, editor, forHTML) => {
const topLevelChildren: VNode[] = [];
const levels: VNode[] = [];
Expand Down

0 comments on commit 5523b07

Please sign in to comment.