diff --git a/.gitignore b/.gitignore
index c791de5c23d..9ebcf83cc28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@ packages/ckeditor5-enter/src/**/*.js
packages/ckeditor5-essentials/src/**/*.js
packages/ckeditor5-horizontal-line/src/**/*.js
packages/ckeditor5-list/src/**/*.js
+packages/ckeditor5-page-break/src/**/*.js
packages/ckeditor5-paragraph/src/**/*.js
packages/ckeditor5-paste-from-office/src/**/*.js
packages/ckeditor5-select-all/src/**/*.js
diff --git a/packages/ckeditor5-page-break/src/index.js b/packages/ckeditor5-page-break/_src/index.js
similarity index 100%
rename from packages/ckeditor5-page-break/src/index.js
rename to packages/ckeditor5-page-break/_src/index.js
diff --git a/packages/ckeditor5-page-break/src/pagebreak.js b/packages/ckeditor5-page-break/_src/pagebreak.js
similarity index 100%
rename from packages/ckeditor5-page-break/src/pagebreak.js
rename to packages/ckeditor5-page-break/_src/pagebreak.js
diff --git a/packages/ckeditor5-page-break/src/pagebreakcommand.js b/packages/ckeditor5-page-break/_src/pagebreakcommand.js
similarity index 100%
rename from packages/ckeditor5-page-break/src/pagebreakcommand.js
rename to packages/ckeditor5-page-break/_src/pagebreakcommand.js
diff --git a/packages/ckeditor5-page-break/src/pagebreakediting.js b/packages/ckeditor5-page-break/_src/pagebreakediting.js
similarity index 100%
rename from packages/ckeditor5-page-break/src/pagebreakediting.js
rename to packages/ckeditor5-page-break/_src/pagebreakediting.js
diff --git a/packages/ckeditor5-page-break/src/pagebreakui.js b/packages/ckeditor5-page-break/_src/pagebreakui.js
similarity index 100%
rename from packages/ckeditor5-page-break/src/pagebreakui.js
rename to packages/ckeditor5-page-break/_src/pagebreakui.js
diff --git a/packages/ckeditor5-page-break/package.json b/packages/ckeditor5-page-break/package.json
index a5831f2ff0e..2ee65a25888 100644
--- a/packages/ckeditor5-page-break/package.json
+++ b/packages/ckeditor5-page-break/package.json
@@ -10,7 +10,7 @@
"ckeditor5-plugin",
"ckeditor5-dll"
],
- "main": "src/index.js",
+ "main": "src/index.ts",
"dependencies": {
"ckeditor5": "^35.4.0"
},
@@ -26,6 +26,7 @@
"@ckeditor/ckeditor5-theme-lark": "^35.4.0",
"@ckeditor/ckeditor5-ui": "^35.4.0",
"@ckeditor/ckeditor5-widget": "^35.4.0",
+ "typescript": "^4.8.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
},
@@ -44,13 +45,16 @@
},
"files": [
"lang",
- "src",
+ "src/**/*.js",
+ "src/**/*.d.ts",
"theme",
"build",
"ckeditor5-metadata.json",
"CHANGELOG.md"
],
"scripts": {
- "dll:build": "webpack"
+ "dll:build": "webpack",
+ "build": "tsc -p ./tsconfig.release.json",
+ "postversion": "npm run build"
}
}
diff --git a/packages/ckeditor5-page-break/src/index.ts b/packages/ckeditor5-page-break/src/index.ts
new file mode 100644
index 00000000000..e8db1747953
--- /dev/null
+++ b/packages/ckeditor5-page-break/src/index.ts
@@ -0,0 +1,12 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module page-break
+ */
+
+export { default as PageBreak } from './pagebreak';
+export { default as PageBreakEditing } from './pagebreakediting';
+export { default as PageBreakUI } from './pagebreakui';
diff --git a/packages/ckeditor5-page-break/src/pagebreak.ts b/packages/ckeditor5-page-break/src/pagebreak.ts
new file mode 100644
index 00000000000..85e881312fc
--- /dev/null
+++ b/packages/ckeditor5-page-break/src/pagebreak.ts
@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module page-break/pagebreak
+ */
+
+import { Plugin, type PluginDependencies } from 'ckeditor5/src/core';
+import { Widget } from 'ckeditor5/src/widget';
+
+import PageBreakEditing from './pagebreakediting';
+import PageBreakUI from './pagebreakui';
+
+/**
+ * The page break feature.
+ *
+ * It provides the possibility to insert a page break into the rich-text editor.
+ *
+ * For a detailed overview, check the {@glink features/page-break Page break feature} documentation.
+ */
+export default class PageBreak extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ public static get requires(): PluginDependencies {
+ return [ PageBreakEditing, PageBreakUI, Widget ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static get pluginName(): 'PageBreak' {
+ return 'PageBreak';
+ }
+}
+
+declare module '@ckeditor/ckeditor5-core' {
+ interface PluginsMap {
+ [ PageBreak.pluginName ]: PageBreak;
+ }
+}
diff --git a/packages/ckeditor5-page-break/src/pagebreakcommand.ts b/packages/ckeditor5-page-break/src/pagebreakcommand.ts
new file mode 100644
index 00000000000..382203d4dd5
--- /dev/null
+++ b/packages/ckeditor5-page-break/src/pagebreakcommand.ts
@@ -0,0 +1,74 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module page-break/pagebreakcommand
+ */
+
+import { Command } from 'ckeditor5/src/core';
+import { findOptimalInsertionRange } from 'ckeditor5/src/widget';
+import type { DocumentSelection, Element, Model, Schema } from 'ckeditor5/src/engine';
+
+/**
+ * The page break command.
+ *
+ * The command is registered by {@link module:page-break/pagebreakediting~PageBreakEditing} as `'pageBreak'`.
+ *
+ * To insert a page break at the current selection, execute the command:
+ *
+ * editor.execute( 'pageBreak' );
+ */
+export default class PageBreakCommand extends Command {
+ /**
+ * @inheritDoc
+ */
+ public override refresh(): void {
+ const model = this.editor.model;
+ const schema = model.schema;
+ const selection = model.document.selection;
+
+ this.isEnabled = isPageBreakAllowedInParent( selection, schema, model );
+ }
+
+ /**
+ * Executes the command.
+ *
+ * @fires execute
+ */
+ public override execute(): void {
+ const model = this.editor.model;
+
+ model.change( writer => {
+ const pageBreakElement = writer.createElement( 'pageBreak' );
+
+ model.insertObject( pageBreakElement, null, null, {
+ setSelection: 'after'
+ } );
+ } );
+ }
+}
+
+/**
+ * Checks if a page break is allowed by the schema in the optimal insertion parent.
+ */
+function isPageBreakAllowedInParent( selection: DocumentSelection, schema: Schema, model: Model ): boolean {
+ const parent = getInsertPageBreakParent( selection, model );
+
+ return schema.checkChild( parent, 'pageBreak' );
+}
+
+/**
+ * Returns a node that will be used to insert a page break with `model.insertContent` to check if the page break can be placed there.
+ */
+function getInsertPageBreakParent( selection: DocumentSelection, model: Model ): Element {
+ const insertionRange = findOptimalInsertionRange( selection, model );
+ const parent = insertionRange.start.parent;
+
+ if ( parent.isEmpty && !parent.is( 'element', '$root' ) ) {
+ return parent.parent as Element;
+ }
+
+ return parent as Element;
+}
diff --git a/packages/ckeditor5-page-break/src/pagebreakediting.ts b/packages/ckeditor5-page-break/src/pagebreakediting.ts
new file mode 100644
index 00000000000..1ea3d337a89
--- /dev/null
+++ b/packages/ckeditor5-page-break/src/pagebreakediting.ts
@@ -0,0 +1,140 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module page-break/pagebreakediting
+ */
+
+import { Plugin } from 'ckeditor5/src/core';
+import { toWidget } from 'ckeditor5/src/widget';
+import type { DowncastWriter, ViewElement } from 'ckeditor5/src/engine';
+
+import PageBreakCommand from './pagebreakcommand';
+
+import '../theme/pagebreak.css';
+
+/**
+ * The page break editing feature.
+ */
+export default class PageBreakEditing extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ public static get pluginName(): 'PageBreakEditing' {
+ return 'PageBreakEditing';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public init(): void {
+ const editor = this.editor;
+ const schema = editor.model.schema;
+ const t = editor.t;
+ const conversion = editor.conversion;
+
+ schema.register( 'pageBreak', {
+ inheritAllFrom: '$blockObject'
+ } );
+
+ conversion.for( 'dataDowncast' ).elementToStructure( {
+ model: 'pageBreak',
+ view: ( modelElement, { writer } ) => {
+ const divElement = writer.createContainerElement( 'div',
+ {
+ class: 'page-break',
+ // If user has no `.ck-content` styles, it should always break a page during print.
+ style: 'page-break-after: always'
+ },
+ // For a rationale of using span inside a div see:
+ // https://github.com/ckeditor/ckeditor5-page-break/pull/1#discussion_r328934062.
+ writer.createContainerElement( 'span', {
+ style: 'display: none'
+ } )
+ );
+
+ return divElement;
+ }
+ } );
+
+ conversion.for( 'editingDowncast' ).elementToStructure( {
+ model: 'pageBreak',
+ view: ( modelElement, { writer } ) => {
+ const label = t( 'Page break' );
+ const viewWrapper = writer.createContainerElement( 'div' );
+ const viewLabelElement = writer.createRawElement(
+ 'span',
+ { class: 'page-break__label' },
+ function( domElement ) {
+ domElement.innerText = t( 'Page break' );
+ }
+ );
+
+ writer.addClass( 'page-break', viewWrapper );
+ writer.insert( writer.createPositionAt( viewWrapper, 0 ), viewLabelElement );
+
+ return toPageBreakWidget( viewWrapper, writer, label );
+ }
+ } );
+
+ conversion.for( 'upcast' )
+ .elementToElement( {
+ view: element => {
+ // For upcast conversion it's enough if we check for element style and verify if it's empty
+ // or contains only hidden span element.
+
+ const hasPageBreakBefore = element.getStyle( 'page-break-before' ) == 'always';
+ const hasPageBreakAfter = element.getStyle( 'page-break-after' ) == 'always';
+
+ if ( !hasPageBreakBefore && !hasPageBreakAfter ) {
+ return null;
+ }
+
+ // The "page break" div accepts only single child or no child at all.
+ if ( element.childCount == 1 ) {
+ const viewSpan = element.getChild( 0 );
+
+ // The child must be the "span" element that is not displayed.
+ if ( !viewSpan!.is( 'element', 'span' ) || viewSpan.getStyle( 'display' ) != 'none' ) {
+ return null;
+ }
+ } else if ( element.childCount > 1 ) {
+ return null;
+ }
+
+ return { name: true };
+ },
+ model: 'pageBreak',
+
+ // This conversion must be checked before
conversion because some editors use
+ //
as a page break marker.
+ converterPriority: 'high'
+ } );
+
+ editor.commands.add( 'pageBreak', new PageBreakCommand( editor ) );
+ }
+}
+
+/**
+ * Converts a given {@link module:engine/view/element~Element} to a page break widget:
+ * * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to
+ * recognize the page break widget element.
+ * * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
+ */
+function toPageBreakWidget( viewElement: ViewElement, writer: DowncastWriter, label: string ): ViewElement {
+ writer.setCustomProperty( 'pageBreak', true, viewElement );
+
+ return toWidget( viewElement, writer, { label } );
+}
+
+declare module '@ckeditor/ckeditor5-core' {
+ interface CommandsMap {
+ pageBreak: PageBreakCommand;
+ }
+
+ interface PluginsMap {
+ [ PageBreakEditing.pluginName ]: PageBreakEditing;
+ }
+}
diff --git a/packages/ckeditor5-page-break/src/pagebreakui.ts b/packages/ckeditor5-page-break/src/pagebreakui.ts
new file mode 100644
index 00000000000..9c6f63dfea7
--- /dev/null
+++ b/packages/ckeditor5-page-break/src/pagebreakui.ts
@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module page-break/pagebreakui
+ */
+
+import { Plugin } from 'ckeditor5/src/core';
+import { ButtonView } from 'ckeditor5/src/ui';
+
+import pageBreakIcon from '../theme/icons/pagebreak.svg';
+
+/**
+ * The page break UI plugin.
+ */
+export default class PageBreakUI extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ public static get pluginName(): 'PageBreakUI' {
+ return 'PageBreakUI';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public init(): void {
+ const editor = this.editor;
+ const t = editor.t;
+
+ // Add pageBreak button to feature components.
+ editor.ui.componentFactory.add( 'pageBreak', locale => {
+ const command = editor.commands.get( 'pageBreak' )!;
+ const view = new ButtonView( locale );
+
+ view.set( {
+ label: t( 'Page break' ),
+ icon: pageBreakIcon,
+ tooltip: true
+ } );
+
+ view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+
+ // Execute command.
+ this.listenTo( view, 'execute', () => {
+ editor.execute( 'pageBreak' );
+ editor.editing.view.focus();
+ } );
+
+ return view;
+ } );
+ }
+}
+
+declare module '@ckeditor/ckeditor5-core' {
+ interface PluginsMap {
+ [ PageBreakUI.pluginName ]: PageBreakUI;
+ }
+}
diff --git a/packages/ckeditor5-page-break/tsconfig.json b/packages/ckeditor5-page-break/tsconfig.json
new file mode 100644
index 00000000000..9d4c891939c
--- /dev/null
+++ b/packages/ckeditor5-page-break/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "ckeditor5/tsconfig.json",
+ "include": [
+ "src",
+ "../../typings"
+ ]
+}
diff --git a/packages/ckeditor5-page-break/tsconfig.release.json b/packages/ckeditor5-page-break/tsconfig.release.json
new file mode 100644
index 00000000000..6d2d43909f9
--- /dev/null
+++ b/packages/ckeditor5-page-break/tsconfig.release.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.release.json",
+ "include": [
+ "./src/",
+ "../../typings/"
+ ],
+ "exclude": [
+ "./tests/"
+ ]
+}