Skip to content

Commit

Permalink
Merge pull request #13937 from ckeditor/ck/13777-add-styles-to-table
Browse files Browse the repository at this point in the history
Feature (style): Add custom styling support for td, th, caption and figcaption elements. Closes #13777.
  • Loading branch information
arkflpc authored Apr 28, 2023
2 parents 257d10f + 0100608 commit 92b418d
Show file tree
Hide file tree
Showing 6 changed files with 559 additions and 32 deletions.
130 changes: 130 additions & 0 deletions packages/ckeditor5-style/src/integrations/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module style/integrations/tablestylesupport
*/

import { Plugin } from 'ckeditor5/src/core';
import type { Element } from 'ckeditor5/src/engine';
import type { TableUtils } from '@ckeditor/ckeditor5-table';

import type { DataFilter } from '@ckeditor/ckeditor5-html-support';

import StyleUtils, {
type BlockStyleDefinition,
type StyleUtilsGetAffectedBlocksEvent,
type StyleUtilsIsEnabledForBlockEvent,
type StyleUtilsConfigureGHSDataFilterEvent
} from '../styleutils';

export default class TableStyleSupport extends Plugin {
private _tableUtils!: TableUtils;
private _styleUtils!: StyleUtils;

/**
* @inheritDoc
*/
public static get pluginName(): 'TableStyleSupport' {
return 'TableStyleSupport';
}

/**
* @inheritDoc
*/
public static get requires() {
return [ StyleUtils ] as const;
}

/**
* @inheritDoc
*/
public init(): void {
const editor = this.editor;

if ( !editor.plugins.has( 'TableEditing' ) ) {
return;
}

this._styleUtils = editor.plugins.get( StyleUtils );
this._tableUtils = this.editor.plugins.get( 'TableUtils' );

this.listenTo<StyleUtilsIsEnabledForBlockEvent>( this._styleUtils, 'isStyleEnabledForBlock', ( evt, [ definition, block ] ) => {
if ( this._isApplicable( definition, block ) ) {
evt.return = this._isStyleEnabledForBlock( definition, block );
evt.stop();
}
}, { priority: 'high' } );

this.listenTo<StyleUtilsGetAffectedBlocksEvent>( this._styleUtils, 'getAffectedBlocks', ( evt, [ definition, block ] ) => {
if ( this._isApplicable( definition, block ) ) {
evt.return = this._getAffectedBlocks( definition, block );
evt.stop();
}
}, { priority: 'high' } );

this.listenTo<StyleUtilsConfigureGHSDataFilterEvent>(
this._styleUtils,
'configureGHSDataFilter',
( evt, [ { block } ] ) => {
const ghsDataFilter: DataFilter = this.editor.plugins.get( 'DataFilter' );
ghsDataFilter.loadAllowedConfig(
block
.filter( definition => definition.element == 'figcaption' )
.map( definition => {
return { name: 'caption', classes: definition.classes };
} )
);
}
);
}

/**
* Checks if this plugin's custom logic should be applied for defintion-block pair.
* @param definition Style definition that is being considered.
* @param block Block element to check if should be styled.
* @returns True if the defintion-block pair meet the plugin criteria, false otherwise.
*/
private _isApplicable( definition: BlockStyleDefinition, block: Element ): boolean {
return [ 'td', 'th' ].includes( definition.element ) && block.name == 'tableCell';
}

/**
* Checks if the style definition should be applied to selected block.
* @param definition Style definition that is being considered.
* @param block Block element to check if should be styled.
* @returns True if the block should be style with the style description, false otherwise.
*/
private _isStyleEnabledForBlock( definition: BlockStyleDefinition, block: Element ): boolean {
const location = this._tableUtils.getCellLocation( block )!;

const tableRow = block.parent!;
const table = tableRow.parent as Element;

const headingRows = table.getAttribute( 'headingRows' ) as number || 0;
const headingColumns = table.getAttribute( 'headingColumns' ) as number || 0;
const isHeadingCell = location.row < headingRows || location.column < headingColumns;

if ( definition.element == 'th' ) {
return isHeadingCell;
}
else {
return !isHeadingCell;
}
}

/**
* Gets all blocks that the style should be applied to.
* @param definition Style definition that is being considered.
* @param block A block element from selection.
* @returns An array with the block that was passed as an argument if meets the criteria, null otherwise.
*/
private _getAffectedBlocks( definition: BlockStyleDefinition, block: Element ): Array<Element> | null {
if ( !this._isStyleEnabledForBlock( definition, block ) ) {
return null;
}
return [ block ];
}
}
23 changes: 19 additions & 4 deletions packages/ckeditor5-style/src/stylecommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module style/stylecommand
*/

import type { Element } from 'ckeditor5/src/engine';
import type { DocumentSelection, Element } from 'ckeditor5/src/engine';
import { Command, type Editor } from 'ckeditor5/src/core';
import { logWarning, first } from 'ckeditor5/src/utils';
import type { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support';
Expand Down Expand Up @@ -93,7 +93,7 @@ export default class StyleCommand extends Command {
}

// Block styles.
const firstBlock = first( selection.getSelectedBlocks() );
const firstBlock = first( selection.getSelectedBlocks() ) || selection.getFirstPosition()!.parent;

if ( firstBlock ) {
const ancestorBlocks = firstBlock.getAncestors( { includeSelf: true, parentFirst: true } ) as Array<Element>;
Expand Down Expand Up @@ -189,7 +189,9 @@ export default class StyleCommand extends Command {
let selectables;

if ( isBlockStyleDefinition( definition ) ) {
selectables = this._findAffectedBlocks( selection.getSelectedBlocks(), definition );
selectables = this._findAffectedBlocks( getBlocksFromSelection( selection ),
definition
);
} else {
selectables = [ selection ];
}
Expand All @@ -212,7 +214,7 @@ export default class StyleCommand extends Command {
* Returns a set of elements that should be affected by the block-style change.
*/
private _findAffectedBlocks(
selectedBlocks: IterableIterator<Element>,
selectedBlocks: Iterable<Element>,
definition: BlockStyleDefinition
): Set<Element> {
const styleUtils: StyleUtils = this.editor.plugins.get( StyleUtils );
Expand Down Expand Up @@ -296,3 +298,16 @@ function getDefinitionExclusiveClasses(
function isBlockStyleDefinition( definition: NormalizedStyleDefinition ): definition is BlockStyleDefinition {
return 'isBlock' in definition;
}

/**
* Gets block elements from selection. If there are none, returns first selected element.
* @param selection Current document's selection.
* @returns Selected blocks if there are any, first selected element otherwise.
*/
function getBlocksFromSelection( selection: DocumentSelection ) {
const blocks = Array.from( selection.getSelectedBlocks() );
if ( blocks.length ) {
return blocks;
}
return [ selection.getFirstPosition()!.parent as Element ];
}
28 changes: 3 additions & 25 deletions packages/ckeditor5-style/src/styleediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import StyleCommand from './stylecommand';
import StyleUtils, { type NormalizedStyleDefinitions } from './styleutils';
import type { StyleConfig, StyleDefinition } from './styleconfig';
import DocumentListStyleSupport from './integrations/documentlist';
import TableStyleSupport from './integrations/table';

/**
* The style engine feature.
Expand All @@ -35,7 +36,7 @@ export default class StyleEditing extends Plugin {
* @inheritDoc
*/
public static get requires() {
return [ 'GeneralHtmlSupport', StyleUtils, DocumentListStyleSupport ] as const;
return [ 'GeneralHtmlSupport', StyleUtils, DocumentListStyleSupport, TableStyleSupport ] as const;
}

/**
Expand All @@ -50,29 +51,6 @@ export default class StyleEditing extends Plugin {

editor.commands.add( 'style', new StyleCommand( editor, normalizedStyleDefinitions ) );

this._configureGHSDataFilter( normalizedStyleDefinitions );
styleUtils.configureGHSDataFilter( normalizedStyleDefinitions );
}

/**
* This is where the styles feature configures the GHS feature. This method translates normalized
* {@link module:style/styleconfig~StyleDefinition style definitions} to
* {@link module:engine/view/matcher~MatcherPattern matcher patterns} and feeds them to the GHS
* {@link module:html-support/datafilter~DataFilter} plugin.
*/
private _configureGHSDataFilter( { block, inline }: NormalizedStyleDefinitions ): void {
const ghsDataFilter: DataFilter = this.editor.plugins.get( 'DataFilter' );

ghsDataFilter.loadAllowedConfig( block.map( normalizedStyleDefinitionToMatcherPattern ) );
ghsDataFilter.loadAllowedConfig( inline.map( normalizedStyleDefinitionToMatcherPattern ) );
}
}

/**
* Translates a normalized style definition to a view matcher pattern.
*/
function normalizedStyleDefinitionToMatcherPattern( { element, classes }: StyleDefinition ): MatcherPattern {
return {
name: element,
classes
};
}
31 changes: 29 additions & 2 deletions packages/ckeditor5-style/src/styleutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
*/

import { Plugin, type Editor } from 'ckeditor5/src/core';
import type { Element } from 'ckeditor5/src/engine';
import type { Element, MatcherPattern } from 'ckeditor5/src/engine';
import type { DecoratedMethodEvent } from 'ckeditor5/src/utils';
import type { TemplateDefinition } from 'ckeditor5/src/ui';

import type { DataSchema, GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support';
import type { DataFilter, DataSchema, GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support';

import type { StyleDefinition } from './styleconfig';
import { isObject } from 'lodash-es';
Expand Down Expand Up @@ -41,6 +41,7 @@ export default class StyleUtils extends Plugin {
this.decorate( 'isStyleActiveForBlock' );
this.decorate( 'getAffectedBlocks' );
this.decorate( 'getStylePreview' );
this.decorate( 'configureGHSDataFilter' );
}

/**
Expand Down Expand Up @@ -184,6 +185,21 @@ export default class StyleUtils extends Plugin {
hasClassesProperty( ghsAttributeValue ) &&
classes.every( className => ghsAttributeValue.classes.includes( className ) );
}

/**
* This is where the styles feature configures the GHS feature. This method translates normalized
* {@link module:style/styleconfig~StyleDefinition style definitions} to
* {@link module:engine/view/matcher~MatcherPattern matcher patterns} and feeds them to the GHS
* {@link module:html-support/datafilter~DataFilter} plugin.
*
* @internal
*/
public configureGHSDataFilter( { block, inline }: NormalizedStyleDefinitions ): void {
const ghsDataFilter: DataFilter = this.editor.plugins.get( 'DataFilter' );

ghsDataFilter.loadAllowedConfig( block.map( normalizedStyleDefinitionToMatcherPattern ) );
ghsDataFilter.loadAllowedConfig( inline.map( normalizedStyleDefinitionToMatcherPattern ) );
}
}

/**
Expand All @@ -206,6 +222,16 @@ function isPreviewable( elementName: string ): boolean {
return !NON_PREVIEWABLE_ELEMENT_NAMES.includes( elementName );
}

/**
* Translates a normalized style definition to a view matcher pattern.
*/
function normalizedStyleDefinitionToMatcherPattern( { element, classes }: StyleDefinition ): MatcherPattern {
return {
name: element,
classes
};
}

export interface NormalizedStyleDefinitions {
block: Array<BlockStyleDefinition>;
inline: Array<InlineStyleDefinition>;
Expand All @@ -228,3 +254,4 @@ export type StyleUtilsIsEnabledForBlockEvent = DecoratedMethodEvent<StyleUtils,
export type StyleUtilsIsActiveForBlockEvent = DecoratedMethodEvent<StyleUtils, 'isStyleActiveForBlock'>;
export type StyleUtilsGetAffectedBlocksEvent = DecoratedMethodEvent<StyleUtils, 'getAffectedBlocks'>;
export type StyleUtilsGetStylePreviewEvent = DecoratedMethodEvent<StyleUtils, 'getStylePreview'>;
export type StyleUtilsConfigureGHSDataFilterEvent = DecoratedMethodEvent<StyleUtils, 'configureGHSDataFilter'>;
Loading

0 comments on commit 92b418d

Please sign in to comment.