@ckeditor/ckeditor5-paragraph
Version:
Paragraph feature for CKEditor 5.
254 lines (248 loc) • 8.96 kB
JavaScript
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { Command, Plugin, icons } from '@ckeditor/ckeditor5-core/dist/index.js';
import { first } from '@ckeditor/ckeditor5-utils/dist/index.js';
import { ButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
class ParagraphCommand extends Command {
/**
* @inheritDoc
*/ refresh() {
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 options Options for the executed command.
* @param 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}.
*/ execute(options = {}) {
const model = this.editor.model;
const document = model.document;
const selection = options.selection || document.selection;
// Don't execute command if selection is in non-editable place.
if (!model.canEditAt(selection)) {
return;
}
model.change((writer)=>{
const blocks = selection.getSelectedBlocks();
for (const block of blocks){
if (!block.is('element', 'paragraph') && checkCanBecomeParagraph(block, model.schema)) {
writer.rename(block, 'paragraph');
}
}
});
}
constructor(editor){
super(editor);
// Since this command may pass selection in execution block, it should be checked directly.
this._isEnabledBasedOnSelection = false;
}
}
/**
* Checks whether the given block can be replaced by a paragraph.
*
* @param block A block to be tested.
* @param schema The schema of the document.
*/ function checkCanBecomeParagraph(block, schema) {
return schema.checkChild(block.parent, 'paragraph') && !schema.isObject(block);
}
class InsertParagraphCommand extends Command {
/**
* Executes the command.
*
* @param options Options for the executed command.
* @param options.position The model position at which the new paragraph will be inserted.
* @param options.attributes Attributes keys and values to set on a inserted paragraph.
* @fires execute
*/ execute(options) {
const model = this.editor.model;
const attributes = options.attributes;
let position = options.position;
// Don't execute command if position is in non-editable place.
if (!model.canEditAt(position)) {
return;
}
model.change((writer)=>{
position = this._findPositionToInsertParagraph(position, writer);
if (!position) {
return;
}
const paragraph = writer.createElement('paragraph');
if (attributes) {
model.schema.setAllowedAttributes(paragraph, attributes, writer);
}
model.insertContent(paragraph, position);
writer.setSelection(paragraph, 'in');
});
}
/**
* Returns the best position to insert a new paragraph.
*/ _findPositionToInsertParagraph(position, writer) {
const model = this.editor.model;
if (model.schema.checkChild(position, 'paragraph')) {
return position;
}
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 null;
}
const positionParent = position.parent;
const isTextAllowed = model.schema.checkChild(positionParent, '$text');
// At empty $block or at the end of $block.
// <paragraph>[]</paragraph> ---> <paragraph></paragraph><paragraph>[]</paragraph>
// <paragraph>foo[]</paragraph> ---> <paragraph>foo</paragraph><paragraph>[]</paragraph>
if (positionParent.isEmpty || isTextAllowed && position.isAtEnd) {
return model.createPositionAfter(positionParent);
}
// At the start of $block with text.
// <paragraph>[]foo</paragraph> ---> <paragraph>[]</paragraph><paragraph>foo</paragraph>
if (!positionParent.isEmpty && isTextAllowed && position.isAtStart) {
return model.createPositionBefore(positionParent);
}
return writer.split(position, allowedParent).position;
}
constructor(editor){
super(editor);
// Since this command passes position in execution block instead of selection, it should be checked directly.
this._isEnabledBasedOnSelection = false;
}
}
/**
* The paragraph feature for the editor.
*
* It introduces the `<paragraph>` element in the model which renders as a `<p>` 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.
*/ class Paragraph extends Plugin {
/**
* @inheritDoc
*/ static get pluginName() {
return 'Paragraph';
}
/**
* @inheritDoc
*/ init() {
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:
*
* ```html
* <h1>Foo</h1>
* <table>
* <tr>
* <td>X</td>
* <td>
* <ul>
* <li>Y</li>
* <li>Z</li>
* </ul>
* </td>
* </tr>
* </table>
* ```
*
* contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`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:
*
* ```html
* <p>Foo</p>
* <p>X</p>
* <p>Y</p>
* <p>Z</p>
* ```
*
* Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
* have a priority upon conversion.
*/ Paragraph.paragraphLikeElements = new Set([
'blockquote',
'dd',
'div',
'dt',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'p',
'td',
'th'
]);
const icon = icons.paragraph;
class ParagraphButtonUI extends Plugin {
/**
* @inheritDoc
*/ static get requires() {
return [
Paragraph
];
}
/**
* @inheritDoc
*/ init() {
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');
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;
});
}
}
export { Paragraph, ParagraphButtonUI };
//# sourceMappingURL=index.js.map