Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/20260428123311_ck_20000.md
Comment thread
martnpaneq marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: Fix
scope:
- ckeditor5-basic-styles
---

The `superscript` and `subscript` text styles are now mutually exclusive.
Comment thread
martnpaneq marked this conversation as resolved.
Outdated
26 changes: 26 additions & 0 deletions packages/ckeditor5-basic-styles/docs/features/basic-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ CKEditor 5 allows for typing both at the inner and outer boundaries of code

{@img assets/img/typing-after-code.gif 770 The animation showing typing after the code element in CKEditor 5 rich text editor.}

## Subscript and superscript exclusivity

By default, the {@link module:basic-styles/subscript~Subscript subscript} and {@link module:basic-styles/superscript~Superscript superscript} features are mutually exclusive: applying one to text that already has the other replaces it, matching the behavior of common word processors. Toggling a style off does not affect the other style.

To allow nesting, set the `allowNesting` option on either feature under the `basicStyles` configuration namespace:

```js
ClassicEditor
.create( {
// ... Other configuration options ...
basicStyles: {
superscript: {
allowNesting: true
}
}
} )
.then( /* ... */ )
.catch( /* ... */ );
```

The flag is symmetric: setting `basicStyles.superscript.allowNesting` or `basicStyles.subscript.allowNesting` to `true` disables the mutual exclusion for both commands.

<info-box info>
The mutual exclusion only applies to command execution. Loading content through the {@link module:core/editor/editor~Editor#setData data API} or pasting HTML such as `<sub><sup>x</sup></sub>` keeps both attributes on the same text regardless of this option.
</info-box>

## Installation

After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the plugins which you need to your plugin list. Then, simply configure the toolbar items to make the features available in the user interface.
Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-basic-styles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"devDependencies": {
"@ckeditor/ckeditor5-editor-classic": "workspace:*",
"@ckeditor/ckeditor5-essentials": "workspace:*",
"@ckeditor/ckeditor5-paragraph": "workspace:*"
"@ckeditor/ckeditor5-paragraph": "workspace:*",
"@ckeditor/ckeditor5-undo": "workspace:*"
},
"scripts": {
"build": "node ../../scripts/nim/build-package.mjs"
Expand Down
11 changes: 11 additions & 0 deletions packages/ckeditor5-basic-styles/src/augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
Strikethrough,
StrikethroughEditing,
StrikethroughUI,
BasicStylesConfig,
SubscriptEditing,
SubscriptUI,
SuperscriptEditing,
Expand All @@ -28,6 +29,16 @@ import type {
} from './index.js';

declare module '@ckeditor/ckeditor5-core' {
interface EditorConfig {

/**
* The configuration of the {@link module:basic-styles basic styles features}.
*
* Read more in {@link module:basic-styles/basicstylesconfig~BasicStylesConfig}.
*/
basicStyles?: BasicStylesConfig;
}
Comment thread
martnpaneq marked this conversation as resolved.

interface PluginsMap {
[ Superscript.pluginName ]: Superscript;
[ Subscript.pluginName ]: Subscript;
Expand Down
49 changes: 49 additions & 0 deletions packages/ckeditor5-basic-styles/src/basicstylesconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

/**
* @module basic-styles/basicstylesconfig
*/

import type { BasicStyleSubscriptConfig } from './subscriptconfig.js';
import type { BasicStyleSuperscriptConfig } from './superscriptconfig.js';

/**
* The configuration of the basic styles features (`Bold`, `Italic`, `Subscript`, `Superscript`, etc.).
*
* ```ts
* ClassicEditor
* .create( editorElement, {
* basicStyles: {
* superscript: {
* allowNesting: true
* },
* subscript: {
* allowNesting: true
* }
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
*/
export interface BasicStylesConfig {

/**
* The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}.
*
* Read more in {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig}.
*/
superscript?: BasicStyleSuperscriptConfig;

/**
* The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}.
*
* Read more in {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig}.
*/
subscript?: BasicStyleSubscriptConfig;
}
22 changes: 22 additions & 0 deletions packages/ckeditor5-basic-styles/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

/**
* @module basic-styles/constants
*/

/**
* The model attribute key for the `superscript` text style.
*
* @internal
*/
export const SUPERSCRIPT = 'superscript';

/**
* The model attribute key for the `subscript` text style.
*
* @internal
*/
export const SUBSCRIPT = 'subscript';
3 changes: 3 additions & 0 deletions packages/ckeditor5-basic-styles/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export { StrikethroughUI } from './strikethrough/strikethroughui.js';
export { Subscript } from './subscript.js';
export { SubscriptEditing } from './subscript/subscriptediting.js';
export { SubscriptUI } from './subscript/subscriptui.js';
export type { BasicStyleSubscriptConfig } from './subscriptconfig.js';
export type { BasicStylesConfig } from './basicstylesconfig.js';
export { Superscript } from './superscript.js';
export { SuperscriptEditing } from './superscript/superscriptediting.js';
export { SuperscriptUI } from './superscript/superscriptui.js';
export type { BasicStyleSuperscriptConfig } from './superscriptconfig.js';
export { Underline } from './underline.js';
export { UnderlineEditing } from './underline/underlineediting.js';
export { UnderlineUI } from './underline/underlineui.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

/**
* @module basic-styles/mutuallyexclusiveattributecommand
*/

import type { Editor } from '@ckeditor/ckeditor5-core';
import { ModelDocumentSelection, type ModelElement, type ModelRange } from '@ckeditor/ckeditor5-engine';

import { AttributeCommand } from './attributecommand.js';

/**
* An {@link module:basic-styles/attributecommand~AttributeCommand} variant that removes a configured
* opposite attribute from the affected ranges whenever the command turns its own attribute on.
*
* Used by the `superscript` and `subscript` commands to enforce their mutual exclusion. The opposite
* attribute is removed in the same model change as the parent's toggle, so the operation is a single
* undo step.
*
* The mutual exclusion can be disabled by setting either
* `config.basicStyles.superscript.allowNesting` or `config.basicStyles.subscript.allowNesting` to `true`.
* In that case, the command behaves exactly the same as the plain
* {@link module:basic-styles/attributecommand~AttributeCommand}.
*
* The exclusion only applies to command execution. Content set through the data pipeline
* (for example `editor.setData( '<sub><sup>x</sup></sub>' )`) is not modified by this command.
*
* @internal
*/
export class MutuallyExclusiveAttributeCommand extends AttributeCommand {
private readonly _oppositeAttributeKey: string;

constructor( editor: Editor, attributeKey: string, oppositeAttributeKey: string ) {
super( editor, attributeKey );

this._oppositeAttributeKey = oppositeAttributeKey;
}

/**
* @inheritDoc
*/
public override execute( options: { forceValue?: boolean } = {} ): void {
const editor = this.editor;
const model = editor.model;
const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;

if ( !value || _isNestingAllowed( editor ) ) {
super.execute( options );

return;
}

const oppositeKey = this._oppositeAttributeKey;

model.change( writer => {
super.execute( options );

const selection = model.document.selection;

if ( selection.isCollapsed ) {
writer.removeSelectionAttribute( oppositeKey );

return;
}

const ranges = model.schema.getValidRanges( selection.getRanges(), oppositeKey, {
includeEmptyRanges: true
} );

for ( const range of ranges ) {
let itemOrRange: ModelRange | ModelElement = range;
let attributeKey = oppositeKey;

if ( range.isCollapsed ) {
itemOrRange = range.start.parent as ModelElement;
attributeKey = ModelDocumentSelection._getStoreAttributeKey( oppositeKey );
}

writer.removeAttribute( attributeKey, itemOrRange );
}
} );
}
}

function _isNestingAllowed( editor: Editor ): boolean {
return Boolean(
editor.config.get( 'basicStyles.superscript.allowNesting' ) ||
editor.config.get( 'basicStyles.subscript.allowNesting' )
);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
*/

import { Plugin } from '@ckeditor/ckeditor5-core';
import { AttributeCommand } from '../attributecommand.js';

const SUBSCRIPT = 'subscript';
import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js';
import { SUBSCRIPT, SUPERSCRIPT } from '../constants.js';

/**
* The subscript editing feature.
Expand Down Expand Up @@ -38,6 +37,9 @@ export class SubscriptEditing extends Plugin {
*/
public init(): void {
const editor = this.editor;

editor.config.define( 'basicStyles', { [ SUBSCRIPT ]: { allowNesting: false } } );

// Allow sub attribute on text nodes.
editor.model.schema.extend( '$text', { allowAttributes: SUBSCRIPT } );
editor.model.schema.setAttributeProperties( SUBSCRIPT, {
Expand All @@ -60,6 +62,6 @@ export class SubscriptEditing extends Plugin {
} );

// Create sub command.
editor.commands.add( SUBSCRIPT, new AttributeCommand( editor, SUBSCRIPT ) );
editor.commands.add( SUBSCRIPT, new MutuallyExclusiveAttributeCommand( editor, SUBSCRIPT, SUPERSCRIPT ) );
}
}
3 changes: 1 addition & 2 deletions packages/ckeditor5-basic-styles/src/subscript/subscriptui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
import { IconSubscript } from '@ckeditor/ckeditor5-icons';
import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui';
import { getButtonCreator } from '../utils.js';

const SUBSCRIPT = 'subscript';
import { SUBSCRIPT } from '../constants.js';

/**
* The subscript UI feature. It introduces the Subscript button.
Expand Down
50 changes: 50 additions & 0 deletions packages/ckeditor5-basic-styles/src/subscriptconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

/**
* @module basic-styles/subscriptconfig
*/

/**
* The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}.
* Nested under {@link module:basic-styles/basicstylesconfig~BasicStylesConfig#subscript `config.basicStyles.subscript`}.
*
* ```ts
* ClassicEditor
* .create( editorElement, {
* basicStyles: {
* subscript: {
* allowNesting: true
* }
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
*/
export interface BasicStyleSubscriptConfig {

/**
* Whether `subscript` and `superscript` attributes are allowed to coexist on the same text.
*
* By default this is `false`: applying subscript to text that is already superscript removes the
* superscript attribute (and vice versa), matching the behavior of common word processors.
*
* Set to `true` to restore the historical behavior where both attributes can be applied to the same
* text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`).
*
* The flag is symmetric with
* {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.basicStyles.superscript.allowNesting`}:
* if either is set to `true`, both commands skip the mutual-exclusion step.
*
* The flag only affects command execution. Content set through the data pipeline (for example
* `editor.setData( '<sub><sup>x</sup></sub>' )`) keeps both attributes regardless of this option.
*
* @default false
*/
allowNesting?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
*/

import { Plugin } from '@ckeditor/ckeditor5-core';
import { AttributeCommand } from '../attributecommand.js';

const SUPERSCRIPT = 'superscript';
import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js';
import { SUBSCRIPT, SUPERSCRIPT } from '../constants.js';

/**
* The superscript editing feature.
Expand Down Expand Up @@ -38,6 +37,9 @@ export class SuperscriptEditing extends Plugin {
*/
public init(): void {
const editor = this.editor;

editor.config.define( 'basicStyles', { [ SUPERSCRIPT ]: { allowNesting: false } } );

// Allow super attribute on text nodes.
editor.model.schema.extend( '$text', { allowAttributes: SUPERSCRIPT } );
editor.model.schema.setAttributeProperties( SUPERSCRIPT, {
Expand All @@ -60,6 +62,6 @@ export class SuperscriptEditing extends Plugin {
} );

// Create super command.
editor.commands.add( SUPERSCRIPT, new AttributeCommand( editor, SUPERSCRIPT ) );
editor.commands.add( SUPERSCRIPT, new MutuallyExclusiveAttributeCommand( editor, SUPERSCRIPT, SUBSCRIPT ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
import { IconSuperscript } from '@ckeditor/ckeditor5-icons';
import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui';
import { getButtonCreator } from '../utils.js';

const SUPERSCRIPT = 'superscript';
import { SUPERSCRIPT } from '../constants.js';

/**
* The superscript UI feature. It introduces the Superscript button.
Expand Down
Loading