foobar
' ); } ); + it( 'should not register default converters for appliesToBlock', () => { + dataSchema.registerInlineElement( { + view: 'xyz', + model: 'htmlXyz', + appliesToBlock: true + } ); + + dataFilter.allowElement( 'xyz' ); + + editor.setData( 'foobar
' ); + } ); + it( 'should use correct priority level for existing features', () => { // 'a' element is registered by data schema with priority 5. // We are checking if this element will be correctly nested due to different diff --git a/packages/ckeditor5-html-support/tests/dataschema.js b/packages/ckeditor5-html-support/tests/dataschema.js index 0c428f60394..8b62fe73ee7 100644 --- a/packages/ckeditor5-html-support/tests/dataschema.js +++ b/packages/ckeditor5-html-support/tests/dataschema.js @@ -41,6 +41,25 @@ describe( 'DataSchema', () => { } ] ); } ); + it( 'should register multiple definitions for the same model attribute', () => { + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def1' } ); + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def2' } ); + + const result1 = dataSchema.getDefinitionsForView( 'def1' ); + const result2 = dataSchema.getDefinitionsForView( 'def2' ); + + expect( Array.from( result1 ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def1', + isInline: true + } ] ); + expect( Array.from( result2 ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def2', + isInline: true + } ] ); + } ); + it( 'should include attribute properties', () => { dataSchema.registerInlineElement( { model: 'htmlDef', @@ -131,6 +150,96 @@ describe( 'DataSchema', () => { expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1' ) ); } ); + it( 'should allow registering multiple view elements with a single model representation', () => { + dataSchema.registerBlockElement( { + view: 'def1', + model: 'htmlDef' + } ); + dataSchema.registerBlockElement( { + view: 'def2', + model: 'htmlDef' + } ); + + const result1 = dataSchema.getDefinitionsForView( 'def1' ); + const result2 = dataSchema.getDefinitionsForView( 'def2' ); + + expect( Array.from( result1 ) ).to.deep.equal( [ + { + isBlock: true, + view: 'def1', + model: 'htmlDef' + } + ] ); + expect( Array.from( result2 ) ).to.deep.equal( [ + { + isBlock: true, + view: 'def2', + model: 'htmlDef' + } + ] ); + } ); + + it( 'should allow registering multiple view elements with a single model representation and dependencies', () => { + dataSchema.registerBlockElement( { + view: 'def1', + model: 'htmlDef', + modelSchema: { + inheritAllFrom: 'htmlBase' + } + } ); + dataSchema.registerBlockElement( { + view: 'def2', + model: 'htmlDef', + modelSchema: { + inheritAllFrom: 'htmlBase' + } + } ); + dataSchema.registerBlockElement( { + model: 'htmlBase', + modelSchema: { + inheritAllFrom: '$block' + } + } ); + + const result1 = dataSchema.getDefinitionsForView( 'def1', true ); + const result2 = dataSchema.getDefinitionsForView( 'def2', true ); + + expect( Array.from( result1 ) ).to.deep.equal( [ + { + isBlock: true, + model: 'htmlBase', + modelSchema: { + inheritAllFrom: '$block' + } + }, + { + isBlock: true, + view: 'def1', + model: 'htmlDef', + modelSchema: { + inheritAllFrom: 'htmlBase' + } + } + ] ); + expect( Array.from( result2 ) ).to.deep.equal( [ + { + isBlock: true, + model: 'htmlBase', + modelSchema: { + inheritAllFrom: '$block' + } + }, + { + isBlock: true, + view: 'def2', + model: 'htmlDef', + modelSchema: { + inheritAllFrom: 'htmlBase' + } + } + ] ); + } ); + it( 'should allow resolving definitions by view name (string)', () => { registerMany( dataSchema, fakeDefinitions ); @@ -235,6 +344,45 @@ describe( 'DataSchema', () => { } ] ); } ); + it( 'should extend schema with new properties (multiple entries for the same model element)', () => { + dataSchema.registerBlockElement( { + view: 'viewName', + model: 'modelName' + } ); + dataSchema.registerBlockElement( { + view: 'viewName2', + model: 'modelName' + } ); + + dataSchema.extendBlockElement( { + model: 'modelName', + paragraphLikeModel: 'htmlDivParagraph', + modelSchema: { + isSelectable: true + } + } ); + + expect( Array.from( dataSchema.getDefinitionsForView( 'viewName' ) ) ).to.deep.equal( [ { + model: 'modelName', + view: 'viewName', + paragraphLikeModel: 'htmlDivParagraph', + modelSchema: { + isSelectable: true + }, + isBlock: true + } ] ); + + expect( Array.from( dataSchema.getDefinitionsForView( 'viewName2' ) ) ).to.deep.equal( [ { + model: 'modelName', + view: 'viewName2', + paragraphLikeModel: 'htmlDivParagraph', + modelSchema: { + isSelectable: true + }, + isBlock: true + } ] ); + } ); + it( 'should append items to array', () => { dataSchema.registerBlockElement( { view: 'viewName', diff --git a/packages/ckeditor5-html-support/tests/generalhtmlsupport.js b/packages/ckeditor5-html-support/tests/generalhtmlsupport.js new file mode 100644 index 00000000000..4e328811ad4 --- /dev/null +++ b/packages/ckeditor5-html-support/tests/generalhtmlsupport.js @@ -0,0 +1,76 @@ +/** + * @license Copyright (c) 2003-2023, 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 ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import { GeneralHtmlSupport } from '../src'; + +describe( 'GeneralHtmlSupport', () => { + let editor, element, dataSchema, generalHtmlSupport; + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ GeneralHtmlSupport ] + } ); + + dataSchema = editor.plugins.get( 'DataSchema' ); + generalHtmlSupport = editor.plugins.get( 'GeneralHtmlSupport' ); + } ); + + afterEach( async () => { + element.remove(); + + await editor.destroy(); + } ); + + describe( 'getGhsAttributeNameForElement()', () => { + beforeEach( () => { + dataSchema.registerBlockElement( { model: 'def', view: 'def1' } ); + dataSchema.registerBlockElement( { model: 'def', view: 'def2' } ); + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def3' } ); + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def4' } ); + dataSchema.registerInlineElement( { model: 'htmlObj', view: 'def5', isObject: true } ); + } ); + + it( 'should return "htmlAttributes" for block elements', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'def1' ) ).to.equal( 'htmlAttributes' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'def2' ) ).to.equal( 'htmlAttributes' ); + } ); + + it( 'should return "htmlAttributes" for inline object elements', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'def5' ) ).to.equal( 'htmlAttributes' ); + } ); + + it( 'should return model attribute name for inline elements with multiple view representations', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'def3' ) ).to.equal( 'htmlDef' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'def4' ) ).to.equal( 'htmlDef' ); + } ); + + it( 'should return model attribute name for block elements with multiple view representations', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'td' ) ).to.equal( 'htmlAttributes' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'th' ) ).to.equal( 'htmlAttributes' ); + } ); + + it( 'should return model attribute name for inline elements with multiple view representations', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'ul' ) ).to.equal( 'htmlListAttributes' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'ol' ) ).to.equal( 'htmlListAttributes' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'li' ) ).to.equal( 'htmlLiAttributes' ); + } ); + + it( 'should return model attribute name for block elements', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'div' ) ).to.equal( 'htmlAttributes' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'p' ) ).to.equal( 'htmlAttributes' ); + } ); + + it( 'should return model attribute name for inline elements', () => { + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'a' ) ).to.equal( 'htmlA' ); + expect( generalHtmlSupport.getGhsAttributeNameForElement( 'strong' ) ).to.equal( 'htmlStrong' ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-html-support/tests/integrations/table.js b/packages/ckeditor5-html-support/tests/integrations/table.js index 25d5f0f2e2f..c98b2ef0076 100644 --- a/packages/ckeditor5-html-support/tests/integrations/table.js +++ b/packages/ckeditor5-html-support/tests/integrations/table.js @@ -373,6 +373,201 @@ describe( 'TableElementSupport', () => { expect( editor.getData() ).to.equal( expectedHtml ); } ); + it( 'should allow enabling only tbody attributes', () => { + dataFilter.loadAllowedConfig( [ { + name: 'tbody', + styles: 'color' + } ] ); + + editor.setData( + '1 | ' + + '
---|
2 | ' + + '
1 | ' + + '
---|
2 | ' + + '
1 | ' + + '
---|
2 | ' + + '
1 | ' + + '
---|
2 | ' + + '
1 | ' + + '
---|
2 | ' + + '
1 | ' + + '
---|
2 | ' + + '
a | ' + + '
---|
b | ' + + '
a | ' + + '
---|
b | ' + + '
a | ' + + '
---|
b | ' + + '
foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '10
' + + 'bar
' + ); + } ); + + it( 'OL style should be enabled for OL blocks (selection in the first list block)', () => { + model.change( writer => writer.setSelection( root.getChild( 1 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'OL style', 'P style' ] ); + } ); + + it( 'OL style should be enabled for OL blocks (selection in the second block of the first list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 2 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'OL style', 'P style' ] ); + } ); + + it( 'OL style should be enabled for OL blocks (selection in the second list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 3 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'OL style', 'P style' ] ); + } ); + + it( 'OL style should be disabled for UL blocks (selection in the nested list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 4 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'UL style', 'P style' ] ); + } ); + + it( 'OL style should be enabled for OL blocks (selection in the nested list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 10 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'OL style', 'P style' ] ); + } ); + + it( 'OL style should be disabled for non list block', () => { + model.change( writer => writer.setSelection( root.getChild( 0 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'P style' ] ); + } ); + + it( 'UL style should be disabled if htmlListAttributes is disabled', () => { + model.schema.addAttributeCheck( ( context, attributeName ) => { + if ( attributeName == 'htmlListAttributes' ) { + return false; + } + } ); + + model.change( writer => writer.setSelection( root.getChild( 1 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'P style' ] ); + } ); + + it( 'OL style should be disabled if htmlListAttributes is disabled', () => { + model.schema.addAttributeCheck( ( context, attributeName ) => { + if ( attributeName == 'htmlListAttributes' ) { + return false; + } + } ); + + model.change( writer => writer.setSelection( root.getChild( 4 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'LI style', 'P style' ] ); + } ); + + it( 'LI style should be disabled if htmlLiAttributes is disabled', () => { + model.schema.addAttributeCheck( ( context, attributeName ) => { + if ( attributeName == 'htmlLiAttributes' ) { + return false; + } + } ); + + model.change( writer => writer.setSelection( root.getChild( 1 ), 0 ) ); + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ 'OL style', 'P style' ] ); + } ); + } ); + + describe( 'active styles', () => { + beforeEach( () => { + editor.setData( + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '10
' + + 'bar
' + ); + } ); + + it( 'OL style should be active for OL blocks (selection in the first list block)', () => { + model.change( writer => writer.setSelection( root.getChild( 1 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.have.members( [ 'LI style', 'OL style' ] ); + } ); + + it( 'OL style should be active for OL blocks (selection in the second block of the first list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 2 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.have.members( [ 'LI style', 'OL style' ] ); + } ); + + it( 'OL style should be active for OL blocks (selection in the second list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 3 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.have.members( [ 'OL style' ] ); + } ); + + it( 'UL style should be active for UL blocks (selection in the nested list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 4 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.have.members( [ 'UL style' ] ); + } ); + + it( 'OL style should be enabled for OL blocks (selection in the nested list item)', () => { + model.change( writer => writer.setSelection( root.getChild( 10 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.have.members( [ 'LI style', 'OL style' ] ); + } ); + + it( 'OL style should be disabled for non list block', () => { + model.change( writer => writer.setSelection( root.getChild( 0 ), 0 ) ); + command.refresh(); + + expect( command.value ).to.be.empty; + } ); + } ); + + describe( 'apply style', () => { + beforeEach( () => { + editor.setData( + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '10
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + 'foo
' + + '1
' + + '2
' + + '3
' + + '4
' + + '5
' + + '6
' + + '7
' + + '8
' + + '9
' + + '11
' + + 'bar
' + + '1
' + + '2
' + + '3
' + + '4
5
6
7
8
' + + '1
' + + '2
' + + '3
' + + '4
5
6
7
8
' + + '1
' + + '2
' + + '3
' + + '4
5
6
7
8
' + + 'Bold Italic Link
-+ +Quote
@@ -180,6 +246,28 @@
Heading 1
Quote
Gran Canaria is a popular tourist destination. The island is often called “a miniature continent”. This is because it offers + a great variety of climates and landscapes, from golden sandy beaches through rocky ravines and white dunes to charming + villages. In the mid-2010s, over 3.5 million tourists visited Gran Canaria each year.
+Florence, established in 59 BC by Julius Caesar himself got its name from two rivers that surrounded the settlement. + Thanks to centuries of history, the capital of Tuscany has plenty to offer to tourists hungry for sightseeing. Some of the + most interesting places to visit include:
+Florence is also regarded as one of the top 15 fashion capitals of the world.
+' + + '' + ); + + command.refresh(); + + expect( command.enabledStyles ).to.have.members( [ + ...blockParagraphStyles.map( ( { name } ) => name ), + ...blockWidgetStyles.map( ( { name } ) => name ) + ] ); + } ); + + it( 'should enable styles for view elements that does not map to model element', () => { + setData( model, + '' + + '
' + + '' + + '[ ' + + '' + + ' ]' + + '' + + '
' + '' + @@ -245,7 +291,8 @@ describe( 'StyleCommand', () => { expect( command.enabledStyles ).to.have.members( [ ...inlineStyles.map( ( { name } ) => name ), - ...blockParagraphStyles.map( ( { name } ) => name ) + ...blockParagraphStyles.map( ( { name } ) => name ), + ...blockWidgetStyles.map( ( { name } ) => name ) ] ); } ); @@ -289,8 +336,8 @@ describe( 'StyleCommand', () => { } ); describe( '#isEnabled', () => { - it( 'should be disabled if selection is on a block widget', () => { - setData( model, '[
]' ); + it( 'should be disabled if none of styles applies to selection', () => { + setData( model, '[ ]' ); expect( command.isEnabled ).to.be.false; } ); @@ -373,7 +420,7 @@ describe( 'StyleCommand', () => { expect( command.value ).to.have.members( [ 'Vibrant code block' ] ); } ); - it( 'should not detect styles for elements outside a limit element', () => { + it( 'should not detect styles for elements outside a widget element', () => { setData( model, ' ' + '' + @@ -388,10 +435,54 @@ describe( 'StyleCommand', () => { model.change( writer => { writer.setAttribute( 'htmlAttributes', { classes: [ 'side-quote' ] }, root.getChild( 0 ) ); + writer.setAttribute( 'htmlAttributes', { classes: [ 'example' ] }, root.getNodeByPath( [ 0, 0 ] ) ); writer.setAttribute( 'htmlAttributes', { classes: [ 'red' ] }, root.getNodeByPath( [ 0, 0, 0, 0, 0 ] ) ); } ); - expect( command.value ).to.have.members( [ 'Red paragraph' ] ); + expect( command.value ).to.have.members( [ 'Red paragraph', 'Table style' ] ); + } ); + + it( 'should detect styles for selected widget element only', () => { + setData( model, + '
' + + '[' + ); + + model.change( writer => { + writer.setAttribute( 'htmlAttributes', { classes: [ 'side-quote' ] }, root.getChild( 0 ) ); + writer.setAttribute( 'htmlAttributes', { classes: [ 'example' ] }, root.getNodeByPath( [ 0, 0 ] ) ); + writer.setAttribute( 'htmlAttributes', { classes: [ 'red' ] }, root.getNodeByPath( [ 0, 0, 0, 0, 0 ] ) ); + } ); + + expect( command.value ).to.have.members( [ 'Table style' ] ); + } ); + + it( 'should detect styles for view elements that does not map to model element', () => { + setData( model, + '' + + '
]' + + '' + + ' ' + + '' + + ' ' + + 'foo ' + + '' + + '
' + ); + + model.change( writer => { + writer.setAttribute( 'htmlFigureAttributes', { classes: [ 'fancy-figure' ] }, root.getChild( 0 ) ); + writer.setAttribute( 'htmlAttributes', { classes: [ 'example' ] }, root.getChild( 0 ) ); + } ); + + expect( command.value ).to.have.members( [ + ...blockWidgetStyles.map( ( { name } ) => name ) + ] ); } ); } ); @@ -718,7 +809,7 @@ describe( 'StyleCommand', () => { ); } ); - it( 'should add htmlAttribute only to elements in the same limit element', () => { + it( 'should add htmlAttribute only to elements in the same widget element boundaries', () => { setData( model, '' + + '[ ' + + '' + + ' ]' + + '' + + ' ' + '' + @@ -750,6 +841,102 @@ describe( 'StyleCommand', () => { ); } ); + it( 'should add htmlAttribute only to elements in the same widget element boundaries (table)', () => { + setData( model, + '
' + + '
' + ); + + command.execute( { styleName: 'Table style' } ); + + expect( getData( model ) ).to.equal( + '' + + ' ' + + '' + + ' ' + + '' + + '
' + + '' + + ' ' + + '' + + ' ' + + 'fo[]o ' + + '' + + '
' + ); + } ); + + it( 'should add style to view element that does not exist in model', () => { + setData( model, + '' + + ' ' + + '' + + ' ' + + '' + + '
' + + '' + + ' ' + + '' + + ' ' + + 'fo[]o ' + + '' + + '
' + ); + + command.execute( { styleName: 'Figure' } ); + + expect( getData( model ) ).to.equal( + '' + + ' ' + + '' + + ' ' + + '[] ' + + '' + + '
' + ); + } ); + + it( 'should remove style from view element that does not exist in model', () => { + setData( model, + '' + + ' ' + + '' + + ' ' + + '[] ' + + '' + + '
' + ); + + command.execute( { styleName: 'Figure' } ); + + expect( getData( model ) ).to.equal( + '' + + ' ' + + '' + + ' ' + + '[] ' + + '' + + '
' + ); + + command.execute( { styleName: 'Figure' } ); + + expect( getData( model ) ).to.equal( + '' + + ' ' + + '' + + ' ' + + '[] ' + + '' + + '
' + ); + } ); + it( 'should remove htmlAttribute from the selected element', () => { setData( model, '' + + ' ' + + '' + + ' ' + + '[] ' + + 'foo[]bar ' ); @@ -811,7 +998,9 @@ describe( 'StyleCommand', () => { document.body.appendChild( editorElement ); editor = await ClassicTestEditor.create( editorElement, { - plugins: [ Paragraph, ImageBlock, ImageCaption, Heading, CodeBlock, BlockQuote, Table, GeneralHtmlSupport, Style ], + plugins: [ + Paragraph, ImageBlock, ImageCaption, Heading, CodeBlock, BlockQuote, Table, HorizontalLine, GeneralHtmlSupport, Style + ], style: { definitions: styleDefinitions }, diff --git a/packages/ckeditor5-style/tests/styleediting.js b/packages/ckeditor5-style/tests/styleediting.js index 78de2771ad6..9aec4403194 100644 --- a/packages/ckeditor5-style/tests/styleediting.js +++ b/packages/ckeditor5-style/tests/styleediting.js @@ -11,6 +11,7 @@ import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtml import StyleEditing from '../src/styleediting'; import StyleCommand from '../src/stylecommand'; import StyleUtils from '../src/styleutils'; +import DocumentListStyleSupport from '../src/integrations/documentlist'; describe( 'StyleEditing', () => { let editor, editorElement; @@ -68,8 +69,8 @@ describe( 'StyleEditing', () => { expect( StyleEditing.pluginName ).to.equal( 'StyleEditing' ); } ); - it( 'should soft-require the GHS plugin and require utils', () => { - expect( StyleEditing.requires ).to.deep.equal( [ 'GeneralHtmlSupport', StyleUtils ] ); + it( 'should soft-require the GHS plugin, and require utils, and integrations', () => { + expect( StyleEditing.requires ).to.deep.equal( [ 'GeneralHtmlSupport', StyleUtils, DocumentListStyleSupport ] ); } ); it( 'should register the "style" command', () => { diff --git a/packages/ckeditor5-style/tests/styleutils.js b/packages/ckeditor5-style/tests/styleutils.js index f8429e0a440..81a32df5c20 100644 --- a/packages/ckeditor5-style/tests/styleutils.js +++ b/packages/ckeditor5-style/tests/styleutils.js @@ -8,12 +8,11 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import Style from '../src/style'; import StyleUtils from '../src/styleutils'; -import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; +import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; describe( 'StyleUtils', () => { - let editor, element; + let editor, element, styleUtils, dataSchema; testUtils.createSinonSandbox(); @@ -22,8 +21,11 @@ describe( 'StyleUtils', () => { document.body.appendChild( element ); editor = await ClassicTestEditor.create( element, { - plugins: [ GeneralHtmlSupport, Style ] + plugins: [ StyleUtils, GeneralHtmlSupport ] } ); + + styleUtils = editor.plugins.get( StyleUtils ); + dataSchema = editor.plugins.get( 'DataSchema' ); } ); afterEach( async () => { @@ -36,7 +38,167 @@ describe( 'StyleUtils', () => { expect( StyleUtils.pluginName ).to.equal( 'StyleUtils' ); } ); - it( 'should be loaded by the Style plugin', () => { - expect( editor.plugins.has( 'StyleUtils' ) ).to.be.true; + describe( 'normalizeConfig()', () => { + it( 'should output empty lists for inline and block styles if there is no styles configured', () => { + const styleDefinitions = styleUtils.normalizeConfig( dataSchema ); + + expect( styleDefinitions ).to.deep.equal( { + block: [], + inline: [] + } ); + } ); + + it( 'should normalize block style', () => { + sinon.stub( styleUtils, 'getStylePreview' ).callsFake( definition => ( { + fake: 'preview for ' + definition.name + } ) ); + + const styleDefinitions = styleUtils.normalizeConfig( dataSchema, [ { + name: 'Foo', + element: 'p', + classes: 'bar' + } ] ); + + expect( styleDefinitions ).to.deep.equal( { + block: [ + { + name: 'Foo', + element: 'p', + classes: 'bar', + + isBlock: true, + modelElements: [ + 'paragraph', + 'htmlP' + ], + previewTemplate: { + fake: 'preview for Foo' + } + } + ], + inline: [] + } ); + } ); + + it( 'should normalize inline style', () => { + sinon.stub( styleUtils, 'getStylePreview' ).callsFake( definition => ( { + fake: 'preview for ' + definition.name + } ) ); + + const styleDefinitions = styleUtils.normalizeConfig( dataSchema, [ { + name: 'Bar', + element: 'acronym', + classes: 'foo' + } ] ); + + expect( styleDefinitions ).to.deep.equal( { + inline: [ + { + name: 'Bar', + element: 'acronym', + classes: 'foo', + + ghsAttributes: [ + 'htmlAcronym' + ], + previewTemplate: { + fake: 'preview for Bar' + } + } + ], + block: [] + } ); + } ); + + it( 'should normalize inline style that applies to model element', () => { + sinon.stub( styleUtils, 'getStylePreview' ).callsFake( definition => ( { + fake: 'preview for ' + definition.name + } ) ); + + const styleDefinitions = styleUtils.normalizeConfig( dataSchema, [ { + name: 'Bar', + element: 'figure', + classes: 'foo' + } ] ); + + expect( styleDefinitions ).to.deep.equal( { + block: [ + { + name: 'Bar', + element: 'figure', + classes: 'foo', + isBlock: true, + modelElements: [ + 'htmlFigure', + 'table', + 'imageBlock' + ], + previewTemplate: { + fake: 'preview for Bar' + } + } + ], + inline: [] + } ); + } ); + } ); + + describe( 'getStylePreview()', () => { + it( 'should build template definition for style', () => { + const preview = styleUtils.getStylePreview( { + name: 'Foo', + element: 'p', + classes: 'bar' + }, [ { text: 'abc' } ] ); + + expect( preview ).to.deep.equal( { + tag: 'p', + attributes: { + class: 'bar' + }, + children: [ + { text: 'abc' } + ] + } ); + } ); + + it( 'should use passed children', () => { + const children = [ { text: 'abc' } ]; + const preview = styleUtils.getStylePreview( { + name: 'Foo', + element: 'p', + classes: 'bar' + }, children ); + + expect( preview ).to.deep.equal( { + tag: 'p', + attributes: { + class: 'bar' + }, + children: [ + { text: 'abc' } + ] + } ); + + expect( preview.children ).to.equal( children ); + } ); + + it( 'should render non-previewable styles as div', () => { + const preview = styleUtils.getStylePreview( { + name: 'Foo', + element: 'li', + classes: 'bar' + }, [ { text: 'abc' } ] ); + + expect( preview ).to.deep.equal( { + tag: 'div', + attributes: { + class: 'bar' + }, + children: [ + { text: 'abc' } + ] + } ); + } ); } ); } ); diff --git a/packages/ckeditor5-style/tests/ui/stylegridbuttonview.js b/packages/ckeditor5-style/tests/ui/stylegridbuttonview.js index 27ada11695e..9366b1743d4 100644 --- a/packages/ckeditor5-style/tests/ui/stylegridbuttonview.js +++ b/packages/ckeditor5-style/tests/ui/stylegridbuttonview.js @@ -16,7 +16,16 @@ describe( 'StyleGridButtonView', () => { button = new StyleGridButtonView( locale, { name: 'Red heading', element: 'h2', - classes: [ 'red-heading', 'foo' ] + classes: [ 'red-heading', 'foo' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: [ 'red-heading', 'foo' ] + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ); } ); @@ -33,7 +42,16 @@ describe( 'StyleGridButtonView', () => { expect( button.styleDefinition ).to.deep.equal( { name: 'Red heading', element: 'h2', - classes: [ 'red-heading', 'foo' ] + classes: [ 'red-heading', 'foo' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: [ 'red-heading', 'foo' ] + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ); } ); @@ -76,21 +94,40 @@ describe( 'StyleGridButtonView', () => { expect( previewElement.textContent ).to.equal( 'AaBbCcDdEeFfGgHhIiJj' ); } ); - it( 'should render the inner preview as a DIV if non-previewable', () => { + it( 'should render the inner preview based on custom template', () => { const button = new StyleGridButtonView( locale, { - name: 'Non-previewable', - element: 'td', - classes: [ 'a', 'b' ] + name: 'Custom preview', + element: 'li', + classes: [ 'a', 'b' ], + previewTemplate: { + tag: 'ol', + children: [ + { + tag: 'li', + attributes: { + class: [ 'a', 'b' ] + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } + ] + } } ); button.render(); const previewElement = button.previewView.element.firstChild; + const childElement = previewElement.firstChild; - expect( previewElement.tagName ).to.equal( 'DIV' ); - expect( previewElement.classList.contains( 'a' ) ).to.be.true; - expect( previewElement.classList.contains( 'b' ) ).to.be.true; - expect( previewElement.textContent ).to.equal( 'AaBbCcDdEeFfGgHhIiJj' ); + expect( previewElement.tagName ).to.equal( 'OL' ); + expect( previewElement.classList.contains( 'a' ) ).to.be.false; + expect( previewElement.classList.contains( 'b' ) ).to.be.false; + + expect( childElement.tagName ).to.equal( 'LI' ); + expect( childElement.classList.contains( 'a' ) ).to.be.true; + expect( childElement.classList.contains( 'b' ) ).to.be.true; + expect( childElement.textContent ).to.equal( 'AaBbCcDdEeFfGgHhIiJj' ); button.destroy(); } ); diff --git a/packages/ckeditor5-style/tests/ui/stylegridview.js b/packages/ckeditor5-style/tests/ui/stylegridview.js index 057c72cd7e4..b16845a3161 100644 --- a/packages/ckeditor5-style/tests/ui/stylegridview.js +++ b/packages/ckeditor5-style/tests/ui/stylegridview.js @@ -18,12 +18,30 @@ describe( 'StyleGridView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] ); } ); @@ -123,12 +141,30 @@ describe( 'StyleGridView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] ); @@ -150,22 +186,58 @@ describe( 'StyleGridView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Yellow heading', element: 'h2', - classes: [ 'yellow-heading' ] + classes: [ 'yellow-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'yellow-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Green heading', element: 'h2', - classes: [ 'green-heading' ] + classes: [ 'green-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'green-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] ); @@ -220,12 +292,30 @@ describe( 'StyleGridView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] ); diff --git a/packages/ckeditor5-style/tests/ui/stylegroupview.js b/packages/ckeditor5-style/tests/ui/stylegroupview.js index 88f4ce7d03f..edbeba0c83b 100644 --- a/packages/ckeditor5-style/tests/ui/stylegroupview.js +++ b/packages/ckeditor5-style/tests/ui/stylegroupview.js @@ -18,12 +18,30 @@ describe( 'StyleGroupView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] ); } ); diff --git a/packages/ckeditor5-style/tests/ui/stylepanelview.js b/packages/ckeditor5-style/tests/ui/stylepanelview.js index f41a4a95b5a..221b0ff882f 100644 --- a/packages/ckeditor5-style/tests/ui/stylepanelview.js +++ b/packages/ckeditor5-style/tests/ui/stylepanelview.js @@ -22,29 +22,74 @@ describe( 'StylePanelView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ], inline: [ { name: 'Deleted text', element: 'span', - classes: [ 'deleted' ] + classes: [ 'deleted' ], + previewTemplate: { + tag: 'span', + attributes: { + class: 'deleted' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Cited work', element: 'span', - classes: [ 'cited', 'another-class' ] + classes: [ 'cited', 'another-class' ], + previewTemplate: { + tag: 'span', + attributes: { + class: [ 'cited', 'another-class' ] + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } }, { name: 'Small text', element: 'span', - classes: [ 'small' ] + classes: [ 'small' ], + previewTemplate: { + tag: 'span', + attributes: { + class: 'small' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] } ); @@ -111,7 +156,16 @@ describe( 'StylePanelView', () => { { name: 'Deleted text', element: 'span', - classes: [ 'deleted' ] + classes: [ 'deleted' ], + previewTemplate: { + tag: 'span', + attributes: { + class: 'deleted' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] } ); @@ -128,7 +182,16 @@ describe( 'StylePanelView', () => { { name: 'Large heading', element: 'h2', - classes: [ 'large-heading' ] + classes: [ 'large-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'large-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ], inline: [] @@ -295,14 +358,32 @@ describe( 'StylePanelView', () => { { name: 'Red heading', element: 'h2', - classes: [ 'red-heading' ] + classes: [ 'red-heading' ], + previewTemplate: { + tag: 'h2', + attributes: { + class: 'red-heading' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ], inline: [ { name: 'Deleted text', element: 'span', - classes: [ 'deleted' ] + classes: [ 'deleted' ], + previewTemplate: { + tag: 'span', + attributes: { + class: 'deleted' + }, + children: [ + { text: 'AaBbCcDdEeFfGgHhIiJj' } + ] + } } ] } );