From ec624b047350cfda5e20a27a063a2961523af043 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 6 Feb 2023 12:07:25 +0100 Subject: [PATCH 1/8] Export clipboard event types --- packages/ckeditor5-clipboard/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/src/index.ts b/packages/ckeditor5-clipboard/src/index.ts index 254c3efde56..e8be603d3ce 100644 --- a/packages/ckeditor5-clipboard/src/index.ts +++ b/packages/ckeditor5-clipboard/src/index.ts @@ -10,8 +10,10 @@ export { default as Clipboard } from './clipboard'; export { default as ClipboardPipeline, + type ClipboardContentInsertionEvent, type ClipboardInputTransformationEvent, - type ClipboardInputTransformationData + type ClipboardInputTransformationData, + type ClipboardOutputEvent } from './clipboardpipeline'; export { default as DragDrop } from './dragdrop'; export { default as PastePlainText } from './pasteplaintext'; From c8d067c2cf880ea4fef67b918a0c6116953189ad Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 6 Feb 2023 12:10:24 +0100 Subject: [PATCH 2/8] Export types for postfixer and schema callbacks from core --- packages/ckeditor5-core/src/editingkeystrokehandler.ts | 7 ++++++- packages/ckeditor5-core/src/index.ts | 1 + packages/ckeditor5-engine/src/index.ts | 8 ++++++-- packages/ckeditor5-engine/src/model/document.ts | 9 +++++++-- packages/ckeditor5-engine/src/model/schema.ts | 8 ++++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-core/src/editingkeystrokehandler.ts b/packages/ckeditor5-core/src/editingkeystrokehandler.ts index 48bb46517eb..2c142793b6f 100644 --- a/packages/ckeditor5-core/src/editingkeystrokehandler.ts +++ b/packages/ckeditor5-core/src/editingkeystrokehandler.ts @@ -63,7 +63,7 @@ export default class EditingKeystrokeHandler extends KeystrokeHandler { */ public override set( keystroke: string | Array, - callback: string | ( ( ev: KeyboardEvent, cancel: () => void ) => void ), + callback: EditingKeystrokeCallback, options: { readonly priority?: PriorityString } = {} ): void { if ( typeof callback == 'string' ) { @@ -78,3 +78,8 @@ export default class EditingKeystrokeHandler extends KeystrokeHandler { super.set( keystroke, callback, options ); } } + +/** + * Command name or a callback to be executed when a given keystroke is pressed. + */ +export type EditingKeystrokeCallback = string | ( ( ev: KeyboardEvent, cancel: () => void ) => void ); diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index f923d5919a8..f1d614bca5f 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -15,6 +15,7 @@ export { PluginsMap, type default as PluginCollection } from './plugincollection export { default as Context } from './context'; export { default as ContextPlugin, type ContextPluginDependencies } from './contextplugin'; +export { type EditingKeystrokeCallback } from './editingkeystrokehandler'; export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from './editor/editor'; export type { diff --git a/packages/ckeditor5-engine/src/index.ts b/packages/ckeditor5-engine/src/index.ts index 629dea06188..26bcddc8a97 100644 --- a/packages/ckeditor5-engine/src/index.ts +++ b/packages/ckeditor5-engine/src/index.ts @@ -70,14 +70,18 @@ export { default as Position } from './model/position'; export { default as DocumentFragment } from './model/documentfragment'; export { default as History } from './model/history'; export { default as Text } from './model/text'; -export type { default as Document } from './model/document'; +export type { default as Document, ModelPostFixer } from './model/document'; export type { Marker } from './model/markercollection'; export type { default as Batch } from './model/batch'; export type { DiffItem, DiffItemAttribute, DiffItemInsert, DiffItemRemove } from './model/differ'; export type { default as Item } from './model/item'; export type { default as Node } from './model/node'; export type { default as RootElement } from './model/rootelement'; -export type { default as Schema } from './model/schema'; +export type { + default as Schema, + SchemaAttributeCheckCallback, + SchemaChildCheckCallback +} from './model/schema'; export type { default as Selection, Selectable } from './model/selection'; export type { default as TypeCheckable } from './model/typecheckable'; export type { default as Writer } from './model/writer'; diff --git a/packages/ckeditor5-engine/src/model/document.ts b/packages/ckeditor5-engine/src/model/document.ts index e0afc694a96..ae328d3b187 100644 --- a/packages/ckeditor5-engine/src/model/document.ts +++ b/packages/ckeditor5-engine/src/model/document.ts @@ -80,7 +80,7 @@ export default class Document extends EmitterMixin() { /** * Post-fixer callbacks registered to the model document. */ - private readonly _postFixers: Set<( writer: Writer ) => boolean>; + private readonly _postFixers: Set; /** * A boolean indicates whether the selection has changed until @@ -266,7 +266,7 @@ export default class Document extends EmitterMixin() { * } ); * ``` */ - public registerPostFixer( postFixer: ( writer: Writer ) => boolean ): void { + public registerPostFixer( postFixer: ModelPostFixer ): void { this._postFixers.add( postFixer ); } @@ -445,6 +445,11 @@ export type DocumentChangeEvent = { args: [ batch: Batch ]; }; +/** + * Callback passed as an argument to the {@link module:engine/model/document~Document#registerPostFixer} method. + */ +export type ModelPostFixer = ( writer: Writer ) => boolean; + /** * Checks whether given range boundary position is valid for document selection, meaning that is not between * unicode surrogate pairs or base character and combining marks. diff --git a/packages/ckeditor5-engine/src/model/schema.ts b/packages/ckeditor5-engine/src/model/schema.ts index 8038b53f4bd..91e0fcd6a70 100644 --- a/packages/ckeditor5-engine/src/model/schema.ts +++ b/packages/ckeditor5-engine/src/model/schema.ts @@ -516,7 +516,7 @@ export default class Schema extends ObservableMixin() { * The callback may return `true/false` to override `checkChild()`'s return value. If it does not return * a boolean value, the default algorithm (or other callbacks) will define `checkChild()`'s return value. */ - public addChildCheck( callback: ( ctx: SchemaContext, def: SchemaCompiledItemDefinition ) => unknown ): void { + public addChildCheck( callback: SchemaChildCheckCallback ): void { this.on( 'checkChild', ( evt, [ ctx, childDef ] ) => { // checkChild() was called with a non-registered child. // In 99% cases such check should return false, so not to overcomplicate all callbacks @@ -577,7 +577,7 @@ export default class Schema extends ObservableMixin() { * The callback may return `true/false` to override `checkAttribute()`'s return value. If it does not return * a boolean value, the default algorithm (or other callbacks) will define `checkAttribute()`'s return value. */ - public addAttributeCheck( callback: ( context: SchemaContext, attributeName: string ) => unknown ): void { + public addAttributeCheck( callback: SchemaAttributeCheckCallback ): void { this.on( 'checkAttribute', ( evt, [ ctx, attributeName ] ) => { const retValue = callback( ctx, attributeName ); @@ -1767,6 +1767,10 @@ export interface AttributeProperties { [ name: string ]: unknown; } +export type SchemaAttributeCheckCallback = ( context: SchemaContext, attributeName: string ) => unknown; + +export type SchemaChildCheckCallback = ( ctx: SchemaContext, def: SchemaCompiledItemDefinition ) => unknown; + function compileBaseItemRule( sourceItemRules: Array, itemName: string ): SchemaCompiledItemDefinitionInternal { const itemRule = { name: itemName, From 028f9c7774af6a1ef1bd2c4c7b582e0570a37a52 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 6 Feb 2023 12:11:38 +0100 Subject: [PATCH 3/8] Export events from the typing plugin --- packages/ckeditor5-typing/src/index.ts | 1 + packages/ckeditor5-typing/src/input.ts | 1 + .../ckeditor5-typing/src/inserttextcommand.ts | 21 +++++++++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-typing/src/index.ts b/packages/ckeditor5-typing/src/index.ts index 1b59d4a7375..86c8f974d40 100644 --- a/packages/ckeditor5-typing/src/index.ts +++ b/packages/ckeditor5-typing/src/index.ts @@ -19,6 +19,7 @@ export { default as inlineHighlight } from './utils/inlinehighlight'; export { default as findAttributeRange } from './utils/findattributerange'; export { default as getLastTextLine, type LastTextLineData } from './utils/getlasttextline'; +export type { InsertTextCommandExecuteEvent } from './inserttextcommand'; export type { TypingConfig } from './typingconfig'; export type { ViewDocumentDeleteEvent } from './deleteobserver'; export type { ViewDocumentInsertTextEvent } from './inserttextobserver'; diff --git a/packages/ckeditor5-typing/src/input.ts b/packages/ckeditor5-typing/src/input.ts index 9f7c835d759..34d0afad97b 100644 --- a/packages/ckeditor5-typing/src/input.ts +++ b/packages/ckeditor5-typing/src/input.ts @@ -149,6 +149,7 @@ export default class Input extends Plugin { declare module '@ckeditor/ckeditor5-core' { interface CommandsMap { + input: InsertTextCommand; insertText: InsertTextCommand; } diff --git a/packages/ckeditor5-typing/src/inserttextcommand.ts b/packages/ckeditor5-typing/src/inserttextcommand.ts index af3651097d9..4ffb94e7772 100644 --- a/packages/ckeditor5-typing/src/inserttextcommand.ts +++ b/packages/ckeditor5-typing/src/inserttextcommand.ts @@ -77,12 +77,7 @@ export default class InsertTextCommand extends Command { * should be placed after the insertion. If not specified, the selection will be placed right after * the inserted text. */ - public override execute( options: { - text?: string; - selection?: Selection | DocumentSelection; - range?: Range; - resultRange?: Range; - } = {} ): void { + public override execute( options: InsertTextCommandOptions = {} ): void { const model = this.editor.model; const doc = model.document; const text = options.text || ''; @@ -119,3 +114,17 @@ export default class InsertTextCommand extends Command { } ); } } + +export interface InsertTextCommandOptions { + text?: string; + selection?: Selection | DocumentSelection; + range?: Range; + resultRange?: Range; +} + +export interface InsertTextCommandExecuteEvent { + name: 'execute'; + args: [ + data: [ options: InsertTextCommandOptions ] + ]; +} From 83bb9ed6f20bd624a070fd6ebc826b5c53ecf76c Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 6 Feb 2023 13:11:18 +0100 Subject: [PATCH 4/8] Move restricted editing plugin to TypeScript --- .gitignore | 1 + .../ckeditor5-restricted-editing/package.json | 10 +- .../src/{index.js => index.ts} | 0 ...s => restrictededitingexceptioncommand.ts} | 27 +- .../src/restrictededitingmode.js | 103 -------- .../src/restrictededitingmode.ts | 112 ++++++++ .../{converters.js => converters.ts} | 65 ++--- .../{utils.js => utils.ts} | 25 +- ...ing.js => restrictededitingmodeediting.ts} | 243 +++++++++++------- ...restrictededitingmodenavigationcommand.ts} | 75 +++--- ...ngmodeui.js => restrictededitingmodeui.ts} | 41 +-- ...deditingmode.js => standardeditingmode.ts} | 12 +- ...iting.js => standardeditingmodeediting.ts} | 14 +- ...tingmodeui.js => standardeditingmodeui.ts} | 17 +- .../tsconfig.json | 7 + .../tsconfig.release.json | 10 + 16 files changed, 449 insertions(+), 313 deletions(-) rename packages/ckeditor5-restricted-editing/src/{index.js => index.ts} (100%) rename packages/ckeditor5-restricted-editing/src/{restrictededitingexceptioncommand.js => restrictededitingexceptioncommand.ts} (69%) delete mode 100644 packages/ckeditor5-restricted-editing/src/restrictededitingmode.js create mode 100644 packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts rename packages/ckeditor5-restricted-editing/src/restrictededitingmode/{converters.js => converters.ts} (76%) rename packages/ckeditor5-restricted-editing/src/restrictededitingmode/{utils.js => utils.ts} (67%) rename packages/ckeditor5-restricted-editing/src/{restrictededitingmodeediting.js => restrictededitingmodeediting.ts} (68%) rename packages/ckeditor5-restricted-editing/src/{restrictededitingmodenavigationcommand.js => restrictededitingmodenavigationcommand.ts} (65%) rename packages/ckeditor5-restricted-editing/src/{restrictededitingmodeui.js => restrictededitingmodeui.ts} (66%) rename packages/ckeditor5-restricted-editing/src/{standardeditingmode.js => standardeditingmode.ts} (76%) rename packages/ckeditor5-restricted-editing/src/{standardeditingmodeediting.js => standardeditingmodeediting.ts} (84%) rename packages/ckeditor5-restricted-editing/src/{standardeditingmodeui.js => standardeditingmodeui.ts} (82%) create mode 100644 packages/ckeditor5-restricted-editing/tsconfig.json create mode 100644 packages/ckeditor5-restricted-editing/tsconfig.release.json diff --git a/.gitignore b/.gitignore index 5481669d441..a5185ad7aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ packages/ckeditor5-minimap/src/**/*.js packages/ckeditor5-page-break/src/**/*.js packages/ckeditor5-paragraph/src/**/*.js packages/ckeditor5-paste-from-office/src/**/*.js +packages/ckeditor5-restricted-editing/src/**/*.js packages/ckeditor5-select-all/src/**/*.js packages/ckeditor5-typing/src/**/*.js packages/ckeditor5-ui/src/**/*.js diff --git a/packages/ckeditor5-restricted-editing/package.json b/packages/ckeditor5-restricted-editing/package.json index 973e98f7613..79661ff7683 100644 --- a/packages/ckeditor5-restricted-editing/package.json +++ b/packages/ckeditor5-restricted-editing/package.json @@ -9,7 +9,7 @@ "ckeditor5-lib", "ckeditor5-dll" ], - "main": "src/index.js", + "main": "src/index.ts", "dependencies": { "ckeditor5": "^36.0.0" }, @@ -29,6 +29,7 @@ "@ckeditor/ckeditor5-ui": "^36.0.0", "@ckeditor/ckeditor5-undo": "^36.0.0", "@ckeditor/ckeditor5-utils": "^36.0.0", + "typescript": "^4.8.4", "webpack": "^5.58.1", "webpack-cli": "^4.9.0" }, @@ -47,13 +48,16 @@ }, "files": [ "lang", - "src", + "src/**/*.js", + "src/**/*.d.ts", "theme", "build", "ckeditor5-metadata.json", "CHANGELOG.md" ], "scripts": { - "dll:build": "webpack" + "dll:build": "webpack", + "build": "tsc -p ./tsconfig.release.json", + "postversion": "npm run build" } } diff --git a/packages/ckeditor5-restricted-editing/src/index.js b/packages/ckeditor5-restricted-editing/src/index.ts similarity index 100% rename from packages/ckeditor5-restricted-editing/src/index.js rename to packages/ckeditor5-restricted-editing/src/index.ts diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js b/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts similarity index 69% rename from packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts index 7336b8fc8a9..3b0c3f6d789 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts @@ -8,15 +8,23 @@ */ import { Command } from 'ckeditor5/src/core'; +import type { TreeWalkerValue } from 'ckeditor5/src/engine'; /** * @extends module:core/command~Command */ export default class RestrictedEditingExceptionCommand extends Command { + /** + * A flag indicating whether the command is active + * + * @readonly + */ + declare public value: boolean; + /** * @inheritDoc */ - refresh() { + public override refresh(): void { const model = this.editor.model; const doc = model.document; @@ -28,7 +36,7 @@ export default class RestrictedEditingExceptionCommand extends Command { /** * @inheritDoc */ - execute( options = {} ) { + public override execute( options: RestrictedEditingExceptionCommandParams = {} ): void { const model = this.editor.model; const document = model.document; const selection = document.selection; @@ -41,10 +49,13 @@ export default class RestrictedEditingExceptionCommand extends Command { if ( valueToSet ) { writer.setSelectionAttribute( 'restrictedEditingException', valueToSet ); } else { - const isSameException = value => value.item.getAttribute( 'restrictedEditingException' ) === this.value; - const exceptionStart = selection.focus.getLastMatchingPosition( isSameException, { direction: 'backward' } ); - const exceptionEnd = selection.focus.getLastMatchingPosition( isSameException ); - const focus = selection.focus; + const isSameException = ( value: TreeWalkerValue ) => { + return value.item.getAttribute( 'restrictedEditingException' ) === this.value; + }; + + const focus = selection.focus!; + const exceptionStart = focus.getLastMatchingPosition( isSameException, { direction: 'backward' } ); + const exceptionEnd = focus.getLastMatchingPosition( isSameException ); writer.removeSelectionAttribute( 'restrictedEditingException' ); @@ -64,3 +75,7 @@ export default class RestrictedEditingExceptionCommand extends Command { } ); } } + +export interface RestrictedEditingExceptionCommandParams { + forceValue?: unknown; +} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.js deleted file mode 100644 index cd62ddbab69..00000000000 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @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 restricted-editing/restrictededitingmode - */ - -import { Plugin } from 'ckeditor5/src/core'; - -import RestrictedEditingModeEditing from './restrictededitingmodeediting'; -import RestrictedEditingModeUI from './restrictededitingmodeui'; - -import '../theme/restrictedediting.css'; - -/** - * The restricted editing mode plugin. - * - * This is a "glue" plugin which loads the following plugins: - * - * * The {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing restricted mode editing feature}. - * * The {@link module:restricted-editing/restrictededitingmodeui~RestrictedEditingModeUI restricted mode UI feature}. - * - * @extends module:core/plugin~Plugin - */ -export default class RestrictedEditingMode extends Plugin { - /** - * @inheritDoc - */ - static get pluginName() { - return 'RestrictedEditingMode'; - } - - /** - * @inheritDoc - */ - static get requires() { - return [ RestrictedEditingModeEditing, RestrictedEditingModeUI ]; - } -} - -/** - * The configuration of the restricted editing mode feature. Introduced by the - * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. - * - * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}. - * - * @member {module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig} - * module:core/editor/editorconfig~EditorConfig#restrictedEditing - */ - -/** - * The configuration of the restricted editing mode feature. - * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. - * - * ClassicEditor - * .create( { - * restrictedEditing: { - * allowedCommands: [ 'bold', 'link', 'unlink' ], - * allowedAttributes: [ 'bold', 'linkHref' ] - * } - * } ) - * .then( ... ) - * .catch( ... ); - * - * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. - * - * @interface module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig - */ - -/** - * The command names allowed in non-restricted areas of the content. - * - * Defines which feature commands should be enabled in the restricted editing mode. The commands used for typing and deleting text - * (`'input'`, `'delete'` and `'deleteForward'`) are allowed by the feature inside non-restricted regions and do not need to be defined. - * - * **Note**: The restricted editing mode always allows to use the restricted mode navigation commands as well as `'undo'` and `'redo'` - * commands. - * - * The default value is: - * - * const restrictedEditingConfig = { - * allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ] - * }; - * - * To make a command always enabled (also outside non-restricted areas) use - * {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing#enableCommand} method. - * - * @member {Array.} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands - */ - -/** - * The text attribute names allowed when pasting content ot non-restricted areas. - * - * The default value is: - * - * const restrictedEditingConfig = { - * allowedAttributes: [ 'bold', 'italic', 'linkHref' ] - * }; - * - * @member {Array.} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedAttributes - */ diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts new file mode 100644 index 00000000000..d12d51152af --- /dev/null +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts @@ -0,0 +1,112 @@ +/** + * @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 restricted-editing/restrictededitingmode + */ + +import { Plugin, type PluginDependencies } from 'ckeditor5/src/core'; + +import RestrictedEditingModeEditing from './restrictededitingmodeediting'; +import RestrictedEditingModeUI from './restrictededitingmodeui'; + +import '../theme/restrictedediting.css'; + +/** + * The restricted editing mode plugin. + * + * This is a "glue" plugin which loads the following plugins: + * + * * The {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing restricted mode editing feature}. + * * The {@link module:restricted-editing/restrictededitingmodeui~RestrictedEditingModeUI restricted mode UI feature}. + * + * @extends module:core/plugin~Plugin + */ +export default class RestrictedEditingMode extends Plugin { + /** + * @inheritDoc + */ + public static get pluginName(): 'RestrictedEditingMode' { + return 'RestrictedEditingMode'; + } + + /** + * @inheritDoc + */ + public static get requires(): PluginDependencies { + return [ RestrictedEditingModeEditing, RestrictedEditingModeUI ]; + } +} + +/** + * The configuration of the restricted editing mode feature. + * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * ```ts + * ClassicEditor + * .create( { + * restrictedEditing: { + * allowedCommands: [ 'bold', 'link', 'unlink' ], + * allowedAttributes: [ 'bold', 'linkHref' ] + * } + * } ) + * .then( ... ) + * .catch( ... ); + * ``` + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + */ +export interface RestrictedEditingModeConfig { + + /** + * The command names allowed in non-restricted areas of the content. + * + * Defines which feature commands should be enabled in the restricted editing mode. The commands used for typing and deleting text + * (`'input'`, `'delete'` and `'deleteForward'`) are allowed by the feature inside non-restricted regions and do not need to be defined. + * + * **Note**: The restricted editing mode always allows to use the restricted mode navigation commands as well as `'undo'` and `'redo'` + * commands. + * + * The default value is: + * + * ```ts + * const restrictedEditingConfig = { + * allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ] + * }; + * ``` + * + * To make a command always enabled (also outside non-restricted areas) use + * {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing#enableCommand} method. + */ + allowedCommands: Array; + + /** + * The text attribute names allowed when pasting content ot non-restricted areas. + * + * The default value is: + * + * ```ts + * const restrictedEditingConfig = { + * allowedAttributes: [ 'bold', 'italic', 'linkHref' ] + * }; + */ + allowedAttributes: Array; +} + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ RestrictedEditingMode.pluginName ]: RestrictedEditingMode; + } + + /** + * The configuration of the restricted editing mode feature. Introduced by the + * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}. + */ + interface EditorConfig { + restrictedEditing?: RestrictedEditingModeConfig; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.ts similarity index 76% rename from packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.ts index 294d7686d3f..7956e5b4c82 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/converters.ts @@ -7,7 +7,17 @@ * @module restricted-editing/restrictededitingmode/converters */ -import { Matcher } from 'ckeditor5/src/engine'; +import type { Editor } from 'ckeditor5/src/core'; +import { + Matcher, + type DowncastWriter, + type MatcherPattern, + type ModelPostFixer, + type Position, + type UpcastDispatcher, + type Writer, + type ViewElement +} from 'ckeditor5/src/engine'; import { getMarkerAtPosition } from './utils'; @@ -24,28 +34,28 @@ const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected'; * * The class is added in the view post-fixer, after other changes in the model tree are converted to the view. * * This way, adding and removing the highlight does not interfere with conversion. - * - * @param {module:core/editor/editor~Editor} editor */ -export function setupExceptionHighlighting( editor ) { +export function setupExceptionHighlighting( editor: Editor ): void { const view = editor.editing.view; const model = editor.model; - const highlightedMarkers = new Set(); + const highlightedMarkers = new Set(); // Adding the class. - view.document.registerPostFixer( writer => { + view.document.registerPostFixer( ( writer: DowncastWriter ): boolean => { const modelSelection = model.document.selection; - const marker = getMarkerAtPosition( editor, modelSelection.anchor ); + const marker = getMarkerAtPosition( editor, modelSelection.anchor! ); if ( !marker ) { - return; + return false; } - for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) { + for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name )! ) { writer.addClass( HIGHLIGHT_CLASS, viewElement ); highlightedMarkers.add( viewElement ); } + + return false; } ); // Removing the class. @@ -69,11 +79,8 @@ export function setupExceptionHighlighting( editor ) { /** * A post-fixer that prevents removing a collapsed marker from the document. - * - * @param {module:core/editor/editor~Editor} editor - * @returns {Function} */ -export function resurrectCollapsedMarkerPostFixer( editor ) { +export function resurrectCollapsedMarkerPostFixer( editor: Editor ): ModelPostFixer { // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778. return writer => { let changeApplied = false; @@ -81,7 +88,7 @@ export function resurrectCollapsedMarkerPostFixer( editor ) { for ( const { name, data } of editor.model.document.differ.getChangedMarkers() ) { if ( name.startsWith( 'restrictedEditingException' ) && data.newRange && data.newRange.root.rootName == '$graveyard' ) { writer.updateMarker( name, { - range: writer.createRange( writer.createPositionAt( data.oldRange.start ) ) + range: writer.createRange( writer.createPositionAt( data.oldRange!.start ) ) } ); changeApplied = true; @@ -94,11 +101,8 @@ export function resurrectCollapsedMarkerPostFixer( editor ) { /** * A post-fixer that extends a marker when the user types on its boundaries. - * - * @param {module:core/editor/editor~Editor} editor - * @returns {Function} */ -export function extendMarkerOnTypingPostFixer( editor ) { +export function extendMarkerOnTypingPostFixer( editor: Editor ): ModelPostFixer { // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778. return writer => { let changeApplied = false; @@ -117,15 +121,10 @@ export function extendMarkerOnTypingPostFixer( editor ) { /** * A view highlight-to-marker conversion helper. * - * @param {Object} config Conversion configuration. - * @param {module:engine/view/matcher~MatcherPattern} [config.view] A pattern matching all view elements which should be converted. If not - * set, the converter will fire for every view element. - * @param {String|module:engine/model/element~Element|Function} config.model The name of the model element, a model element - * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. - * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + * @param config Conversion configuration. */ -export function upcastHighlightToMarker( config ) { - return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => { +export function upcastHighlightToMarker( config: { view: MatcherPattern; model: () => string } ) { + return ( dispatcher: UpcastDispatcher ): void => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => { const { writer } = conversionApi; const matcher = new Matcher( config.view ); @@ -144,7 +143,7 @@ export function upcastHighlightToMarker( config ) { const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor ); conversionApi.consumable.consume( data.viewItem, match ); - const markerName = config.model( data.viewItem ); + const markerName = config.model(); const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } ); const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } ); @@ -160,8 +159,10 @@ export function upcastHighlightToMarker( config ) { } ); } -// Extend marker if change detected on marker's start position. -function _tryExtendMarkerStart( editor, position, length, writer ) { +/** + * Extend marker if change detected on marker's start position. + */ +function _tryExtendMarkerStart( editor: Editor, position: Position, length: number, writer: Writer ): boolean { const markerAtStart = getMarkerAtPosition( editor, position.getShiftedBy( length ) ); if ( markerAtStart && markerAtStart.getStart().isEqual( position.getShiftedBy( length ) ) ) { @@ -175,8 +176,10 @@ function _tryExtendMarkerStart( editor, position, length, writer ) { return false; } -// Extend marker if change detected on marker's end position. -function _tryExtendMarkedEnd( editor, position, length, writer ) { +/** + * Extend marker if change detected on marker's end position. + */ +function _tryExtendMarkedEnd( editor: Editor, position: Position, length: number, writer: Writer ): boolean { const markerAtEnd = getMarkerAtPosition( editor, position ); if ( markerAtEnd && markerAtEnd.getEnd().isEqual( position ) ) { diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts similarity index 67% rename from packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts index 03e4122d463..37bf18997b7 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts @@ -3,6 +3,9 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import type { Editor } from 'ckeditor5/src/core'; +import type { DocumentSelection, Marker, Position, Range } from 'ckeditor5/src/engine'; + /** * @module restricted-editing/restrictededitingmode/utils */ @@ -11,12 +14,8 @@ * Returns a single "restricted-editing-exception" marker at a given position. Contrary to * {@link module:engine/model/markercollection~MarkerCollection#getMarkersAtPosition}, it returnd a marker also when the postion is * equal to one of the marker's start or end positions. - * - * @param {module:core/editor/editor~Editor} editor - * @param {module:engine/model/position~Position} position - * @returns {module:engine/model/markercollection~Marker|undefined} marker */ -export function getMarkerAtPosition( editor, position ) { +export function getMarkerAtPosition( editor: Editor, position: Position ): Marker | undefined { for ( const marker of editor.model.markers ) { const markerRange = marker.getRange(); @@ -30,12 +29,8 @@ export function getMarkerAtPosition( editor, position ) { /** * Checks if the position is fully contained in the range. Positions equal to range start or end are considered "in". - * - * @param {module:engine/model/range~Range} range - * @param {module:engine/model/position~Position} position - * @returns {Boolean} */ -export function isPositionInRangeBoundaries( range, position ) { +export function isPositionInRangeBoundaries( range: Range, position: Position ): boolean { return ( range.containsPosition( position ) || range.end.isEqual( position ) || @@ -50,12 +45,8 @@ export function isPositionInRangeBoundaries( range, position ) { * f[oo] -> true * f[oo ba]r -> false * foo []bar -> false - * - * @param {module:engine/model/selection~Selection} selection - * @param {module:engine/model/markercollection~Marker} marker - * @returns {Boolean} */ -export function isSelectionInMarker( selection, marker ) { +export function isSelectionInMarker( selection: DocumentSelection, marker?: Marker ): boolean { if ( !marker ) { return false; } @@ -63,8 +54,8 @@ export function isSelectionInMarker( selection, marker ) { const markerRange = marker.getRange(); if ( selection.isCollapsed ) { - return isPositionInRangeBoundaries( markerRange, selection.focus ); + return isPositionInRangeBoundaries( markerRange, selection.focus! ); } - return markerRange.containsRange( selection.getFirstRange(), true ); + return markerRange.containsRange( selection.getFirstRange()!, true ); } diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts similarity index 68% rename from packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts index 0b03f909ce3..5e24d5b5b21 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts @@ -7,9 +7,23 @@ * @module restricted-editing/restrictededitingmodeediting */ -import { Plugin } from 'ckeditor5/src/core'; +import { Plugin, type Editor, type EditingKeystrokeCallback } from 'ckeditor5/src/core'; +import type { + DocumentSelection, + Marker, + ModelDeleteContentEvent, + ModelPostFixer, + Range, + SchemaAttributeCheckCallback, + SchemaChildCheckCallback +} from 'ckeditor5/src/engine'; +import type { BaseEvent, GetCallback } from 'ckeditor5/src/utils'; +import type { ClipboardContentInsertionEvent, ClipboardOutputEvent } from 'ckeditor5/src/clipboard'; -import RestrictedEditingNavigationCommand from './restrictededitingmodenavigationcommand'; +import { + default as RestrictedEditingNavigationCommand, + RestrictedEditingModeNavigationDirection +} from './restrictededitingmodenavigationcommand'; import { extendMarkerOnTypingPostFixer, resurrectCollapsedMarkerPostFixer, @@ -17,6 +31,8 @@ import { upcastHighlightToMarker } from './restrictededitingmode/converters'; import { getMarkerAtPosition, isSelectionInMarker } from './restrictededitingmode/utils'; +import type { RestrictedEditingModeConfig } from './restrictededitingmode'; +import type { InsertTextCommandExecuteEvent } from 'ckeditor5/src/typing'; const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode'; @@ -30,17 +46,30 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode'; * @extends module:core/plugin~Plugin */ export default class RestrictedEditingModeEditing extends Plugin { + /** + * Command names that are enabled outside the non-restricted regions. + */ + private _alwaysEnabled: Set; + + /** + * Commands allowed in non-restricted areas. + * + * Commands always enabled combine typing feature commands: `'input'`, `'insertText'`, `'delete'`, and `'deleteForward'` with + * commands defined in the feature configuration. + */ + private _allowedInException: Set; + /** * @inheritDoc */ - static get pluginName() { + public static get pluginName(): 'RestrictedEditingModeEditing' { return 'RestrictedEditingModeEditing'; } /** * @inheritDoc */ - constructor( editor ) { + constructor( editor: Editor ) { super( editor ); editor.config.define( 'restrictedEditing', { @@ -48,33 +77,17 @@ export default class RestrictedEditingModeEditing extends Plugin { allowedAttributes: [ 'bold', 'italic', 'linkHref' ] } ); - /** - * Command names that are enabled outside the non-restricted regions. - * - * @type {Set.} - * @private - */ this._alwaysEnabled = new Set( [ 'undo', 'redo' ] ); - - /** - * Commands allowed in non-restricted areas. - * - * Commands always enabled combine typing feature commands: `'input'`, `'insertText'`, `'delete'`, and `'deleteForward'` with - * commands defined in the feature configuration. - * - * @type {Set} - * @private - */ this._allowedInException = new Set( [ 'input', 'insertText', 'delete', 'deleteForward' ] ); } /** * @inheritDoc */ - init() { + public init(): void { const editor = this.editor; const editingView = editor.editing.view; - const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' ); + const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' )!; allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) ); @@ -83,8 +96,16 @@ export default class RestrictedEditingModeEditing extends Plugin { this._setupRestrictions(); // Commands & keystrokes that allow navigation in the content. - editor.commands.add( 'goToPreviousRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'backward' ) ); - editor.commands.add( 'goToNextRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'forward' ) ); + editor.commands.add( + 'goToPreviousRestrictedEditingException', + new RestrictedEditingNavigationCommand( editor, RestrictedEditingModeNavigationDirection.BACKWARD ) + ); + + editor.commands.add( + 'goToNextRestrictedEditingException', + new RestrictedEditingNavigationCommand( editor, RestrictedEditingModeNavigationDirection.FORWARD ) + ); + editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) ); editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) ); editor.keystrokes.set( 'Ctrl+A', getSelectAllHandler( editor ) ); @@ -103,10 +124,10 @@ export default class RestrictedEditingModeEditing extends Plugin { * To enable some commands in non-restricted areas of the content use * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands} configuration option. * - * @param {String} commandName Name of the command to enable. + * @param commandName Name of the command to enable. */ - enableCommand( commandName ) { - const command = this.editor.commands.get( commandName ); + public enableCommand( commandName: string ): void { + const command = this.editor.commands.get( commandName )!; command.clearForceDisabled( COMMAND_FORCE_DISABLE_ID ); @@ -119,10 +140,8 @@ export default class RestrictedEditingModeEditing extends Plugin { * * ucpast & downcast converters, * * marker highlighting in the edting area, * * marker post-fixers. - * - * @private */ - _setupConversion() { + private _setupConversion(): void { const editor = this.editor; const model = editor.model; const doc = model.document; @@ -192,17 +211,15 @@ export default class RestrictedEditingModeEditing extends Plugin { * * disabling input command outside exception marker * * restricting clipboard holder to text only * * restricting text attributes in content - * - * @private */ - _setupRestrictions() { + private _setupRestrictions() { const editor = this.editor; const model = editor.model; const selection = model.document.selection; const viewDoc = editor.editing.view.document; const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - this.listenTo( model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } ); + this.listenTo( model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } ); const inputCommand = editor.commands.get( 'input' ); const insertTextCommand = editor.commands.get( 'insertText' ); @@ -210,40 +227,48 @@ export default class RestrictedEditingModeEditing extends Plugin { // The restricted editing might be configured without input support - ie allow only bolding or removing text. // This check is bit synthetic since only tests are used this way. if ( inputCommand ) { - this.listenTo( inputCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } ); + this.listenTo( + inputCommand, + 'execute', + disallowInputExecForWrongRange( editor ), + { priority: 'high' } + ); } // The restricted editing might be configured without insert text support - ie allow only bolding or removing text. // This check is bit synthetic since only tests are used this way. if ( insertTextCommand ) { - this.listenTo( insertTextCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } ); + this.listenTo( + insertTextCommand, + 'execute', + disallowInputExecForWrongRange( editor ), + { priority: 'high' } + ); } // Block clipboard outside exception marker on paste and drop. - this.listenTo( clipboard, 'contentInsertion', evt => { - if ( !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) { + this.listenTo( clipboard, 'contentInsertion', evt => { + if ( !isRangeInsideSingleMarker( editor, selection.getFirstRange()! ) ) { evt.stop(); } } ); // Block clipboard outside exception marker on cut. - this.listenTo( viewDoc, 'clipboardOutput', ( evt, data ) => { - if ( data.method == 'cut' && !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) { + this.listenTo( viewDoc, 'clipboardOutput', ( evt, data ) => { + if ( data.method == 'cut' && !isRangeInsideSingleMarker( editor, selection.getFirstRange()! ) ) { evt.stop(); } }, { priority: 'high' } ); - const allowedAttributes = editor.config.get( 'restrictedEditing.allowedAttributes' ); + const allowedAttributes = editor.config.get( 'restrictedEditing.allowedAttributes' )!; model.schema.addAttributeCheck( onlyAllowAttributesFromList( allowedAttributes ) ); - model.schema.addChildCheck( allowTextOnlyInClipboardHolder ); + model.schema.addChildCheck( allowTextOnlyInClipboardHolder() ); } /** * Sets up the command toggling which enables or disables commands based on the user selection. - * - * @private */ - _setupCommandsToggling() { + private _setupCommandsToggling(): void { const editor = this.editor; const model = editor.model; const doc = model.document; @@ -256,10 +281,8 @@ export default class RestrictedEditingModeEditing extends Plugin { /** * Checks if commands should be enabled or disabled based on the current selection. - * - * @private */ - _checkCommands() { + private _checkCommands(): void { const editor = this.editor; const selection = editor.model.document.selection; @@ -269,22 +292,19 @@ export default class RestrictedEditingModeEditing extends Plugin { return; } - const marker = getMarkerAtPosition( editor, selection.focus ); + const marker = getMarkerAtPosition( editor, selection.focus! ); this._disableCommands(); if ( isSelectionInMarker( selection, marker ) ) { - this._enableCommands( marker ); + this._enableCommands( marker! ); } } /** * Enables commands in non-restricted regions. - * - * @returns {module:engine/model/markercollection~Marker} marker - * @private */ - _enableCommands( marker ) { + private _enableCommands( marker: Marker ): void { const editor = this.editor; for ( const [ commandName, command ] of editor.commands ) { @@ -308,10 +328,8 @@ export default class RestrictedEditingModeEditing extends Plugin { /** * Disables commands outside non-restricted regions. - * - * @private */ - _disableCommands() { + private _disableCommands(): void { const editor = this.editor; for ( const [ commandName, command ] of editor.commands ) { @@ -324,10 +342,12 @@ export default class RestrictedEditingModeEditing extends Plugin { } } -// Helper method for executing enabled commands only. -function getCommandExecuter( editor, commandName ) { - return ( data, cancel ) => { - const command = editor.commands.get( commandName ); +/** + * Helper method for executing enabled commands only. + */ +function getCommandExecuter( editor: Editor, commandName: string ): EditingKeystrokeCallback { + return ( _, cancel ) => { + const command = editor.commands.get( commandName )!; if ( command.isEnabled ) { editor.execute( commandName ); @@ -336,12 +356,14 @@ function getCommandExecuter( editor, commandName ) { }; } -// Helper for handling Ctrl+A keydown behaviour. -function getSelectAllHandler( editor ) { - return ( data, cancel ) => { +/** + * Helper for handling Ctrl+A keydown behaviour. + */ +function getSelectAllHandler( editor: Editor ): EditingKeystrokeCallback { + return ( _, cancel ) => { const model = editor.model; const selection = editor.model.document.selection; - const marker = getMarkerAtPosition( editor, selection.focus ); + const marker = getMarkerAtPosition( editor, selection.focus! ); if ( !marker ) { return; @@ -350,7 +372,7 @@ function getSelectAllHandler( editor ) { // If selection range is inside a restricted editing exception, select text only within the exception. // // Note: Second Ctrl+A press is also blocked and it won't select the entire text in the editor. - const selectionRange = selection.getFirstRange(); + const selectionRange = selection.getFirstRange()!; const markerRange = marker.getRange(); if ( markerRange.containsRange( selectionRange, true ) || selection.isCollapsed ) { @@ -363,30 +385,34 @@ function getSelectAllHandler( editor ) { }; } -// Additional rule for enabling "delete" and "deleteForward" commands if selection is on range boundaries: -// -// Does not allow to enable command when selection focus is: -// - is on marker start - "delete" - to prevent removing content before marker -// - is on marker end - "deleteForward" - to prevent removing content after marker -function isDeleteCommandOnMarkerBoundaries( commandName, selection, markerRange ) { - if ( commandName == 'delete' && markerRange.start.isEqual( selection.focus ) ) { +/** + * Additional rule for enabling "delete" and "deleteForward" commands if selection is on range boundaries: + * + * Does not allow to enable command when selection focus is: + * - is on marker start - "delete" - to prevent removing content before marker + * - is on marker end - "deleteForward" - to prevent removing content after marker + */ +function isDeleteCommandOnMarkerBoundaries( commandName: string, selection: DocumentSelection, markerRange: Range ) { + if ( commandName == 'delete' && markerRange.start.isEqual( selection.focus! ) ) { return true; } // Only for collapsed selection - non-collapsed selection that extends over a marker is handled elsewhere. - if ( commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) { + if ( commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isEqual( selection.focus! ) ) { return true; } return false; } -// Ensures that model.deleteContent() does not delete outside exception markers ranges. -// -// The enforced restrictions are: -// - only execute deleteContent() inside exception markers -// - restrict passed selection to exception marker -function restrictDeleteContent( editor ) { +/** + * Ensures that model.deleteContent() does not delete outside exception markers ranges. + * + * The enforced restrictions are: + * - only execute deleteContent() inside exception markers + * - restrict passed selection to exception marker + */ +function restrictDeleteContent( editor: Editor ): GetCallback { return ( evt, args ) => { const [ selection ] = args; @@ -421,10 +447,12 @@ function restrictDeleteContent( editor ) { }; } -// Ensures that input command is executed with a range that is inside exception marker. -// -// This restriction is due to fact that using native spell check changes text outside exception marker. -function disallowInputExecForWrongRange( editor ) { +/** + * Ensures that input command is executed with a range that is inside exception marker. + * + * This restriction is due to fact that using native spell check changes text outside exception marker. + */ +function disallowInputExecForWrongRange( editor: Editor ): GetCallback { return ( evt, args ) => { const [ options ] = args; const { range } = options; @@ -441,31 +469,35 @@ function disallowInputExecForWrongRange( editor ) { }; } -function isRangeInsideSingleMarker( editor, range ) { +function isRangeInsideSingleMarker( editor: Editor, range: Range ) { const markerAtStart = getMarkerAtPosition( editor, range.start ); const markerAtEnd = getMarkerAtPosition( editor, range.end ); return markerAtStart && markerAtEnd && markerAtEnd === markerAtStart; } -// Checks if new marker range is flat. Non-flat ranges might appear during upcast conversion in nested structures, ie tables. -// -// Note: This marker fixer only consider case which is possible to create using StandardEditing mode plugin. -// Markers created by developer in the data might break in many other ways. -// -// See #6003. -function ensureNewMarkerIsFlatPostFixer( editor ) { +/** + * Checks if new marker range is flat. Non-flat ranges might appear during upcast conversion in nested structures, ie tables. + * + * Note: This marker fixer only consider case which is possible to create using StandardEditing mode plugin. + * Markers created by developer in the data might break in many other ways. + * + * See #6003. + */ +function ensureNewMarkerIsFlatPostFixer( editor: Editor ): ModelPostFixer { return writer => { let changeApplied = false; const changedMarkers = editor.model.document.differ.getChangedMarkers(); - for ( const { data: { newRange, oldRange }, name } of changedMarkers ) { + for ( const { data, name } of changedMarkers ) { if ( !name.startsWith( 'restrictedEditingException' ) ) { continue; } - if ( !oldRange && !newRange.isFlat ) { + const newRange = data.newRange!; + + if ( !data.oldRange && !newRange.isFlat ) { const start = newRange.start; const end = newRange.end; @@ -486,7 +518,9 @@ function ensureNewMarkerIsFlatPostFixer( editor ) { }; } -function onlyAllowAttributesFromList( allowedAttributes ) { +function onlyAllowAttributesFromList( + allowedAttributes: RestrictedEditingModeConfig['allowedAttributes'] +): SchemaAttributeCheckCallback { return ( context, attributeName ) => { if ( context.startsWith( '$clipboardHolder' ) ) { return allowedAttributes.includes( attributeName ); @@ -494,8 +528,21 @@ function onlyAllowAttributesFromList( allowedAttributes ) { }; } -function allowTextOnlyInClipboardHolder( context, childDefinition ) { - if ( context.startsWith( '$clipboardHolder' ) ) { - return childDefinition.name === '$text'; +function allowTextOnlyInClipboardHolder(): SchemaChildCheckCallback { + return ( context, childDefinition ) => { + if ( context.startsWith( '$clipboardHolder' ) ) { + return childDefinition.name === '$text'; + } + }; +} + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ RestrictedEditingModeEditing.pluginName ]: RestrictedEditingModeEditing; + } + + interface CommandsMap { + goToPreviousRestrictedEditingException: RestrictedEditingNavigationCommand; + goToNextRestrictedEditingException: RestrictedEditingNavigationCommand; } } diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts similarity index 65% rename from packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts index 09064e7f95a..61e8d75e254 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts @@ -7,7 +7,8 @@ * @module restricted-editing/restrictededitingmodenavigationcommand */ -import { Command } from 'ckeditor5/src/core'; +import type { Model, Range } from 'ckeditor5/src/engine'; +import { Command, type Editor } from 'ckeditor5/src/core'; /** * The command that allows navigation across the exceptions in the edited document. @@ -15,32 +16,29 @@ import { Command } from 'ckeditor5/src/core'; * @extends module:core/command~Command */ export default class RestrictedEditingModeNavigationCommand extends Command { + /** + * The direction of the command. + */ + private _direction: RestrictedEditingModeNavigationDirection; + /** * Creates an instance of the command. * - * @param {module:core/editor/editor~Editor} editor The editor instance. - * @param {String} direction The direction that the command works. Can be either `'forward'` or `'backward'`. + * @param editor The editor instance. + * @param direction The direction that the command works. */ - constructor( editor, direction ) { + constructor( editor: Editor, direction: RestrictedEditingModeNavigationDirection ) { super( editor ); // It does not affect data so should be enabled in read-only mode and in restricted editing mode. this.affectsData = false; - - /** - * The direction of the command. Can be `'forward'` or `'backward'`. - * - * @readonly - * @private - * @member {String} - */ this._direction = direction; } /** * @inheritDoc */ - refresh() { + public override refresh(): void { this.isEnabled = this._checkEnabled(); } @@ -49,9 +47,13 @@ export default class RestrictedEditingModeNavigationCommand extends Command { * * @fires execute */ - execute() { + public override execute(): void { const position = getNearestExceptionRange( this.editor.model, this._direction ); + if ( !position ) { + return; + } + this.editor.model.change( writer => { writer.setSelection( position ); } ); @@ -60,23 +62,19 @@ export default class RestrictedEditingModeNavigationCommand extends Command { /** * Checks whether the command can be enabled in the current context. * - * @private - * @returns {Boolean} Whether the command should be enabled. + * @returns Whether the command should be enabled. */ - _checkEnabled() { + private _checkEnabled(): boolean { return !!getNearestExceptionRange( this.editor.model, this._direction ); } } -// Returns the range of the exception marker closest to the last position of the -// model selection. -// -// @param {module:engine/model/model~Model} model -// @param {String} direction Either "forward" or "backward". -// @returns {module:engine/model/range~Range|null} -function getNearestExceptionRange( model, direction ) { +/** + * Returns the range of the exception marker closest to the last position of the model selection. + */ +function getNearestExceptionRange( model: Model, direction: RestrictedEditingModeNavigationDirection ): Range | undefined { const selection = model.document.selection; - const selectionPosition = selection.getFirstPosition(); + const selectionPosition = selection.getFirstPosition()!; const markerRanges = []; // Get all exception marker positions that start after/before the selection position. @@ -105,16 +103,27 @@ function getNearestExceptionRange( model, direction ) { } if ( !markerRanges.length ) { - return null; + return; } // Get the marker closest to the selection position among many. To know that, we need to sort // them first. - return markerRanges.sort( ( rangeA, rangeB ) => { - if ( direction === 'forward' ) { - return rangeA.start.isAfter( rangeB.start ) ? 1 : -1; - } else { - return rangeA.start.isBefore( rangeB.start ) ? 1 : -1; - } - } ).shift(); + return markerRanges + .sort( ( rangeA, rangeB ) => { + if ( direction === 'forward' ) { + return rangeA.start.isAfter( rangeB.start ) ? 1 : -1; + } else { + return rangeA.start.isBefore( rangeB.start ) ? 1 : -1; + } + } ) + .shift(); +} + +/** + * Directions in which the + * {@link module:restricted-editing/restrictededitingmodenavigationcommand~RestrictedEditingModeNavigationCommand} can work. + */ +export enum RestrictedEditingModeNavigationDirection { + FORWARD = 'forward', + BACKWARD = 'backward' } diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.js b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts similarity index 66% rename from packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.js rename to packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts index 5bb48a935b5..981599fe458 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.js +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts @@ -8,7 +8,13 @@ */ import { Plugin } from 'ckeditor5/src/core'; -import { Model, createDropdown, addListToDropdown } from 'ckeditor5/src/ui'; +import { + Model, + createDropdown, + addListToDropdown, + type ButtonExecuteEvent, + type ListDropdownItemDefinition +} from 'ckeditor5/src/ui'; import { Collection } from 'ckeditor5/src/utils'; import lockIcon from '../theme/icons/contentlock.svg'; @@ -25,20 +31,20 @@ export default class RestrictedEditingModeUI extends Plugin { /** * @inheritDoc */ - static get pluginName() { + public static get pluginName(): 'RestrictedEditingModeUI' { return 'RestrictedEditingModeUI'; } /** * @inheritDoc */ - init() { + public init(): void { const editor = this.editor; const t = editor.t; editor.ui.componentFactory.add( 'restrictedEditing', locale => { const dropdownView = createDropdown( locale ); - const listItems = new Collection(); + const listItems = new Collection(); listItems.add( this._getButtonDefinition( 'goToPreviousRestrictedEditingException', @@ -61,8 +67,9 @@ export default class RestrictedEditingModeUI extends Plugin { isOn: false } ); - this.listenTo( dropdownView, 'execute', evt => { - editor.execute( evt.source._commandName ); + this.listenTo( dropdownView, 'execute', evt => { + const { _commandName } = evt.source as any; + editor.execute( _commandName ); editor.editing.view.focus(); } ); @@ -72,18 +79,16 @@ export default class RestrictedEditingModeUI extends Plugin { /** * Returns a definition of the navigation button to be used in the dropdown. - * - * @private - * @param {String} commandName The name of the command that the button represents. - * @param {String} label The translated label of the button. - * @param {String} keystroke The button keystroke. - * @returns {module:ui/dropdown/utils~ListDropdownItemDefinition} + + * @param commandName The name of the command that the button represents. + * @param label The translated label of the button. + * @param keystroke The button keystroke. */ - _getButtonDefinition( commandName, label, keystroke ) { + private _getButtonDefinition( commandName: string, label: string, keystroke: string ): ListDropdownItemDefinition { const editor = this.editor; - const command = editor.commands.get( commandName ); + const command = editor.commands.get( commandName )!; const definition = { - type: 'button', + type: 'button' as const, model: new Model( { label, withText: true, @@ -98,3 +103,9 @@ export default class RestrictedEditingModeUI extends Plugin { return definition; } } + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ RestrictedEditingModeUI.pluginName ]: RestrictedEditingModeUI; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmode.js b/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts similarity index 76% rename from packages/ckeditor5-restricted-editing/src/standardeditingmode.js rename to packages/ckeditor5-restricted-editing/src/standardeditingmode.ts index a9e7a0425bc..c48ed980c23 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmode.js +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts @@ -7,7 +7,7 @@ * @module restricted-editing/standardeditingmode */ -import { Plugin } from 'ckeditor5/src/core'; +import { Plugin, type PluginDependencies } from 'ckeditor5/src/core'; import StandardEditingModeEditing from './standardeditingmodeediting'; import StandardEditingModeUI from './standardeditingmodeui'; @@ -28,11 +28,17 @@ export default class StandardEditingMode extends Plugin { /** * @inheritDoc */ - static get pluginName() { + public static get pluginName(): 'StandardEditingMode' { return 'StandardEditingMode'; } - static get requires() { + public static get requires(): PluginDependencies { return [ StandardEditingModeEditing, StandardEditingModeUI ]; } } + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ StandardEditingMode.pluginName ]: StandardEditingMode; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.js b/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts similarity index 84% rename from packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.js rename to packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts index 58299cd8938..4ecb65d4ff2 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.js +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts @@ -24,14 +24,14 @@ export default class StandardEditingModeEditing extends Plugin { /** * @inheritDoc */ - static get pluginName() { + public static get pluginName(): 'StandardEditingModeEditing' { return 'StandardEditingModeEditing'; } /** * @inheritDoc */ - init() { + public init(): void { const editor = this.editor; editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } ); @@ -63,3 +63,13 @@ export default class StandardEditingModeEditing extends Plugin { } ); } } + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ StandardEditingModeEditing.pluginName ]: StandardEditingModeEditing; + } + + interface CommandsMap { + restrictedEditingException: RestrictedEditingExceptionCommand; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.js b/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts similarity index 82% rename from packages/ckeditor5-restricted-editing/src/standardeditingmodeui.js rename to packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts index 7b5a2d92ab2..c26e74c9c49 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.js +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts @@ -23,12 +23,19 @@ export default class StandardEditingModeUI extends Plugin { /** * @inheritDoc */ - init() { + public static get pluginName(): 'StandardEditingModeUI' { + return 'StandardEditingModeUI'; + } + + /** + * @inheritDoc + */ + public init(): void { const editor = this.editor; const t = editor.t; editor.ui.componentFactory.add( 'restrictedEditingException', locale => { - const command = editor.commands.get( 'restrictedEditingException' ); + const command = editor.commands.get( 'restrictedEditingException' )!; const view = new ButtonView( locale ); view.set( { @@ -51,3 +58,9 @@ export default class StandardEditingModeUI extends Plugin { } ); } } + +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ StandardEditingModeUI.pluginName ]: StandardEditingModeUI; + } +} diff --git a/packages/ckeditor5-restricted-editing/tsconfig.json b/packages/ckeditor5-restricted-editing/tsconfig.json new file mode 100644 index 00000000000..9d4c891939c --- /dev/null +++ b/packages/ckeditor5-restricted-editing/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "ckeditor5/tsconfig.json", + "include": [ + "src", + "../../typings" + ] +} diff --git a/packages/ckeditor5-restricted-editing/tsconfig.release.json b/packages/ckeditor5-restricted-editing/tsconfig.release.json new file mode 100644 index 00000000000..6d2d43909f9 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/tsconfig.release.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.release.json", + "include": [ + "./src/", + "../../typings/" + ], + "exclude": [ + "./tests/" + ] +} From bdee9a12091434ca07ae058dd1f306935526b26a Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 9 Feb 2023 12:50:23 +0100 Subject: [PATCH 5/8] Update types and comments --- .../src/restrictededitingconfig.ts | 77 +++++++++++++++++++ .../src/restrictededitingexceptioncommand.ts | 8 +- .../src/restrictededitingmode.ts | 68 +--------------- .../src/restrictededitingmode/utils.ts | 10 ++- .../src/restrictededitingmodeediting.ts | 62 +++++++-------- .../restrictededitingmodenavigationcommand.ts | 10 ++- .../src/restrictededitingmodeui.ts | 9 ++- .../src/standardeditingmode.ts | 2 - .../src/standardeditingmodeediting.ts | 6 -- .../src/standardeditingmodeui.ts | 5 +- packages/ckeditor5-typing/src/index.ts | 3 +- 11 files changed, 139 insertions(+), 121 deletions(-) create mode 100644 packages/ckeditor5-restricted-editing/src/restrictededitingconfig.ts diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingconfig.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingconfig.ts new file mode 100644 index 00000000000..90333582a35 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingconfig.ts @@ -0,0 +1,77 @@ +/** + * @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 restricted-editing/restrictededitingconfig + */ + +/** + * The configuration of the restricted editing mode feature. + * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * ```ts + * ClassicEditor + * .create( { + * restrictedEditing: { + * allowedCommands: [ 'bold', 'link', 'unlink' ], + * allowedAttributes: [ 'bold', 'linkHref' ] + * } + * } ) + * .then( ... ) + * .catch( ... ); + * ``` + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + */ +export interface RestrictedEditingConfig { + + /** + * The command names allowed in non-restricted areas of the content. + * + * Defines which feature commands should be enabled in the restricted editing mode. The commands used for typing and deleting text + * (`'input'`, `'delete'` and `'deleteForward'`) are allowed by the feature inside non-restricted regions and do not need to be defined. + * + * **Note**: The restricted editing mode always allows to use the restricted mode navigation commands as well as `'undo'` and `'redo'` + * commands. + * + * The default value is: + * + * ```ts + * const restrictedEditingConfig = { + * allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ] + * }; + * ``` + * + * To make a command always enabled (also outside non-restricted areas) use + * {@link module:restricted-editing/restrictededitingconfig~RestrictedEditingModeEditing#enableCommand} method. + */ + allowedCommands: Array; + + /** + * The text attribute names allowed when pasting content ot non-restricted areas. + * + * The default value is: + * + * ```ts + * const restrictedEditingConfig = { + * allowedAttributes: [ 'bold', 'italic', 'linkHref' ] + * }; + * ``` + */ + allowedAttributes: Array; +} + +declare module '@ckeditor/ckeditor5-core' { + + /** + * The configuration of the restricted editing mode feature. Introduced by the + * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}. + */ + interface EditorConfig { + restrictedEditing?: RestrictedEditingConfig; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts index 3b0c3f6d789..d8d534e953c 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingexceptioncommand.ts @@ -11,7 +11,7 @@ import { Command } from 'ckeditor5/src/core'; import type { TreeWalkerValue } from 'ckeditor5/src/engine'; /** - * @extends module:core/command~Command + * The command that toggles exceptions from the restricted editing on text. */ export default class RestrictedEditingExceptionCommand extends Command { /** @@ -79,3 +79,9 @@ export default class RestrictedEditingExceptionCommand extends Command { export interface RestrictedEditingExceptionCommandParams { forceValue?: unknown; } + +declare module '@ckeditor/ckeditor5-core' { + interface CommandsMap { + restrictedEditingException: RestrictedEditingExceptionCommand; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts index d12d51152af..98ca7c71196 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts @@ -12,6 +12,7 @@ import { Plugin, type PluginDependencies } from 'ckeditor5/src/core'; import RestrictedEditingModeEditing from './restrictededitingmodeediting'; import RestrictedEditingModeUI from './restrictededitingmodeui'; +import './restrictededitingconfig'; import '../theme/restrictedediting.css'; /** @@ -21,8 +22,6 @@ import '../theme/restrictedediting.css'; * * * The {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing restricted mode editing feature}. * * The {@link module:restricted-editing/restrictededitingmodeui~RestrictedEditingModeUI restricted mode UI feature}. - * - * @extends module:core/plugin~Plugin */ export default class RestrictedEditingMode extends Plugin { /** @@ -40,73 +39,8 @@ export default class RestrictedEditingMode extends Plugin { } } -/** - * The configuration of the restricted editing mode feature. - * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. - * - * ```ts - * ClassicEditor - * .create( { - * restrictedEditing: { - * allowedCommands: [ 'bold', 'link', 'unlink' ], - * allowedAttributes: [ 'bold', 'linkHref' ] - * } - * } ) - * .then( ... ) - * .catch( ... ); - * ``` - * - * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. - */ -export interface RestrictedEditingModeConfig { - - /** - * The command names allowed in non-restricted areas of the content. - * - * Defines which feature commands should be enabled in the restricted editing mode. The commands used for typing and deleting text - * (`'input'`, `'delete'` and `'deleteForward'`) are allowed by the feature inside non-restricted regions and do not need to be defined. - * - * **Note**: The restricted editing mode always allows to use the restricted mode navigation commands as well as `'undo'` and `'redo'` - * commands. - * - * The default value is: - * - * ```ts - * const restrictedEditingConfig = { - * allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ] - * }; - * ``` - * - * To make a command always enabled (also outside non-restricted areas) use - * {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing#enableCommand} method. - */ - allowedCommands: Array; - - /** - * The text attribute names allowed when pasting content ot non-restricted areas. - * - * The default value is: - * - * ```ts - * const restrictedEditingConfig = { - * allowedAttributes: [ 'bold', 'italic', 'linkHref' ] - * }; - */ - allowedAttributes: Array; -} - declare module '@ckeditor/ckeditor5-core' { interface PluginsMap { [ RestrictedEditingMode.pluginName ]: RestrictedEditingMode; } - - /** - * The configuration of the restricted editing mode feature. Introduced by the - * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. - * - * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}. - */ - interface EditorConfig { - restrictedEditing?: RestrictedEditingModeConfig; - } } diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts index 37bf18997b7..089507742ec 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts @@ -41,10 +41,12 @@ export function isPositionInRangeBoundaries( range: Range, position: Position ): /** * Checks if the selection is fully contained in the marker. Positions on marker boundaries are considered "in". * - * []foo -> true - * f[oo] -> true - * f[oo ba]r -> false - * foo []bar -> false + * ```ts + * []foo -> true + * f[oo] -> true + * f[oo ba]r -> false + * foo []bar -> false + * ``` */ export function isSelectionInMarker( selection: DocumentSelection, marker?: Marker ): boolean { if ( !marker ) { diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts index 5e24d5b5b21..60845d8bb9d 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts @@ -7,7 +7,12 @@ * @module restricted-editing/restrictededitingmodeediting */ -import { Plugin, type Editor, type EditingKeystrokeCallback } from 'ckeditor5/src/core'; +import { + Plugin, + type Command, + type Editor, + type EditingKeystrokeCallback +} from 'ckeditor5/src/core'; import type { DocumentSelection, Marker, @@ -18,10 +23,15 @@ import type { SchemaChildCheckCallback } from 'ckeditor5/src/engine'; import type { BaseEvent, GetCallback } from 'ckeditor5/src/utils'; -import type { ClipboardContentInsertionEvent, ClipboardOutputEvent } from 'ckeditor5/src/clipboard'; +import type { InsertTextCommand, InsertTextCommandExecuteEvent } from 'ckeditor5/src/typing'; +import type { + ClipboardContentInsertionEvent, + ClipboardOutputEvent, + ClipboardPipeline +} from 'ckeditor5/src/clipboard'; import { - default as RestrictedEditingNavigationCommand, + default as RestrictedEditingModeNavigationCommand, RestrictedEditingModeNavigationDirection } from './restrictededitingmodenavigationcommand'; import { @@ -31,8 +41,7 @@ import { upcastHighlightToMarker } from './restrictededitingmode/converters'; import { getMarkerAtPosition, isSelectionInMarker } from './restrictededitingmode/utils'; -import type { RestrictedEditingModeConfig } from './restrictededitingmode'; -import type { InsertTextCommandExecuteEvent } from 'ckeditor5/src/typing'; +import type { RestrictedEditingConfig } from './restrictededitingconfig'; const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode'; @@ -42,8 +51,6 @@ const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode'; * * It introduces the exception marker group that renders to `` elements with the `restricted-editing-exception` CSS class. * * It registers the `'goToPreviousRestrictedEditingException'` and `'goToNextRestrictedEditingException'` commands. * * It also enables highlighting exception markers that are selected. - * - * @extends module:core/plugin~Plugin */ export default class RestrictedEditingModeEditing extends Plugin { /** @@ -87,7 +94,7 @@ export default class RestrictedEditingModeEditing extends Plugin { public init(): void { const editor = this.editor; const editingView = editor.editing.view; - const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' )!; + const allowedCommands: RestrictedEditingConfig['allowedCommands'] = editor.config.get( 'restrictedEditing.allowedCommands' )!; allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) ); @@ -98,12 +105,12 @@ export default class RestrictedEditingModeEditing extends Plugin { // Commands & keystrokes that allow navigation in the content. editor.commands.add( 'goToPreviousRestrictedEditingException', - new RestrictedEditingNavigationCommand( editor, RestrictedEditingModeNavigationDirection.BACKWARD ) + new RestrictedEditingModeNavigationCommand( editor, RestrictedEditingModeNavigationDirection.BACKWARD ) ); editor.commands.add( 'goToNextRestrictedEditingException', - new RestrictedEditingNavigationCommand( editor, RestrictedEditingModeNavigationDirection.FORWARD ) + new RestrictedEditingModeNavigationCommand( editor, RestrictedEditingModeNavigationDirection.FORWARD ) ); editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) ); @@ -122,12 +129,12 @@ export default class RestrictedEditingModeEditing extends Plugin { * of selection location). * * To enable some commands in non-restricted areas of the content use - * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands} configuration option. + * {@link module:restricted-editing/restrictededitingconfig~RestrictedEditingConfig#allowedCommands} configuration option. * * @param commandName Name of the command to enable. */ public enableCommand( commandName: string ): void { - const command = this.editor.commands.get( commandName )!; + const command: Command = this.editor.commands.get( commandName )!; command.clearForceDisabled( COMMAND_FORCE_DISABLE_ID ); @@ -217,12 +224,12 @@ export default class RestrictedEditingModeEditing extends Plugin { const model = editor.model; const selection = model.document.selection; const viewDoc = editor.editing.view.document; - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + const clipboard: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' ); this.listenTo( model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } ); - const inputCommand = editor.commands.get( 'input' ); - const insertTextCommand = editor.commands.get( 'insertText' ); + const inputCommand: InsertTextCommand | undefined = editor.commands.get( 'input' ); + const insertTextCommand: InsertTextCommand | undefined = editor.commands.get( 'insertText' ); // The restricted editing might be configured without input support - ie allow only bolding or removing text. // This check is bit synthetic since only tests are used this way. @@ -260,7 +267,7 @@ export default class RestrictedEditingModeEditing extends Plugin { } }, { priority: 'high' } ); - const allowedAttributes = editor.config.get( 'restrictedEditing.allowedAttributes' )!; + const allowedAttributes: RestrictedEditingConfig['allowedAttributes'] = editor.config.get( 'restrictedEditing.allowedAttributes' )!; model.schema.addAttributeCheck( onlyAllowAttributesFromList( allowedAttributes ) ); model.schema.addChildCheck( allowTextOnlyInClipboardHolder() ); } @@ -342,12 +349,18 @@ export default class RestrictedEditingModeEditing extends Plugin { } } +declare module '@ckeditor/ckeditor5-core' { + interface PluginsMap { + [ RestrictedEditingModeEditing.pluginName ]: RestrictedEditingModeEditing; + } +} + /** * Helper method for executing enabled commands only. */ function getCommandExecuter( editor: Editor, commandName: string ): EditingKeystrokeCallback { return ( _, cancel ) => { - const command = editor.commands.get( commandName )!; + const command: Command = editor.commands.get( commandName )!; if ( command.isEnabled ) { editor.execute( commandName ); @@ -518,9 +531,7 @@ function ensureNewMarkerIsFlatPostFixer( editor: Editor ): ModelPostFixer { }; } -function onlyAllowAttributesFromList( - allowedAttributes: RestrictedEditingModeConfig['allowedAttributes'] -): SchemaAttributeCheckCallback { +function onlyAllowAttributesFromList( allowedAttributes: RestrictedEditingConfig['allowedAttributes'] ): SchemaAttributeCheckCallback { return ( context, attributeName ) => { if ( context.startsWith( '$clipboardHolder' ) ) { return allowedAttributes.includes( attributeName ); @@ -535,14 +546,3 @@ function allowTextOnlyInClipboardHolder(): SchemaChildCheckCallback { } }; } - -declare module '@ckeditor/ckeditor5-core' { - interface PluginsMap { - [ RestrictedEditingModeEditing.pluginName ]: RestrictedEditingModeEditing; - } - - interface CommandsMap { - goToPreviousRestrictedEditingException: RestrictedEditingNavigationCommand; - goToNextRestrictedEditingException: RestrictedEditingNavigationCommand; - } -} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts index 61e8d75e254..ba7ebe16d3b 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts @@ -12,8 +12,6 @@ import { Command, type Editor } from 'ckeditor5/src/core'; /** * The command that allows navigation across the exceptions in the edited document. - * - * @extends module:core/command~Command */ export default class RestrictedEditingModeNavigationCommand extends Command { /** @@ -127,3 +125,11 @@ export enum RestrictedEditingModeNavigationDirection { FORWARD = 'forward', BACKWARD = 'backward' } + +declare module '@ckeditor/ckeditor5-core' { + + interface CommandsMap { + goToPreviousRestrictedEditingException: RestrictedEditingModeNavigationCommand; + goToNextRestrictedEditingException: RestrictedEditingModeNavigationCommand; + } +} diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts index 981599fe458..b23c2b99d26 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeui.ts @@ -7,7 +7,10 @@ * @module restricted-editing/restrictededitingmodeui */ -import { Plugin } from 'ckeditor5/src/core'; +import { + Plugin, + type Command +} from 'ckeditor5/src/core'; import { Model, createDropdown, @@ -24,8 +27,6 @@ import lockIcon from '../theme/icons/contentlock.svg'; * * It introduces the `'restrictedEditing'` dropdown that offers tools to navigate between exceptions across * the document. - * - * @extends module:core/plugin~Plugin */ export default class RestrictedEditingModeUI extends Plugin { /** @@ -86,7 +87,7 @@ export default class RestrictedEditingModeUI extends Plugin { */ private _getButtonDefinition( commandName: string, label: string, keystroke: string ): ListDropdownItemDefinition { const editor = this.editor; - const command = editor.commands.get( commandName )!; + const command: Command = editor.commands.get( commandName )!; const definition = { type: 'button' as const, model: new Model( { diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts b/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts index c48ed980c23..379cb9140ac 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmode.ts @@ -21,8 +21,6 @@ import '../theme/restrictedediting.css'; * * * The {@link module:restricted-editing/standardeditingmodeediting~StandardEditingModeEditing standard mode editing feature}. * * The {@link module:restricted-editing/standardeditingmodeui~StandardEditingModeUI standard mode UI feature}. - * - * @extends module:core/plugin~Plugin */ export default class StandardEditingMode extends Plugin { /** diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts b/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts index 4ecb65d4ff2..59b3d0bf72a 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmodeediting.ts @@ -17,8 +17,6 @@ import RestrictedEditingExceptionCommand from './restrictededitingexceptioncomma * * It introduces the `restrictedEditingException` text attribute that is rendered as * a `` element with the `restricted-editing-exception` CSS class. * * It registers the `'restrictedEditingException'` command. - * - * @extends module:core/plugin~Plugin */ export default class StandardEditingModeEditing extends Plugin { /** @@ -68,8 +66,4 @@ declare module '@ckeditor/ckeditor5-core' { interface PluginsMap { [ StandardEditingModeEditing.pluginName ]: StandardEditingModeEditing; } - - interface CommandsMap { - restrictedEditingException: RestrictedEditingExceptionCommand; - } } diff --git a/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts b/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts index c26e74c9c49..8cc4a651816 100644 --- a/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts +++ b/packages/ckeditor5-restricted-editing/src/standardeditingmodeui.ts @@ -11,13 +11,12 @@ import { Plugin } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; import unlockIcon from '../theme/icons/contentunlock.svg'; +import type RestrictedEditingExceptionCommand from './restrictededitingexceptioncommand'; /** * The standard editing mode UI feature. * * It introduces the `'restrictedEditingException'` button that marks text as unrestricted for editing. - * - * @extends module:core/plugin~Plugin */ export default class StandardEditingModeUI extends Plugin { /** @@ -35,7 +34,7 @@ export default class StandardEditingModeUI extends Plugin { const t = editor.t; editor.ui.componentFactory.add( 'restrictedEditingException', locale => { - const command = editor.commands.get( 'restrictedEditingException' )!; + const command: RestrictedEditingExceptionCommand = editor.commands.get( 'restrictedEditingException' )!; const view = new ButtonView( locale ); view.set( { diff --git a/packages/ckeditor5-typing/src/index.ts b/packages/ckeditor5-typing/src/index.ts index 86c8f974d40..eb510a1405f 100644 --- a/packages/ckeditor5-typing/src/index.ts +++ b/packages/ckeditor5-typing/src/index.ts @@ -19,7 +19,8 @@ export { default as inlineHighlight } from './utils/inlinehighlight'; export { default as findAttributeRange } from './utils/findattributerange'; export { default as getLastTextLine, type LastTextLineData } from './utils/getlasttextline'; -export type { InsertTextCommandExecuteEvent } from './inserttextcommand'; +export { default as InsertTextCommand, type InsertTextCommandExecuteEvent } from './inserttextcommand'; + export type { TypingConfig } from './typingconfig'; export type { ViewDocumentDeleteEvent } from './deleteobserver'; export type { ViewDocumentInsertTextEvent } from './inserttextobserver'; From d7c831134090baad900857981c7b0505dbec33cd Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Thu, 9 Feb 2023 14:11:44 +0100 Subject: [PATCH 6/8] Remove unnecessary import --- .../ckeditor5-restricted-editing/src/restrictededitingmode.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts index 98ca7c71196..d9cc98e69b8 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode.ts @@ -12,7 +12,6 @@ import { Plugin, type PluginDependencies } from 'ckeditor5/src/core'; import RestrictedEditingModeEditing from './restrictededitingmodeediting'; import RestrictedEditingModeUI from './restrictededitingmodeui'; -import './restrictededitingconfig'; import '../theme/restrictedediting.css'; /** From e6dab54efc26c692cee7f7720b8c52318527b987 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Fri, 10 Feb 2023 10:19:53 +0100 Subject: [PATCH 7/8] Misc. fixes after review of ckeditor5-restricted-editing. --- .../src/restrictededitingmode/utils.ts | 2 +- .../src/restrictededitingmodeediting.ts | 9 +++------ .../src/restrictededitingmodenavigationcommand.ts | 5 +---- packages/ckeditor5-typing/src/input.ts | 5 ----- packages/ckeditor5-typing/src/inserttextcommand.ts | 7 +++++++ 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts index 089507742ec..e429a456b96 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmode/utils.ts @@ -41,7 +41,7 @@ export function isPositionInRangeBoundaries( range: Range, position: Position ): /** * Checks if the selection is fully contained in the marker. Positions on marker boundaries are considered "in". * - * ```ts + * ```xml * []foo -> true * f[oo] -> true * f[oo ba]r -> false diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts index 60845d8bb9d..f19d0c20434 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodeediting.ts @@ -30,10 +30,7 @@ import type { ClipboardPipeline } from 'ckeditor5/src/clipboard'; -import { - default as RestrictedEditingModeNavigationCommand, - RestrictedEditingModeNavigationDirection -} from './restrictededitingmodenavigationcommand'; +import RestrictedEditingModeNavigationCommand from './restrictededitingmodenavigationcommand'; import { extendMarkerOnTypingPostFixer, resurrectCollapsedMarkerPostFixer, @@ -105,12 +102,12 @@ export default class RestrictedEditingModeEditing extends Plugin { // Commands & keystrokes that allow navigation in the content. editor.commands.add( 'goToPreviousRestrictedEditingException', - new RestrictedEditingModeNavigationCommand( editor, RestrictedEditingModeNavigationDirection.BACKWARD ) + new RestrictedEditingModeNavigationCommand( editor, 'backward' ) ); editor.commands.add( 'goToNextRestrictedEditingException', - new RestrictedEditingModeNavigationCommand( editor, RestrictedEditingModeNavigationDirection.FORWARD ) + new RestrictedEditingModeNavigationCommand( editor, 'forward' ) ); editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) ); diff --git a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts index ba7ebe16d3b..1a789af6d45 100644 --- a/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts +++ b/packages/ckeditor5-restricted-editing/src/restrictededitingmodenavigationcommand.ts @@ -121,10 +121,7 @@ function getNearestExceptionRange( model: Model, direction: RestrictedEditingMod * Directions in which the * {@link module:restricted-editing/restrictededitingmodenavigationcommand~RestrictedEditingModeNavigationCommand} can work. */ -export enum RestrictedEditingModeNavigationDirection { - FORWARD = 'forward', - BACKWARD = 'backward' -} +export type RestrictedEditingModeNavigationDirection = 'forward' | 'backward'; declare module '@ckeditor/ckeditor5-core' { diff --git a/packages/ckeditor5-typing/src/input.ts b/packages/ckeditor5-typing/src/input.ts index 34d0afad97b..546e94251a3 100644 --- a/packages/ckeditor5-typing/src/input.ts +++ b/packages/ckeditor5-typing/src/input.ts @@ -148,11 +148,6 @@ export default class Input extends Plugin { } declare module '@ckeditor/ckeditor5-core' { - interface CommandsMap { - input: InsertTextCommand; - insertText: InsertTextCommand; - } - interface PluginsMap { [ Input.pluginName ]: Input; } diff --git a/packages/ckeditor5-typing/src/inserttextcommand.ts b/packages/ckeditor5-typing/src/inserttextcommand.ts index 4ffb94e7772..5d58d4acc9d 100644 --- a/packages/ckeditor5-typing/src/inserttextcommand.ts +++ b/packages/ckeditor5-typing/src/inserttextcommand.ts @@ -128,3 +128,10 @@ export interface InsertTextCommandExecuteEvent { data: [ options: InsertTextCommandOptions ] ]; } + +declare module '@ckeditor/ckeditor5-core' { + interface CommandsMap { + input: InsertTextCommand; + insertText: InsertTextCommand; + } +} From 978ee2bc6fbbf9a68d8246ea82752f27601247ed Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Fri, 10 Feb 2023 10:21:22 +0100 Subject: [PATCH 8/8] Added ckeditor5-restricted-editing/_src. --- .../_src/index.js | 15 + .../_src/restrictededitingexceptioncommand.js | 66 +++ .../_src/restrictededitingmode.js | 103 ++++ .../_src/restrictededitingmode/converters.js | 191 +++++++ .../_src/restrictededitingmode/utils.js | 70 +++ .../_src/restrictededitingmodeediting.js | 501 ++++++++++++++++++ .../restrictededitingmodenavigationcommand.js | 120 +++++ .../_src/restrictededitingmodeui.js | 100 ++++ .../_src/standardeditingmode.js | 38 ++ .../_src/standardeditingmodeediting.js | 65 +++ .../_src/standardeditingmodeui.js | 53 ++ 11 files changed, 1322 insertions(+) create mode 100644 packages/ckeditor5-restricted-editing/_src/index.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingexceptioncommand.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmode.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmode/converters.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmode/utils.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmodeediting.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmodenavigationcommand.js create mode 100644 packages/ckeditor5-restricted-editing/_src/restrictededitingmodeui.js create mode 100644 packages/ckeditor5-restricted-editing/_src/standardeditingmode.js create mode 100644 packages/ckeditor5-restricted-editing/_src/standardeditingmodeediting.js create mode 100644 packages/ckeditor5-restricted-editing/_src/standardeditingmodeui.js diff --git a/packages/ckeditor5-restricted-editing/_src/index.js b/packages/ckeditor5-restricted-editing/_src/index.js new file mode 100644 index 00000000000..35e9093cc8a --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/index.js @@ -0,0 +1,15 @@ +/** + * @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 restricted-editing + */ + +export { default as RestrictedEditingMode } from './restrictededitingmode'; +export { default as RestrictedEditingModeEditing } from './restrictededitingmodeediting'; +export { default as RestrictedEditingModeUI } from './restrictededitingmodeui'; +export { default as StandardEditingMode } from './standardeditingmode'; +export { default as StandardEditingModeEditing } from './standardeditingmodeediting'; +export { default as StandardEditingModeUI } from './standardeditingmodeui'; diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingexceptioncommand.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingexceptioncommand.js new file mode 100644 index 00000000000..7336b8fc8a9 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingexceptioncommand.js @@ -0,0 +1,66 @@ +/** + * @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 restricted-editing/restrictededitingexceptioncommand + */ + +import { Command } from 'ckeditor5/src/core'; + +/** + * @extends module:core/command~Command + */ +export default class RestrictedEditingExceptionCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const model = this.editor.model; + const doc = model.document; + + this.value = !!doc.selection.getAttribute( 'restrictedEditingException' ); + + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'restrictedEditingException' ); + } + + /** + * @inheritDoc + */ + execute( options = {} ) { + const model = this.editor.model; + const document = model.document; + const selection = document.selection; + const valueToSet = ( options.forceValue === undefined ) ? !this.value : options.forceValue; + + model.change( writer => { + const ranges = model.schema.getValidRanges( selection.getRanges(), 'restrictedEditingException' ); + + if ( selection.isCollapsed ) { + if ( valueToSet ) { + writer.setSelectionAttribute( 'restrictedEditingException', valueToSet ); + } else { + const isSameException = value => value.item.getAttribute( 'restrictedEditingException' ) === this.value; + const exceptionStart = selection.focus.getLastMatchingPosition( isSameException, { direction: 'backward' } ); + const exceptionEnd = selection.focus.getLastMatchingPosition( isSameException ); + const focus = selection.focus; + + writer.removeSelectionAttribute( 'restrictedEditingException' ); + + if ( !( focus.isEqual( exceptionStart ) || focus.isEqual( exceptionEnd ) ) ) { + writer.removeAttribute( 'restrictedEditingException', writer.createRange( exceptionStart, exceptionEnd ) ); + } + } + } else { + for ( const range of ranges ) { + if ( valueToSet ) { + writer.setAttribute( 'restrictedEditingException', valueToSet, range ); + } else { + writer.removeAttribute( 'restrictedEditingException', range ); + } + } + } + } ); + } +} diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmode.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode.js new file mode 100644 index 00000000000..cd62ddbab69 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode.js @@ -0,0 +1,103 @@ +/** + * @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 restricted-editing/restrictededitingmode + */ + +import { Plugin } from 'ckeditor5/src/core'; + +import RestrictedEditingModeEditing from './restrictededitingmodeediting'; +import RestrictedEditingModeUI from './restrictededitingmodeui'; + +import '../theme/restrictedediting.css'; + +/** + * The restricted editing mode plugin. + * + * This is a "glue" plugin which loads the following plugins: + * + * * The {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing restricted mode editing feature}. + * * The {@link module:restricted-editing/restrictededitingmodeui~RestrictedEditingModeUI restricted mode UI feature}. + * + * @extends module:core/plugin~Plugin + */ +export default class RestrictedEditingMode extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'RestrictedEditingMode'; + } + + /** + * @inheritDoc + */ + static get requires() { + return [ RestrictedEditingModeEditing, RestrictedEditingModeUI ]; + } +} + +/** + * The configuration of the restricted editing mode feature. Introduced by the + * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * Read more in {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig}. + * + * @member {module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig} + * module:core/editor/editorconfig~EditorConfig#restrictedEditing + */ + +/** + * The configuration of the restricted editing mode feature. + * The option is used by the {@link module:restricted-editing/restrictededitingmode~RestrictedEditingMode} feature. + * + * ClassicEditor + * .create( { + * restrictedEditing: { + * allowedCommands: [ 'bold', 'link', 'unlink' ], + * allowedAttributes: [ 'bold', 'linkHref' ] + * } + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig + */ + +/** + * The command names allowed in non-restricted areas of the content. + * + * Defines which feature commands should be enabled in the restricted editing mode. The commands used for typing and deleting text + * (`'input'`, `'delete'` and `'deleteForward'`) are allowed by the feature inside non-restricted regions and do not need to be defined. + * + * **Note**: The restricted editing mode always allows to use the restricted mode navigation commands as well as `'undo'` and `'redo'` + * commands. + * + * The default value is: + * + * const restrictedEditingConfig = { + * allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ] + * }; + * + * To make a command always enabled (also outside non-restricted areas) use + * {@link module:restricted-editing/restrictededitingmodeediting~RestrictedEditingModeEditing#enableCommand} method. + * + * @member {Array.} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands + */ + +/** + * The text attribute names allowed when pasting content ot non-restricted areas. + * + * The default value is: + * + * const restrictedEditingConfig = { + * allowedAttributes: [ 'bold', 'italic', 'linkHref' ] + * }; + * + * @member {Array.} module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedAttributes + */ diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/converters.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/converters.js new file mode 100644 index 00000000000..294d7686d3f --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/converters.js @@ -0,0 +1,191 @@ +/** + * @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 restricted-editing/restrictededitingmode/converters + */ + +import { Matcher } from 'ckeditor5/src/engine'; + +import { getMarkerAtPosition } from './utils'; + +const HIGHLIGHT_CLASS = 'restricted-editing-exception_selected'; + +/** + * Adds a visual highlight style to a restricted editing exception that the selection is anchored to. + * + * The highlight is turned on by adding the `.restricted-editing-exception_selected` class to the + * exception in the view: + * + * * The class is removed before the conversion starts, as callbacks added with the `'highest'` priority + * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events. + * * The class is added in the view post-fixer, after other changes in the model tree are converted to the view. + * + * This way, adding and removing the highlight does not interfere with conversion. + * + * @param {module:core/editor/editor~Editor} editor + */ +export function setupExceptionHighlighting( editor ) { + const view = editor.editing.view; + const model = editor.model; + const highlightedMarkers = new Set(); + + // Adding the class. + view.document.registerPostFixer( writer => { + const modelSelection = model.document.selection; + + const marker = getMarkerAtPosition( editor, modelSelection.anchor ); + + if ( !marker ) { + return; + } + + for ( const viewElement of editor.editing.mapper.markerNameToElements( marker.name ) ) { + writer.addClass( HIGHLIGHT_CLASS, viewElement ); + highlightedMarkers.add( viewElement ); + } + } ); + + // Removing the class. + editor.conversion.for( 'editingDowncast' ).add( dispatcher => { + // Make sure the highlight is removed on every possible event, before conversion is started. + dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } ); + dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } ); + dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } ); + dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } ); + + function removeHighlight() { + view.change( writer => { + for ( const item of highlightedMarkers.values() ) { + writer.removeClass( HIGHLIGHT_CLASS, item ); + highlightedMarkers.delete( item ); + } + } ); + } + } ); +} + +/** + * A post-fixer that prevents removing a collapsed marker from the document. + * + * @param {module:core/editor/editor~Editor} editor + * @returns {Function} + */ +export function resurrectCollapsedMarkerPostFixer( editor ) { + // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778. + return writer => { + let changeApplied = false; + + for ( const { name, data } of editor.model.document.differ.getChangedMarkers() ) { + if ( name.startsWith( 'restrictedEditingException' ) && data.newRange && data.newRange.root.rootName == '$graveyard' ) { + writer.updateMarker( name, { + range: writer.createRange( writer.createPositionAt( data.oldRange.start ) ) + } ); + + changeApplied = true; + } + } + + return changeApplied; + }; +} + +/** + * A post-fixer that extends a marker when the user types on its boundaries. + * + * @param {module:core/editor/editor~Editor} editor + * @returns {Function} + */ +export function extendMarkerOnTypingPostFixer( editor ) { + // This post-fixer shouldn't be necessary after https://github.com/ckeditor/ckeditor5/issues/5778. + return writer => { + let changeApplied = false; + + for ( const change of editor.model.document.differ.getChanges() ) { + if ( change.type == 'insert' && change.name == '$text' ) { + changeApplied = _tryExtendMarkerStart( editor, change.position, change.length, writer ) || changeApplied; + changeApplied = _tryExtendMarkedEnd( editor, change.position, change.length, writer ) || changeApplied; + } + } + + return changeApplied; + }; +} + +/** + * A view highlight-to-marker conversion helper. + * + * @param {Object} config Conversion configuration. + * @param {module:engine/view/matcher~MatcherPattern} [config.view] A pattern matching all view elements which should be converted. If not + * set, the converter will fire for every view element. + * @param {String|module:engine/model/element~Element|Function} config.model The name of the model element, a model element + * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. + * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. + */ +export function upcastHighlightToMarker( config ) { + return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => { + const { writer } = conversionApi; + + const matcher = new Matcher( config.view ); + const matcherResult = matcher.match( data.viewItem ); + + // If there is no match, this callback should not do anything. + if ( !matcherResult ) { + return; + } + + const match = matcherResult.match; + + // Force consuming element's name (taken from upcast helpers elementToElement converter). + match.name = true; + + const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor ); + conversionApi.consumable.consume( data.viewItem, match ); + + const markerName = config.model( data.viewItem ); + const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } ); + const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } ); + + // Insert in reverse order to use converter content positions directly (without recalculating). + writer.insert( fakeMarkerEnd, convertedChildrenRange.end ); + writer.insert( fakeMarkerStart, convertedChildrenRange.start ); + + data.modelRange = writer.createRange( + writer.createPositionBefore( fakeMarkerStart ), + writer.createPositionAfter( fakeMarkerEnd ) + ); + data.modelCursor = data.modelRange.end; + } ); +} + +// Extend marker if change detected on marker's start position. +function _tryExtendMarkerStart( editor, position, length, writer ) { + const markerAtStart = getMarkerAtPosition( editor, position.getShiftedBy( length ) ); + + if ( markerAtStart && markerAtStart.getStart().isEqual( position.getShiftedBy( length ) ) ) { + writer.updateMarker( markerAtStart, { + range: writer.createRange( markerAtStart.getStart().getShiftedBy( -length ), markerAtStart.getEnd() ) + } ); + + return true; + } + + return false; +} + +// Extend marker if change detected on marker's end position. +function _tryExtendMarkedEnd( editor, position, length, writer ) { + const markerAtEnd = getMarkerAtPosition( editor, position ); + + if ( markerAtEnd && markerAtEnd.getEnd().isEqual( position ) ) { + writer.updateMarker( markerAtEnd, { + range: writer.createRange( markerAtEnd.getStart(), markerAtEnd.getEnd().getShiftedBy( length ) ) + } ); + + return true; + } + + return false; +} diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/utils.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/utils.js new file mode 100644 index 00000000000..03e4122d463 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmode/utils.js @@ -0,0 +1,70 @@ +/** + * @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 restricted-editing/restrictededitingmode/utils + */ + +/** + * Returns a single "restricted-editing-exception" marker at a given position. Contrary to + * {@link module:engine/model/markercollection~MarkerCollection#getMarkersAtPosition}, it returnd a marker also when the postion is + * equal to one of the marker's start or end positions. + * + * @param {module:core/editor/editor~Editor} editor + * @param {module:engine/model/position~Position} position + * @returns {module:engine/model/markercollection~Marker|undefined} marker + */ +export function getMarkerAtPosition( editor, position ) { + for ( const marker of editor.model.markers ) { + const markerRange = marker.getRange(); + + if ( isPositionInRangeBoundaries( markerRange, position ) ) { + if ( marker.name.startsWith( 'restrictedEditingException:' ) ) { + return marker; + } + } + } +} + +/** + * Checks if the position is fully contained in the range. Positions equal to range start or end are considered "in". + * + * @param {module:engine/model/range~Range} range + * @param {module:engine/model/position~Position} position + * @returns {Boolean} + */ +export function isPositionInRangeBoundaries( range, position ) { + return ( + range.containsPosition( position ) || + range.end.isEqual( position ) || + range.start.isEqual( position ) + ); +} + +/** + * Checks if the selection is fully contained in the marker. Positions on marker boundaries are considered "in". + * + * []foo -> true + * f[oo] -> true + * f[oo ba]r -> false + * foo []bar -> false + * + * @param {module:engine/model/selection~Selection} selection + * @param {module:engine/model/markercollection~Marker} marker + * @returns {Boolean} + */ +export function isSelectionInMarker( selection, marker ) { + if ( !marker ) { + return false; + } + + const markerRange = marker.getRange(); + + if ( selection.isCollapsed ) { + return isPositionInRangeBoundaries( markerRange, selection.focus ); + } + + return markerRange.containsRange( selection.getFirstRange(), true ); +} diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeediting.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeediting.js new file mode 100644 index 00000000000..0b03f909ce3 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeediting.js @@ -0,0 +1,501 @@ +/** + * @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 restricted-editing/restrictededitingmodeediting + */ + +import { Plugin } from 'ckeditor5/src/core'; + +import RestrictedEditingNavigationCommand from './restrictededitingmodenavigationcommand'; +import { + extendMarkerOnTypingPostFixer, + resurrectCollapsedMarkerPostFixer, + setupExceptionHighlighting, + upcastHighlightToMarker +} from './restrictededitingmode/converters'; +import { getMarkerAtPosition, isSelectionInMarker } from './restrictededitingmode/utils'; + +const COMMAND_FORCE_DISABLE_ID = 'RestrictedEditingMode'; + +/** + * The restricted editing mode editing feature. + * + * * It introduces the exception marker group that renders to `` elements with the `restricted-editing-exception` CSS class. + * * It registers the `'goToPreviousRestrictedEditingException'` and `'goToNextRestrictedEditingException'` commands. + * * It also enables highlighting exception markers that are selected. + * + * @extends module:core/plugin~Plugin + */ +export default class RestrictedEditingModeEditing extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'RestrictedEditingModeEditing'; + } + + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + editor.config.define( 'restrictedEditing', { + allowedCommands: [ 'bold', 'italic', 'link', 'unlink' ], + allowedAttributes: [ 'bold', 'italic', 'linkHref' ] + } ); + + /** + * Command names that are enabled outside the non-restricted regions. + * + * @type {Set.} + * @private + */ + this._alwaysEnabled = new Set( [ 'undo', 'redo' ] ); + + /** + * Commands allowed in non-restricted areas. + * + * Commands always enabled combine typing feature commands: `'input'`, `'insertText'`, `'delete'`, and `'deleteForward'` with + * commands defined in the feature configuration. + * + * @type {Set} + * @private + */ + this._allowedInException = new Set( [ 'input', 'insertText', 'delete', 'deleteForward' ] ); + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const editingView = editor.editing.view; + const allowedCommands = editor.config.get( 'restrictedEditing.allowedCommands' ); + + allowedCommands.forEach( commandName => this._allowedInException.add( commandName ) ); + + this._setupConversion(); + this._setupCommandsToggling(); + this._setupRestrictions(); + + // Commands & keystrokes that allow navigation in the content. + editor.commands.add( 'goToPreviousRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'backward' ) ); + editor.commands.add( 'goToNextRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'forward' ) ); + editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) ); + editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) ); + editor.keystrokes.set( 'Ctrl+A', getSelectAllHandler( editor ) ); + + editingView.change( writer => { + for ( const root of editingView.document.roots ) { + writer.addClass( 'ck-restricted-editing_mode_restricted', root ); + } + } ); + } + + /** + * Makes the given command always enabled in the restricted editing mode (regardless + * of selection location). + * + * To enable some commands in non-restricted areas of the content use + * {@link module:restricted-editing/restrictededitingmode~RestrictedEditingModeConfig#allowedCommands} configuration option. + * + * @param {String} commandName Name of the command to enable. + */ + enableCommand( commandName ) { + const command = this.editor.commands.get( commandName ); + + command.clearForceDisabled( COMMAND_FORCE_DISABLE_ID ); + + this._alwaysEnabled.add( commandName ); + } + + /** + * Sets up the restricted mode editing conversion: + * + * * ucpast & downcast converters, + * * marker highlighting in the edting area, + * * marker post-fixers. + * + * @private + */ + _setupConversion() { + const editor = this.editor; + const model = editor.model; + const doc = model.document; + + // The restricted editing does not attach additional data to the zones so there's no need for smarter markers managing. + // Also, the markers will only be created when loading the data. + let markerNumber = 0; + + editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( { + view: { + name: 'span', + classes: 'restricted-editing-exception' + }, + model: () => { + markerNumber++; // Starting from restrictedEditingException:1 marker. + + return `restrictedEditingException:${ markerNumber }`; + } + } ) ); + + // Currently the marker helpers are tied to other use-cases and do not render a collapsed marker as highlight. + // That's why there are 2 downcast converters for them: + // 1. The default marker-to-highlight will wrap selected text with ``. + editor.conversion.for( 'downcast' ).markerToHighlight( { + model: 'restrictedEditingException', + // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker. + view: () => { + return { + name: 'span', + classes: 'restricted-editing-exception', + priority: -10 + }; + } + } ); + + // 2. But for collapsed marker we need to render it as an element. + // Additionally the editing pipeline should always display a collapsed marker. + editor.conversion.for( 'editingDowncast' ).markerToElement( { + model: 'restrictedEditingException', + view: ( markerData, { writer } ) => { + return writer.createUIElement( 'span', { + class: 'restricted-editing-exception restricted-editing-exception_collapsed' + } ); + } + } ); + + editor.conversion.for( 'dataDowncast' ).markerToElement( { + model: 'restrictedEditingException', + view: ( markerData, { writer } ) => { + return writer.createEmptyElement( 'span', { + class: 'restricted-editing-exception' + } ); + } + } ); + + doc.registerPostFixer( extendMarkerOnTypingPostFixer( editor ) ); + doc.registerPostFixer( resurrectCollapsedMarkerPostFixer( editor ) ); + doc.registerPostFixer( ensureNewMarkerIsFlatPostFixer( editor ) ); + + setupExceptionHighlighting( editor ); + } + + /** + * Setups additional editing restrictions beyond command toggling: + * + * * delete content range trimming + * * disabling input command outside exception marker + * * restricting clipboard holder to text only + * * restricting text attributes in content + * + * @private + */ + _setupRestrictions() { + const editor = this.editor; + const model = editor.model; + const selection = model.document.selection; + const viewDoc = editor.editing.view.document; + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + this.listenTo( model, 'deleteContent', restrictDeleteContent( editor ), { priority: 'high' } ); + + const inputCommand = editor.commands.get( 'input' ); + const insertTextCommand = editor.commands.get( 'insertText' ); + + // The restricted editing might be configured without input support - ie allow only bolding or removing text. + // This check is bit synthetic since only tests are used this way. + if ( inputCommand ) { + this.listenTo( inputCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } ); + } + + // The restricted editing might be configured without insert text support - ie allow only bolding or removing text. + // This check is bit synthetic since only tests are used this way. + if ( insertTextCommand ) { + this.listenTo( insertTextCommand, 'execute', disallowInputExecForWrongRange( editor ), { priority: 'high' } ); + } + + // Block clipboard outside exception marker on paste and drop. + this.listenTo( clipboard, 'contentInsertion', evt => { + if ( !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) { + evt.stop(); + } + } ); + + // Block clipboard outside exception marker on cut. + this.listenTo( viewDoc, 'clipboardOutput', ( evt, data ) => { + if ( data.method == 'cut' && !isRangeInsideSingleMarker( editor, selection.getFirstRange() ) ) { + evt.stop(); + } + }, { priority: 'high' } ); + + const allowedAttributes = editor.config.get( 'restrictedEditing.allowedAttributes' ); + model.schema.addAttributeCheck( onlyAllowAttributesFromList( allowedAttributes ) ); + model.schema.addChildCheck( allowTextOnlyInClipboardHolder ); + } + + /** + * Sets up the command toggling which enables or disables commands based on the user selection. + * + * @private + */ + _setupCommandsToggling() { + const editor = this.editor; + const model = editor.model; + const doc = model.document; + + this._disableCommands(); + + this.listenTo( doc.selection, 'change', this._checkCommands.bind( this ) ); + this.listenTo( doc, 'change:data', this._checkCommands.bind( this ) ); + } + + /** + * Checks if commands should be enabled or disabled based on the current selection. + * + * @private + */ + _checkCommands() { + const editor = this.editor; + const selection = editor.model.document.selection; + + if ( selection.rangeCount > 1 ) { + this._disableCommands(); + + return; + } + + const marker = getMarkerAtPosition( editor, selection.focus ); + + this._disableCommands(); + + if ( isSelectionInMarker( selection, marker ) ) { + this._enableCommands( marker ); + } + } + + /** + * Enables commands in non-restricted regions. + * + * @returns {module:engine/model/markercollection~Marker} marker + * @private + */ + _enableCommands( marker ) { + const editor = this.editor; + + for ( const [ commandName, command ] of editor.commands ) { + if ( !command.affectsData || this._alwaysEnabled.has( commandName ) ) { + continue; + } + + // Enable ony those commands that are allowed in the exception marker. + if ( !this._allowedInException.has( commandName ) ) { + continue; + } + + // Do not enable 'delete' and 'deleteForward' commands on the exception marker boundaries. + if ( isDeleteCommandOnMarkerBoundaries( commandName, editor.model.document.selection, marker.getRange() ) ) { + continue; + } + + command.clearForceDisabled( COMMAND_FORCE_DISABLE_ID ); + } + } + + /** + * Disables commands outside non-restricted regions. + * + * @private + */ + _disableCommands() { + const editor = this.editor; + + for ( const [ commandName, command ] of editor.commands ) { + if ( !command.affectsData || this._alwaysEnabled.has( commandName ) ) { + continue; + } + + command.forceDisabled( COMMAND_FORCE_DISABLE_ID ); + } + } +} + +// Helper method for executing enabled commands only. +function getCommandExecuter( editor, commandName ) { + return ( data, cancel ) => { + const command = editor.commands.get( commandName ); + + if ( command.isEnabled ) { + editor.execute( commandName ); + cancel(); + } + }; +} + +// Helper for handling Ctrl+A keydown behaviour. +function getSelectAllHandler( editor ) { + return ( data, cancel ) => { + const model = editor.model; + const selection = editor.model.document.selection; + const marker = getMarkerAtPosition( editor, selection.focus ); + + if ( !marker ) { + return; + } + + // If selection range is inside a restricted editing exception, select text only within the exception. + // + // Note: Second Ctrl+A press is also blocked and it won't select the entire text in the editor. + const selectionRange = selection.getFirstRange(); + const markerRange = marker.getRange(); + + if ( markerRange.containsRange( selectionRange, true ) || selection.isCollapsed ) { + cancel(); + + model.change( writer => { + writer.setSelection( marker.getRange() ); + } ); + } + }; +} + +// Additional rule for enabling "delete" and "deleteForward" commands if selection is on range boundaries: +// +// Does not allow to enable command when selection focus is: +// - is on marker start - "delete" - to prevent removing content before marker +// - is on marker end - "deleteForward" - to prevent removing content after marker +function isDeleteCommandOnMarkerBoundaries( commandName, selection, markerRange ) { + if ( commandName == 'delete' && markerRange.start.isEqual( selection.focus ) ) { + return true; + } + + // Only for collapsed selection - non-collapsed selection that extends over a marker is handled elsewhere. + if ( commandName == 'deleteForward' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) { + return true; + } + + return false; +} + +// Ensures that model.deleteContent() does not delete outside exception markers ranges. +// +// The enforced restrictions are: +// - only execute deleteContent() inside exception markers +// - restrict passed selection to exception marker +function restrictDeleteContent( editor ) { + return ( evt, args ) => { + const [ selection ] = args; + + const marker = getMarkerAtPosition( editor, selection.focus ) || getMarkerAtPosition( editor, selection.anchor ); + + // Stop method execution if marker was not found at selection focus. + if ( !marker ) { + evt.stop(); + + return; + } + + // Collapsed selection inside exception marker does not require fixing. + if ( selection.isCollapsed ) { + return; + } + + // Shrink the selection to the range inside exception marker. + const allowedToDelete = marker.getRange().getIntersection( selection.getFirstRange() ); + + // Some features uses selection passed to model.deleteContent() to set the selection afterwards. For this we need to properly modify + // either the document selection using change block... + if ( selection.is( 'documentSelection' ) ) { + editor.model.change( writer => { + writer.setSelection( allowedToDelete ); + } ); + } + // ... or by modifying passed selection instance directly. + else { + selection.setTo( allowedToDelete ); + } + }; +} + +// Ensures that input command is executed with a range that is inside exception marker. +// +// This restriction is due to fact that using native spell check changes text outside exception marker. +function disallowInputExecForWrongRange( editor ) { + return ( evt, args ) => { + const [ options ] = args; + const { range } = options; + + // Only check "input" command executed with a range value. + // Selection might be set in exception marker but passed range might point elsewhere. + if ( !range ) { + return; + } + + if ( !isRangeInsideSingleMarker( editor, range ) ) { + evt.stop(); + } + }; +} + +function isRangeInsideSingleMarker( editor, range ) { + const markerAtStart = getMarkerAtPosition( editor, range.start ); + const markerAtEnd = getMarkerAtPosition( editor, range.end ); + + return markerAtStart && markerAtEnd && markerAtEnd === markerAtStart; +} + +// Checks if new marker range is flat. Non-flat ranges might appear during upcast conversion in nested structures, ie tables. +// +// Note: This marker fixer only consider case which is possible to create using StandardEditing mode plugin. +// Markers created by developer in the data might break in many other ways. +// +// See #6003. +function ensureNewMarkerIsFlatPostFixer( editor ) { + return writer => { + let changeApplied = false; + + const changedMarkers = editor.model.document.differ.getChangedMarkers(); + + for ( const { data: { newRange, oldRange }, name } of changedMarkers ) { + if ( !name.startsWith( 'restrictedEditingException' ) ) { + continue; + } + + if ( !oldRange && !newRange.isFlat ) { + const start = newRange.start; + const end = newRange.end; + + const startIsHigherInTree = start.path.length > end.path.length; + + const fixedStart = startIsHigherInTree ? newRange.start : writer.createPositionAt( end.parent, 0 ); + const fixedEnd = startIsHigherInTree ? writer.createPositionAt( start.parent, 'end' ) : newRange.end; + + writer.updateMarker( name, { + range: writer.createRange( fixedStart, fixedEnd ) + } ); + + changeApplied = true; + } + } + + return changeApplied; + }; +} + +function onlyAllowAttributesFromList( allowedAttributes ) { + return ( context, attributeName ) => { + if ( context.startsWith( '$clipboardHolder' ) ) { + return allowedAttributes.includes( attributeName ); + } + }; +} + +function allowTextOnlyInClipboardHolder( context, childDefinition ) { + if ( context.startsWith( '$clipboardHolder' ) ) { + return childDefinition.name === '$text'; + } +} diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmodenavigationcommand.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodenavigationcommand.js new file mode 100644 index 00000000000..09064e7f95a --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodenavigationcommand.js @@ -0,0 +1,120 @@ +/** + * @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 restricted-editing/restrictededitingmodenavigationcommand + */ + +import { Command } from 'ckeditor5/src/core'; + +/** + * The command that allows navigation across the exceptions in the edited document. + * + * @extends module:core/command~Command + */ +export default class RestrictedEditingModeNavigationCommand extends Command { + /** + * Creates an instance of the command. + * + * @param {module:core/editor/editor~Editor} editor The editor instance. + * @param {String} direction The direction that the command works. Can be either `'forward'` or `'backward'`. + */ + constructor( editor, direction ) { + super( editor ); + + // It does not affect data so should be enabled in read-only mode and in restricted editing mode. + this.affectsData = false; + + /** + * The direction of the command. Can be `'forward'` or `'backward'`. + * + * @readonly + * @private + * @member {String} + */ + this._direction = direction; + } + + /** + * @inheritDoc + */ + refresh() { + this.isEnabled = this._checkEnabled(); + } + + /** + * Executes the command. + * + * @fires execute + */ + execute() { + const position = getNearestExceptionRange( this.editor.model, this._direction ); + + this.editor.model.change( writer => { + writer.setSelection( position ); + } ); + } + + /** + * Checks whether the command can be enabled in the current context. + * + * @private + * @returns {Boolean} Whether the command should be enabled. + */ + _checkEnabled() { + return !!getNearestExceptionRange( this.editor.model, this._direction ); + } +} + +// Returns the range of the exception marker closest to the last position of the +// model selection. +// +// @param {module:engine/model/model~Model} model +// @param {String} direction Either "forward" or "backward". +// @returns {module:engine/model/range~Range|null} +function getNearestExceptionRange( model, direction ) { + const selection = model.document.selection; + const selectionPosition = selection.getFirstPosition(); + const markerRanges = []; + + // Get all exception marker positions that start after/before the selection position. + for ( const marker of model.markers.getMarkersGroup( 'restrictedEditingException' ) ) { + const markerRange = marker.getRange(); + + // Checking parent because there two positions foo^^bar + // are touching but they will represent different markers. + const isMarkerRangeTouching = + selectionPosition.isTouching( markerRange.start ) && selectionPosition.hasSameParentAs( markerRange.start ) || + selectionPosition.isTouching( markerRange.end ) && selectionPosition.hasSameParentAs( markerRange.end ); + + // foo baz + // foo ba]z + // foo [] baz + // foo [] baz + if ( markerRange.containsPosition( selectionPosition ) || isMarkerRangeTouching ) { + continue; + } + + if ( direction === 'forward' && markerRange.start.isAfter( selectionPosition ) ) { + markerRanges.push( markerRange ); + } else if ( direction === 'backward' && markerRange.end.isBefore( selectionPosition ) ) { + markerRanges.push( markerRange ); + } + } + + if ( !markerRanges.length ) { + return null; + } + + // Get the marker closest to the selection position among many. To know that, we need to sort + // them first. + return markerRanges.sort( ( rangeA, rangeB ) => { + if ( direction === 'forward' ) { + return rangeA.start.isAfter( rangeB.start ) ? 1 : -1; + } else { + return rangeA.start.isBefore( rangeB.start ) ? 1 : -1; + } + } ).shift(); +} diff --git a/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeui.js b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeui.js new file mode 100644 index 00000000000..5bb48a935b5 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/restrictededitingmodeui.js @@ -0,0 +1,100 @@ +/** + * @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 restricted-editing/restrictededitingmodeui + */ + +import { Plugin } from 'ckeditor5/src/core'; +import { Model, createDropdown, addListToDropdown } from 'ckeditor5/src/ui'; +import { Collection } from 'ckeditor5/src/utils'; + +import lockIcon from '../theme/icons/contentlock.svg'; + +/** + * The restricted editing mode UI feature. + * + * It introduces the `'restrictedEditing'` dropdown that offers tools to navigate between exceptions across + * the document. + * + * @extends module:core/plugin~Plugin + */ +export default class RestrictedEditingModeUI extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'RestrictedEditingModeUI'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + + editor.ui.componentFactory.add( 'restrictedEditing', locale => { + const dropdownView = createDropdown( locale ); + const listItems = new Collection(); + + listItems.add( this._getButtonDefinition( + 'goToPreviousRestrictedEditingException', + t( 'Previous editable region' ), + 'Shift+Tab' + ) ); + listItems.add( this._getButtonDefinition( + 'goToNextRestrictedEditingException', + t( 'Next editable region' ), + 'Tab' + ) ); + + addListToDropdown( dropdownView, listItems ); + + dropdownView.buttonView.set( { + label: t( 'Navigate editable regions' ), + icon: lockIcon, + tooltip: true, + isEnabled: true, + isOn: false + } ); + + this.listenTo( dropdownView, 'execute', evt => { + editor.execute( evt.source._commandName ); + editor.editing.view.focus(); + } ); + + return dropdownView; + } ); + } + + /** + * Returns a definition of the navigation button to be used in the dropdown. + * + * @private + * @param {String} commandName The name of the command that the button represents. + * @param {String} label The translated label of the button. + * @param {String} keystroke The button keystroke. + * @returns {module:ui/dropdown/utils~ListDropdownItemDefinition} + */ + _getButtonDefinition( commandName, label, keystroke ) { + const editor = this.editor; + const command = editor.commands.get( commandName ); + const definition = { + type: 'button', + model: new Model( { + label, + withText: true, + keystroke, + withKeystroke: true, + _commandName: commandName + } ) + }; + + definition.model.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + return definition; + } +} diff --git a/packages/ckeditor5-restricted-editing/_src/standardeditingmode.js b/packages/ckeditor5-restricted-editing/_src/standardeditingmode.js new file mode 100644 index 00000000000..a9e7a0425bc --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/standardeditingmode.js @@ -0,0 +1,38 @@ +/** + * @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 restricted-editing/standardeditingmode + */ + +import { Plugin } from 'ckeditor5/src/core'; + +import StandardEditingModeEditing from './standardeditingmodeediting'; +import StandardEditingModeUI from './standardeditingmodeui'; + +import '../theme/restrictedediting.css'; + +/** + * The standard editing mode plugin. + * + * This is a "glue" plugin that loads the following plugins: + * + * * The {@link module:restricted-editing/standardeditingmodeediting~StandardEditingModeEditing standard mode editing feature}. + * * The {@link module:restricted-editing/standardeditingmodeui~StandardEditingModeUI standard mode UI feature}. + * + * @extends module:core/plugin~Plugin + */ +export default class StandardEditingMode extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'StandardEditingMode'; + } + + static get requires() { + return [ StandardEditingModeEditing, StandardEditingModeUI ]; + } +} diff --git a/packages/ckeditor5-restricted-editing/_src/standardeditingmodeediting.js b/packages/ckeditor5-restricted-editing/_src/standardeditingmodeediting.js new file mode 100644 index 00000000000..58299cd8938 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/standardeditingmodeediting.js @@ -0,0 +1,65 @@ +/** + * @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 restricted-editing/standardeditingmodeediting + */ + +import { Plugin } from 'ckeditor5/src/core'; + +import RestrictedEditingExceptionCommand from './restrictededitingexceptioncommand'; + +/** + * The standard editing mode editing feature. + * + * * It introduces the `restrictedEditingException` text attribute that is rendered as + * a `` element with the `restricted-editing-exception` CSS class. + * * It registers the `'restrictedEditingException'` command. + * + * @extends module:core/plugin~Plugin + */ +export default class StandardEditingModeEditing extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'StandardEditingModeEditing'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + editor.model.schema.extend( '$text', { allowAttributes: [ 'restrictedEditingException' ] } ); + + editor.conversion.for( 'upcast' ).elementToAttribute( { + model: 'restrictedEditingException', + view: { + name: 'span', + classes: 'restricted-editing-exception' + } + } ); + + editor.conversion.for( 'downcast' ).attributeToElement( { + model: 'restrictedEditingException', + view: ( modelAttributeValue, { writer } ) => { + if ( modelAttributeValue ) { + // Make the restricted editing outer-most in the view. + return writer.createAttributeElement( 'span', { class: 'restricted-editing-exception' }, { priority: -10 } ); + } + } + } ); + + editor.commands.add( 'restrictedEditingException', new RestrictedEditingExceptionCommand( editor ) ); + + editor.editing.view.change( writer => { + for ( const root of editor.editing.view.document.roots ) { + writer.addClass( 'ck-restricted-editing_mode_standard', root ); + } + } ); + } +} diff --git a/packages/ckeditor5-restricted-editing/_src/standardeditingmodeui.js b/packages/ckeditor5-restricted-editing/_src/standardeditingmodeui.js new file mode 100644 index 00000000000..7b5a2d92ab2 --- /dev/null +++ b/packages/ckeditor5-restricted-editing/_src/standardeditingmodeui.js @@ -0,0 +1,53 @@ +/** + * @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 restricted-editing/standardeditingmodeui + */ + +import { Plugin } from 'ckeditor5/src/core'; +import { ButtonView } from 'ckeditor5/src/ui'; + +import unlockIcon from '../theme/icons/contentunlock.svg'; + +/** + * The standard editing mode UI feature. + * + * It introduces the `'restrictedEditingException'` button that marks text as unrestricted for editing. + * + * @extends module:core/plugin~Plugin + */ +export default class StandardEditingModeUI extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + + editor.ui.componentFactory.add( 'restrictedEditingException', locale => { + const command = editor.commands.get( 'restrictedEditingException' ); + const view = new ButtonView( locale ); + + view.set( { + icon: unlockIcon, + tooltip: true, + isToggleable: true + } ); + + view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); + view.bind( 'label' ).to( command, 'value', value => { + return value ? t( 'Disable editing' ) : t( 'Enable editing' ); + } ); + + this.listenTo( view, 'execute', () => { + editor.execute( 'restrictedEditingException' ); + editor.editing.view.focus(); + } ); + + return view; + } ); + } +}