@ckeditor/ckeditor5-table
Advanced tools
Comparing version 35.0.1 to 35.1.0
@@ -1,1 +0,1 @@ | ||
!function(e){const t=e.ur=e.ur||{};t.dictionary=Object.assign(t.dictionary||{},{"Align cell text to the bottom":"","Align cell text to the center":"","Align cell text to the left":"","Align cell text to the middle":"","Align cell text to the right":"","Align cell text to the top":"","Align table to the left":"","Align table to the right":"",Alignment:"",Background:"",Border:"","Cell properties":"","Center table":"",Color:"","Color picker":"",Column:"ستون",Dashed:"","Delete column":"ستون حذف کریں","Delete row":"قطار حذف کریں",Dimensions:"",Dotted:"",Double:"","Enter table caption":"",Groove:"","Header column":"سر ستون","Header row":"سر قطار",Height:"","Horizontal text alignment toolbar":"","Insert column left":"","Insert column right":"","Insert row above":"قطار بالا نصب کریں","Insert row below":"قطار زیریں نصب کریں","Insert table":"جدول داخل کریں",Inset:"","Justify cell text":"","Merge cell down":"سیل نچلی طرف یکجا کریں","Merge cell left":"سیل بائیں طرف یکجا کریں","Merge cell right":"سیل دائیں طرف یکجا کریں","Merge cell up":"سیل اوپر یکجا کریں","Merge cells":"سیل یکجا کریں",None:"",Outset:"",Padding:"",Ridge:"",Row:"قطار","Select column":"","Select row":"",Solid:"","Split cell horizontally":"سیل کی افقی تقسیم","Split cell vertically":"سیل کی عمودی تقسیم",Style:"","Table alignment toolbar":"","Table cell text alignment":"","Table properties":"","Table toolbar":"آلہ جات برائے جدول",'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".':"",'The value is invalid. Try "10px" or "2em" or simply "2".':"","Toggle caption off":"","Toggle caption on":"","Vertical text alignment toolbar":"",Width:""})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); | ||
!function(e){const t=e.ur=e.ur||{};t.dictionary=Object.assign(t.dictionary||{},{"Align cell text to the bottom":"","Align cell text to the center":"","Align cell text to the left":"","Align cell text to the middle":"","Align cell text to the right":"","Align cell text to the top":"","Align table to the left":"","Align table to the right":"",Alignment:"",Background:"",Border:"حاشیہ","Cell properties":"","Center table":"",Color:"رنگ","Color picker":"",Column:"ستون",Dashed:"قطعہ دار","Delete column":"ستون حذف کریں","Delete row":"قطار حذف کریں",Dimensions:"",Dotted:"نقطہ دار",Double:"دو گنا","Enter table caption":"",Groove:"","Header column":"سر ستون","Header row":"سر قطار",Height:"اونچائی","Horizontal text alignment toolbar":"","Insert column left":"بائیں جانب کالم بنائیں","Insert column right":"دائیں جانب کالم بنائیں","Insert row above":"قطار بالا نصب کریں","Insert row below":"قطار زیریں نصب کریں","Insert table":"جدول داخل کریں",Inset:"","Justify cell text":"","Merge cell down":"سیل نچلی طرف یکجا کریں","Merge cell left":"سیل بائیں طرف یکجا کریں","Merge cell right":"سیل دائیں طرف یکجا کریں","Merge cell up":"سیل اوپر یکجا کریں","Merge cells":"سیل یکجا کریں",None:"",Outset:"",Padding:"",Ridge:"",Row:"قطار","Select column":"","Select row":"",Solid:"","Split cell horizontally":"سیل کی افقی تقسیم","Split cell vertically":"سیل کی عمودی تقسیم",Style:"","Table alignment toolbar":"","Table cell text alignment":"","Table properties":"","Table toolbar":"آلہ جات برائے جدول",'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".':"",'The value is invalid. Try "10px" or "2em" or simply "2".':"","Toggle caption off":"","Toggle caption on":"","Vertical text alignment toolbar":"",Width:"چوڑائی"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})); |
{ | ||
"name": "@ckeditor/ckeditor5-table", | ||
"version": "35.0.1", | ||
"version": "35.1.0", | ||
"description": "Table feature for CKEditor 5.", | ||
@@ -15,30 +15,30 @@ "keywords": [ | ||
"dependencies": { | ||
"ckeditor5": "^35.0.1", | ||
"ckeditor5": "^35.1.0", | ||
"lodash-es": "^4.17.15" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-alignment": "^35.0.1", | ||
"@ckeditor/ckeditor5-basic-styles": "^35.0.1", | ||
"@ckeditor/ckeditor5-block-quote": "^35.0.1", | ||
"@ckeditor/ckeditor5-clipboard": "^35.0.1", | ||
"@ckeditor/ckeditor5-core": "^35.0.1", | ||
"@ckeditor/ckeditor5-alignment": "^35.1.0", | ||
"@ckeditor/ckeditor5-basic-styles": "^35.1.0", | ||
"@ckeditor/ckeditor5-block-quote": "^35.1.0", | ||
"@ckeditor/ckeditor5-clipboard": "^35.1.0", | ||
"@ckeditor/ckeditor5-core": "^35.1.0", | ||
"@ckeditor/ckeditor5-dev-utils": "^30.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^35.0.1", | ||
"@ckeditor/ckeditor5-engine": "^35.0.1", | ||
"@ckeditor/ckeditor5-highlight": "^35.0.1", | ||
"@ckeditor/ckeditor5-horizontal-line": "^35.0.1", | ||
"@ckeditor/ckeditor5-html-support": "^35.0.1", | ||
"@ckeditor/ckeditor5-image": "^35.0.1", | ||
"@ckeditor/ckeditor5-indent": "^35.0.1", | ||
"@ckeditor/ckeditor5-link": "^35.0.1", | ||
"@ckeditor/ckeditor5-list": "^35.0.1", | ||
"@ckeditor/ckeditor5-media-embed": "^35.0.1", | ||
"@ckeditor/ckeditor5-paragraph": "^35.0.1", | ||
"@ckeditor/ckeditor5-theme-lark": "^35.0.1", | ||
"@ckeditor/ckeditor5-typing": "^35.0.1", | ||
"@ckeditor/ckeditor5-ui": "^35.0.1", | ||
"@ckeditor/ckeditor5-undo": "^35.0.1", | ||
"@ckeditor/ckeditor5-utils": "^35.0.1", | ||
"@ckeditor/ckeditor5-widget": "^35.0.1", | ||
"@ckeditor/ckeditor5-source-editing": "^35.0.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^35.1.0", | ||
"@ckeditor/ckeditor5-engine": "^35.1.0", | ||
"@ckeditor/ckeditor5-highlight": "^35.1.0", | ||
"@ckeditor/ckeditor5-horizontal-line": "^35.1.0", | ||
"@ckeditor/ckeditor5-html-support": "^35.1.0", | ||
"@ckeditor/ckeditor5-image": "^35.1.0", | ||
"@ckeditor/ckeditor5-indent": "^35.1.0", | ||
"@ckeditor/ckeditor5-link": "^35.1.0", | ||
"@ckeditor/ckeditor5-list": "^35.1.0", | ||
"@ckeditor/ckeditor5-media-embed": "^35.1.0", | ||
"@ckeditor/ckeditor5-paragraph": "^35.1.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^35.1.0", | ||
"@ckeditor/ckeditor5-typing": "^35.1.0", | ||
"@ckeditor/ckeditor5-ui": "^35.1.0", | ||
"@ckeditor/ckeditor5-undo": "^35.1.0", | ||
"@ckeditor/ckeditor5-utils": "^35.1.0", | ||
"@ckeditor/ckeditor5-widget": "^35.1.0", | ||
"@ckeditor/ckeditor5-source-editing": "^35.1.0", | ||
"json-diff": "^0.5.4", | ||
@@ -45,0 +45,0 @@ "webpack": "^5.58.1", |
@@ -66,2 +66,4 @@ /** | ||
} | ||
editor.editing.view.focus(); | ||
} ); | ||
@@ -68,0 +70,0 @@ |
@@ -16,5 +16,5 @@ /** | ||
/** | ||
* The table column resizer feature. | ||
* The table column resize feature. | ||
* | ||
* It provides the possibility to set the width of each column in a table using a resize handle. | ||
* It provides the possibility to set the width of each column in a table using a resize handler. | ||
* | ||
@@ -21,0 +21,0 @@ * @extends module:core/plugin~Plugin |
@@ -10,4 +10,2 @@ /** | ||
/* istanbul ignore file */ | ||
/** | ||
@@ -14,0 +12,0 @@ * The minimum column width given as a percentage value. Used in situations when the table is not yet rendered, so it is impossible to |
@@ -10,6 +10,4 @@ /** | ||
/* istanbul ignore file */ | ||
import { normalizeColumnWidths } from './utils'; | ||
import { getNumberOfColumn } from './utils'; | ||
/** | ||
@@ -19,23 +17,24 @@ * Returns a helper for converting a view `<colgroup>` and `<col>` elements to the model table `columnWidths` attribute. | ||
* Only the inline width, provided as a percentage value, in the `<col>` element is taken into account. If there are not enough `<col>` | ||
* elements matching this condition, the special value 'auto' is returned. It indicates that the width of a column will be automatically | ||
* elements matching this condition, the special value `auto` is returned. It indicates that the width of a column will be automatically | ||
* calculated in the | ||
* {@link module:table/tablecolumnresize/tablecolumnresizeediting~TableColumnResizeEditing#_setupPostFixer post-fixer}, depending | ||
* {@link module:table/tablecolumnresize/tablecolumnresizeediting~TableColumnResizeEditing#_registerPostFixer post-fixer}, depending | ||
* on the available table space. | ||
* | ||
* @param {module:core/editor/editor~Editor} editor The editor instance. | ||
* @param {module:core/plugin~Plugin} tableUtilsPlugin The {@link module:table/tableutils~TableUtils} plugin instance. | ||
* @returns {Function} Conversion helper. | ||
*/ | ||
export function upcastColgroupElement( editor ) { | ||
export function upcastColgroupElement( tableUtilsPlugin ) { | ||
return dispatcher => dispatcher.on( 'element:colgroup', ( evt, data, conversionApi ) => { | ||
const modelTable = data.modelCursor.findAncestor( 'table' ); | ||
const viewColgroupElement = data.viewItem; | ||
if ( !modelTable ) { | ||
if ( !conversionApi.consumable.test( viewColgroupElement, { name: true } ) ) { | ||
return; | ||
} | ||
const modelWriter = conversionApi.writer; | ||
const viewColgroupElement = data.viewItem; | ||
const numberOfColumns = getNumberOfColumn( modelTable, editor ); | ||
conversionApi.consumable.consume( viewColgroupElement, { name: true } ); | ||
const columnWidthsAttribute = [ ...Array( numberOfColumns ).keys() ] | ||
const modelTable = data.modelCursor.findAncestor( 'table' ); | ||
const numberOfColumns = tableUtilsPlugin.getColumns( modelTable ); | ||
let columnWidths = [ ...Array( numberOfColumns ).keys() ] | ||
.map( columnIndex => { | ||
@@ -55,6 +54,9 @@ const viewChild = viewColgroupElement.getChild( columnIndex ); | ||
return viewColWidth; | ||
} ) | ||
.join( ',' ); | ||
} ); | ||
modelWriter.setAttribute( 'columnWidths', columnWidthsAttribute, modelTable ); | ||
if ( columnWidths.includes( 'auto' ) ) { | ||
columnWidths = normalizeColumnWidths( columnWidths ).map( width => width + '%' ); | ||
} | ||
conversionApi.writer.setAttribute( 'columnWidths', columnWidths.join( ',' ), modelTable ); | ||
} ); | ||
@@ -77,7 +79,10 @@ } | ||
if ( data.attributeNewValue ) { | ||
if ( data.attributeNewValue !== data.attributeOldValue ) { | ||
insertColgroupElement( viewWriter, viewTable, data.attributeNewValue ); | ||
} | ||
// If new value is the same as the old, the operation is not applied (see the `writer.setAttributeOnItem()`). | ||
// OTOH the model element has the attribute already applied, so we can't compare the values. | ||
// Hence we need to just recreate the <colgroup> element every time. | ||
insertColgroupElement( viewWriter, viewTable, data.attributeNewValue ); | ||
viewWriter.addClass( 'ck-table-resized', viewTable ); | ||
} else { | ||
removeColgroupElement( viewWriter, viewTable ); | ||
viewWriter.removeClass( 'ck-table-resized', viewTable ); | ||
} | ||
@@ -93,3 +98,3 @@ } ); | ||
// @param {module:engine/view/element~Element} viewTable View table. | ||
// @param {String} columnWidthsAttribute Column width attribute from model table. | ||
// @param {String} columnWidthsAttribute Column widths attribute from model table. | ||
function insertColgroupElement( viewWriter, viewTable, columnWidthsAttribute ) { | ||
@@ -102,8 +107,8 @@ const columnWidths = columnWidthsAttribute.split( ',' ); | ||
viewColgroupElement = viewWriter.createContainerElement( 'colgroup' ); | ||
} else { | ||
for ( const viewChild of [ ...viewColgroupElement.getChildren() ] ) { | ||
viewWriter.remove( viewChild ); | ||
} | ||
} | ||
for ( const viewChild of [ ...viewColgroupElement.getChildren() ] ) { | ||
viewWriter.remove( viewChild ); | ||
} | ||
for ( const columnIndex of Array( columnWidths.length ).keys() ) { | ||
@@ -127,7 +132,3 @@ const viewColElement = viewWriter.createEmptyElement( 'col' ); | ||
if ( !viewColgroupElement ) { | ||
return; | ||
} | ||
viewWriter.remove( viewColgroupElement ); | ||
} |
@@ -10,4 +10,2 @@ /** | ||
/* istanbul ignore file */ | ||
import { throttle } from 'lodash-es'; | ||
@@ -19,4 +17,8 @@ import { global, DomEmitterMixin } from 'ckeditor5/src/utils'; | ||
import TableEditing from '../tableediting'; | ||
import TableUtils from '../tableutils'; | ||
import TableWalker from '../tablewalker'; | ||
import TableWidthResizeCommand from './tablewidthresizecommand'; | ||
import TableColumnWidthsCommand from './tablecolumnwidthscommand'; | ||
import { | ||
@@ -29,16 +31,13 @@ upcastColgroupElement, | ||
clamp, | ||
fillArray, | ||
createFilledArray, | ||
sumArray, | ||
getAffectedTables, | ||
getColumnIndex, | ||
getColumnWidthsInPixels, | ||
getColumnEdgesIndexes, | ||
getChangedResizedTables, | ||
getColumnMinWidthAsPercentage, | ||
getElementWidthInPixels, | ||
getTableWidthInPixels, | ||
getNumberOfColumn, | ||
isTableRendered, | ||
normalizeColumnWidthsAttribute, | ||
normalizeColumnWidths, | ||
toPrecision, | ||
insertColumnResizerElements, | ||
removeColumnResizerElements | ||
ensureColumnResizerElement, | ||
getDomCellOuterWidth | ||
} from './utils'; | ||
@@ -58,3 +57,3 @@ | ||
static get requires() { | ||
return [ TableEditing ]; | ||
return [ TableEditing, TableUtils ]; | ||
} | ||
@@ -84,9 +83,10 @@ | ||
/** | ||
* A flag indicating if the column resizing is allowed. It is not allowed if the editor is in read-only mode or the | ||
* `TableColumnResize` plugin is disabled. | ||
* A flag indicating if the column resizing is allowed. It is not allowed if the editor is in read-only | ||
* or comments-only mode or the `TableColumnResize` plugin is disabled. | ||
* | ||
* @private | ||
* @observable | ||
* @member {Boolean} | ||
*/ | ||
this._isResizingAllowed = true; | ||
this.set( '_isResizingAllowed', true ); | ||
@@ -103,18 +103,23 @@ /** | ||
/** | ||
* Internal map to store reference between a cell and its columnIndex. This information is required in postfixer to properly | ||
* recognize if the cell was inserted or deleted. | ||
* DOM emitter. | ||
* | ||
* @private | ||
* @member {Map} | ||
* @member {DomEmitterMixin} | ||
*/ | ||
this._columnIndexMap = new Map(); | ||
this._domEmitter = Object.create( DomEmitterMixin ); | ||
/** | ||
* Internal map to store reference between a cell and operation that was performed on it (insert/remove). This is required | ||
* in order to add/remove resizers based on operation performed (which is done on 'render'). | ||
* A local reference to the {@link module:table/tableutils~TableUtils} plugin. | ||
* | ||
* @private | ||
* @member {Map} | ||
* @member {module:table/tableutils~TableUtils} | ||
*/ | ||
this._cellsModified = new Map(); | ||
this._tableUtilsPlugin = editor.plugins.get( 'TableUtils' ); | ||
this.on( 'change:_isResizingAllowed', ( evt, name, value ) => { | ||
// Toggling the `ck-column-resize_disabled` class shows and hides the resizers through CSS. | ||
editor.editing.view.change( writer => { | ||
writer[ value ? 'removeClass' : 'addClass' ]( 'ck-column-resize_disabled', editor.editing.view.document.getRoot() ); | ||
} ); | ||
} ); | ||
} | ||
@@ -127,5 +132,5 @@ | ||
this._extendSchema(); | ||
this._setupConversion(); | ||
this._setupPostFixer(); | ||
this._setupColumnResizers(); | ||
this._registerPostFixer(); | ||
this._registerConverters(); | ||
this._registerResizingListeners(); | ||
this._registerColgroupFixer(); | ||
@@ -137,6 +142,19 @@ this._registerResizerInserter(); | ||
editor.commands.add( 'resizeTableWidth', new TableWidthResizeCommand( editor ) ); | ||
editor.commands.add( 'resizeColumnWidths', new TableColumnWidthsCommand( editor ) ); | ||
const resizeTableWidthCommand = editor.commands.get( 'resizeTableWidth' ); | ||
const resizeColumnWidthsCommand = editor.commands.get( 'resizeColumnWidths' ); | ||
// Currently the states of column resize and table resize (which is actually the last column resize) features | ||
// are bound together. They can be separated in the future by adding distinct listeners and applying | ||
// different CSS classes (e.g. `ck-column-resize_disabled` and `ck-table-resize_disabled`) to the editor root. | ||
// See #12148 for the details. | ||
this.bind( '_isResizingAllowed' ).to( | ||
editor, 'isReadOnly', | ||
columnResizePlugin, 'isEnabled', | ||
( isEditorReadOnly, isPluginEnabled ) => !isEditorReadOnly && isPluginEnabled | ||
resizeTableWidthCommand, 'isEnabled', | ||
resizeColumnWidthsCommand, 'isEnabled', | ||
( isEditorReadOnly, isPluginEnabled, isResizeTableWidthCommandEnabled, isResizeColumnWidthsCommandEnabled ) => | ||
!isEditorReadOnly && isPluginEnabled && isResizeTableWidthCommandEnabled && isResizeColumnWidthsCommandEnabled | ||
); | ||
@@ -146,319 +164,182 @@ } | ||
/** | ||
* Registers new attributes for a table and a table cell model elements. | ||
* | ||
* @private | ||
* @inheritDoc | ||
*/ | ||
_extendSchema() { | ||
const editor = this.editor; | ||
const schema = editor.model.schema; | ||
schema.extend( 'table', { | ||
allowAttributes: [ 'tableWidth', 'columnWidths' ] | ||
} ); | ||
destroy() { | ||
this._domEmitter.stopListening(); | ||
super.destroy(); | ||
} | ||
/** | ||
* Registers table column resizer converters. | ||
* Registers new attributes for a table model element. | ||
* | ||
* @private | ||
*/ | ||
_setupConversion() { | ||
const editor = this.editor; | ||
const conversion = editor.conversion; | ||
conversion.for( 'upcast' ).attributeToAttribute( { | ||
view: { | ||
name: 'figure', | ||
key: 'style', | ||
value: { | ||
width: /[\s\S]+/ | ||
} | ||
}, | ||
model: { | ||
name: 'table', | ||
key: 'tableWidth', | ||
value: viewElement => viewElement.getStyle( 'width' ) | ||
} | ||
_extendSchema() { | ||
this.editor.model.schema.extend( 'table', { | ||
allowAttributes: [ 'tableWidth', 'columnWidths' ] | ||
} ); | ||
conversion.for( 'downcast' ).attributeToAttribute( { | ||
model: { | ||
name: 'table', | ||
key: 'tableWidth' | ||
}, | ||
view: width => ( { | ||
name: 'figure', | ||
key: 'style', | ||
value: { | ||
width | ||
} | ||
} ) | ||
} ); | ||
conversion.for( 'upcast' ).add( upcastColgroupElement( editor ) ); | ||
conversion.for( 'downcast' ).add( downcastTableColumnWidthsAttribute() ); | ||
} | ||
/** | ||
* Registers table column resizer post-fixer. | ||
* Registers table column resize post-fixer. | ||
* | ||
* It checks if the change from the differ concerns a table-related element or an attribute. If yes, then it is responsible for the | ||
* following: | ||
* (1) Depending on whether the `enableResize` event is not prevented... | ||
* (1.1) ...removing the `columnWidths` attribute from the table and all the cells from column index map, or | ||
* (1.2) ...adding the `columnWidths` attribute to the table. | ||
* (2) Adjusting the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%. | ||
* (2.1) Add all cells to column index map with its column index (to properly handle column insertion and deletion). | ||
* (3) Checking if columns have been added or removed... | ||
* (3.1) ... in the middle of the table, or | ||
* (3.2) ... at the table end. | ||
* (4) Checking if the inline cell width has been configured and transferring its value to the appropriate column, but currently only | ||
* for a cell that is not spanned horizontally. | ||
* It checks if the change from the differ concerns a table-related element or attribute. For detected changes it: | ||
* * Adjusts the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%. | ||
* * Checks if the `columnWidths` attribute gets updated accordingly after columns have been added or removed. | ||
* | ||
* @private | ||
*/ | ||
_setupPostFixer() { | ||
_registerPostFixer() { | ||
const editor = this.editor; | ||
const columnIndexMap = this._columnIndexMap; | ||
const cellsModified = this._cellsModified; | ||
const model = editor.model; | ||
editor.model.document.registerPostFixer( writer => { | ||
const changes = editor.model.document.differ.getChanges(); | ||
model.document.registerPostFixer( writer => { | ||
let changed = false; | ||
for ( const table of getAffectedTables( changes, editor.model ) ) { | ||
// (1.1) Remove the `columnWidths` attribute from the table and all the cells from column index map if the | ||
// manual width is not allowed for a given cell. There is no need to process the given table anymore. | ||
if ( this.fire( 'disableResize', table ) ) { | ||
if ( table.hasAttribute( 'columnWidths' ) ) { | ||
writer.removeAttribute( 'columnWidths', table ); | ||
for ( const table of getChangedResizedTables( model ) ) { | ||
// (1) Adjust the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%. | ||
const columnWidths = normalizeColumnWidths( table.getAttribute( 'columnWidths' ).split( ',' ) ); | ||
for ( const { cell } of new TableWalker( table ) ) { | ||
columnIndexMap.delete( cell ); | ||
cellsModified.set( cell, 'remove' ); | ||
} | ||
// (2) If the number of columns has changed, then we need to adjust the widths of the affected columns. | ||
adjustColumnWidths( columnWidths, table, this ); | ||
changed = true; | ||
} | ||
const columnWidthsAttribute = columnWidths.map( width => `${ width }%` ).join( ',' ); | ||
if ( table.getAttribute( 'columnWidths' ) === columnWidthsAttribute ) { | ||
continue; | ||
} | ||
// (1.2) Add the `columnWidths` attribute to the table with the 'auto' special value for each column, what means that it is | ||
// calculated proportionally to the whole table width. | ||
const numberOfColumns = getNumberOfColumn( table, editor ); | ||
writer.setAttribute( 'columnWidths', columnWidthsAttribute, table ); | ||
if ( !table.hasAttribute( 'columnWidths' ) ) { | ||
const columnWidthsAttribute = fillArray( numberOfColumns, 'auto' ).join( ',' ); | ||
changed = true; | ||
} | ||
writer.setAttribute( 'columnWidths', columnWidthsAttribute, table ); | ||
return changed; | ||
} ); | ||
changed = true; | ||
} | ||
// Adjusts if necessary the `columnWidths` in case if the number of column has changed. | ||
// | ||
// @private | ||
// @param {Array.<Number>} columnWidths Note: this array **may be modified** by the function. | ||
// @param {module:engine/model/element~Element} table Table to be checked. | ||
// @param {module:table/tablecolumnresize/tablecolumnresizeediting~TableColumnResizeEditing} plugin | ||
function adjustColumnWidths( columnWidths, table, plugin ) { | ||
const newTableColumnsCount = plugin._tableUtilsPlugin.getColumns( table ); | ||
const columnsCountDelta = newTableColumnsCount - columnWidths.length; | ||
// (2) Adjust the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%. | ||
const columnWidths = normalizeColumnWidthsAttribute( table.getAttribute( 'columnWidths' ) ); | ||
if ( columnsCountDelta === 0 ) { | ||
return; | ||
} | ||
let removedColumnWidths = null; | ||
let isColumnInsertionHandled = false; | ||
let isColumnDeletionHandled = false; | ||
// Collect all cells that are affected by the change. | ||
const cellSet = getAffectedCells( plugin.editor.model.document.differ, table ); | ||
for ( const { cell, cellWidth: cellColumnWidth, column } of new TableWalker( table ) ) { | ||
// (2.1) Add all cells to column index map with its column index. Do not process the given cell anymore, because the | ||
// `columnIndex` reference in the map is required to properly handle column insertion and deletion. | ||
if ( !columnIndexMap.has( cell ) ) { | ||
columnIndexMap.set( cell, column ); | ||
cellsModified.set( cell, 'insert' ); | ||
for ( const cell of cellSet ) { | ||
const currentColumnsDelta = newTableColumnsCount - columnWidths.length; | ||
changed = true; | ||
if ( currentColumnsDelta === 0 ) { | ||
continue; | ||
} | ||
continue; | ||
} | ||
// If the column count in the table changed, adjust the widths of the affected columns. | ||
const hasMoreColumns = currentColumnsDelta > 0; | ||
const currentColumnIndex = plugin._tableUtilsPlugin.getCellLocation( cell ).column; | ||
const previousColumn = columnIndexMap.get( cell ); | ||
if ( hasMoreColumns ) { | ||
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, plugin.editor ); | ||
const columnWidthsToInsert = createFilledArray( currentColumnsDelta, columnMinWidthAsPercentage ); | ||
const isColumnInsertion = previousColumn < column; | ||
const isColumnDeletion = previousColumn > column; | ||
columnWidths.splice( currentColumnIndex, 0, ...columnWidthsToInsert ); | ||
} else { | ||
// Moves the widths of the removed columns to the preceding one. | ||
// Other editors either reduce the width of the whole table or adjust the widths | ||
// proportionally, so change of this behavior can be considered in the future. | ||
const removedColumnWidths = columnWidths.splice( currentColumnIndex, Math.abs( currentColumnsDelta ) ); | ||
// (3.1) Handle column insertion and update the `columnIndex` references in column index map for affected cells. | ||
if ( isColumnInsertion ) { | ||
if ( !isColumnInsertionHandled ) { | ||
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor ); | ||
const isColumnSwapped = columnIndexMap.get( cell.previousSibling ) === column; | ||
const columnWidthsToInsert = isColumnSwapped ? | ||
removedColumnWidths : | ||
fillArray( column - previousColumn, columnMinWidthAsPercentage ); | ||
columnWidths[ currentColumnIndex ] += sumArray( removedColumnWidths ); | ||
} | ||
} | ||
} | ||
columnWidths.splice( previousColumn, 0, ...columnWidthsToInsert ); | ||
// Returns a set of cells that have been changed in a given table. | ||
// | ||
// @private | ||
// @param {module:engine/model/differ~Differ} differ | ||
// @param {module:engine/model/element~Element} table | ||
// @returns {Set.<module:engine/model/element~Element>} | ||
function getAffectedCells( differ, table ) { | ||
const cellSet = new Set(); | ||
isColumnInsertionHandled = true; | ||
} | ||
for ( const change of differ.getChanges() ) { | ||
if ( | ||
change.type == 'insert' && | ||
change.position.nodeAfter && | ||
change.position.nodeAfter.name == 'tableCell' && | ||
change.position.nodeAfter.getAncestors().includes( table ) | ||
) { | ||
cellSet.add( change.position.nodeAfter ); | ||
} else if ( change.type == 'remove' ) { | ||
// If the first cell was removed, use the node after the change position instead. | ||
const referenceNode = change.position.nodeBefore || change.position.nodeAfter; | ||
columnIndexMap.set( cell, column ); | ||
cellsModified.set( cell, 'insert' ); | ||
changed = true; | ||
if ( referenceNode.name == 'tableCell' && referenceNode.getAncestors().includes( table ) ) { | ||
cellSet.add( referenceNode ); | ||
} | ||
// (3.1) Handle column deletion and update the `columnIndex` references in column index map for affected cells. | ||
if ( isColumnDeletion ) { | ||
if ( !isColumnDeletionHandled ) { | ||
removedColumnWidths = columnWidths.splice( column, previousColumn - column ); | ||
const isColumnSwapped = cell.nextSibling && columnIndexMap.get( cell.nextSibling ) === column; | ||
if ( !isColumnSwapped ) { | ||
const columnToExpand = column > 0 ? column - 1 : column; | ||
columnWidths[ columnToExpand ] += sumArray( removedColumnWidths ); | ||
} | ||
isColumnDeletionHandled = true; | ||
} | ||
columnIndexMap.set( cell, column ); | ||
cellsModified.set( cell, 'insert' ); | ||
changed = true; | ||
} | ||
// (4) Check if the inline cell width has been configured and transfer its value to the appropriate column. | ||
if ( cell.hasAttribute( 'width' ) ) { | ||
// Currently, only the inline width from the cells that are not horizontally spanned are supported. | ||
if ( cellColumnWidth !== 1 ) { | ||
continue; | ||
} | ||
// It may happen that the table is not yet fully rendered in the editing view (i.e. it does not contain the | ||
// `<colgroup>` yet), but the cell has an inline width set. In that case it is not possible to properly convert the | ||
// inline cell width as a percentage value to the whole table width. Currently, we just ignore this case and | ||
// initialize the table with all the default (equal) column widths. | ||
if ( !isTableRendered( table, editor ) ) { | ||
writer.removeAttribute( 'width', cell ); | ||
changed = true; | ||
continue; | ||
} | ||
const tableWidthInPixels = getTableWidthInPixels( table, editor ); | ||
const columnWidthsInPixels = getColumnWidthsInPixels( table, editor ); | ||
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor ); | ||
const cellWidth = parseFloat( cell.getAttribute( 'width' ) ); | ||
const isWidthInPixels = cell.getAttribute( 'width' ).endsWith( 'px' ); | ||
const isWidthAsPercentage = cell.getAttribute( 'width' ).endsWith( '%' ); | ||
// Currently, only inline width in pixels or as percentage is supported. | ||
if ( !isWidthInPixels && !isWidthAsPercentage ) { | ||
continue; | ||
} | ||
const isRightEdge = !cell.nextSibling; | ||
if ( isRightEdge ) { | ||
const rootWidthInPixels = getElementWidthInPixels( editor.editing.view.getDomRoot() ); | ||
const lastColumnIndex = numberOfColumns - 1; | ||
const lastColumnWidthInPixels = columnWidthsInPixels[ lastColumnIndex ]; | ||
let tableWidthNew; | ||
if ( isWidthInPixels ) { | ||
const cellWidthLowerBound = COLUMN_MIN_WIDTH_IN_PIXELS; | ||
const cellWidthUpperBound = rootWidthInPixels - ( tableWidthInPixels - lastColumnWidthInPixels ); | ||
columnWidthsInPixels[ lastColumnIndex ] = clamp( cellWidth, cellWidthLowerBound, cellWidthUpperBound ); | ||
tableWidthNew = sumArray( columnWidthsInPixels ); | ||
// Update all the column widths. | ||
for ( let columnIndex = 0; columnIndex <= lastColumnIndex; columnIndex++ ) { | ||
columnWidths[ columnIndex ] = toPrecision( columnWidthsInPixels[ columnIndex ] * 100 / tableWidthNew ); | ||
} | ||
} else { | ||
const cellWidthLowerBound = columnMinWidthAsPercentage; | ||
const cellWidthUpperBound = 100 - ( tableWidthInPixels - lastColumnWidthInPixels ) * 100 / | ||
rootWidthInPixels; | ||
columnWidths[ lastColumnIndex ] = clamp( cellWidth, cellWidthLowerBound, cellWidthUpperBound ); | ||
tableWidthNew = ( tableWidthInPixels - lastColumnWidthInPixels ) * 100 / | ||
( 100 - columnWidths[ lastColumnIndex ] ); | ||
// Update all the column widths, except the last one, which has been already adjusted. | ||
for ( let columnIndex = 0; columnIndex <= lastColumnIndex - 1; columnIndex++ ) { | ||
columnWidths[ columnIndex ] = toPrecision( columnWidthsInPixels[ columnIndex ] * 100 / tableWidthNew ); | ||
} | ||
} | ||
writer.setAttribute( 'width', `${ toPrecision( tableWidthNew * 100 / rootWidthInPixels ) }%`, table ); | ||
} else { | ||
const currentColumnWidth = columnWidthsInPixels[ column ]; | ||
const nextColumnWidth = columnWidthsInPixels[ column + 1 ]; | ||
const bothColumnWidth = currentColumnWidth + nextColumnWidth; | ||
const cellMaxWidthAsPercentage = ( bothColumnWidth - COLUMN_MIN_WIDTH_IN_PIXELS ) * 100 / tableWidthInPixels; | ||
let cellWidthAsPercentage = isWidthInPixels ? | ||
cellWidth * 100 / tableWidthInPixels : | ||
cellWidth; | ||
cellWidthAsPercentage = clamp( cellWidthAsPercentage, columnMinWidthAsPercentage, cellMaxWidthAsPercentage ); | ||
const dxAsPercentage = cellWidthAsPercentage - columnWidths[ column ]; | ||
columnWidths[ column ] += dxAsPercentage; | ||
columnWidths[ column + 1 ] -= dxAsPercentage; | ||
} | ||
writer.removeAttribute( 'width', cell ); | ||
changed = true; | ||
} | ||
} | ||
} | ||
const isColumnInsertionAtEnd = numberOfColumns > columnWidths.length; | ||
const isColumnDeletionAtEnd = numberOfColumns < columnWidths.length; | ||
return cellSet; | ||
} | ||
} | ||
// (3.2) Handle column insertion at table end. | ||
if ( isColumnInsertionAtEnd ) { | ||
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor ); | ||
const numberOfInsertedColumns = numberOfColumns - columnWidths.length; | ||
const insertedColumnWidths = fillArray( numberOfInsertedColumns, columnMinWidthAsPercentage ); | ||
columnWidths.splice( columnWidths.length, 0, ...insertedColumnWidths ); | ||
/** | ||
* Registers table column resize converters. | ||
* | ||
* @private | ||
*/ | ||
_registerConverters() { | ||
const editor = this.editor; | ||
const conversion = editor.conversion; | ||
const widthStyleToTableWidthDefinition = { | ||
view: { | ||
name: 'figure', | ||
key: 'style', | ||
value: { | ||
width: /[\s\S]+/ | ||
} | ||
// (3.2) Handle column deletion at table end. | ||
if ( isColumnDeletionAtEnd ) { | ||
const removedColumnWidths = columnWidths.splice( numberOfColumns ); | ||
columnWidths[ numberOfColumns - 1 ] += sumArray( removedColumnWidths ); | ||
}, | ||
model: { | ||
name: 'table', | ||
key: 'tableWidth', | ||
value: viewElement => viewElement.getStyle( 'width' ) | ||
} | ||
}; | ||
const tableWidthToWidthStyleDefinition = { | ||
model: { | ||
name: 'table', | ||
key: 'tableWidth' | ||
}, | ||
view: width => ( { | ||
name: 'figure', | ||
key: 'style', | ||
value: { | ||
width | ||
} | ||
} ) | ||
}; | ||
const columnWidthsAttribute = columnWidths.map( width => `${ width }%` ).join( ',' ); | ||
conversion.for( 'upcast' ).attributeToAttribute( widthStyleToTableWidthDefinition ); | ||
conversion.for( 'upcast' ).add( upcastColgroupElement( this._tableUtilsPlugin ) ); | ||
if ( table.getAttribute( 'columnWidths' ) === columnWidthsAttribute ) { | ||
continue; | ||
} | ||
writer.setAttribute( 'columnWidths', columnWidthsAttribute, table ); | ||
changed = true; | ||
} | ||
return changed; | ||
} ); | ||
conversion.for( 'downcast' ).attributeToAttribute( tableWidthToWidthStyleDefinition ); | ||
conversion.for( 'downcast' ).add( downcastTableColumnWidthsAttribute() ); | ||
} | ||
/** | ||
* Initializes column resizing feature by registering mouse event handlers for `mousedown`, `mouseup` and `mousemove` events. | ||
* Registers listeners to handle resizing process. | ||
* | ||
* @private | ||
*/ | ||
_setupColumnResizers() { | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
_registerResizingListeners() { | ||
const editingView = this.editor.editing.view; | ||
@@ -468,20 +349,21 @@ editingView.addObserver( MouseEventsObserver ); | ||
const domEmitter = Object.create( DomEmitterMixin ); | ||
domEmitter.listenTo( global.window.document, 'mouseup', this._onMouseUpHandler.bind( this ) ); | ||
domEmitter.listenTo( global.window.document, 'mousemove', throttle( this._onMouseMoveHandler.bind( this ), 50 ) ); | ||
this._domEmitter.listenTo( global.window.document, 'mousemove', throttle( this._onMouseMoveHandler.bind( this ), 50 ) ); | ||
this._domEmitter.listenTo( global.window.document, 'mouseup', this._onMouseUpHandler.bind( this ) ); | ||
} | ||
/** | ||
* Handles the `mousedown` event on column resizer element. | ||
* Handles the `mousedown` event on column resizer element: | ||
* * calculates the initial column pixel widths, | ||
* * inserts the `<colgroup>` element if it is not present in the `<table>`, | ||
* * puts the necessary data in the temporary storage, | ||
* * applies the attributes to the `<table>` view element. | ||
* | ||
* @private | ||
* @param {module:utils/eventinfo~EventInfo} eventInfo | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData | ||
* @param {module:utils/eventinfo~EventInfo} eventInfo An object containing information about the fired event. | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData The data related to the DOM event. | ||
*/ | ||
_onMouseDownHandler( eventInfo, domEventData ) { | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
const target = domEventData.target; | ||
if ( !domEventData.target.hasClass( 'table-column-resizer' ) ) { | ||
if ( !target.hasClass( 'ck-table-column-resizer' ) ) { | ||
return; | ||
@@ -497,97 +379,96 @@ } | ||
this._isResizingActive = true; | ||
this._resizingData = this._getResizingData( domEventData ); | ||
const editor = this.editor; | ||
const modelTable = editor.editing.mapper.toModelElement( target.findAncestor( 'figure' ) ); | ||
editingView.change( writer => { | ||
writer.addClass( 'table-column-resizer__active', this._resizingData.elements.viewResizer ); | ||
} ); | ||
} | ||
/** | ||
* Handles the `mouseup` event if previously the `mousedown` event was triggered from the column resizer element. | ||
* | ||
* @private | ||
* @param {module:utils/eventinfo~EventInfo} eventInfo | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData | ||
*/ | ||
_onMouseUpHandler() { | ||
const editor = this.editor; | ||
// The column widths are calculated upon mousedown to allow lazy applying the `columnWidths` attribute on the table. | ||
const columnWidthsInPx = _calculateDomColumnWidths( modelTable, this._tableUtilsPlugin, editor ); | ||
const viewTable = target.findAncestor( 'table' ); | ||
const editingView = editor.editing.view; | ||
if ( !this._isResizingActive ) { | ||
return; | ||
// Insert colgroup for the table that is resized for the first time. | ||
if ( ![ ...viewTable.getChildren() ].find( viewCol => viewCol.is( 'element', 'colgroup' ) ) ) { | ||
editingView.change( viewWriter => { | ||
_insertColgroupElement( viewWriter, columnWidthsInPx, viewTable ); | ||
} ); | ||
} | ||
const { | ||
modelTable, | ||
viewColgroup, | ||
viewFigure, | ||
viewResizer | ||
} = this._resizingData.elements; | ||
this._isResizingActive = true; | ||
this._resizingData = this._getResizingData( domEventData, columnWidthsInPx ); | ||
const columnWidthsAttributeOld = modelTable.getAttribute( 'columnWidths' ); | ||
const columnWidthsAttributeNew = [ ...viewColgroup.getChildren() ] | ||
.map( viewCol => viewCol.getStyle( 'width' ) ) | ||
.join( ',' ); | ||
// At this point we change only the editor view - we don't want other users to see our changes yet, | ||
// so we can't apply them in the model. | ||
editingView.change( writer => _applyResizingAttributesToTable( writer, viewTable, this._resizingData ) ); | ||
const isColumnWidthsAttributeChanged = columnWidthsAttributeOld !== columnWidthsAttributeNew; | ||
// Calculates the DOM columns' widths. It is done by taking the width of the widest cell | ||
// from each table column (we rely on the {@link module:table/tablewalker~TableWalker} | ||
// to determine which column the cell belongs to). | ||
// | ||
// @private | ||
// @param {module:engine/model/element~Element} modelTable A table which columns should be measured. | ||
// @param {module:table/tableutils~TableUtils} tableUtils The Table Utils plugin instance. | ||
// @param {module:core/editor/editor~Editor} editor The editor instance. | ||
// @returns {Array.<Number>} Columns' widths expressed in pixels (without unit). | ||
function _calculateDomColumnWidths( modelTable, tableUtilsPlugin, editor ) { | ||
const columnWidthsInPx = Array( tableUtilsPlugin.getColumns( modelTable ) ); | ||
const tableWalker = new TableWalker( modelTable ); | ||
const tableWidthAttributeOld = modelTable.getAttribute( 'tableWidth' ); | ||
const tableWidthAttributeNew = viewFigure.getStyle( 'width' ); | ||
for ( const cellSlot of tableWalker ) { | ||
const viewCell = editor.editing.mapper.toViewElement( cellSlot.cell ); | ||
const domCell = editor.editing.view.domConverter.mapViewToDom( viewCell ); | ||
const domCellWidth = getDomCellOuterWidth( domCell ); | ||
const isTableWidthAttributeChanged = tableWidthAttributeOld !== tableWidthAttributeNew; | ||
if ( !columnWidthsInPx[ cellSlot.column ] || domCellWidth < columnWidthsInPx[ cellSlot.column ] ) { | ||
columnWidthsInPx[ cellSlot.column ] = toPrecision( domCellWidth ); | ||
} | ||
} | ||
if ( isColumnWidthsAttributeChanged || isTableWidthAttributeChanged ) { | ||
if ( this._isResizingAllowed ) { | ||
// Commit all changes to the model. | ||
editor.model.change( writer => { | ||
if ( isColumnWidthsAttributeChanged ) { | ||
writer.setAttribute( 'columnWidths', columnWidthsAttributeNew, modelTable ); | ||
} | ||
return columnWidthsInPx; | ||
} | ||
if ( isTableWidthAttributeChanged ) { | ||
writer.setAttribute( 'tableWidth', `${ toPrecision( tableWidthAttributeNew ) }%`, modelTable ); | ||
} | ||
} ); | ||
} else { | ||
// In read-only mode revert all changes in the editing view. The model is not touched so it does not need to be restored. | ||
editingView.change( writer => { | ||
if ( isColumnWidthsAttributeChanged ) { | ||
const columnWidths = columnWidthsAttributeOld.split( ',' ); | ||
// Creates a `<colgroup>` element with `<col>`s and inserts it into a given view table. | ||
// | ||
// @private | ||
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter A writer instance. | ||
// @param {Array.<Number>} columnWidthsInPx Column widths. | ||
// @param {module:engine/view/element~Element} viewTable A table view element. | ||
function _insertColgroupElement( viewWriter, columnWidthsInPx, viewTable ) { | ||
const colgroup = viewWriter.createContainerElement( 'colgroup' ); | ||
for ( const viewCol of viewColgroup.getChildren() ) { | ||
writer.setStyle( 'width', columnWidths.shift(), viewCol ); | ||
} | ||
} | ||
for ( let i = 0; i < columnWidthsInPx.length; i++ ) { | ||
const viewColElement = viewWriter.createEmptyElement( 'col' ); | ||
const columnWidthInPc = `${ toPrecision( columnWidthsInPx[ i ] / sumArray( columnWidthsInPx ) * 100 ) }%`; | ||
if ( isTableWidthAttributeChanged ) { | ||
if ( tableWidthAttributeOld ) { | ||
writer.setStyle( 'width', tableWidthAttributeOld, viewFigure ); | ||
} else { | ||
writer.removeStyle( 'width', viewFigure ); | ||
} | ||
} | ||
} ); | ||
viewWriter.setStyle( 'width', columnWidthInPc, viewColElement ); | ||
viewWriter.insert( viewWriter.createPositionAt( colgroup, 'end' ), viewColElement ); | ||
} | ||
viewWriter.insert( viewWriter.createPositionAt( viewTable, 'start' ), colgroup ); | ||
} | ||
editingView.change( writer => { | ||
writer.removeClass( 'table-column-resizer__active', viewResizer ); | ||
} ); | ||
// Applies the style and classes to the view table as the resizing begun. | ||
// | ||
// @private | ||
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter A writer instance. | ||
// @param {module:engine/view/element~Element} viewTable A table containing the clicked resizer. | ||
// @param {Object} resizingData Data related to the resizing. | ||
function _applyResizingAttributesToTable( viewWriter, viewTable, resizingData ) { | ||
const figureInitialPcWidth = resizingData.widths.viewFigureWidth / resizingData.widths.viewFigureParentWidth; | ||
this._isResizingActive = false; | ||
this._resizingData = null; | ||
viewWriter.addClass( 'ck-table-resized', viewTable ); | ||
viewWriter.addClass( 'ck-table-column-resizer__active', resizingData.elements.viewResizer ); | ||
viewWriter.setStyle( 'width', `${ toPrecision( figureInitialPcWidth * 100 ) }%`, viewTable.findAncestor( 'figure' ) ); | ||
} | ||
} | ||
/** | ||
* Handles the `mousemove` event if previously the `mousedown` event was triggered from the column resizer element. | ||
* Handles the `mousemove` event. | ||
* * If resizing process is not in progress, it does nothing. | ||
* * If resizing is active but not allowed, it stops the resizing process instantly calling the `mousedown` event handler. | ||
* * Otherwise it dynamically updates the widths of the resized columns. | ||
* | ||
* @private | ||
* @param {module:utils/eventinfo~EventInfo} eventInfo | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData | ||
* @param {module:utils/eventinfo~EventInfo} eventInfo An object containing information about the fired event. | ||
* @param {Event} mouseEventData The native DOM event. | ||
*/ | ||
_onMouseMoveHandler( eventInfo, domEventData ) { | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
_onMouseMoveHandler( eventInfo, mouseEventData ) { | ||
if ( !this._isResizingActive ) { | ||
@@ -607,5 +488,10 @@ return; | ||
isRightEdge, | ||
isLtrContent, | ||
isTableCentered | ||
isTableCentered, | ||
isLtrContent | ||
}, | ||
elements: { | ||
viewFigure, | ||
viewLeftColumn, | ||
viewRightColumn | ||
}, | ||
widths: { | ||
@@ -616,7 +502,2 @@ viewFigureParentWidth, | ||
rightColumnWidth | ||
}, | ||
elements: { | ||
viewFigure, | ||
viewLeftColumn, | ||
viewRightColumn | ||
} | ||
@@ -637,3 +518,3 @@ } = this._resizingData; | ||
const dx = clamp( | ||
( domEventData.clientX - columnPosition ) * multiplier, | ||
( mouseEventData.clientX - columnPosition ) * multiplier, | ||
Math.min( dxLowerBound, 0 ), | ||
@@ -647,3 +528,3 @@ Math.max( dxUpperBound, 0 ) | ||
editingView.change( writer => { | ||
this.editor.editing.view.change( writer => { | ||
const leftColumnWidthAsPercentage = toPrecision( ( leftColumnWidth + dx ) * 100 / tableWidth ); | ||
@@ -666,11 +547,108 @@ | ||
/** | ||
* Retrieves and returns required data needed to correctly calculate the widths of the resized columns. | ||
* Handles the `mouseup` event. | ||
* * If resizing process is not in progress, it does nothing. | ||
* * If resizing is active but not allowed, it cancels the resizing process restoring the original widths. | ||
* * Otherwise it propagates the changes from view to the model by executing the adequate commands. | ||
* | ||
* @private | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData | ||
* @returns {Object} | ||
*/ | ||
_getResizingData( domEventData ) { | ||
_onMouseUpHandler() { | ||
if ( !this._isResizingActive ) { | ||
return; | ||
} | ||
const { | ||
viewResizer, | ||
modelTable, | ||
viewFigure, | ||
viewColgroup | ||
} = this._resizingData.elements; | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
const columnWidthsAttributeOld = modelTable.getAttribute( 'columnWidths' ); | ||
const columnWidthsAttributeNew = [ ...viewColgroup.getChildren() ] | ||
.map( viewCol => viewCol.getStyle( 'width' ) ) | ||
.join( ',' ); | ||
const isColumnWidthsAttributeChanged = columnWidthsAttributeOld !== columnWidthsAttributeNew; | ||
const tableWidthAttributeOld = modelTable.getAttribute( 'tableWidth' ); | ||
const tableWidthAttributeNew = viewFigure.getStyle( 'width' ); | ||
const isTableWidthAttributeChanged = tableWidthAttributeOld !== tableWidthAttributeNew; | ||
if ( isColumnWidthsAttributeChanged || isTableWidthAttributeChanged ) { | ||
if ( this._isResizingAllowed ) { | ||
// Commit all changes to the model. | ||
if ( isTableWidthAttributeChanged ) { | ||
editor.execute( | ||
'resizeTableWidth', | ||
{ | ||
table: modelTable, | ||
tableWidth: `${ toPrecision( tableWidthAttributeNew ) }%`, | ||
columnWidths: columnWidthsAttributeNew | ||
} | ||
); | ||
} else { | ||
editor.execute( 'resizeColumnWidths', { columnWidths: columnWidthsAttributeNew, table: modelTable } ); | ||
} | ||
} else { | ||
// In read-only mode revert all changes in the editing view. The model is not touched so it does not need to be restored. | ||
// This case can occur if the read-only mode kicks in during the resizing process. | ||
editingView.change( writer => { | ||
// If table had resized columns before, restore the previous column widths. | ||
// Otherwise clean up the view from the temporary column resizing markup. | ||
if ( columnWidthsAttributeOld ) { | ||
const columnWidths = columnWidthsAttributeOld.split( ',' ); | ||
for ( const viewCol of viewColgroup.getChildren() ) { | ||
writer.setStyle( 'width', columnWidths.shift(), viewCol ); | ||
} | ||
} else { | ||
writer.remove( viewColgroup ); | ||
} | ||
if ( isTableWidthAttributeChanged ) { | ||
// If the whole table was already resized before, restore the previous table width. | ||
// Otherwise clean up the view from the temporary table resizing markup. | ||
if ( tableWidthAttributeOld ) { | ||
writer.setStyle( 'width', tableWidthAttributeOld, viewFigure ); | ||
} else { | ||
writer.removeStyle( 'width', viewFigure ); | ||
} | ||
} | ||
// If a table and its columns weren't resized before, | ||
// prune the remaining common resizing markup. | ||
if ( !columnWidthsAttributeOld && !tableWidthAttributeOld ) { | ||
writer.removeClass( | ||
'ck-table-resized', | ||
[ ...viewFigure.getChildren() ].find( element => element.name === 'table' ) | ||
); | ||
} | ||
} ); | ||
} | ||
} | ||
editingView.change( writer => { | ||
writer.removeClass( 'ck-table-column-resizer__active', viewResizer ); | ||
} ); | ||
this._isResizingActive = false; | ||
this._resizingData = null; | ||
} | ||
/** | ||
* Retrieves and returns required data needed for the resizing process. | ||
* | ||
* @private | ||
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData The data of the `mousedown` event. | ||
* @param {Array.<Number>} columnWidths The current widths of the columns. | ||
* @returns {Object} The data needed for the resizing process. | ||
*/ | ||
_getResizingData( domEventData, columnWidths ) { | ||
const editor = this.editor; | ||
const columnPosition = domEventData.domEvent.clientX; | ||
@@ -683,4 +661,4 @@ | ||
const leftColumnIndex = getColumnIndex( modelLeftCell, this._columnIndexMap ).rightEdge; | ||
const lastColumnIndex = getNumberOfColumn( modelTable, editor ) - 1; | ||
const leftColumnIndex = getColumnEdgesIndexes( modelLeftCell, this._tableUtilsPlugin ).rightEdge; | ||
const lastColumnIndex = this._tableUtilsPlugin.getColumns( modelTable ) - 1; | ||
@@ -698,4 +676,4 @@ const isRightEdge = leftColumnIndex === lastColumnIndex; | ||
const viewFigureParentWidth = getElementWidthInPixels( editor.editing.view.domConverter.mapViewToDom( viewFigure.parent ) ); | ||
const viewFigureWidth = getElementWidthInPixels( editor.editing.view.domConverter.mapViewToDom( viewFigure ) ); | ||
const tableWidth = getTableWidthInPixels( modelTable, editor ); | ||
const columnWidths = getColumnWidthsInPixels( modelTable, editor ); | ||
const leftColumnWidth = columnWidths[ leftColumnIndex ]; | ||
@@ -706,3 +684,9 @@ const rightColumnWidth = isRightEdge ? undefined : columnWidths[ leftColumnIndex + 1 ]; | ||
columnPosition, | ||
flags: { | ||
isRightEdge, | ||
isTableCentered, | ||
isLtrContent | ||
}, | ||
elements: { | ||
viewResizer, | ||
modelTable, | ||
@@ -712,15 +696,10 @@ viewFigure, | ||
viewLeftColumn, | ||
viewRightColumn, | ||
viewResizer | ||
viewRightColumn | ||
}, | ||
widths: { | ||
viewFigureParentWidth, | ||
viewFigureWidth, | ||
tableWidth, | ||
leftColumnWidth, | ||
rightColumnWidth | ||
}, | ||
flags: { | ||
isRightEdge, | ||
isTableCentered, | ||
isLtrContent | ||
} | ||
@@ -731,3 +710,3 @@ }; | ||
/** | ||
* Inserts colgroup if it is missing from table (e.g. after table insertion into table). | ||
* Inserts the `<colgroup>` element if it is missing in the view table (e.g. after table insertion into table). | ||
* | ||
@@ -740,12 +719,12 @@ * @private | ||
this.listenTo( editor.editing.view.document, 'layoutChanged', () => { | ||
const table = editor.model.document.selection.getFirstPosition().findAncestor( 'table' ); | ||
const tableView = editor.editing.view.document.selection.getFirstPosition().getAncestors().reverse().find( | ||
element => element.name === 'table' | ||
const viewTable = editor.editing.view.document.selection.getFirstPosition().getAncestors().reverse().find( | ||
viewElement => viewElement.name === 'table' | ||
); | ||
const tableViewContainsColgroup = tableView && [ ...tableView.getChildren() ].find( | ||
const viewTableContainsColgroup = viewTable && [ ...viewTable.getChildren() ].find( | ||
viewElement => viewElement.is( 'element', 'colgroup' ) | ||
); | ||
const modelTable = editor.model.document.selection.getFirstPosition().findAncestor( 'table' ); | ||
if ( table && table.hasAttribute( 'columnWidths' ) && tableView && !tableViewContainsColgroup ) { | ||
editor.editing.reconvertItem( table ); | ||
if ( modelTable && modelTable.hasAttribute( 'columnWidths' ) && viewTable && !viewTableContainsColgroup ) { | ||
editor.editing.reconvertItem( modelTable ); | ||
} | ||
@@ -756,3 +735,3 @@ }, { priority: 'low' } ); | ||
/** | ||
* Registers a handler on 'render' to properly insert/remove resizers after all postfixers finished their job. | ||
* Registers a listener ensuring that each resizable cell have a resizer handle. | ||
* | ||
@@ -762,21 +741,16 @@ * @private | ||
_registerResizerInserter() { | ||
const editor = this.editor; | ||
const view = editor.editing.view; | ||
const cellsModified = this._cellsModified; | ||
const view = this.editor.editing.view; | ||
view.on( 'render', () => { | ||
for ( const [ cell, operation ] of cellsModified.entries() ) { | ||
const viewCell = editor.editing.mapper.toViewElement( cell ); | ||
for ( const item of view.createRangeIn( view.document.getRoot() ) ) { | ||
if ( ![ 'td', 'th' ].includes( item.item.name ) ) { | ||
continue; | ||
} | ||
view.change( viewWriter => { | ||
if ( operation === 'insert' ) { | ||
insertColumnResizerElements( viewWriter, viewCell ); | ||
} else if ( operation === 'remove' ) { | ||
removeColumnResizerElements( viewWriter, viewCell ); | ||
} | ||
ensureColumnResizerElement( viewWriter, item.item ); | ||
} ); | ||
} | ||
cellsModified.clear(); | ||
}, { priority: 'lowest' } ); | ||
} | ||
} |
@@ -10,4 +10,2 @@ /** | ||
/* istanbul ignore file */ | ||
import { global } from 'ckeditor5/src/utils'; | ||
@@ -21,12 +19,14 @@ import { | ||
/** | ||
* Collects all affected by the differ table model elements. The returned set may be empty. | ||
* Returns all the inserted or changed table model elements in a given change set. Only the tables | ||
* with 'columnsWidth' attribute are taken into account. The returned set may be empty. | ||
* | ||
* @param {Array.<module:engine/model/differ~DiffItem>} changes | ||
* @param {module:engine/model/model~Model} model | ||
* @returns {Set.<module:engine/model/element~Element>} | ||
* Most notably if an entire table is removed it will not be included in returned set. | ||
* | ||
* @param {module:engine/model/model~Model} model The model to collect the affected elements from. | ||
* @returns {Set.<module:engine/model/element~Element>} A set of table model elements. | ||
*/ | ||
export function getAffectedTables( changes, model ) { | ||
const tablesToProcess = new Set(); | ||
export function getChangedResizedTables( model ) { | ||
const affectedTables = new Set(); | ||
for ( const change of changes ) { | ||
for ( const change of model.document.differ.getChanges() ) { | ||
let referencePosition = null; | ||
@@ -39,3 +39,2 @@ | ||
case 'insert': | ||
case 'remove': | ||
referencePosition = [ 'table', 'tableRow', 'tableCell' ].includes( change.name ) ? | ||
@@ -47,2 +46,10 @@ change.position : | ||
case 'remove': | ||
// If the whole table is removed, there's no need to update its column widths (#12201). | ||
referencePosition = [ 'tableRow', 'tableCell' ].includes( change.name ) ? | ||
change.position : | ||
null; | ||
break; | ||
case 'attribute': | ||
@@ -58,24 +65,13 @@ if ( change.range.start.nodeAfter ) { | ||
const affectedTables = []; | ||
if ( referencePosition ) { | ||
const tableNode = ( referencePosition.nodeAfter && referencePosition.nodeAfter.name === 'table' ) ? | ||
referencePosition.nodeAfter : referencePosition.findAncestor( 'table' ); | ||
if ( tableNode ) { | ||
const range = model.createRangeOn( tableNode ); | ||
for ( const node of range.getItems() ) { | ||
if ( node.is( 'element' ) && node.name === 'table' ) { | ||
affectedTables.push( node ); | ||
} | ||
} | ||
} | ||
if ( !referencePosition ) { | ||
continue; | ||
} | ||
const table = affectedTables; | ||
const tableNode = ( referencePosition.nodeAfter && referencePosition.nodeAfter.name === 'table' ) ? | ||
referencePosition.nodeAfter : referencePosition.findAncestor( 'table' ); | ||
if ( table ) { | ||
for ( const tableItem of table ) { | ||
tablesToProcess.add( tableItem ); | ||
// We iterate over the whole table looking for the nested tables that are also affected. | ||
for ( const node of model.createRangeOn( tableNode ).getItems() ) { | ||
if ( node.is( 'element' ) && node.name === 'table' && node.hasAttribute( 'columnWidths' ) ) { | ||
affectedTables.add( node ); | ||
} | ||
@@ -85,13 +81,14 @@ } | ||
return tablesToProcess; | ||
return affectedTables; | ||
} | ||
/** | ||
* Returns the computed width (in pixels) of the DOM element. | ||
* Calculates the percentage of the minimum column width given in pixels for a given table. | ||
* | ||
* @param {HTMLElement} domElement | ||
* @returns {Number} | ||
* @param {module:engine/model/element~Element} modelTable A table model element. | ||
* @param {module:core/editor/editor~Editor} editor The editor instance. | ||
* @returns {Number} The minimal column width in percentage. | ||
*/ | ||
export function getElementWidthInPixels( domElement ) { | ||
return parseFloat( global.window.getComputedStyle( domElement ).width ); | ||
export function getColumnMinWidthAsPercentage( modelTable, editor ) { | ||
return COLUMN_MIN_WIDTH_IN_PIXELS * 100 / getTableWidthInPixels( modelTable, editor ); | ||
} | ||
@@ -102,56 +99,63 @@ | ||
* | ||
* @param {module:engine/model/element~Element} table | ||
* @param {module:core/editor/editor~Editor} editor | ||
* @returns {Number} | ||
* @param {module:engine/model/element~Element} modelTable A table model element. | ||
* @param {module:core/editor/editor~Editor} editor The editor instance. | ||
* @returns {Number} The width of the table in pixels. | ||
*/ | ||
export function getTableWidthInPixels( table, editor ) { | ||
const viewTbody = getTbodyViewElement( table, editor ); | ||
const domTbody = editor.editing.view.domConverter.mapViewToDom( viewTbody ); | ||
export function getTableWidthInPixels( modelTable, editor ) { | ||
// It is possible for a table to not have a <tbody> element - see #11878. | ||
const referenceElement = getChildrenViewElement( modelTable, 'tbody', editor ) || getChildrenViewElement( modelTable, 'thead', editor ); | ||
const domReferenceElement = editor.editing.view.domConverter.mapViewToDom( referenceElement ); | ||
return getElementWidthInPixels( domTbody ); | ||
return getElementWidthInPixels( domReferenceElement ); | ||
} | ||
/** | ||
* Calculates the column widths in pixels basing on the `columnWidths` table attribute: | ||
* - If the value for a given column is provided in pixels then it is just converted to a number and returned. | ||
* - Otherwise, it is assumed that unit is percentage and the column width is calculated proportionally to the whole table width. | ||
* | ||
* @param {module:engine/model/element~Element} table | ||
* @param {module:core/editor/editor~Editor} editor | ||
* @returns {Array.<Number>} | ||
*/ | ||
export function getColumnWidthsInPixels( table, editor ) { | ||
const tableWidthInPixels = getTableWidthInPixels( table, editor ); | ||
// Returns the a view element with a given name that is nested directly in a `<table>` element | ||
// related to a given `modelTable`. | ||
// | ||
// @private | ||
// @param {module:engine/model/element~Element} table | ||
// @param {module:core/editor/editor~Editor} editor | ||
// @param {String} elementName Name of a view to be looked for, e.g. `'colgroup`', `'thead`'. | ||
// @returns {module:engine/view/element~Element|undefined} Matched view or `undefined` otherwise. | ||
function getChildrenViewElement( modelTable, elementName, editor ) { | ||
const viewFigure = editor.editing.mapper.toViewElement( modelTable ); | ||
const viewTable = [ ...viewFigure.getChildren() ].find( viewChild => viewChild.is( 'element', 'table' ) ); | ||
return table.getAttribute( 'columnWidths' ) | ||
.split( ',' ) | ||
.map( columnWidth => columnWidth.trim() ) | ||
.map( columnWidth => { | ||
return columnWidth.endsWith( 'px' ) ? | ||
parseFloat( columnWidth ) : | ||
parseFloat( columnWidth ) * tableWidthInPixels / 100; | ||
} ); | ||
return [ ...viewTable.getChildren() ].find( viewChild => viewChild.is( 'element', elementName ) ); | ||
} | ||
/** | ||
* Calculates the percentage of the minimum column width given in pixels for a given table. | ||
* Returns the computed width (in pixels) of the DOM element without padding and borders. | ||
* | ||
* @param {module:engine/model/element~Element} table | ||
* @param {module:core/editor/editor~Editor} editor | ||
* @returns {Number} | ||
* @param {HTMLElement} domElement A DOM element. | ||
* @returns {Number} The width of the DOM element in pixels. | ||
*/ | ||
export function getColumnMinWidthAsPercentage( table, editor ) { | ||
const tableWidthInPixels = getTableWidthInPixels( table, editor ); | ||
export function getElementWidthInPixels( domElement ) { | ||
const styles = global.window.getComputedStyle( domElement ); | ||
return COLUMN_MIN_WIDTH_IN_PIXELS * 100 / tableWidthInPixels; | ||
// In the 'border-box' box sizing algorithm, the element's width | ||
// already includes the padding and border width (#12335). | ||
if ( styles.boxSizing === 'border-box' ) { | ||
return parseFloat( styles.width ) - | ||
parseFloat( styles.paddingLeft ) - | ||
parseFloat( styles.paddingRight ) - | ||
parseFloat( styles.borderLeftWidth ) - | ||
parseFloat( styles.borderRightWidth ); | ||
} else { | ||
return parseFloat( styles.width ); | ||
} | ||
} | ||
/** | ||
* Returns the column indexes on the left and right edges of a cell. | ||
* Returns the column indexes on the left and right edges of a cell. They differ if the cell spans | ||
* across multiple columns. | ||
* | ||
* @param {module:engine/model/element~Element} cell | ||
* @returns {Object} | ||
* @param {module:engine/model/element~Element} cell A table cell model element. | ||
* @param {module:table/tableutils~TableUtils} tableUtils The Table Utils plugin instance. | ||
* @returns {Object} An object containing the indexes of the left and right edges of the cell. | ||
* @returns {Number} return.leftEdge The index of the left edge of the cell. | ||
* @returns {Number} return.rightEdge The index of the right edge of the cell. | ||
*/ | ||
export function getColumnIndex( cell, columnIndexMap ) { | ||
const cellColumnIndex = columnIndexMap.get( cell ); | ||
export function getColumnEdgesIndexes( cell, tableUtils ) { | ||
const cellColumnIndex = tableUtils.getCellLocation( cell ).column; | ||
const cellWidth = cell.getAttribute( 'colspan' ) || 1; | ||
@@ -166,54 +170,6 @@ | ||
/** | ||
* Returns the total number of columns in a table. | ||
* | ||
* @param {module:engine/model/element~Element} table | ||
* @param {module:core/editor/editor~Editor} editor | ||
* @returns {Number} | ||
*/ | ||
export function getNumberOfColumn( table, editor ) { | ||
return editor.plugins.get( 'TableUtils' ).getColumns( table ); | ||
} | ||
/** | ||
* Checks if the table is already fully rendered, with the `<colgroup>` element that defines the widths for each column. | ||
* | ||
* @param {module:engine/model/element~Element} table | ||
* @param {module:core/editor/editor~Editor} editor | ||
* @returns {Number} | ||
*/ | ||
export function isTableRendered( table, editor ) { | ||
return !!getColgroupViewElement( table, editor ); | ||
} | ||
// Returns the `<colgroup>` view element, if it exists in a table. Returns `undefined` otherwise. | ||
// | ||
// @private | ||
// @param {module:engine/model/element~Element} table | ||
// @param {module:core/editor/editor~Editor} editor | ||
// @returns {module:engine/view/element~Element|undefined} | ||
function getColgroupViewElement( table, editor ) { | ||
const viewFigure = editor.editing.mapper.toViewElement( table ); | ||
const viewTable = [ ...viewFigure.getChildren() ].find( viewChild => viewChild.is( 'element', 'table' ) ); | ||
return [ ...viewTable.getChildren() ].find( viewChild => viewChild.is( 'element', 'colgroup' ) ); | ||
} | ||
// Returns the `<tbody>` view element, if it exists in a table. Returns `undefined` otherwise. | ||
// | ||
// @private | ||
// @param {module:engine/model/element~Element} table | ||
// @param {module:core/editor/editor~Editor} editor | ||
// @returns {module:engine/view/element~Element|undefined} | ||
function getTbodyViewElement( table, editor ) { | ||
const viewFigure = editor.editing.mapper.toViewElement( table ); | ||
const viewTable = [ ...viewFigure.getChildren() ].find( viewChild => viewChild.is( 'element', 'table' ) ); | ||
return [ ...viewTable.getChildren() ].find( viewChild => viewChild.is( 'element', 'tbody' ) ); | ||
} | ||
/** | ||
* Rounds the provided value to a fixed-point number with defined number of digits after the decimal point. | ||
* | ||
* @param {Number|String} value | ||
* @returns {Number} | ||
* @param {Number|String} value A number to be rounded. | ||
* @returns {Number} The rounded number. | ||
*/ | ||
@@ -231,6 +187,6 @@ export function toPrecision( value ) { | ||
* | ||
* @param {Number} number | ||
* @param {Number} min | ||
* @param {Number} max | ||
* @returns {Number} | ||
* @param {Number} number A number to be clamped. | ||
* @param {Number} min A lower bound. | ||
* @param {Number} max An upper bound. | ||
* @returns {Number} The clamped number. | ||
*/ | ||
@@ -252,7 +208,7 @@ export function clamp( number, min, max ) { | ||
* | ||
* @param {Number} length | ||
* @param {*} value | ||
* @returns {Array.<*>} | ||
* @param {Number} length The length of the array. | ||
* @param {*} value The value to fill the array with. | ||
* @returns {Array.<*>} An array with defined length and filled with defined value. | ||
*/ | ||
export function fillArray( length, value ) { | ||
export function createFilledArray( length, value ) { | ||
return Array( length ).fill( value ); | ||
@@ -264,4 +220,4 @@ } | ||
* | ||
* @param {Array.<Number>} array | ||
* @returns {Number} | ||
* @param {Array.<Number>} array An array of numbers. | ||
* @returns {Number} The sum of all array values. | ||
*/ | ||
@@ -277,11 +233,12 @@ export function sumArray( array ) { | ||
* Makes sure that the sum of the widths from all columns is 100%. If the sum of all the widths is not equal 100%, all the widths are | ||
* changed proportionally so that they all sum back to 100%. | ||
* changed proportionally so that they all sum back to 100%. If there are columns without specified width, the amount remaining | ||
* after assigning the known widths will be distributed equally between them. | ||
* | ||
* Currently, only widths provided as percentage values are supported. | ||
* | ||
* @param {String} columnWidthsAttribute | ||
* @returns {Array.<Number>} | ||
* @param {Array.<Number>} columnWidths An array of column widths. | ||
* @returns {Array.<Number>} An array of column widths guaranteed to sum up to 100%. | ||
*/ | ||
export function normalizeColumnWidthsAttribute( columnWidthsAttribute ) { | ||
const columnWidths = prepareColumnWidths( columnWidthsAttribute ); | ||
export function normalizeColumnWidths( columnWidths ) { | ||
columnWidths = calculateMissingColumnWidths( columnWidths ); | ||
const totalWidth = sumArray( columnWidths ); | ||
@@ -316,12 +273,8 @@ | ||
// - Otherwise, just set the minimum allowed width for all uninitialized columns. The sum of all column widths will be greater than 100%, | ||
// but then it will be adjusted proportionally to 100% in {@link #normalizeColumnWidthsAttribute `normalizeColumnWidthsAttribute()`}. | ||
// but then it will be adjusted proportionally to 100% in {@link #normalizeColumnWidths `normalizeColumnWidths()`}. | ||
// | ||
// @private | ||
// @param {String} columnWidthsAttribute | ||
// @returns {Array.<Number>} | ||
function prepareColumnWidths( columnWidthsAttribute ) { | ||
const columnWidths = columnWidthsAttribute | ||
.split( ',' ) | ||
.map( columnWidth => columnWidth.trim() ); | ||
// @param {Array.<Number>} columnWidths An array of column widths. | ||
// @returns {Array.<Number>} An array with 'auto' values replaced with calculated widths. | ||
function calculateMissingColumnWidths( columnWidths ) { | ||
const numberOfUninitializedColumns = columnWidths.filter( columnWidth => columnWidth === 'auto' ).length; | ||
@@ -345,9 +298,11 @@ | ||
// Inserts column resizer element into a view cell. | ||
// | ||
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter View writer instance. | ||
// @param {module:engine/view/element~Element} viewCell View cell. | ||
export function insertColumnResizerElements( viewWriter, viewCell ) { | ||
/** | ||
* Inserts column resizer element into a view cell if it is missing. | ||
* | ||
* @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter View writer instance. | ||
* @param {module:engine/view/element~Element} viewCell View cell where resizer should be put. | ||
*/ | ||
export function ensureColumnResizerElement( viewWriter, viewCell ) { | ||
let viewTableColumnResizerElement = [ ...viewCell.getChildren() ] | ||
.find( viewElement => viewElement.hasClass( 'table-column-resizer' ) ); | ||
.find( viewElement => viewElement.hasClass( 'ck-table-column-resizer' ) ); | ||
@@ -359,3 +314,3 @@ if ( viewTableColumnResizerElement ) { | ||
viewTableColumnResizerElement = viewWriter.createUIElement( 'div', { | ||
class: 'table-column-resizer' | ||
class: 'ck-table-column-resizer' | ||
} ); | ||
@@ -369,15 +324,24 @@ | ||
// Removes column resizer element from a view cell. | ||
// | ||
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter View writer instance. | ||
// @param {module:engine/view/element~Element} viewCell View cell. | ||
export function removeColumnResizerElements( viewWriter, viewCell ) { | ||
const viewTableColumnResizerElement = [ ...viewCell.getChildren() ] | ||
.find( viewElement => viewElement.hasClass( 'table-column-resizer' ) ); | ||
/** | ||
* Calculates the total horizontal space taken by the cell. That includes: | ||
* * width, | ||
* * left and red padding, | ||
* * border width. | ||
* | ||
* @param {HTMLElement} domCell A DOM cell element. | ||
* @returns {Number} Width in pixels without `px` at the end. | ||
*/ | ||
export function getDomCellOuterWidth( domCell ) { | ||
const styles = global.window.getComputedStyle( domCell ); | ||
if ( !viewTableColumnResizerElement ) { | ||
return; | ||
// In the 'border-box' box sizing algorithm, the element's width | ||
// already includes the padding and border width (#12335). | ||
if ( styles.boxSizing === 'border-box' ) { | ||
return parseInt( styles.width ); | ||
} else { | ||
return parseFloat( styles.width ) + | ||
parseFloat( styles.paddingLeft ) + | ||
parseFloat( styles.paddingRight ) + | ||
parseFloat( styles.borderWidth ); | ||
} | ||
viewWriter.remove( viewTableColumnResizerElement ); | ||
} |
@@ -76,8 +76,2 @@ /** | ||
dropdownView.buttonView.on( 'open', () => { | ||
// Reset the chooser before showing it to the user. | ||
insertTableView.rows = 0; | ||
insertTableView.columns = 0; | ||
} ); | ||
dropdownView.on( 'execute', () => { | ||
@@ -84,0 +78,0 @@ editor.execute( 'insertTable', { rows: insertTableView.rows, columns: insertTableView.columns } ); |
@@ -10,4 +10,6 @@ /** | ||
import { View } from 'ckeditor5/src/ui'; | ||
import { View, addKeyboardHandlingForGrid } from 'ckeditor5/src/ui'; | ||
import { KeystrokeHandler, FocusTracker, uid } from 'ckeditor5/src/utils'; | ||
import './../../theme/inserttable.css'; | ||
@@ -33,2 +35,12 @@ | ||
/** | ||
* A unique id of a label element displaying the current geometry of the table. | ||
* Used by every {@link #items item} of the view as a pointer to an accessible label. | ||
* | ||
* @private | ||
* @readonly | ||
* @member {String} | ||
*/ | ||
this._geometryLabelId = `ck-editor__label_${ uid() }`; | ||
/** | ||
* A collection of table size box items. | ||
@@ -41,3 +53,13 @@ * | ||
this.keystrokes = new KeystrokeHandler(); | ||
/** | ||
* Tracks information about the DOM focus in the grid. | ||
* | ||
* @readonly | ||
* @member {module:utils/focustracker~FocusTracker} | ||
*/ | ||
this.focusTracker = new FocusTracker(); | ||
/** | ||
* The currently selected number of rows of the new table. | ||
@@ -87,3 +109,7 @@ * | ||
attributes: { | ||
class: [ 'ck-insert-table-dropdown__label' ] | ||
id: this._geometryLabelId, | ||
class: [ | ||
'ck', | ||
'ck-insert-table-dropdown__label' | ||
] | ||
}, | ||
@@ -105,2 +131,9 @@ children: [ | ||
this.fire( 'execute' ); | ||
} ), | ||
keydown: bind.to( evt => { | ||
if ( evt.key === 'Enter' ) { | ||
this.fire( 'execute' ); | ||
evt.preventDefault(); | ||
} | ||
} ) | ||
@@ -110,5 +143,19 @@ } | ||
// #rows and #columns are set via changes to #focusTracker on mouse over. | ||
this.on( 'boxover', ( evt, domEvt ) => { | ||
const { row, column } = domEvt.target.dataset; | ||
this.items.get( ( row - 1 ) * 10 + ( column - 1 ) ).focus(); | ||
} ); | ||
// This allows the #rows and #columns to be updated when: | ||
// * the user navigates the grid using the keyboard, | ||
// * the user moves the mouse over grid items. | ||
this.focusTracker.on( 'change:focusedElement', ( evt, name, focusedElement ) => { | ||
if ( !focusedElement ) { | ||
return; | ||
} | ||
const { row, column } = focusedElement.dataset; | ||
// As row & column indexes are zero-based transform it to number of selected rows & columns. | ||
@@ -121,9 +168,21 @@ this.set( { | ||
this.on( 'change:columns', () => { | ||
this._highlightGridBoxes(); | ||
this.on( 'change:columns', () => this._highlightGridBoxes() ); | ||
this.on( 'change:rows', () => this._highlightGridBoxes() ); | ||
} | ||
render() { | ||
super.render(); | ||
addKeyboardHandlingForGrid( { | ||
keystrokeHandler: this.keystrokes, | ||
focusTracker: this.focusTracker, | ||
gridItems: this.items, | ||
numberOfColumns: 10 | ||
} ); | ||
this.on( 'change:rows', () => { | ||
this._highlightGridBoxes(); | ||
} ); | ||
for ( const item of this.items ) { | ||
this.focusTracker.add( item.element ); | ||
} | ||
this.keystrokes.listenTo( this.element ); | ||
} | ||
@@ -135,4 +194,3 @@ | ||
focus() { | ||
// The dropdown panel expects DropdownPanelFocusable interface on views passed to dropdown panel. See #30. | ||
// The method should be implemented while working on keyboard support for this view. See #22. | ||
this.items.get( 0 ).focus(); | ||
} | ||
@@ -144,4 +202,3 @@ | ||
focusLast() { | ||
// The dropdown panel expects DropdownPanelFocusable interface on views passed to dropdown panel. See #30. | ||
// The method should be implemented while working on keyboard support for this view. See #22. | ||
this.items.get( 0 ).focus(); | ||
} | ||
@@ -182,3 +239,3 @@ | ||
boxes.push( new TableSizeGridBoxView( this.locale, row + 1, column + 1 ) ); | ||
boxes.push( new TableSizeGridBoxView( this.locale, row + 1, column + 1, this._geometryLabelId ) ); | ||
} | ||
@@ -207,3 +264,3 @@ | ||
*/ | ||
constructor( locale, row, column ) { | ||
constructor( locale, row, column, ariaLabelledById ) { | ||
super( locale ); | ||
@@ -225,2 +282,3 @@ | ||
class: [ | ||
'ck', | ||
'ck-insert-table-dropdown-grid-box', | ||
@@ -230,6 +288,15 @@ bind.if( 'isOn', 'ck-on' ) | ||
'data-row': row, | ||
'data-column': column | ||
'data-column': column, | ||
'tabindex': -1, | ||
'aria-labelledby': ariaLabelledById | ||
} | ||
} ); | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
focus() { | ||
this.element.focus(); | ||
} | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1388791
216
13861
Updatedckeditor5@^35.1.0