/** * @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 */ /* global document */ import BlockQuote from '../src/blockquote.js'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js'; import Image from '@ckeditor/ckeditor5-image/src/image.js'; import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption.js'; import LegacyList from '@ckeditor/ckeditor5-list/src/legacylist.js'; import Enter from '@ckeditor/ckeditor5-enter/src/enter.js'; import Delete from '@ckeditor/ckeditor5-typing/src/delete.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js'; import Table from '@ckeditor/ckeditor5-table/src/table.js'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; import { parse as parseModel, getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; describe( 'BlockQuote integration', () => { let editor, model, element, viewDocument; beforeEach( () => { element = document.createElement( 'div' ); document.body.appendChild( element ); return ClassicTestEditor .create( element, { plugins: [ BlockQuote, Paragraph, Bold, Image, ImageCaption, LegacyList, Enter, Delete, Heading, Table ] } ) .then( newEditor => { editor = newEditor; model = editor.model; viewDocument = editor.editing.view.document; } ); } ); afterEach( () => { element.remove(); return editor.destroy(); } ); describe( 'enter key support', () => { function fakeEventData() { return { preventDefault: sinon.spy() }; } it( 'does nothing if selection is in an empty block but not in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, 'x[]x' ); viewDocument.fire( 'enter', data ); // Only enter command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); } ); it( 'does nothing if selection is in a non-empty block (at the end) in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
xx[]
' ); viewDocument.fire( 'enter', data ); // Only enter command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); } ); it( 'does nothing if selection is in a non-empty block (at the beginning) in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
[]xx
' ); viewDocument.fire( 'enter', data ); // Only enter command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); } ); it( 'does nothing if selection is not collapsed', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
[]
' ); viewDocument.fire( 'enter', data ); // Only enter command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); } ); it( 'does not interfere with a similar handler in the list feature', () => { const data = fakeEventData(); setModelData( model, 'x' + '
' + 'a' + '[]' + '
' + 'x' ); viewDocument.fire( 'enter', data ); expect( data.preventDefault.called ).to.be.true; expect( getModelData( model ) ).to.equal( 'x' + '
' + 'a' + '[]' + '
' + 'x' ); } ); it( 'escapes block quote if selection is in an empty block in an empty block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, 'x
[]
x' ); viewDocument.fire( 'enter', data ); expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); expect( getModelData( model ) ).to.equal( 'x[]x' ); } ); it( 'escapes block quote if selection is in an empty block in the middle of a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, 'x' + '
a[]b
' + 'x' ); viewDocument.fire( 'enter', data ); expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); expect( getModelData( model ) ).to.equal( 'x' + '
a
' + '[]' + '
b
' + 'x' ); } ); it( 'escapes block quote if selection is in an empty block at the end of a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, 'x' + '
a[]
' + 'x' ); viewDocument.fire( 'enter', data ); expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); expect( getModelData( model ) ).to.equal( 'x' + '
a
' + '[]' + 'x' ); } ); it( 'scrolls the view document to the selection after the command is executed', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); const scrollSpy = sinon.stub( editor.editing.view, 'scrollToTheSelection' ); setModelData( model, 'x' + '
a[]
' + 'x' ); viewDocument.fire( 'enter', data ); sinon.assert.calledOnce( scrollSpy ); sinon.assert.callOrder( execSpy, scrollSpy ); } ); } ); describe( 'backspace key support', () => { function fakeEventData() { return { preventDefault: sinon.spy(), direction: 'backward', inputType: 'deleteContentBackward', unit: 'character' }; } it( 'merges paragraph into paragraph in the quote', () => { const data = fakeEventData(); setModelData( model, '
ab
' + '[]c' + 'd' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( '
ab[]c
' + 'd' ); } ); it( 'merges paragraph from a quote into a paragraph before quote', () => { const data = fakeEventData(); setModelData( model, 'x' + '
[]ab
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x[]a' + '
b
' + 'y' ); } ); it( 'merges two quotes', () => { const data = fakeEventData(); setModelData( model, 'x' + '
ab
' + '
[]cd
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x' + '
ab[]cd
' + 'y' ); } ); it( 'unwraps empty quote when the backspace key pressed in the first empty paragraph in a quote', () => { const data = fakeEventData(); setModelData( model, 'x' + '
a
' + '
[]
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x' + '
a
' + '[]' + 'y' ); } ); it( 'unwraps empty quote when the backspace key pressed in the empty paragraph that is the only content of quote', () => { const data = fakeEventData(); setModelData( model, 'x' + '
[]
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x' + '[]' + 'y' ); } ); it( 'unwraps quote from the first paragraph when the backspace key pressed', () => { const data = fakeEventData(); setModelData( model, 'x' + '
[]foo
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x' + '[]' + '
foo
' + 'y' ); } ); it( 'merges paragraphs in a quote when the backspace key pressed not in the first paragraph', () => { const data = fakeEventData(); setModelData( model, 'x' + '
[]
' + 'y' ); viewDocument.fire( 'delete', data ); expect( getModelData( model ) ).to.equal( 'x' + '
[]
' + 'y' ); } ); it( 'does nothing if selection is in an empty block but not in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, 'x[]x' ); viewDocument.fire( 'delete', data ); // Only delete command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' ); } ); it( 'does nothing if selection is in a non-empty block (at the end) in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
xx[]
' ); viewDocument.fire( 'delete', data ); // Only delete command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' ); } ); it( 'does nothing if selection is in a non-empty block (at the beginning) in a block quote', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
[]xx
' ); viewDocument.fire( 'delete', data ); // Only delete command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' ); } ); it( 'does nothing if selection is not collapsed', () => { const data = fakeEventData(); const execSpy = sinon.spy( editor, 'execute' ); setModelData( model, '
[]
' ); viewDocument.fire( 'delete', data ); // Only delete command should be executed. expect( data.preventDefault.called ).to.be.true; expect( execSpy.calledOnce ).to.be.true; expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'delete' ); } ); } ); // Historically, due to problems with schema, images were not quotable. // These tests were left here to confirm that after schema was fixed, images are properly quotable. describe( 'compatibility with images', () => { it( 'quotes a simple image', () => { const element = document.createElement( 'div' ); document.body.appendChild( element ); // We can't load ImageCaption in this test because it adds to all images automatically. return ClassicTestEditor .create( element, { plugins: [ BlockQuote, Paragraph, Image ] } ) .then( editor => { setModelData( editor.model, 'fo[o' + '' + 'b]ar' ); editor.execute( 'blockQuote' ); expect( getModelData( editor.model ) ).to.equal( '
' + 'fo[o' + '' + 'b]ar' + '
' ); element.remove(); return editor.destroy(); } ); } ); it( 'quotes an image with caption', () => { setModelData( model, 'fo[o' + '' + 'xxx' + '' + 'b]ar' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
' + 'fo[o' + '' + 'xxx' + '' + 'b]ar' + '
' ); } ); it( 'adds an image to an existing quote', () => { setModelData( model, 'fo[o' + '' + 'xxx' + '' + '
b]ar
' ); editor.execute( 'blockQuote' ); // Selection incorrectly trimmed. expect( getModelData( model ) ).to.equal( '
' + 'foo' + '' + 'xxx' + '' + '[b]ar' + '
' ); } ); it( 'wraps paragraph+image', () => { setModelData( model, '[foofoo]' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
[foofoo]
' ); } ); it( 'unwraps paragraph+image', () => { setModelData( model, '
[foofoo]
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '[foofoo]' ); } ); it( 'wraps image+paragraph', () => { setModelData( model, '[foofoo]' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
[foofoo]
' ); } ); it( 'unwraps image+paragraph', () => { setModelData( model, '[foofoo]' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
[foofoo]
' ); } ); } ); // When blockQuote with a paragraph was pasted into a list item, the item contained the paragraph. It was invalid. // There is a test which checks whether blockQuote will split the list items instead of merging with. describe( 'compatibility with lists', () => { it( 'does not merge the paragraph with list item', () => { setModelData( model, 'fo[]o' ); const df = parseModel( '
xxx
yyy', model.schema ); model.insertContent( df, model.document.selection ); expect( getModelData( model ) ).to.equal( 'fo' + '
' + 'xxx' + '
' + 'yyy[]o' ); } ); } ); describe( 'compatibility with tables', () => { it( 'wraps whole table', () => { setModelData( model, '[foo
]' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
[foo
]
' ); } ); it( 'unwraps whole table', () => { setModelData( model, '
[foo
]
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '[foo
]' ); } ); it( 'wraps paragraph in table cell', () => { setModelData( model, '[]foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '
[]foo
' ); } ); it( 'unwraps paragraph in table cell', () => { setModelData( model, '
[]foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '[]foo
' ); } ); it( 'wraps image in table cell', () => { setModelData( model, '' + '' + '[]' + ' ' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '
[
]' + '' + '
foo
' ); } ); it( 'unwraps image in table cell', () => { setModelData( model, '' + '' + '
[
]' + '' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '[]' + '' + '
foo
' ); } ); it( 'wraps paragraph+image in table cell', () => { setModelData( model, '' + '' + '[foo]' + '' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '' + '
[foo
]' + '' + '' + '
foo
' ); } ); it( 'unwraps paragraph+image in table cell', () => { setModelData( model, '' + '' + '' + '
[foo
]' + '' + '' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '[foo]' + '' + '
foo
' ); } ); it( 'wraps image+paragraph in table cell', () => { setModelData( model, '' + '' + '[foo]' + '' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '' + '
[
foo]' + '' + '' + '
foo
' ); } ); it( 'unwraps image+paragraph in table cell', () => { setModelData( model, '' + '' + '[foo]' + '' + '
foo
' ); editor.execute( 'blockQuote' ); expect( getModelData( model ) ).to.equal( '' + '' + '' + '
[
foo]' + '' + '' + '
foo
' ); } ); } ); describe( 'autoparagraphing', () => { it( 'text in block quote in div', () => { const data = '
' + '
foobar
' + '
' + 'xyz'; editor.setData( data ); expect( editor.getData() ).to.equal( '
' + '

foobar

' + '
' + '

xyz

' ); } ); it( 'text directly in block quote', () => { const data = '
' + 'foobar' + '
' + 'xyz'; editor.setData( data ); expect( editor.getData() ).to.equal( '
' + '

foobar

' + '
' + '

xyz

' ); } ); it( 'text after block quote in div', () => { const data = '
' + 'foobar' + '
' + '
xyz
'; editor.setData( data ); expect( editor.getData() ).to.equal( '
' + '

foobar

' + '
' + '

xyz

' ); } ); it( 'text inside block quote in and after div', () => { const data = '
' + '
foo
bar' + '
' + 'xyz'; editor.setData( data ); expect( editor.getData() ).to.equal( '
' + '

foo

bar

' + '
' + '

xyz

' ); } ); it( 'text inside block quote in div split by heading', () => { const data = '
' + '
foo

bar

baz
' + '
' + 'xyz'; editor.setData( data ); expect( editor.getData() ).to.equal( '
' + '

foo

bar

baz

' + '
' + '

xyz

' ); } ); } ); } );