Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial update of link text with attributes preservation. #17607

Draft
wants to merge 1 commit into
base: ck/epic/17230-linking-experience
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions packages/ckeditor5-link/src/linkcommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { Command } from 'ckeditor5/src/core.js';
import { findAttributeRange } from 'ckeditor5/src/typing.js';
import { Collection, first, toMap } from 'ckeditor5/src/utils.js';
import { Collection, diff, first, toMap } from 'ckeditor5/src/utils.js';
import { LivePosition, type Range } from 'ckeditor5/src/engine.js';

import AutomaticDecorators from './utils/automaticdecorators.js';
Expand Down Expand Up @@ -200,7 +200,28 @@ export default class LinkCommand extends Command {

// Only if needed.
if ( newText != linkText ) {
return model.insertContent( writer.createText( newText ), range );
const changes = findChanges( linkText, newText );
let insertsLength = 0;

for ( const { offset, actual, expected } of changes ) {
const updatedOffset = offset + insertsLength;
const subRange = writer.createRange(
range.start.getShiftedBy( updatedOffset ),
range.start.getShiftedBy( updatedOffset + actual.length )
);

// Collect formatting attributes from replaced text.
const textNode = getLinkPartTextNode( subRange, range )!;
const attributes = textNode.getAttributes();
const formattingAttributes = Array.from( attributes )
.filter( ( [ key ] ) => model.schema.getAttributeProperties( key ).isFormatting );

// Replace text with formatting.
model.insertContent( writer.createText( expected, formattingAttributes ), subRange );
insertsLength += expected.length; // Sum of all previous inserts.
}

return writer.createRange( range.start, range.start.getShiftedBy( newText.length ) );
}
};

Expand Down Expand Up @@ -343,3 +364,63 @@ export default class LinkCommand extends Command {
return true;
}
}

/**
* TODO
*/
function findChanges( oldText: string, newText: string ) {
const changes = diff( oldText, newText );
const counter = { equal: 0, insert: 0, delete: 0 };
const result = [];

let actualSlice = '';
let expectedSlice = '';

for ( const action of [ ...changes, null ] ) {
if ( action == 'insert' ) {
expectedSlice += newText[ counter.equal + counter.insert ];
}
else if ( action == 'delete' ) {
actualSlice += oldText[ counter.equal + counter.delete ];
}
else if ( actualSlice.length || expectedSlice.length ) {
// Save change and reset stored slices on 'equal' or end.
result.push( {
offset: counter.equal,
actual: actualSlice,
expected: expectedSlice
} );

actualSlice = '';
expectedSlice = '';
}

if ( action ) {
counter[ action ]++;
}
}

return result;
}

/**
* TODO
*/
function getLinkPartTextNode( range: Range, linkRange: Range ) {
if ( !range.isCollapsed ) {
return first( range.getItems() );
}

const position = range.start;

if ( position.textNode ) {
return position.textNode;
}

// If the range is at the start of a link range then prefer node inside a link range.
if ( position.isEqual( linkRange.start ) ) {
return position.nodeAfter || position.nodeBefore;
} else {
return position.nodeBefore || position.nodeAfter;
}
}