Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements in insertImage command, tooltip manager and mention #17177

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ckeditor5-clipboard/src/clipboardpipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ export default class ClipboardPipeline extends Plugin {
}, { priority: 'low' } );

this.listenTo<ClipboardOutputTransformationEvent>( this, 'outputTransformation', ( evt, data ) => {
const content = editor.data.toView( data.content );
const content = editor.data.toView( data.content, { isClipboardPipeline: true } );

viewDocument.fire<ViewDocumentClipboardOutputEvent>( 'clipboardOutput', {
dataTransfer: data.dataTransfer,
Expand All @@ -330,7 +330,7 @@ export default class ClipboardPipeline extends Plugin {
this.listenTo<ViewDocumentClipboardOutputEvent>( viewDocument, 'clipboardOutput', ( evt, data ) => {
if ( !data.content.isEmpty ) {
data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) );
data.dataTransfer.setData( 'text/plain', viewToPlainText( data.content ) );
data.dataTransfer.setData( 'text/plain', viewToPlainText( editor.data.htmlProcessor.domConverter, data.content ) );
}

if ( data.method == 'cut' ) {
Expand Down
38 changes: 35 additions & 3 deletions packages/ckeditor5-clipboard/src/utils/viewtoplaintext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module clipboard/utils/viewtoplaintext
*/

import type { ViewDocumentFragment, ViewElement, ViewItem } from '@ckeditor/ckeditor5-engine';
import type { DomConverter, ViewDocumentFragment, ViewElement, ViewItem } from '@ckeditor/ckeditor5-engine';

// Elements which should not have empty-line padding.
// Most `view.ContainerElement` want to be separate by new-line, but some are creating one structure
Expand All @@ -19,10 +19,14 @@ const listElements = [ 'ol', 'ul' ];
/**
* Converts {@link module:engine/view/item~Item view item} and all of its children to plain text.
*
* @param converter The converter instance.
* @param viewItem View item to convert.
* @returns Plain text representation of `viewItem`.
*/
export default function viewToPlainText( viewItem: ViewItem | ViewDocumentFragment ): string {
export default function viewToPlainText(
converter: DomConverter,
viewItem: ViewItem | ViewDocumentFragment
): string {
if ( viewItem.is( '$text' ) || viewItem.is( '$textProxy' ) ) {
return viewItem.data;
}
Expand All @@ -44,10 +48,38 @@ export default function viewToPlainText( viewItem: ViewItem | ViewDocumentFragme
let prev: ViewElement | null = null;

for ( const child of ( viewItem as ViewElement | ViewDocumentFragment ).getChildren() ) {
text += newLinePadding( child as ViewElement, prev ) + viewToPlainText( child );
text += newLinePadding( child as ViewElement, prev ) + viewToPlainText( converter, child );
prev = child as ViewElement;
}

// If item is a raw element, the only way to get its content is to render it and read the text directly from DOM.
if ( viewItem.is( 'rawElement' ) ) {
const tempElement = document.createElement( 'div' );

viewItem.render( tempElement, converter );

text += domElementToPlainText( tempElement );
}

return text;
}

/**
* Recursively converts DOM element and all of its children to plain text.
*/
function domElementToPlainText( element: HTMLElement ): string {
let text = '';

if ( element.nodeType === Node.TEXT_NODE ) {
return element.textContent!;
} else if ( element.tagName === 'BR' ) {
return '\n';
}

for ( const child of element.childNodes ) {
text += domElementToPlainText( child as HTMLElement );
}

return text;
}

Expand Down
20 changes: 19 additions & 1 deletion packages/ckeditor5-clipboard/tests/clipboardpipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ describe( 'ClipboardPipeline feature', () => {
expect( data.dataTransfer ).to.equal( dataTransferMock );
expect( data.content ).is.instanceOf( ModelDocumentFragment );
expect( stringifyModel( data.content ) ).to.equal( '<paragraph>bc</paragraph><paragraph>de</paragraph>' );

done();
} );

Expand All @@ -495,6 +494,25 @@ describe( 'ClipboardPipeline feature', () => {
} );
} );

it( 'triggers the conversion with the `isClipboardPipeline` flag', done => {
const dataTransferMock = createDataTransfer();
const preventDefaultSpy = sinon.spy();
const toViewSpy = sinon.spy( editor.data, 'toView' );

setModelData( editor.model, '<paragraph>a[bc</paragraph><paragraph>de]f</paragraph>' );

clipboardPlugin.on( 'outputTransformation', ( evt, data ) => {
expect( toViewSpy ).calledWithExactly( data.content, { isClipboardPipeline: true } );

done();
}, { priority: 'lowest' } );

viewDocument.fire( 'copy', {
dataTransfer: dataTransferMock,
preventDefault: preventDefaultSpy
} );
} );

it( 'fires clipboardOutput for copy with the selected content and correct method', done => {
const dataTransferMock = createDataTransfer();
const preventDefaultSpy = sinon.spy();
Expand Down
26 changes: 24 additions & 2 deletions packages/ckeditor5-clipboard/tests/utils/viewtoplaintext.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import { DomConverter, StylesProcessor, ViewDocument, DowncastWriter } from '@ckeditor/ckeditor5-engine';
import viewToPlainText from '../../src/utils/viewtoplaintext.js';

import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view.js';

describe( 'viewToPlainText()', () => {
let converter, viewDocument;

beforeEach( () => {
viewDocument = new ViewDocument( new StylesProcessor() );
converter = new DomConverter( viewDocument );
} );

afterEach( () => {
viewDocument.destroy();
} );

function testViewToPlainText( viewString, expectedText ) {
const view = parseView( viewString );
const text = viewToPlainText( view );
const text = viewToPlainText( converter, view );

expect( text ).to.equal( expectedText );
}
Expand Down Expand Up @@ -41,7 +53,7 @@ describe( 'viewToPlainText()', () => {
const view = parseView( viewString );
view.getChild( 1 )._setCustomProperty( 'dataPipeline:transparentRendering', true );

const text = viewToPlainText( view );
const text = viewToPlainText( converter, view );

expect( text ).to.equal( expectedText );
} );
Expand Down Expand Up @@ -126,4 +138,14 @@ describe( 'viewToPlainText()', () => {
'Foo\n\nA\n\nB\n\nBar'
);
} );

it( 'should convert a view RawElement', () => {
const writer = new DowncastWriter( viewDocument );
const rawElement = writer.createRawElement( 'div', { 'data-foo': 'bar' }, function( domElement ) {
domElement.innerHTML = '<p>Foo</p><br><p>Bar</p>';
} );
const text = viewToPlainText( converter, rawElement );

expect( text ).to.equal( 'Foo\nBar' );
} );
} );
4 changes: 4 additions & 0 deletions packages/ckeditor5-image/src/image/insertimagecommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ export default class InsertImageCommand extends Command {
* @param options Options for the executed command.
* @param options.imageType The type of the image to insert. If not specified, the type will be determined automatically.
* @param options.source The image source or an array of image sources to insert.
* @param options.breakBlock If set to `true`, the block at the selection start will be broken before inserting the image.
* See the documentation of the command to learn more about accepted formats.
*/
public override execute(
options: {
source: ArrayOrItem<string | Record<string, unknown>>;
imageType?: 'imageBlock' | 'imageInline' | null;
breakBlock?: boolean;
}
): void {
const sourceDefinitions = toArray<string | Record<string, unknown>>( options.source );
Expand Down Expand Up @@ -132,6 +134,8 @@ export default class InsertImageCommand extends Command {
const position = this.editor.model.createPositionAfter( selectedElement );

imageUtils.insertImage( { ...sourceDefinition, ...selectionAttributes }, position, options.imageType );
} else if ( options.breakBlock ) {
imageUtils.insertImage( { ...sourceDefinition, ...selectionAttributes }, selection.getFirstPosition(), options.imageType );
} else {
imageUtils.insertImage( { ...sourceDefinition, ...selectionAttributes }, null, options.imageType );
}
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-image/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { default as ImageCaptionUI } from './imagecaption/imagecaptionui.js';
export { createImageTypeRegExp } from './imageupload/utils.js';

export type { ImageConfig } from './imageconfig.js';
export type { ImageLoadedEvent } from './image/imageloadobserver.js';
export type { default as ImageTypeCommand } from './image/imagetypecommand.js';
export type { default as InsertImageCommand } from './image/insertimagecommand.js';
export type { default as ReplaceImageSourceCommand } from './image/replaceimagesourcecommand.js';
Expand Down
16 changes: 16 additions & 0 deletions packages/ckeditor5-image/tests/image/insertimagecommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ describe( 'InsertImageCommand', () => {
);
} );

it( 'should be possible to break the block with an inserted image', () => {
const imgSrc = 'foo/bar.jpg';

setModelData( model, '<paragraph>f[]oo</paragraph>' );

command.execute( {
imageType: 'imageBlock',
source: imgSrc,
breakBlock: true
} );

expect( getModelData( model ) ).to.equal(
`<paragraph>f</paragraph>[<imageBlock src="${ imgSrc }"></imageBlock>]<paragraph>oo</paragraph>`
);
} );

it( 'should insert multiple images at selection position as other widgets for inline type images', () => {
const imgSrc1 = 'foo/bar.jpg';
const imgSrc2 = 'foo/baz.jpg';
Expand Down
22 changes: 2 additions & 20 deletions packages/ckeditor5-mention/src/mentioncommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,9 @@ export default class MentionCommand extends Command {

const mention = _addMentionAttributes( { _text: mentionText, id: mentionID }, mentionData );

if ( options.marker.length != 1 ) {
if ( !mentionID.startsWith( options.marker ) ) {
/**
* The marker must be a single character.
*
* Correct markers: `'@'`, `'#'`.
*
* Incorrect markers: `'@@'`, `'[@'`.
*
* See {@link module:mention/mentionconfig~MentionConfig}.
*
* @error mentioncommand-incorrect-marker
*/
throw new CKEditorError(
'mentioncommand-incorrect-marker',
this
);
}

if ( mentionID.charAt( 0 ) != options.marker ) {
/**
* The feed item ID must start with the marker character.
* The feed item ID must start with the marker character(s).
*
* Correct mention feed setting:
*
Expand Down
10 changes: 5 additions & 5 deletions packages/ckeditor5-mention/src/mentionui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,12 @@ export function createRegExp( marker: string, minimumCharacters: number ): RegEx
// The pattern consists of 3 groups:
//
// - 0 (non-capturing): Opening sequence - start of the line, space or an opening punctuation character like "(" or "\"",
// - 1: The marker character,
// - 1: The marker character(s),
// - 2: Mention input (taking the minimal length into consideration to trigger the UI),
//
// The pattern matches up to the caret (end of string switch - $).
// (0: opening sequence )(1: marker )(2: typed mention )$
const pattern = `(?:^|[ ${ openAfterCharacters }])([${ marker }])(${ mentionCharacters }${ numberOfCharacters })$`;
// (0: opening sequence )(1: marker )(2: typed mention )$
const pattern = `(?:^|[ ${ openAfterCharacters }])(${ marker })(${ mentionCharacters }${ numberOfCharacters })$`;

return new RegExp( pattern, 'u' );
}
Expand Down Expand Up @@ -822,8 +822,8 @@ function isMarkerInExistingMention( markerPosition: Position ): boolean | null {
/**
* Checks if string is a valid mention marker.
*/
function isValidMentionMarker( marker: string ): boolean | string {
return marker && marker.length == 1;
function isValidMentionMarker( marker: string ): boolean {
return !!marker;
}

/**
Expand Down
13 changes: 0 additions & 13 deletions packages/ckeditor5-mention/tests/mentioncommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,6 @@ describe( 'MentionCommand', () => {
expect( textNode.hasAttribute( 'bold' ) ).to.be.true;
} );

it( 'should throw if marker is not one character', () => {
setData( model, '<paragraph>foo @Jo[]bar</paragraph>' );

const testCases = [
{ marker: '##', mention: '##foo' },
{ marker: '', mention: '@foo' }
];

for ( const options of testCases ) {
expectToThrowCKEditorError( () => command.execute( options ), /mentioncommand-incorrect-marker/, editor );
}
} );

it( 'should throw if marker does not match mention id', () => {
setData( model, '<paragraph>foo @Jo[]bar</paragraph>' );

Expand Down
Loading
Loading