diff --git a/packages/ckeditor5-table/package.json b/packages/ckeditor5-table/package.json index 2aa89f0068a..55941c8ac8a 100644 --- a/packages/ckeditor5-table/package.json +++ b/packages/ckeditor5-table/package.json @@ -24,6 +24,7 @@ "@ckeditor/ckeditor5-editor-classic": "^32.0.0", "@ckeditor/ckeditor5-engine": "^32.0.0", "@ckeditor/ckeditor5-horizontal-line": "^32.0.0", + "@ckeditor/ckeditor5-html-support": "^32.0.0", "@ckeditor/ckeditor5-image": "^32.0.0", "@ckeditor/ckeditor5-indent": "^32.0.0", "@ckeditor/ckeditor5-list": "^32.0.0", @@ -35,6 +36,7 @@ "@ckeditor/ckeditor5-undo": "^32.0.0", "@ckeditor/ckeditor5-utils": "^32.0.0", "@ckeditor/ckeditor5-widget": "^32.0.0", + "@ckeditor/ckeditor5-source-editing": "^32.0.0", "json-diff": "^0.5.4", "webpack": "^5.58.1", "webpack-cli": "^4.9.0" diff --git a/packages/ckeditor5-table/src/plaintableoutput.js b/packages/ckeditor5-table/src/plaintableoutput.js new file mode 100644 index 00000000000..f538c68a0b5 --- /dev/null +++ b/packages/ckeditor5-table/src/plaintableoutput.js @@ -0,0 +1,153 @@ +/** + * @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 table/plaintableoutput + */ + +import { Plugin } from 'ckeditor5/src/core'; +import Table from './table'; + +/** + * The plain table output feature. + * + * @extends module:core/plugin~Plugin + */ +export default class PlainTableOutput extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'PlainTableOutput'; + } + + /** + * @inheritDoc + */ + static get requires() { + return [ Table ]; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + // Override default table data downcast converter. + editor.conversion.for( 'dataDowncast' ).elementToStructure( { + model: 'table', + view: downcastTableElement, + converterPriority: 'high' + } ); + + // Make sure table is always downcasted to in the data pipeline. + if ( editor.plugins.has( 'TableCaption' ) ) { + editor.conversion.for( 'dataDowncast' ).elementToElement( { + model: 'caption', + view: ( modelElement, { writer } ) => { + if ( modelElement.parent.name === 'table' ) { + return writer.createContainerElement( 'caption' ); + } + + return writer.createContainerElement( 'figcaption' ); + }, + converterPriority: 'high' + } ); + } + + // Handle border-style, border-color, border-width and background-color table attributes. + if ( editor.plugins.has( 'TableProperties' ) ) { + downcastTableBorderAndBackgroundAttributes( editor ); + } + } +} + +// The plain table downcast converter callback. +// +// @private +// @param {module:engine/model/element~Element} Table model element. +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object. +// @returns {module:engine/view/containerelement~ContainerElement} Created element. +function downcastTableElement( table, { writer } ) { + const headingRows = table.getAttribute( 'headingRows' ) || 0; + + // Table head rows slot. + const headRowsSlot = writer.createSlot( element => + element.is( 'element', 'tableRow' ) && element.index < headingRows + ); + + // Table body rows slot. + const bodyRowsSlot = writer.createSlot( element => + element.is( 'element', 'tableRow' ) && element.index >= headingRows + ); + + // Table children slot. + const childrenSlot = writer.createSlot( element => !element.is( 'element', 'tableRow' ) ); + + // Table element with all the heading rows. + const theadElement = writer.createContainerElement( 'thead', null, headRowsSlot ); + + // Table element with all the body rows. + const tbodyElement = writer.createContainerElement( 'tbody', null, bodyRowsSlot ); + + // Table contents element containing and when necessary. + const tableContentElements = []; + + if ( headingRows ) { + tableContentElements.push( theadElement ); + } + + if ( headingRows < table.childCount ) { + tableContentElements.push( tbodyElement ); + } + + // Create table structure. + // + // + // {children-slot-like-caption} + // + // {table-head-rows-slot} + // + // + // {table-body-rows-slot} + // + //
+ return writer.createContainerElement( 'table', null, [ childrenSlot, ...tableContentElements ] ); +} + +// Register table border and background attributes converters. +// +// @private +// @param {module:core/editor/editor~Editor} editor +function downcastTableBorderAndBackgroundAttributes( editor ) { + const modelAttributes = { + 'border-width': 'tableBorderWidth', + 'border-color': 'tableBorderColor', + 'border-style': 'tableBorderStyle', + 'background-color': 'tableBackgroundColor' + }; + + for ( const [ styleName, modelAttribute ] of Object.entries( modelAttributes ) ) { + editor.conversion.for( 'dataDowncast' ).add( dispatcher => { + return dispatcher.on( `attribute:${ modelAttribute }:table`, ( evt, data, conversionApi ) => { + const { item, attributeNewValue } = data; + const { mapper, writer } = conversionApi; + + if ( !conversionApi.consumable.consume( item, evt.name ) ) { + return; + } + + const table = mapper.toViewElement( item ); + + if ( attributeNewValue ) { + writer.setStyle( styleName, attributeNewValue, table ); + } else { + writer.removeStyle( styleName, table ); + } + }, { priority: 'high' } ); + } ); + } +} diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.html b/packages/ckeditor5-table/tests/manual/plaintableoutput.html new file mode 100644 index 00000000000..3ad135adf2e --- /dev/null +++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.html @@ -0,0 +1,32 @@ +
+ + + + + + + + + + +
Monthly savings
MonthSavings
January$100
+
+ +
CKEditor logo - caption
+
+
+ + + +
+
Editor data
+

+
diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.js b/packages/ckeditor5-table/tests/manual/plaintableoutput.js new file mode 100644 index 00000000000..48647d4c6cb --- /dev/null +++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.js @@ -0,0 +1,90 @@ +/** + * @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 + */ + +/* globals console, document, window */ + +import { formatHtml } from '@ckeditor/ckeditor5-source-editing/src/utils/formathtml'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; +import Table from '../../src/table'; +import TableToolbar from '../../src/tabletoolbar'; +import TableSelection from '../../src/tableselection'; +import TableClipboard from '../../src/tableclipboard'; +import TableProperties from '../../src/tableproperties'; +import TableCellProperties from '../../src/tablecellproperties'; +import TableCaption from '../../src/tablecaption'; +import PlainTableOutput from '../../src/plaintableoutput'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + ArticlePluginSet, + Table, + TableToolbar, + TableSelection, + TableClipboard, + TableProperties, + TableCellProperties, + TableCaption, + PlainTableOutput, + GeneralHtmlSupport, + SourceEditing + ], + toolbar: [ + 'heading', + '|', + 'insertTable', + '|', + 'bold', + 'italic', + 'link', + '|', + 'bulletedList', + 'numberedList', + 'blockQuote', + '|', + 'undo', + 'redo', + 'sourceEditing' + ], + table: { + contentToolbar: [ + 'tableColumn', + 'tableRow', + 'mergeTableCells', + 'tableProperties', + 'tableCellProperties', + 'toggleTableCaption' + ] + }, + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ] + }, + htmlSupport: { + allow: [ + { + name: /^(table|tbody|thead|tr|td|th|caption)$/, + attributes: true, + classes: true, + styles: true + } + ] + } + } ) + .then( editor => { + window.editor = editor; + + const element = document.getElementById( 'editor-data' ); + element.innerText = formatHtml( editor.getData() ); + + editor.model.document.on( 'change:data', () => { + element.innerText = formatHtml( editor.getData() ); + } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.md b/packages/ckeditor5-table/tests/manual/plaintableoutput.md new file mode 100644 index 00000000000..b916454a9be --- /dev/null +++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.md @@ -0,0 +1,35 @@ +### Plain table output + +Any table added to this editor should be downcasted in the data pipeline to the plain table HTML. + +It should have no `
` or `
` elements. + +The markup should be similar to this: + +```html + + + + + + + + + + + + +
Monthly savings
+ Month + + Savings +
+ January + + $100 +
+``` + +#### Image captions + +Image captions should still be `` elements. diff --git a/packages/ckeditor5-table/tests/plaintableoutput.js b/packages/ckeditor5-table/tests/plaintableoutput.js new file mode 100644 index 00000000000..8947f1ee7c7 --- /dev/null +++ b/packages/ckeditor5-table/tests/plaintableoutput.js @@ -0,0 +1,481 @@ +/** + * @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 + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Table from '../src/table'; +import PlainTableOutput from '../src/plaintableoutput'; +import { modelTable } from './_utils/utils'; +import TableCaption from '../src/tablecaption'; +import TableProperties from '../src/tableproperties'; + +describe( 'PlainTableOutput', () => { + let editor, editorElement, model; + + beforeEach( async () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ Paragraph, Table, TableCaption, TableProperties, PlainTableOutput ] + } ); + + model = editor.model; + } ); + + afterEach( async () => { + editorElement.remove(); + await editor.destroy(); + } ); + + it( 'requires Table', () => { + expect( PlainTableOutput.requires ).to.deep.equal( [ Table ] ); + } ); + + it( 'should have pluginName', () => { + expect( PlainTableOutput.pluginName ).to.equal( 'PlainTableOutput' ); + } ); + + describe( 'conversion in data pipeline', () => { + describe( 'model to view', () => { + it( 'should create tbody section', () => { + setModelData( model, modelTable( [ + [ 'foo' ] + ] ) ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '
foo
' + ); + } ); + + it( 'should create heading rows', () => { + setModelData( model, modelTable( [ + [ '1', '2' ], + [ '3', '4' ], + [ '5', '6' ] + ], { headingRows: 2 } ) ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
12
34
56
' + ); + } ); + + it( 'should create heading columns', () => { + setModelData( model, modelTable( [ + [ '1', '2' ], + [ '3', '4' ], + [ '5', '6' ] + ], { headingColumns: 1 } ) ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '' + + '' + + '
12
34
56
' + ); + } ); + + it( 'should create heading rows and columns', () => { + setModelData( model, modelTable( [ + [ '1', '2' ], + [ '3', '4' ], + [ '5', '6' ] + ], { headingRows: 1, headingColumns: 1 } ) ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
12
34
56
' + ); + } ); + + it( 'should work when heading rows number is bigger than number of rows', () => { + setModelData( model, modelTable( [ + [ '1', '2' ], + [ '3', '4' ] + ], { headingRows: 3 } ) ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '' + + '
12
34
' + ); + } ); + + it( 'should create caption element', () => { + setModelData( model, + '' + + '' + + '1' + + '2' + + '' + + '' + + '
Foo
' + ); + + expect( editor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '' + + '
Foo
12
' + ); + } ); + + it( 'should not create caption element without TableCaption plugin', async () => { + const testEditor = await ClassicTestEditor.create( editorElement, { + plugins: [ Paragraph, Table, PlainTableOutput ] + } ); + + testEditor.setData( + '' + + '' + + '' + + '' + + '' + + '
Foo
12
' + ); + + expect( testEditor.getData() ).to.equal( + '' + + '' + + '' + + '' + + '
12
' + ); + + testEditor.destroy(); + } ); + + it( 'should be overridable', () => { + const table = createEmptyTable(); + + editor.conversion.for( 'dataDowncast' ).add( dispatcher => + dispatcher.on( 'attribute:tableBorderColor:table', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'highest' } ) ); + + model.change( writer => writer.setAttribute( 'tableBorderColor', '#f00', table ) ); + + assertPlainTableStyle( editor, '' ); + } ); + + describe( 'should create attribute', () => { + let table; + + beforeEach( () => { + table = createEmptyTable(); + } ); + + it( 'tableBorderStyle', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + + assertPlainTableStyle( editor, 'border-style:dotted;' ); + } ); + + it( 'tableBorderColor', () => { + model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) ); + + assertPlainTableStyle( editor, 'border-color:red;' ); + } ); + + it( 'tableBorderWidth', () => { + model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) ); + + assertPlainTableStyle( editor, 'border-width:1px;' ); + } ); + + it( 'border shorthand', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) ); + + assertPlainTableStyle( editor, 'border:1px dotted red;' ); + } ); + + it( 'tableAlignment', () => { + model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) ); + + assertPlainTableStyle( editor, 'float:right;' ); + } ); + + it( 'tableWidth', () => { + model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) ); + + assertPlainTableStyle( editor, 'width:500px;' ); + } ); + + it( 'tableHeight', () => { + model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) ); + + assertPlainTableStyle( editor, 'height:500px;' ); + } ); + + it( 'tableBackgroundColor', () => { + model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) ); + + assertPlainTableStyle( editor, 'background-color:red;' ); + } ); + } ); + + describe( 'should remove attribute', () => { + let table; + + beforeEach( () => { + table = createEmptyTable(); + } ); + + it( 'tableBorderStyle', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) ); + + assertPlainTableStyle( editor, 'border-color:red;border-style:dotted;' ); + + model.change( writer => writer.setAttribute( 'tableBorderStyle', '', table ) ); + + assertPlainTableStyle( editor, 'border-color:red;' ); + } ); + + it( 'tableBorderColor', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) ); + + assertPlainTableStyle( editor, 'border-color:red;border-style:dotted;' ); + + model.change( writer => writer.setAttribute( 'tableBorderColor', '', table ) ); + + assertPlainTableStyle( editor, 'border-style:dotted;' ); + } ); + + it( 'tableBorderWidth', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) ); + + assertPlainTableStyle( editor, 'border-style:dotted;border-width:1px;' ); + + model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) ); + + assertPlainTableStyle( editor, 'border-style:dotted;' ); + } ); + + it( 'from border shorthand', () => { + model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) ); + model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) ); + + assertPlainTableStyle( editor, 'border:1px dotted red;' ); + + model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) ); + + assertPlainTableStyle( editor, 'border-color:red;border-style:dotted;' ); + } ); + + it( 'tableAlignment', () => { + model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) ); + + assertPlainTableStyle( editor, 'float:right;' ); + + model.change( writer => writer.removeAttribute( 'tableAlignment', table ) ); + + assertPlainTableStyle( editor, '' ); + } ); + + it( 'tableWidth', () => { + model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) ); + + assertPlainTableStyle( editor, 'width:500px;' ); + + model.change( writer => writer.removeAttribute( 'tableWidth', table ) ); + + assertPlainTableStyle( editor, '' ); + } ); + + it( 'tableHeight', () => { + model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) ); + + assertPlainTableStyle( editor, 'height:500px;' ); + + model.change( writer => writer.removeAttribute( 'tableHeight', table ) ); + + assertPlainTableStyle( editor, '' ); + } ); + + it( 'tableBackgroundColor', () => { + model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) ); + + assertPlainTableStyle( editor, 'background-color:red;' ); + + model.change( writer => writer.removeAttribute( 'tableBackgroundColor', table ) ); + + assertPlainTableStyle( editor, '' ); + } ); + } ); + + describe( 'should not create attribute', () => { + let table, testEditor; + + beforeEach( async () => { + testEditor = await ClassicTestEditor.create( editorElement, { + plugins: [ Paragraph, Table, PlainTableOutput ] + } ); + + model = testEditor.model; + table = createEmptyTable(); + } ); + + afterEach( async () => { + await testEditor.destroy(); + } ); + + it( 'tableBorderStyle without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableBorderStyle', 'dotted', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableBorderColor without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableBorderColor', 'red', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableBorderWidth without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableBorderWidth', '1px', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'border shorthand without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableBorderStyle', 'dotted', table ) + ); + model.change( writer => + writer.setAttribute( 'tableBorderColor', 'red', table ) + ); + model.change( writer => + writer.setAttribute( 'tableBorderWidth', '1px', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableAlignment without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableAlignment', 'right', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableWidth without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableWidth', '500px', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableHeight without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableHeight', '500px', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + + it( 'tableBackgroundColor without TableProperties plugin', () => { + model.change( writer => + writer.setAttribute( 'tableBackgroundColor', 'red', table ) + ); + + assertPlainTableStyle( testEditor ); + } ); + } ); + + it( 'should not convert image captions', async () => { + const testEditor = await ClassicTestEditor.create( editorElement, { + plugins: [ ArticlePluginSet, Table, TableCaption, PlainTableOutput ], + image: { toolbar: [ '|' ] } + } ); + + testEditor.setData( + '
' + + '' + + '
Caption
' + + '
' + ); + + expect( testEditor.getData() ).to.equal( + '
' + + '' + + '
Caption
' + + '
' + ); + + testEditor.destroy(); + } ); + + function createEmptyTable() { + setModelData( + model, + '' + + '' + + '' + + 'foo' + + '' + + '' + + '
' + ); + + return model.document.getRoot().getNodeByPath( [ 0 ] ); + } + + function assertPlainTableStyle( editor, tableStyle ) { + const tableStyleEntry = tableStyle ? ` style="${ tableStyle }"` : ''; + + expect( editor.getData() ).to.equalMarkup( + `` + + 'foo' + + '' + ); + } + } ); + } ); +} ); diff --git a/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js b/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js new file mode 100644 index 00000000000..94acda21d54 --- /dev/null +++ b/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js @@ -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 + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import TableCaptionEditing from '../../src/tablecaption/tablecaptionediting'; +import TableCaptionUI from '../../src/tablecaption/tablecaptionui'; +import TableEditing from '../../src/tableediting'; + +describe( 'TableCaptionUI', () => { + let editor, tableCaption, editorElement; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor.create( editorElement, { + plugins: [ Paragraph, TableEditing, TableCaptionEditing, TableCaptionUI ] + } ).then( newEditor => { + editor = newEditor; + + tableCaption = editor.ui.componentFactory.create( 'toggleTableCaption' ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + return editor.destroy(); + } ); + + it( 'should register toggleTableCaption feature component', () => { + expect( tableCaption ).to.be.instanceOf( ButtonView ); + expect( tableCaption.icon ).to.match( / { + const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + + tableCaption.fire( 'execute' ); + + sinon.assert.calledOnce( executeSpy ); + sinon.assert.calledWithExactly( executeSpy, 'toggleTableCaption', { + focusCaptionOnShow: true + } ); + } ); + + it( 'should scroll the editing view to the caption on the #execute event if the caption showed up', () => { + editor.setData( '
foo
' ); + + const executeSpy = testUtils.sinon.spy( editor.editing.view, 'scrollToTheSelection' ); + + tableCaption.fire( 'execute' ); + + sinon.assert.calledOnce( executeSpy ); + } ); + + it( 'should not scroll the editing view on the #execute event if the caption was hidden', () => { + editor.setData( '
foo
foo
' ); + + const executeSpy = testUtils.sinon.spy( editor.editing.view, 'scrollToTheSelection' ); + + tableCaption.fire( 'execute' ); + + sinon.assert.notCalled( executeSpy ); + } ); + + it( 'should highlight the figcaption element in the view on the #execute event if the caption showed up', () => { + editor.setData( '
foo
' ); + + tableCaption.fire( 'execute' ); + + const figcaptionElement = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 2 ); + + expect( figcaptionElement.hasClass( 'table__caption_highlighted' ) ).to.be.true; + } ); + + it( 'should not scroll or highlight anything if figcaption element is missing', () => { + sinon.stub( editor.editing.mapper, 'toViewElement' ).returns( null ); + + editor.setData( '
foo
' ); + + const executeSpy = testUtils.sinon.spy( editor.editing.view, 'scrollToTheSelection' ); + const figcaptionElement = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 2 ); + + tableCaption.fire( 'execute' ); + + sinon.assert.notCalled( executeSpy ); + expect( figcaptionElement ).to.be.undefined; + } ); + + it( 'should bind model to toggleTableCaption command', () => { + const command = editor.commands.get( 'toggleTableCaption' ); + + command.value = true; + expect( tableCaption.isOn ).to.be.true; + } ); + + it( 'should have #label bound to the toggleTableCaption command', () => { + const command = editor.commands.get( 'toggleTableCaption' ); + + command.value = true; + expect( tableCaption.label ).to.equal( 'Toggle caption off' ); + + command.value = false; + expect( tableCaption.label ).to.equal( 'Toggle caption on' ); + } ); +} ); diff --git a/packages/ckeditor5-widget/src/widgettoolbarrepository.js b/packages/ckeditor5-widget/src/widgettoolbarrepository.js index f0461343673..c3885dbd2a3 100644 --- a/packages/ckeditor5-widget/src/widgettoolbarrepository.js +++ b/packages/ckeditor5-widget/src/widgettoolbarrepository.js @@ -124,10 +124,18 @@ export default class WidgetToolbarRepository extends Plugin { // Trying to register a toolbar without any item. if ( !items.length ) { /** - * When {@link #register} a new toolbar, you need to provide a non-empty array with + * When {@link #register registering} a new widget toolbar, you need to provide a non-empty array with * the items that will be inserted into the toolbar. * + * If you see this error when integrating the editor, you likely forgot to configure one of the widget toolbars. + * + * See for instance: + * + * * {@link module:table/table~TableConfig#contentToolbar `editor.config.table.contentToolbar`} + * * {@link module:image/image~ImageConfig#toolbar `editor.config.image.toolbar`} + * * @error widget-toolbar-no-items + * @param {String} toolbarId The id of the toolbar that has not been configured correctly. */ logWarning( 'widget-toolbar-no-items', { toolbarId } );