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/" + ] +}