Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit c088814

Browse files
authored
Merge pull request #271 from ckeditor/i/6368
Fix: Merge left and right commands should be always enabled if the execution does not cross the heading column boundary. Closes ckeditor/ckeditor5#6368.
2 parents b231e4d + d6d542f commit c088814

File tree

5 files changed

+142
-74
lines changed

5 files changed

+142
-74
lines changed

src/commands/mergecellcommand.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import Command from '@ckeditor/ckeditor5-core/src/command';
1111
import TableWalker from '../tablewalker';
12-
import { updateNumericAttribute } from './utils';
12+
import {
13+
updateNumericAttribute,
14+
isHeadingColumnCell
15+
} from './utils';
1316
import { getTableCellsContainingSelection } from '../utils';
1417

1518
/**
@@ -157,7 +160,7 @@ function getHorizontalCell( tableCell, direction, tableUtils ) {
157160
const tableRow = tableCell.parent;
158161
const table = tableRow.parent;
159162
const horizontalCell = direction == 'right' ? tableCell.nextSibling : tableCell.previousSibling;
160-
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
163+
const hasHeadingColumns = ( table.getAttribute( 'headingColumns' ) || 0 ) > 0;
161164

162165
if ( !horizontalCell ) {
163166
return;
@@ -172,13 +175,12 @@ function getHorizontalCell( tableCell, direction, tableUtils ) {
172175
const { column: rightCellColumn } = tableUtils.getCellLocation( cellOnRight );
173176

174177
const leftCellSpan = parseInt( cellOnLeft.getAttribute( 'colspan' ) || 1 );
175-
const rightCellSpan = parseInt( cellOnRight.getAttribute( 'colspan' ) || 1 );
176178

177-
// We cannot merge cells if the result will extend over heading section.
178-
const isMergeWithBodyCell = direction == 'right' && ( rightCellColumn + rightCellSpan > headingColumns );
179-
const isMergeWithHeadCell = direction == 'left' && ( leftCellColumn + leftCellSpan > headingColumns - 1 );
179+
const isCellOnLeftInHeadingColumn = isHeadingColumnCell( tableUtils, cellOnLeft, table );
180+
const isCellOnRightInHeadingColumn = isHeadingColumnCell( tableUtils, cellOnRight, table );
180181

181-
if ( headingColumns && ( isMergeWithBodyCell || isMergeWithHeadCell ) ) {
182+
// We cannot merge heading columns cells with regular cells.
183+
if ( hasHeadingColumns && isCellOnLeftInHeadingColumn != isCellOnRightInHeadingColumn ) {
182184
return;
183185
}
184186

src/commands/setheadercolumncommand.js

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import Command from '@ckeditor/ckeditor5-core/src/command';
1111

12-
import { updateNumericAttribute } from './utils';
12+
import {
13+
updateNumericAttribute,
14+
isHeadingColumnCell
15+
} from './utils';
1316
import { getTableCellsContainingSelection } from '../utils';
1417

1518
/**
@@ -35,7 +38,7 @@ export default class SetHeaderColumnCommand extends Command {
3538
const model = this.editor.model;
3639
const doc = model.document;
3740
const tableCell = getTableCellsContainingSelection( doc.selection )[ 0 ];
38-
41+
const tableUtils = this.editor.plugins.get( 'TableUtils' );
3942
const isInTable = !!tableCell;
4043

4144
this.isEnabled = isInTable;
@@ -48,7 +51,7 @@ export default class SetHeaderColumnCommand extends Command {
4851
* @readonly
4952
* @member {Boolean} #value
5053
*/
51-
this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent );
54+
this.value = isInTable && isHeadingColumnCell( tableUtils, tableCell );
5255
}
5356

5457
/**
@@ -85,22 +88,4 @@ export default class SetHeaderColumnCommand extends Command {
8588
updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
8689
} );
8790
}
88-
89-
/**
90-
* Checks if a table cell is in the heading section.
91-
*
92-
* @param {module:engine/model/element~Element} tableCell
93-
* @param {module:engine/model/element~Element} table
94-
* @returns {Boolean}
95-
* @private
96-
*/
97-
_isInHeading( tableCell, table ) {
98-
const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
99-
100-
const tableUtils = this.editor.plugins.get( 'TableUtils' );
101-
102-
const { column } = tableUtils.getCellLocation( tableCell );
103-
104-
return !!headingColumns && column < headingColumns;
105-
}
10691
}

src/commands/utils.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,18 @@ export function addDefaultUnitToNumericValue( value, defaultUnit ) {
114114

115115
return `${ numericValue }${ defaultUnit }`;
116116
}
117+
118+
/**
119+
* Checks if a table cell belongs to the heading column section.
120+
*
121+
* @param {module:table/tableutils~TableUtils} tableUtils
122+
* @param {module:engine/model/element~Element} tableCell
123+
* @returns {Boolean}
124+
*/
125+
export function isHeadingColumnCell( tableUtils, tableCell ) {
126+
const table = tableCell.parent.parent;
127+
const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
128+
const { column } = tableUtils.getCellLocation( tableCell );
129+
130+
return !!headingColumns && column < headingColumns;
131+
}

tests/commands/mergecellcommand.js

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -95,44 +95,46 @@ describe( 'MergeCellCommand', () => {
9595
expect( command.isEnabled ).to.be.false;
9696
} );
9797

98-
it( 'should be false if mergeable cell is in other table section then current cell', () => {
99-
setData( model, modelTable( [
100-
[ '00[]', '01' ]
101-
], { headingColumns: 1 } ) );
98+
describe( 'when the heading section is in the table', () => {
99+
it( 'should be false if mergeable cell is in other table section then current cell', () => {
100+
setData( model, modelTable( [
101+
[ '00[]', '01' ]
102+
], { headingColumns: 1 } ) );
102103

103-
expect( command.isEnabled ).to.be.false;
104-
} );
104+
expect( command.isEnabled ).to.be.false;
105+
} );
105106

106-
it( 'should be false if merged cell would cross heading section (mergeable cell with colspan)', () => {
107-
setData( model, modelTable( [
108-
[ '00[]', { colspan: 2, contents: '01' }, '02', '03' ]
109-
], { headingColumns: 2 } ) );
107+
it( 'should be true if merged cell would not cross heading section (mergeable cell with colspan)', () => {
108+
setData( model, modelTable( [
109+
[ '00[]', { colspan: 2, contents: '01' }, '02', '03' ]
110+
], { headingColumns: 3 } ) );
110111

111-
expect( command.isEnabled ).to.be.false;
112-
} );
112+
expect( command.isEnabled ).to.be.true;
113+
} );
113114

114-
it( 'should be true if merged cell would not cross heading section (mergeable cell with colspan)', () => {
115-
setData( model, modelTable( [
116-
[ '00[]', { colspan: 2, contents: '01' }, '02', '03' ]
117-
], { headingColumns: 3 } ) );
115+
it( 'should be false if merged cell would cross heading section (current cell with colspan)', () => {
116+
setData( model, modelTable( [
117+
[ { colspan: 2, contents: '00[]' }, '01', '02', '03' ]
118+
], { headingColumns: 2 } ) );
118119

119-
expect( command.isEnabled ).to.be.true;
120-
} );
120+
expect( command.isEnabled ).to.be.false;
121+
} );
121122

122-
it( 'should be false if merged cell would cross heading section (current cell with colspan)', () => {
123-
setData( model, modelTable( [
124-
[ { colspan: 2, contents: '00[]' }, '01', '02', '03' ]
125-
], { headingColumns: 2 } ) );
123+
it( 'should be true if merged cell would not cross heading section (current cell with colspan)', () => {
124+
setData( model, modelTable( [
125+
[ { colspan: 2, contents: '00[]' }, '01', '02', '03' ]
126+
], { headingColumns: 3 } ) );
126127

127-
expect( command.isEnabled ).to.be.false;
128-
} );
128+
expect( command.isEnabled ).to.be.true;
129+
} );
129130

130-
it( 'should be true if merged cell would not cross heading section (current cell with colspan)', () => {
131-
setData( model, modelTable( [
132-
[ { colspan: 2, contents: '00[]' }, '01', '02', '03' ]
133-
], { headingColumns: 3 } ) );
131+
it( 'should be true if merged cell would not cross the section boundary (regular section)', () => {
132+
setData( model, modelTable( [
133+
[ '00', '01', '02[]', '03' ]
134+
], { headingColumns: 1 } ) );
134135

135-
expect( command.isEnabled ).to.be.true;
136+
expect( command.isEnabled ).to.be.true;
137+
} );
136138
} );
137139
} );
138140

@@ -315,20 +317,38 @@ describe( 'MergeCellCommand', () => {
315317
expect( command.isEnabled ).to.be.false;
316318
} );
317319

318-
it( 'should be false if mergeable cell is in other table section then current cell', () => {
319-
setData( model, modelTable( [
320-
[ '00', '01[]' ]
321-
], { headingColumns: 1 } ) );
320+
describe( 'when the heading section is in the table', () => {
321+
it( 'should be false if mergeable cell is in other table section then current cell', () => {
322+
setData( model, modelTable( [
323+
[ '00', '01[]' ]
324+
], { headingColumns: 1 } ) );
322325

323-
expect( command.isEnabled ).to.be.false;
324-
} );
326+
expect( command.isEnabled ).to.be.false;
327+
} );
325328

326-
it( 'should be false if merged cell would cross heading section (mergeable cell with colspan)', () => {
327-
setData( model, modelTable( [
328-
[ { colspan: 2, contents: '00' }, '02[]', '03' ]
329-
], { headingColumns: 2 } ) );
329+
it( 'should be false if merged cell would cross heading section (mergeable cell with colspan)', () => {
330+
setData( model, modelTable( [
331+
[ { colspan: 2, contents: '00' }, '02[]', '03' ]
332+
], { headingColumns: 2 } ) );
330333

331-
expect( command.isEnabled ).to.be.false;
334+
expect( command.isEnabled ).to.be.false;
335+
} );
336+
337+
it( 'should be true if merged cell would not cross the section boundary (in regular section)', () => {
338+
setData( model, modelTable( [
339+
[ '00', '01', '02[]', '03' ]
340+
], { headingColumns: 1 } ) );
341+
342+
expect( command.isEnabled ).to.be.true;
343+
} );
344+
345+
it( 'should be true if merged cell would not cross the section boundary (in heading section)', () => {
346+
setData( model, modelTable( [
347+
[ '00', '01[]', '02', '03' ]
348+
], { headingColumns: 2 } ) );
349+
350+
expect( command.isEnabled ).to.be.true;
351+
} );
332352
} );
333353
} );
334354

tests/commands/utils.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
*/
55

66
import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
7+
import TableUtils from '../../src/tableutils';
78
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
8-
99
import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
10-
11-
import { findAncestor } from '../../src/commands/utils';
10+
import { findAncestor, isHeadingColumnCell } from '../../src/commands/utils';
1211

1312
describe( 'commands utils', () => {
14-
let editor, model;
13+
let editor, model, modelRoot, tableUtils;
1514

1615
beforeEach( () => {
17-
return ModelTestEditor.create()
16+
return ModelTestEditor
17+
.create( {
18+
plugins: [ TableUtils ]
19+
} )
1820
.then( newEditor => {
1921
editor = newEditor;
2022
model = editor.model;
23+
modelRoot = model.document.getRoot();
24+
tableUtils = editor.plugins.get( TableUtils );
2125

2226
defaultSchema( model.schema );
2327
defaultConversion( editor.conversion );
@@ -28,7 +32,7 @@ describe( 'commands utils', () => {
2832
return editor.destroy();
2933
} );
3034

31-
describe( 'getParentTable()', () => {
35+
describe( 'findAncestor()', () => {
3236
it( 'should return undefined if not in table', () => {
3337
setData( model, '<paragraph>foo[]</paragraph>' );
3438

@@ -44,4 +48,46 @@ describe( 'commands utils', () => {
4448
expect( parentTable.is( 'table' ) ).to.be.true;
4549
} );
4650
} );
51+
52+
describe( 'isHeadingColumnCell()', () => {
53+
it( 'should return "true" for a heading column cell', () => {
54+
setData( model, modelTable( [
55+
[ '00', '01', '02', '03' ]
56+
], { headingColumns: 2 } ) );
57+
58+
const tableCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
59+
60+
expect( isHeadingColumnCell( tableUtils, tableCell ) ).to.be.true;
61+
} );
62+
63+
it( 'should return "true" for a heading column cell with colspan', () => {
64+
setData( model, modelTable( [
65+
[ { colspan: 2, contents: '00' }, '01', '02', '03' ]
66+
], { headingColumns: 2 } ) );
67+
68+
const tableCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
69+
70+
expect( isHeadingColumnCell( tableUtils, tableCell ) ).to.be.true;
71+
} );
72+
73+
it( 'should return "false" for a regular column cell', () => {
74+
setData( model, modelTable( [
75+
[ '00', '01', '02', '03' ]
76+
], { headingColumns: 2 } ) );
77+
78+
const tableCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
79+
80+
expect( isHeadingColumnCell( tableUtils, tableCell ) ).to.be.false;
81+
} );
82+
83+
it( 'should return "false" for a regular column cell with colspan', () => {
84+
setData( model, modelTable( [
85+
[ '00', { colspan: 2, contents: '01' }, '02', '03' ]
86+
], { headingColumns: 1 } ) );
87+
88+
const tableCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
89+
90+
expect( isHeadingColumnCell( tableUtils, tableCell ) ).to.be.false;
91+
} );
92+
} );
4793
} );

0 commit comments

Comments
 (0)