Multiline input and breaks
Choose content with DocumentConfiguration and its editing surface separately. A multiline surface accepts paragraph and line input according to the active element's grammar. A code block's newline stays literal; Enter in ordinary prose splits the paragraph unless an input rule handles that Enter.
| Surface | Enter and boundaries |
|---|---|
DocumentSurface.line(onSubmit:) | Enter submits; pasted line breaks become spaces. |
DocumentSurface.field(...) | Multiline input; grows to maxLines, then scrolls. |
DocumentSurface.pageless(...) | Continuous flow with authored page/column controls retained for later pagination. |
DocumentSurface.pages() | Physical page and column boundaries affect layout and export. |
Shift+Enter inserts a soft line break. Literal mandatory separators, including CRLF, vertical tab, form feed, NEL and Unicode line/paragraph separators, retain their source and produce line boundaries in supported text. A form-feed character is distinct from an authored PageBreak: use the structural element when the intent is a physical page boundary.
Author boundaries with the typed API
PageBreak and ColumnBreak come from package:vyuh_document/elements.dart. Include their declarations in a custom configuration; both are already present in notesConfiguration and documentConfiguration from vyuh_document_elements.
dart
import 'package:vyuh_document/elements.dart';
import 'package:vyuh_document/vyuh_document.dart';
/// A complete headless program: boundaries survive JSON and tracked history.
void main() {
final configuration = DocumentConfiguration(
elements: [Paragraph.element, PageBreak.element, ColumnBreak.element],
);
final document = Document.of(configuration, [
Paragraph('Before'),
PageBreak(),
Paragraph('Between'),
ColumnBreak(),
Paragraph('After'),
]).valueOrNull!;
final reopened = Document.fromJson(
configuration,
document.toJson(),
).valueOrNull!;
final controller = DocumentController(
configuration,
document: reopened,
author: const Author.person('Editor'),
mode: DocumentInteractionMode.suggesting,
);
try {
final paragraph = controller.document.body.first<Paragraph>();
switch (controller.run(
InsertText(Position(paragraph, offset: 6), ' the break'),
)) {
case Done():
print(controller.document.plainText);
case Failed(:final error):
throw StateError(error.message);
}
controller.run(const Undo());
controller.run(const Redo());
print(
'Page boundaries: ${controller.document.body.all<PageBreak>().length}',
);
print(
'Column boundaries: ${controller.document.body.all<ColumnBreak>().length}',
);
} finally {
controller.close();
}
}This program uses the public typed API without Flutter. The same canonical boundaries survive JSON reopening, review and history. Rendering them is the surface's responsibility.
Pageless indicators follow Formatting marks
Page and column controls do not create artificial page gaps or columns in pageless flow. With Formatting marks off they occupy no extra flow height. With marks on they show labelled indicators with the shared selection and hit-testing behavior. Changing that view preference does not edit the document or add an Undo step. On physical pages, the controls affect pagination whether or not their indicators are shown.
The runtime editor takes the host preference as showFormattingMarks:
dart
import 'package:vyuh_document_editor_elements/vyuh_document_editor_elements.dart';
import 'package:material_ui/material_ui.dart';
import 'package:vyuh_document_editor/vyuh_document_editor.dart';
void main() => runApp(const MaterialApp(home: Scaffold(body: FlowBreaks())));
/// The runtime host supplies its Formatting marks preference to the view.
/// Change showFormattingMarks to false: the authored boundaries stay in JSON,
/// but reserve no extra pageless space. Paged layout still obeys both controls.
class FlowBreaks extends StatefulWidget {
const FlowBreaks({super.key, this.showFormattingMarks = true});
final bool showFormattingMarks;
@override
State<FlowBreaks> createState() => _FlowBreaksState();
}
class _FlowBreaksState extends State<FlowBreaks> {
final controller = DocumentEditorController(
extensions: DocumentEditorElements.defaults,
layoutMode: DocumentEditorLayoutMode.pageless,
document: Document(
metadata: const DocumentMetadata(title: 'Flow boundaries'),
sections: [
DocumentSection(
id: 'body',
blocks: [
ParagraphBlock.text(
'Before the page boundary.',
id: const BlockId('before'),
),
const PageBreakBlock(id: BlockId('page-break')),
ParagraphBlock.text(
'Between the boundaries.',
id: const BlockId('between'),
),
const ColumnBreakBlock(id: BlockId('column-break')),
ParagraphBlock.text(
'Keep typing here. Enter starts a paragraph.',
id: const BlockId('after'),
),
],
),
],
),
);
@override
Widget build(BuildContext context) => DocumentEditor(
controller: controller,
autofocus: true,
showFormattingMarks: widget.showFormattingMarks,
configuration: const DocumentEditorConfiguration(
layoutMode: DocumentEditorLayoutMode.pageless,
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}The typed DocumentEditor is configured through surface and chrome; its constructor does not expose that runtime flag. See the editor references for the two widget signatures.
Compose edits using their result
Runtime deleteRange and replaceRange return bool: true means the canonical edit committed. Stop a composed edit when its first operation is refused. The default trigger service consumes the query and invokes the selected action as one accepted user action and one completion history intent; the next keystroke can continue while successor geometry prepares.
A host input rule can use the same refusal-aware range operation:
dart
apply: (controller, match) {
if (!controller.deleteRange(match.start, match.end)) return;
controller.insertText('—');
},In suggesting mode, typing and provisional IME candidates remain covered by canonical suggestion records. Completing the composition records its final candidate as one history intent. Source formatting, caret and history use the same engine contracts as ordinary typing; the host does not maintain an extra text buffer or selection.
See Host contributions, Authority, and Transactions and history.