Skip to content

Commit

Permalink
Merge branch 'master' into ck/epic/17230-linking-experience
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Dec 23, 2024
2 parents 75fb874 + c5143d0 commit 09ccaf1
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/ckeditor5-editor-classic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export { default as ClassicEditor } from './classiceditor.js';
export { default as ClassicEditorUIView } from './classiceditoruiview.js';
12 changes: 11 additions & 1 deletion packages/ckeditor5-engine/src/model/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,12 +667,22 @@ export default class Selection extends /* #__PURE__ */ EmitterMixin( TypeCheckab
yield startBlock as any;
}

for ( const value of range.getWalker() ) {
const treewalker = range.getWalker();

for ( const value of treewalker ) {
const block = value.item;

if ( value.type == 'elementEnd' && isUnvisitedTopBlock( block as any, visited, range ) ) {
yield block as Element;
}
// If element is block, we can skip its children and jump to the end of it.
else if (
value.type == 'elementStart' &&
block.is( 'model:element' ) &&
block.root.document!.model.schema.isBlock( block )
) {
treewalker.jumpTo( Position._createAt( block, 'end' ) );
}
}

const endBlock = getParentBlock( range.end, visited );
Expand Down
26 changes: 26 additions & 0 deletions packages/ckeditor5-engine/src/model/treewalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,32 @@ export default class TreeWalker implements Iterable<TreeWalkerValue> {
}
}

/**
* Moves tree walker {@link #position} to provided `position`. Tree walker will
* continue traversing from that position.
*
* Note: in contrary to {@link ~TreeWalker#skip}, this method does not iterate over the nodes along the way.
* It simply sets the current tree walker position to a new one.
* From the performance standpoint, it is better to use {@link ~TreeWalker#jumpTo} rather than {@link ~TreeWalker#skip}.
*
* If the provided position is before the start boundary, the position will be
* set to the start boundary. If the provided position is after the end boundary,
* the position will be set to the end boundary.
* This is done to prevent the treewalker from traversing outside the boundaries.
*
* @param position Position to jump to.
*/
public jumpTo( position: Position ): void {
if ( this._boundaryStartParent && position.isBefore( this.boundaries!.start ) ) {
position = this.boundaries!.start;
} else if ( this._boundaryEndParent && position.isAfter( this.boundaries!.end ) ) {
position = this.boundaries!.end;
}

this._position = position.clone();
this._visitedParent = position.parent;
}

/**
* Gets the next tree walker's value.
*/
Expand Down
25 changes: 25 additions & 0 deletions packages/ckeditor5-engine/src/view/treewalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ export default class TreeWalker implements IterableIterator<TreeWalkerValue> {
}
}

/**
* Moves tree walker {@link #position} to provided `position`. Tree walker will
* continue traversing from that position.
*
* Note: in contrary to {@link ~TreeWalker#skip}, this method does not iterate over the nodes along the way.
* It simply sets the current tree walker position to a new one.
* From the performance standpoint, it is better to use {@link ~TreeWalker#jumpTo} rather than {@link ~TreeWalker#skip}.
*
* If the provided position is before the start boundary, the position will be
* set to the start boundary. If the provided position is after the end boundary,
* the position will be set to the end boundary.
* This is done to prevent the treewalker from traversing outside the boundaries.
*
* @param position Position to jump to.
*/
public jumpTo( position: Position ): void {
if ( this._boundaryStartParent && position.isBefore( this.boundaries!.start ) ) {
position = this.boundaries!.start;
} else if ( this._boundaryEndParent && position.isAfter( this.boundaries!.end ) ) {
position = this.boundaries!.end;
}

this._position = position.clone();
}

/**
* Gets the next tree walker's value.
*
Expand Down
64 changes: 64 additions & 0 deletions packages/ckeditor5-engine/tests/model/treewalker.js
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,70 @@ describe( 'TreeWalker', () => {
} );
} );
} );

describe( 'jumpTo', () => {
it( 'should jump to the given position', () => {
const walker = new TreeWalker( {
startPosition: Position._createAt( paragraph, 0 )
} );

walker.jumpTo( new Position( paragraph, [ 2 ] ) );

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 3 );
} );

it( 'cannot move position before the #_boundaryStartParent', () => {
const range = new Range(
new Position( paragraph, [ 2 ] ),
new Position( paragraph, [ 4 ] )
);
const walker = new TreeWalker( {
boundaries: range
} );

const positionBeforeAllowedRange = new Position( paragraph, [ 0 ] );

walker.jumpTo( positionBeforeAllowedRange );

// `jumpTo()` autocorrected the position to the first allowed position.
expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 3 );
} );

it( 'cannot move position after the #_boundaryEndParent', () => {
const range = new Range(
new Position( paragraph, [ 0 ] ),
new Position( paragraph, [ 2 ] )
);
const walker = new TreeWalker( {
boundaries: range
} );

const positionAfterAllowedRange = new Position( paragraph, [ 4 ] );

// `jumpTo()` autocorrected the position to the last allowed position.
walker.jumpTo( positionAfterAllowedRange );

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );
} );
} );
} );

function expectValue( value, expected, options ) {
Expand Down
64 changes: 64 additions & 0 deletions packages/ckeditor5-engine/tests/view/treewalker.js
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,70 @@ describe( 'TreeWalker', () => {
} );
} );
} );

describe( 'jumpTo', () => {
it( 'should jump to the given position', () => {
const walker = new TreeWalker( {
startPosition: Position._createAt( paragraph, 0 )
} );

walker.jumpTo( new Position( paragraph, 2 ) );

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( img2 );
expect( walker.position.offset ).to.equal( 0 );
} );

it( 'cannot move position before the #_boundaryStartParent', () => {
const range = new Range(
new Position( paragraph, 2 ),
new Position( paragraph, 4 )
);
const walker = new TreeWalker( {
boundaries: range
} );

const positionBeforeAllowedRange = new Position( paragraph, 0 );

walker.jumpTo( positionBeforeAllowedRange );

// `jumpTo()` autocorrected the position to the first allowed position.
expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( img2 );
expect( walker.position.offset ).to.equal( 0 );
} );

it( 'cannot move position after the #_boundaryEndParent', () => {
const range = new Range(
new Position( paragraph, 0 ),
new Position( paragraph, 2 )
);
const walker = new TreeWalker( {
boundaries: range
} );

const positionAfterAllowedRange = new Position( paragraph, 4 );

// `jumpTo()` autocorrected the position to the last allowed position.
walker.jumpTo( positionAfterAllowedRange );

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );

walker.next();

expect( walker.position.parent ).to.equal( paragraph );
expect( walker.position.offset ).to.equal( 2 );
} );
} );
} );

function expectValue( value, expected, options = {} ) {
Expand Down
27 changes: 20 additions & 7 deletions packages/ckeditor5-link/src/autolink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,27 +201,40 @@ export default class AutoLink extends Plugin {
const editor = this.editor;

const watcher = new TextWatcher( editor.model, text => {
let mappedText = text;

// 1. Detect <kbd>Space</kbd> after a text with a potential link.
if ( !isSingleSpaceAtTheEnd( text ) ) {
if ( !isSingleSpaceAtTheEnd( mappedText ) ) {
return;
}

// 2. Check text before last typed <kbd>Space</kbd>.
const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) );
// 2. Remove the last space character.
mappedText = mappedText.slice( 0, -1 );

// 3. Remove punctuation at the end of the URL if it exists.
if ( '!.:,;?'.includes( mappedText[ mappedText.length - 1 ] ) ) {
mappedText = mappedText.slice( 0, -1 );
}

// 4. Check text before last typed <kbd>Space</kbd> or punctuation.
const url = getUrlAtTextEnd( mappedText );

if ( url ) {
return { url };
return {
url,
removedTrailingCharacters: text.length - mappedText.length
};
}
} );

watcher.on<TextWatcherMatchedDataEvent<{ url: string }>>( 'matched:data', ( evt, data ) => {
const { batch, range, url } = data;
watcher.on<TextWatcherMatchedDataEvent<{ url: string; removedTrailingCharacters: number }>>( 'matched:data', ( evt, data ) => {
const { batch, range, url, removedTrailingCharacters } = data;

if ( !batch.isTyping ) {
return;
}

const linkEnd = range.end.getShiftedBy( -1 ); // Executed after a space character.
const linkEnd = range.end.getShiftedBy( -removedTrailingCharacters ); // Executed after a space character or punctuation.
const linkStart = linkEnd.getShiftedBy( -url.length );

const linkRange = editor.model.createRange( linkStart, linkEnd );
Expand Down
10 changes: 10 additions & 0 deletions packages/ckeditor5-link/tests/autolink.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,16 @@ describe( 'AutoLink', () => {
sinon.assert.notCalled( spy );
} );

for ( const punctuation of '!.:,;?' ) {
it( `does not include "${ punctuation }" at the end of the link after space`, () => {
simulateTyping( `https://www.cksource.com${ punctuation } ` );

expect( getData( model ) ).to.equal(
`<paragraph><$text linkHref="https://www.cksource.com">https://www.cksource.com</$text>${ punctuation } []</paragraph>`
);
} );
}

// Some examples came from https://mathiasbynens.be/demo/url-regex.
describe( 'supported URL', () => {
const supportedURLs = [
Expand Down
4 changes: 3 additions & 1 deletion packages/ckeditor5-ui/src/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ export default class Dialog extends Plugin {
public override destroy(): void {
super.destroy();

this._unlockBodyScroll();
if ( Dialog._visibleDialogPlugin === this ) {
this._unlockBodyScroll();
}
}

/**
Expand Down
18 changes: 18 additions & 0 deletions packages/ckeditor5-ui/tests/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,24 @@ describe( 'Dialog', () => {

expect( document.documentElement.classList.contains( 'ck-dialog-scroll-locked' ) ).to.be.false;
} );

it( 'should not unlock scrolling on the document if modal was displayed by another plugin instance', () => {
const tempDialogPlugin = new Dialog( editor );

tempDialogPlugin._show( {
position: DialogViewPosition.EDITOR_CENTER,
isModal: true,
className: 'foo'
} );

expect( document.documentElement.classList.contains( 'ck-dialog-scroll-locked' ) ).to.be.true;

dialogPlugin.destroy();

expect( document.documentElement.classList.contains( 'ck-dialog-scroll-locked' ) ).to.be.true;

tempDialogPlugin.destroy();
} );
} );

describe( 'show()', () => {
Expand Down

0 comments on commit 09ccaf1

Please sign in to comment.