diff --git a/packages/ckeditor5-engine/src/index.ts b/packages/ckeditor5-engine/src/index.ts index 113862f9f78..79039f42ced 100644 --- a/packages/ckeditor5-engine/src/index.ts +++ b/packages/ckeditor5-engine/src/index.ts @@ -21,6 +21,7 @@ export { default as MarkerOperation } from './model/operation/markeroperation'; export { default as OperationFactory } from './model/operation/operationfactory'; export { transformSets } from './model/operation/transform'; +export { default as Selection } from './model/selection'; export { default as DocumentSelection } from './model/documentselection'; export { default as Range } from './model/range'; export { default as LiveRange } from './model/liverange'; @@ -32,6 +33,7 @@ export { default as Position } from './model/position'; export { default as DocumentFragment } from './model/documentfragment'; export { default as History } from './model/history'; export { default as Text } from './model/text'; +export { default as Schema } from './model/schema'; export { default as DomConverter } from './view/domconverter'; export { default as Renderer } from './view/renderer'; diff --git a/packages/ckeditor5-paragraph/src/index.js b/packages/ckeditor5-paragraph/_src/index.js similarity index 100% rename from packages/ckeditor5-paragraph/src/index.js rename to packages/ckeditor5-paragraph/_src/index.js diff --git a/packages/ckeditor5-paragraph/src/insertparagraphcommand.js b/packages/ckeditor5-paragraph/_src/insertparagraphcommand.js similarity index 100% rename from packages/ckeditor5-paragraph/src/insertparagraphcommand.js rename to packages/ckeditor5-paragraph/_src/insertparagraphcommand.js diff --git a/packages/ckeditor5-paragraph/src/paragraph.js b/packages/ckeditor5-paragraph/_src/paragraph.js similarity index 100% rename from packages/ckeditor5-paragraph/src/paragraph.js rename to packages/ckeditor5-paragraph/_src/paragraph.js diff --git a/packages/ckeditor5-paragraph/src/paragraphbuttonui.js b/packages/ckeditor5-paragraph/_src/paragraphbuttonui.js similarity index 100% rename from packages/ckeditor5-paragraph/src/paragraphbuttonui.js rename to packages/ckeditor5-paragraph/_src/paragraphbuttonui.js diff --git a/packages/ckeditor5-paragraph/src/paragraphcommand.js b/packages/ckeditor5-paragraph/_src/paragraphcommand.js similarity index 100% rename from packages/ckeditor5-paragraph/src/paragraphcommand.js rename to packages/ckeditor5-paragraph/_src/paragraphcommand.js diff --git a/packages/ckeditor5-paragraph/package.json b/packages/ckeditor5-paragraph/package.json index 5e9f7c65db4..1944b49b82d 100644 --- a/packages/ckeditor5-paragraph/package.json +++ b/packages/ckeditor5-paragraph/package.json @@ -10,7 +10,7 @@ "ckeditor5-plugin", "ckeditor5-dll" ], - "main": "src/index.js", + "main": "src/index.ts", "dependencies": { "@ckeditor/ckeditor5-core": "^35.2.1", "@ckeditor/ckeditor5-ui": "^35.2.1", @@ -25,7 +25,10 @@ "@ckeditor/ckeditor5-heading": "^35.2.1", "@ckeditor/ckeditor5-link": "^35.2.1", "@ckeditor/ckeditor5-typing": "^35.2.1", - "@ckeditor/ckeditor5-undo": "^35.2.1" + "@ckeditor/ckeditor5-undo": "^35.2.1", + "typescript": "^4.8.4", + "webpack": "^5.58.1", + "webpack-cli": "^4.9.0" }, "engines": { "node": ">=14.0.0", @@ -42,9 +45,14 @@ }, "files": [ "lang", - "src", + "src/**/*.js", + "src/**/*.d.ts", "theme", "ckeditor5-metadata.json", "CHANGELOG.md" - ] + ], + "scripts": { + "build": "tsc -p ./tsconfig.release.json", + "postversion": "npm run build" + } } diff --git a/packages/ckeditor5-paragraph/src/index.ts b/packages/ckeditor5-paragraph/src/index.ts new file mode 100644 index 00000000000..b1f1f3037fd --- /dev/null +++ b/packages/ckeditor5-paragraph/src/index.ts @@ -0,0 +1,11 @@ +/** + * @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 paragraph + */ + +export { default as Paragraph } from './paragraph'; +export { default as ParagraphButtonUI } from './paragraphbuttonui'; diff --git a/packages/ckeditor5-paragraph/src/insertparagraphcommand.ts b/packages/ckeditor5-paragraph/src/insertparagraphcommand.ts new file mode 100644 index 00000000000..4d47b02559b --- /dev/null +++ b/packages/ckeditor5-paragraph/src/insertparagraphcommand.ts @@ -0,0 +1,73 @@ +/** + * @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 paragraph/insertparagraphcommand + */ + +import Command from '@ckeditor/ckeditor5-core/src/command'; +import type { Element, Position } from '@ckeditor/ckeditor5-engine'; + +/** + * The insert paragraph command. It inserts a new paragraph at a specific + * {@link module:engine/model/position~Position document position}. + * + * // Insert a new paragraph before an element in the document. + * editor.execute( 'insertParagraph', { + * position: editor.model.createPositionBefore( element ) + * } ); + * + * If a paragraph is disallowed in the context of the specific position, the command + * will attempt to split position ancestors to find a place where it is possible + * to insert a paragraph. + * + * **Note**: This command moves the selection to the inserted paragraph. + * + * @extends module:core/command~Command + */ +export default class InsertParagraphCommand extends Command { + /** + * Executes the command. + * + * @param {Object} options Options for the executed command. + * @param {module:engine/model/position~Position} options.position The model position at which + * the new paragraph will be inserted. + * @param {Object} attributes Attributes keys and values to set on a inserted paragraph + * @fires execute + */ + public override execute( options: { + position: Position; + attributes: Record; + } ): void { + const model = this.editor.model; + const attributes = options.attributes; + + let position = options.position; + + model.change( writer => { + const paragraph = writer.createElement( 'paragraph' ); + + if ( attributes ) { + model.schema.setAllowedAttributes( paragraph, attributes, writer ); + } + + if ( !model.schema.checkChild( position.parent as Element, paragraph ) ) { + const allowedParent = model.schema.findAllowedParent( position, paragraph ); + + // It could be there's no ancestor limit that would allow paragraph. + // In theory, "paragraph" could be disallowed even in the "$root". + if ( !allowedParent ) { + return; + } + + position = writer.split( position, allowedParent ).position; + } + + model.insertContent( paragraph, position ); + + writer.setSelection( paragraph, 'in' ); + } ); + } +} diff --git a/packages/ckeditor5-paragraph/src/paragraph.ts b/packages/ckeditor5-paragraph/src/paragraph.ts new file mode 100644 index 00000000000..28a39b0f6b1 --- /dev/null +++ b/packages/ckeditor5-paragraph/src/paragraph.ts @@ -0,0 +1,118 @@ +/** + * @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 paragraph/paragraph + */ + +import ParagraphCommand from './paragraphcommand'; +import InsertParagraphCommand from './insertparagraphcommand'; + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +/** + * The paragraph feature for the editor. + * + * It introduces the `` element in the model which renders as a `

` element in the DOM and data. + * + * It also brings two editors commands: + * + * * The {@link module:paragraph/paragraphcommand~ParagraphCommand `'paragraph'`} command that converts all + * blocks in the model selection into paragraphs. + * * The {@link module:paragraph/insertparagraphcommand~InsertParagraphCommand `'insertParagraph'`} command + * that inserts a new paragraph at a specified location in the model. + * + * @extends module:core/plugin~Plugin + */ +export default class Paragraph extends Plugin { + /** + * @inheritDoc + */ + public static get pluginName(): string { + return 'Paragraph'; + } + + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const model = editor.model; + + editor.commands.add( 'paragraph', new ParagraphCommand( editor ) ); + editor.commands.add( 'insertParagraph', new InsertParagraphCommand( editor ) ); + + // Schema. + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + + editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } ); + + // Conversion for paragraph-like elements which has not been converted by any plugin. + editor.conversion.for( 'upcast' ).elementToElement( { + model: ( viewElement, { writer } ) => { + if ( !Paragraph.paragraphLikeElements.has( viewElement.name ) ) { + return null; + } + + // Do not auto-paragraph empty elements. + if ( viewElement.isEmpty ) { + return null; + } + + return writer.createElement( 'paragraph' ); + }, + view: /.+/, + converterPriority: 'low' + } ); + } + + /** + * A list of element names which should be treated by the autoparagraphing algorithms as + * paragraph-like. This means that e.g. the following content: + * + *

Foo

+ * + * + * + * + * + *
X + *
    + *
  • Y
  • + *
  • Z
  • + *
+ *
+ * + * contains five paragraph-like elements: `

`, two ``s and two `
  • `s. + * Hence, if none of the features is going to convert those elements the above content will be automatically handled + * by the paragraph feature and converted to: + * + *

    Foo

    + *

    X

    + *

    Y

    + *

    Z

    + * + * Note: The `` containing two `
  • ` elements was ignored as the innermost paragraph-like elements + * have a priority upon conversion. + * + * @member {Set.} module:paragraph/paragraph~Paragraph.paragraphLikeElements + */ + public static paragraphLikeElements = new Set( [ + 'blockquote', + 'dd', + 'div', + 'dt', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'p', + 'td', + 'th' + ] ); +} diff --git a/packages/ckeditor5-paragraph/src/paragraphbuttonui.ts b/packages/ckeditor5-paragraph/src/paragraphbuttonui.ts new file mode 100644 index 00000000000..fa3c29d48e4 --- /dev/null +++ b/packages/ckeditor5-paragraph/src/paragraphbuttonui.ts @@ -0,0 +1,55 @@ +/** + * @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 paragraph/paragraphbuttonui + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import icon from '@ckeditor/ckeditor5-core/theme/icons/paragraph.svg'; +import type ParagraphCommand from './paragraphcommand'; + +/** + * This plugin defines the `'paragraph'` button. It can be used together with + * {@link module:heading/headingbuttonsui~HeadingButtonsUI} to replace the standard heading dropdown. + * + * This plugin is not loaded automatically by the {@link module:paragraph/paragraph~Paragraph} plugin. It must + * be added manually. + * + * ClassicEditor + * .create( { + * plugins: [ ..., Heading, Paragraph, HeadingButtonsUI, ParagraphButtonUI ] + * toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3' ] + * } ) + * .then( ... ) + * .catch( ... ); + * + * @extends module:core/plugin~Plugin + */ +export default class ParagraphButtonUI extends Plugin { + public init(): void { + const editor = this.editor; + const t = editor.t; + + editor.ui.componentFactory.add( 'paragraph', locale => { + const view = new ButtonView( locale ); + const command = editor.commands.get( 'paragraph' ) as ParagraphCommand; + + view.label = t( 'Paragraph' ); + view.icon = icon; + view.tooltip = true; + view.isToggleable = true; + view.bind( 'isEnabled' ).to( command ); + view.bind( 'isOn' ).to( command, 'value' ); + + view.on( 'execute', () => { + editor.execute( 'paragraph' ); + } ); + + return view; + } ); + } +} diff --git a/packages/ckeditor5-paragraph/src/paragraphcommand.ts b/packages/ckeditor5-paragraph/src/paragraphcommand.ts new file mode 100644 index 00000000000..eed02d3c376 --- /dev/null +++ b/packages/ckeditor5-paragraph/src/paragraphcommand.ts @@ -0,0 +1,78 @@ +/** + * @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 paragraph/paragraphcommand + */ + +import Command from '@ckeditor/ckeditor5-core/src/command'; +import first from '@ckeditor/ckeditor5-utils/src/first'; + +import type { Schema, Selection, DocumentSelection, Element } from '@ckeditor/ckeditor5-engine'; + +/** + * The paragraph command. + * + * @extends module:core/command~Command + */ +export default class ParagraphCommand extends Command { + /** + * The value of the command. Indicates whether the selection start is placed in a paragraph. + * + * @readonly + * @observable + * @member {Boolean} #value + */ + declare public value: boolean; + + /** + * @inheritDoc + */ + public override refresh(): void { + const model = this.editor.model; + const document = model.document; + const block = first( document.selection.getSelectedBlocks() ); + + this.value = !!block && block.is( 'element', 'paragraph' ); + this.isEnabled = !!block && checkCanBecomeParagraph( block, model.schema ); + } + + /** + * Executes the command. All the blocks (see {@link module:engine/model/schema~Schema}) in the selection + * will be turned to paragraphs. + * + * @fires execute + * @param {Object} [options] Options for the executed command. + * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} [options.selection] + * The selection that the command should be applied to. + * By default, if not provided, the command is applied to the {@link module:engine/model/document~Document#selection}. + */ + public override execute( options: { + selection?: Selection | DocumentSelection; + } = {} ): void { + const model = this.editor.model; + const document = model.document; + + model.change( writer => { + const blocks = ( options.selection || document.selection ).getSelectedBlocks(); + + for ( const block of blocks ) { + if ( !block.is( 'element', 'paragraph' ) && checkCanBecomeParagraph( block, model.schema ) ) { + writer.rename( block, 'paragraph' ); + } + } + } ); + } +} + +// Checks whether the given block can be replaced by a paragraph. +// +// @private +// @param {module:engine/model/element~Element} block A block to be tested. +// @param {module:engine/model/schema~Schema} schema The schema of the document. +// @returns {Boolean} +function checkCanBecomeParagraph( block: Element, schema: Schema ) { + return schema.checkChild( block.parent as Element, 'paragraph' ) && !schema.isObject( block ); +} diff --git a/packages/ckeditor5-paragraph/tsconfig.json b/packages/ckeditor5-paragraph/tsconfig.json new file mode 100644 index 00000000000..9d4c891939c --- /dev/null +++ b/packages/ckeditor5-paragraph/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "ckeditor5/tsconfig.json", + "include": [ + "src", + "../../typings" + ] +} diff --git a/packages/ckeditor5-paragraph/tsconfig.release.json b/packages/ckeditor5-paragraph/tsconfig.release.json new file mode 100644 index 00000000000..6d2d43909f9 --- /dev/null +++ b/packages/ckeditor5-paragraph/tsconfig.release.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.release.json", + "include": [ + "./src/", + "../../typings/" + ], + "exclude": [ + "./tests/" + ] +}