Skip to content

Commit

Permalink
Merge pull request #13759 from ckeditor/ck/13395-3
Browse files Browse the repository at this point in the history
Other (engine): `RootAttributeOperation` is now correctly handled by `Differ`. Root attribute changes will be returned in `Differ#getChangedRoots()`.

Internal (editor-multi-root): Introduced API to easily save and load root attributes. Introduced `EditorConfig#rootsAttributes` and `MultiRootEditor#getRootsAttributes()`. Closes #13395.

Internal (editor-multi-root): Introduced `MultiRootEditor#getFullData()`. The method returns document data for all roots in multi-root editor.

Internal (editor-multi-root): `MultiRootEditor#addRoot()` has a new option `attributes`.

Internal (engine): `model.Writer#detachRoot()` will now remove attributes from the detached root.
  • Loading branch information
scofalik committed Mar 28, 2023
2 parents 8c540f9 + 0c6b7ff commit c121061
Show file tree
Hide file tree
Showing 13 changed files with 877 additions and 64 deletions.
2 changes: 1 addition & 1 deletion packages/ckeditor5-core/src/editor/utils/dataapimixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface DataApi {
* editor.getData( { rootName: 'header' } ); // -> '<p>Content for header part.</p>'
* ```
*
* By default the editor outputs HTML. This can be controlled by injecting a different data processor.
* By default, the editor outputs HTML. This can be controlled by injecting a different data processor.
* See the {@glink features/markdown Markdown output} guide for more details.
*
* A warning is logged when you try to retrieve data for a detached root, as most probably this is a mistake. A detached root should
Expand Down
83 changes: 83 additions & 0 deletions packages/ckeditor5-editor-multi-root/src/augmentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @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
*/

import { type RootAttributes } from './multirooteditor';

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

/**
* Initial roots attributes for the document roots.
*
* **Note: This configuration option is supported only by the
* {@link module:editor-multi-root/multirooteditor~MultiRootEditor multi-root} editor type.**
*
* **Note: You must provide full set of attributes for each root. If an attribute is not set on a root, set the value to `null`.
* Only provided attribute keys will be returned by
* {@link module:editor-multi-root/multirooteditor~MultiRootEditor#getRootsAttributes}.**
*
* Roots attributes hold additional data related to the document roots, in addition to the regular document data (which usually is
* HTML). In roots attributes, for each root, you can store arbitrary key-value pairs with attributes connected with that root.
* Use it to store any custom data that is specific to your integration or custom features.
*
* Currently, roots attributes are not used only by any official plugins. This is a mechanism that is prepared for custom features
* and non-standard integrations. If you do not provide any custom feature that would use root attributes, you do not need to
* handle (save and load) this property.
*
* ```ts
* MultiRootEditor.create(
* // Roots for the editor:
* {
* uid1: document.querySelector( '#uid1' ),
* uid2: document.querySelector( '#uid2' ),
* uid3: document.querySelector( '#uid3' ),
* uid4: document.querySelector( '#uid4' )
* },
* // Config:
* {
* rootsAttributes: {
* uid1: { order: 20, isLocked: false }, // Third, unlocked.
* uid2: { order: 10, isLocked: true }, // Second, locked.
* uid3: { order: 30, isLocked: true }, // Fourth, locked.
* uid4: { order: 0, isLocked: false } // First, unlocked.
* }
* }
* )
* .then( ... )
* .catch( ... );
* ```
*
* Note, that the above code snippet is only an example. You need to implement your own features that will use these attributes.
*
* Roots attributes can be changed the same way as attributes set on other model nodes:
*
* ```ts
* editor.model.change( writer => {
* const root = editor.model.getRoot( 'uid3' );
*
* writer.setAttribute( 'order', 40, root );
* } );
* ```
*
* You can react to root attributes changes by listening to
* {@link module:engine/model/document~Document#change:data document `change:data` event}:
*
* ```ts
* editor.model.document.on( 'change:data', () => {
* const changedRoots = editor.model.document.differ.getChangedRoots();
*
* for ( const change of changedRoots ) {
* if ( change.attributes ) {
* const root = editor.model.getRoot( change.name );
*
* // ...
* }
* }
* } );
* ```
*/
rootsAttributes?: Record<string, RootAttributes>;
}
}
2 changes: 2 additions & 0 deletions packages/ckeditor5-editor-multi-root/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
*/

export { default as MultiRootEditor } from './multirooteditor';

import './augmentation';
142 changes: 133 additions & 9 deletions packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
*/
public readonly sourceElements: Record<string, HTMLElement>;

/**
* Holds attributes keys that were passed in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`}
* config property and should be returned by {@link #getRootsAttributes}.
*/
private readonly _registeredRootsAttributesKeys = new Set<string>();

/**
* Creates an instance of the multi-root editor.
*
Expand Down Expand Up @@ -121,6 +127,42 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
this.model.document.createRoot( '$root', rootName );
}

if ( this.config.get( 'rootsAttributes' ) ) {
const rootsAttributes = this.config.get( 'rootsAttributes' )!;

for ( const [ rootName, attributes ] of Object.entries( rootsAttributes ) ) {
if ( !rootNames.includes( rootName ) ) {
/**
* Trying to set attributes on a non-existing root.
*
* Roots specified in {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes} do not match initial
* editor roots.
*
* @error multi-root-editor-root-attributes-no-root
*/
throw new CKEditorError( 'multi-root-editor-root-attributes-no-root', null );
}

for ( const key of Object.keys( attributes ) ) {
this._registeredRootsAttributesKeys.add( key );
}
}

this.data.on( 'init', () => {
this.model.enqueueChange( { isUndoable: false }, writer => {
for ( const [ name, attributes ] of Object.entries( rootsAttributes ) ) {
const root = this.model.document.getRoot( name )!;

for ( const [ key, value ] of Object.entries( attributes ) ) {
if ( value !== null ) {
writer.setAttribute( key, value, root! );
}
}
}
} );
} );
}

const options = {
shouldToolbarGroupWhenFull: !this.config.get( 'toolbar.shouldNotGroupWhenFull' ),
editableElements: sourceIsData ? undefined : sourceElementsOrData as Record<string, HTMLElement>
Expand All @@ -133,12 +175,12 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
this.model.document.on( 'change:data', () => {
const changedRoots = this.model.document.differ.getChangedRoots();

for ( const [ rootName, isAttached ] of changedRoots ) {
const root = this.model.document.getRoot( rootName )!;
for ( const changes of changedRoots ) {
const root = this.model.document.getRoot( changes.name )!;

if ( isAttached ) {
if ( changes.state == 'attached' ) {
this.fire<AddRootEvent>( 'addRoot', root );
} else {
} else if ( changes.state == 'detached' ) {
this.fire<DetachRootEvent>( 'detachRoot', root );
}
}
Expand Down Expand Up @@ -234,6 +276,18 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
* editor.addRoot( 'myRoot' ); // Will create a root, a DOM editable element and append it to `#editors` container element.
* ```
*
* You can set root attributes on the new root while you add it:
*
* ```ts
* // Add a collapsed root at fourth position from top.
* // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes.
* editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } );
* ```
*
* See also {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes` configuration option}.
*
* Note that attributes keys of attributes added in `attributes` option are also included in {@link #getRootsAttributes} return value.
*
* By setting `isUndoable` flag to `true`, you can allow for detaching the root using the undo feature.
*
* Additionally, you can group adding multiple roots in one undo step. This can be useful if you add multiple roots that are
Expand All @@ -254,8 +308,12 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
* @param rootName Name of the root to add.
* @param options Additional options for the added root.
*/
public addRoot( rootName: string, { data = '', elementName = '$root', isUndoable = false }: AddRootOptions = {} ): void {
public addRoot(
rootName: string,
{ data = '', attributes = {}, elementName = '$root', isUndoable = false }: AddRootOptions = {}
): void {
const dataController = this.data;
const registeredKeys = this._registeredRootsAttributesKeys;

if ( isUndoable ) {
this.model.change( _addRoot );
Expand All @@ -269,6 +327,11 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
if ( data ) {
writer.insert( dataController.parse( data, root ), root, 0 );
}

for ( const key of Object.keys( attributes ) ) {
registeredKeys.add( key );
writer.setAttribute( key, attributes[ key ], root );
}
}
}

Expand Down Expand Up @@ -370,6 +433,49 @@ export default class MultiRootEditor extends DataApiMixin( Editor ) {
return editable.element!;
}

/**
* Returns the document data for all attached roots.
*
* @param options Additional configuration for the retrieved data.
* Editor features may introduce more configuration options that can be set through this parameter.
* @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
* which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
* use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
* @returns The full document data.
*/
public getFullData( options?: Record<string, unknown> ): Record<string, string> {
const data: Record<string, string> = {};

for ( const rootName of this.model.document.getRootNames() ) {
data[ rootName ] = this.data.get( { ...options, rootName } );
}

return data;
}

/**
* Returns currently set roots attributes for attributes specified in
* {@link module:core/editor/editorconfig~EditorConfig#rootsAttributes `rootsAttributes`} configuration option.
*
* @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root.
*/
public getRootsAttributes(): Record<string, RootAttributes> {
const rootsAttributes: Record<string, RootAttributes> = {};
const keys = Array.from( this._registeredRootsAttributesKeys );

for ( const rootName of this.model.document.getRootNames() ) {
rootsAttributes[ rootName ] = {};

const root = this.model.document.getRoot( rootName )!;

for ( const key of keys ) {
rootsAttributes[ rootName ][ key ] = root.hasAttribute( key ) ? root.getAttribute( key ) : null;
}
}

return rootsAttributes;
}

/**
* Creates a new multi-root editor instance.
*
Expand Down Expand Up @@ -595,13 +701,31 @@ export type DetachRootEvent = {

/**
* Additional options available when adding a root.
*
* @param data Initial data for the root.
* @param elementName Element name for the root element in the model. It can be used to set different schema rules for different roots.
* @param isUndoable Whether creating the root can be undone (using the undo feature) or not.
*/
export type AddRootOptions = {

/**
* Initial data for the root.
*/
data?: string;

/**
* Initial attributes for the root.
*/
attributes?: RootAttributes;

/**
* Element name for the root element in the model. It can be used to set different schema rules for different roots.
*/
elementName?: string;

/**
* Whether creating the root can be undone (using the undo feature) or not.
*/
isUndoable?: boolean;
};

/**
* Attributes set on a model root element.
*/
export type RootAttributes = Record<string, unknown>;

0 comments on commit c121061

Please sign in to comment.