Skip to content

Commit

Permalink
Merge pull request #17340 from ckeditor/ck/17230-bookmarks-view
Browse files Browse the repository at this point in the history
Feature (link): Add the Bookmarks panel to the link UI. See #17230.
  • Loading branch information
niegowski authored Oct 29, 2024
2 parents 6c5857f + 327a8ca commit a6e5dd6
Show file tree
Hide file tree
Showing 9 changed files with 941 additions and 18 deletions.
9 changes: 8 additions & 1 deletion packages/ckeditor5-bookmark/src/bookmarkediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module bookmark/bookmarkediting
*/

import { type Editor, Plugin, icons } from 'ckeditor5/src/core.js';
import { Plugin, icons, type Editor } from 'ckeditor5/src/core.js';
import { toWidget } from 'ckeditor5/src/widget.js';
import { IconView } from 'ckeditor5/src/ui.js';
import type { EventInfo } from 'ckeditor5/src/utils.js';
Expand Down Expand Up @@ -81,6 +81,13 @@ export default class BookmarkEditing extends Plugin {
return null;
}

/**
* Returns all unique bookmark names existing in the content.
*/
public getAllBookmarkNames(): Set<string> {
return new Set( this._bookmarkElements.values() );
}

/**
* Defines the schema for the bookmark feature.
*/
Expand Down
39 changes: 39 additions & 0 deletions packages/ckeditor5-bookmark/tests/bookmarkediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,45 @@ describe( 'BookmarkEditing', () => {
} );
} );

describe( 'getAllBookmarkNames', () => {
it( 'should return all bookmark names', () => {
const bookmarkEditing = editor.plugins.get( 'BookmarkEditing' );

editor.setData(
'<p>' +
'<a id="foo"></a>' +
'</p>' +
'<p>' +
'<a id="bar"></a>' +
'</p>' +
'<p>' +
'<a id="baz"></a>' +
'</p>'
);

expect( bookmarkEditing.getAllBookmarkNames() ).is.instanceof( Set );
expect( bookmarkEditing.getAllBookmarkNames() ).is.deep.equal( new Set( [ 'foo', 'bar', 'baz' ] ) );
} );

it( 'should return all unique bookmark names', () => {
const bookmarkEditing = editor.plugins.get( 'BookmarkEditing' );

editor.setData(
'<p>' +
'<a id="foo"></a>' +
'</p>' +
'<p>' +
'<a id="bar"></a>' +
'</p>' +
'<p>' +
'<a id="bar"></a>' +
'</p>'
);

expect( bookmarkEditing.getAllBookmarkNames() ).is.deep.equal( new Set( [ 'foo', 'bar' ] ) );
} );
} );

describe( 'clipboard', () => {
let clipboardPlugin, viewDocument;

Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-link/lang/contexts.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Create link": "Keystroke description for assistive technologies: keystroke for creating a link.",
"Move out of a link": "Keystroke description for assistive technologies: keystroke for moving out of a link.",
"Bookmarks": "Title for a feature displaying a list of bookmarks.",
"No bookmarks available.": "A message displayed instead of a list of bookmarks if it is empty.",
"Advanced": "Title for a feature displaying advanced link settings.",
"Displayed text": "The label of the input field for the displayed text of the link."
}
141 changes: 130 additions & 11 deletions packages/ckeditor5-link/src/linkui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module link/linkui
*/

import { Plugin, type Editor } from 'ckeditor5/src/core.js';
import { Plugin, icons, type Editor } from 'ckeditor5/src/core.js';
import {
ClickObserver,
type ViewAttributeElement,
Expand All @@ -24,10 +24,12 @@ import {
MenuBarMenuListItemButtonView,
type ViewWithCssTransitionDisabler
} from 'ckeditor5/src/ui.js';

import type { PositionOptions } from 'ckeditor5/src/utils.js';
import { isWidget } from 'ckeditor5/src/widget.js';

import LinkFormView, { type LinkFormValidatorCallback } from './ui/linkformview.js';
import LinkBookmarksView from './ui/linkbookmarksview.js';
import LinkAdvancedView from './ui/linkadvancedview.js';
import LinkButtonView from './ui/linkbuttonview.js';
import LinkActionsView from './ui/linkactionsview.js';
Expand Down Expand Up @@ -61,6 +63,11 @@ export default class LinkUI extends Plugin {
*/
public formView: LinkFormView & ViewWithCssTransitionDisabler | null = null;

/**
* The view displaying bookmarks list.
*/
public bookmarksView: LinkBookmarksView | null = null;

/**
* The form view displaying advanced link settings.
*/
Expand Down Expand Up @@ -170,6 +177,10 @@ export default class LinkUI extends Plugin {
if ( this.actionsView ) {
this.actionsView.destroy();
}

if ( this.bookmarksView ) {
this.bookmarksView.destroy();
}
}

/**
Expand All @@ -180,6 +191,11 @@ export default class LinkUI extends Plugin {
this.formView = this._createFormView();
this.advancedView = this._createAdvancedView();

if ( this.editor.plugins.has( 'BookmarkEditing' ) ) {
this.bookmarksView = this._createBookmarksView();
this.formView.listChildren.add( this._createBookmarksButton() );
}

// Attach lifecycle actions to the the balloon.
this._enableUserBalloonInteractions();
}
Expand Down Expand Up @@ -238,10 +254,6 @@ export default class LinkUI extends Plugin {

const formView = new ( CssTransitionDisablerMixin( LinkFormView ) )( editor.locale, getFormValidators( editor ) );

if ( editor.plugins.has( 'BookmarkEditing' ) ) {
formView.listChildren.add( this._createBookmarksButton() );
}

formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );

// TODO: Bind to the "Displayed text" input
Expand Down Expand Up @@ -299,6 +311,63 @@ export default class LinkUI extends Plugin {
return formView;
}

/**
* Creates a sorted array of buttons with bookmark names.
*/
private _createBookmarksListView(): Array<ButtonView> {
const editor = this.editor;
const bookmarkEditing = editor.plugins.get( 'BookmarkEditing' );
const bookmarksNames = Array.from( bookmarkEditing.getAllBookmarkNames() );

bookmarksNames.sort( ( a, b ) => a.localeCompare( b ) );

return bookmarksNames.map( bookmarkName => {
const buttonView = new ButtonView();

buttonView.set( {
label: bookmarkName,
tooltip: false,
icon: icons.bookmark,
withText: true
} );

buttonView.on( 'execute', () => {
this.formView!.urlInputView.fieldView.value = '#' + bookmarkName;

// Set focus to the editing view to prevent from losing it while current view is removed.
editor.editing.view.focus();

this._removeBookmarksView();

// Set the focus to the URL input field.
this.formView!.focus();
} );

return buttonView;
} );
}

/**
* Creates a view for bookmarks.
*/
private _createBookmarksView(): LinkBookmarksView {
const editor = this.editor;
const view = new LinkBookmarksView( editor.locale );

// Hide the panel after clicking the "Cancel" button.
this.listenTo( view, 'cancel', () => {
// Set focus to the editing view to prevent from losing it while current view is removed.
editor.editing.view.focus();

this._removeBookmarksView();

// Set the focus to the URL input field.
this.formView!.focus();
} );

return view;
}

/**
* Creates the {@link module:link/ui/linkadvancedview~LinkAdvancedView} instance.
*/
Expand Down Expand Up @@ -401,6 +470,10 @@ export default class LinkUI extends Plugin {
label: t( 'Bookmarks' )
} );

this.listenTo( bookmarksButton, 'execute', () => {
this._addBookmarksView();
} );

return bookmarksButton;
}

Expand Down Expand Up @@ -554,6 +627,24 @@ export default class LinkUI extends Plugin {
this.formView!.enableCssTransitions();
}

/**
* Adds the {@link #bookmarksView} to the {@link #_balloon}.
*/
private _addBookmarksView(): void {
// Clear the collection of bookmarks.
this.bookmarksView!.listChildren.clear();

// Add bookmarks to the collection.
this.bookmarksView!.listChildren.addMany( this._createBookmarksListView() );

this._balloon.add( {
view: this.bookmarksView!,
position: this._getBalloonPositionData()
} );

this.bookmarksView!.focus();
}

/**
* Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
* decided upon the link command value (which has a value if the document selection is in the link).
Expand Down Expand Up @@ -585,6 +676,15 @@ export default class LinkUI extends Plugin {
}
}

/**
* Removes the {@link #bookmarksView} from the {@link #_balloon}.
*/
private _removeBookmarksView(): void {
if ( this._areBookmarksInPanel ) {
this._balloon.remove( this.bookmarksView! );
}
}

/**
* Removes the {@link #formView} from the {@link #_balloon}.
*/
Expand Down Expand Up @@ -674,7 +774,10 @@ export default class LinkUI extends Plugin {

// TODO: Remove dynamically registered views

// Remove the advanced form view first because it's on top of the stack.
// If the bookmarks view is visible, remove it because it can be on top of the stack.
this._removeBookmarksView();

// If the advanced form view is visible, remove it because it can be on top of the stack.
this._removeAdvancedView();

// Then remove the form view because it's beneath the advanced form.
Expand Down Expand Up @@ -749,6 +852,13 @@ export default class LinkUI extends Plugin {
return !!this.advancedView && this._balloon.hasView( this.advancedView );
}

/**
* Returns `true` when {@link #bookmarksView} is in the {@link #_balloon}.
*/
private get _areBookmarksInPanel(): boolean {
return !!this.bookmarksView && this._balloon.hasView( this.bookmarksView );
}

/**
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
*/
Expand Down Expand Up @@ -788,18 +898,27 @@ export default class LinkUI extends Plugin {
}

/**
* Returns `true` when {@link #advancedView}, {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
* Returns `true` when {@link #bookmarksView} is in the {@link #_balloon} and it is
* currently visible.
*/
private get _areBookmarksVisible(): boolean {
return !!this.bookmarksView && this._balloon.visibleView === this.bookmarksView;
}

/**
* Returns `true` when {@link #advancedView}, {@link #actionsView}, {@link #bookmarksView}
* or {@link #formView} is in the {@link #_balloon}.
*/
private get _isUIInPanel(): boolean {
return this._isAdvancedInPanel || this._isFormInPanel || this._areActionsInPanel;
return this._isAdvancedInPanel || this._areBookmarksInPanel || this._isFormInPanel || this._areActionsInPanel;
}

/**
* Returns `true` when {@link #advancedView}, {@link #actionsView} or {@link #formView} is in the {@link #_balloon}
* and it is currently visible.
* Returns `true` when {@link #advancedView}, {@link #bookmarksView}, {@link #actionsView}
* or {@link #formView} is in the {@link #_balloon} and it is currently visible.
*/
private get _isUIVisible(): boolean {
return this._isAdvancedVisible || this._isFormVisible || this._areActionsVisible;
return this._isAdvancedVisible || this._areBookmarksVisible || this._isFormVisible || this._areActionsVisible;
}

/**
Expand Down
Loading

0 comments on commit a6e5dd6

Please sign in to comment.