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

[Bookmark] handling of clicking on link when url starts with #. #17261

Merged
7 changes: 7 additions & 0 deletions packages/ckeditor5-bookmark/src/bookmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ export default class Bookmark extends Plugin {
public static get requires() {
return [ BookmarkEditing, BookmarkUI, Widget ] as const;
}

/**
* @inheritDoc
*/
public static override get isOfficialPlugin(): true {
return true;
}
}
7 changes: 7 additions & 0 deletions packages/ckeditor5-bookmark/src/bookmarkediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export default class BookmarkEditing extends Plugin {
return 'BookmarkEditing' as const;
}

/**
* @inheritDoc
*/
public static override get isOfficialPlugin(): true {
return true;
}

/**
* @inheritDoc
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/ckeditor5-bookmark/src/bookmarkui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export default class BookmarkUI extends Plugin {
return 'BookmarkUI' as const;
}

/**
* @inheritDoc
*/
public static override get isOfficialPlugin(): true {
return true;
}

/**
* @inheritDoc
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/ckeditor5-bookmark/tests/bookmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ describe( 'Bookmark', () => {
Widget
] );
} );

it( 'should have `isOfficialPlugin` static flag set to `true`', () => {
expect( Bookmark.isOfficialPlugin ).to.be.true;
} );

it( 'should have `isPremiumPlugin` static flag set to `false`', () => {
expect( Bookmark.isPremiumPlugin ).to.be.false;
} );
} );
8 changes: 8 additions & 0 deletions packages/ckeditor5-bookmark/tests/bookmarkediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ describe( 'BookmarkEditing', () => {
expect( BookmarkEditing.pluginName ).to.equal( 'BookmarkEditing' );
} );

it( 'should have `isOfficialPlugin` static flag set to `true`', () => {
expect( BookmarkEditing.isOfficialPlugin ).to.be.true;
} );

it( 'should have `isPremiumPlugin` static flag set to `false`', () => {
expect( BookmarkEditing.isPremiumPlugin ).to.be.false;
} );

describe( 'init', () => {
it( 'adds an "insertBookmark" command', () => {
expect( editor.commands.get( 'insertBookmark' ) ).to.be.instanceOf( InsertBookmarkCommand );
Expand Down
8 changes: 8 additions & 0 deletions packages/ckeditor5-bookmark/tests/bookmarkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ describe( 'BookmarkUI', () => {
expect( BookmarkUI.pluginName ).to.equal( 'BookmarkUI' );
} );

it( 'should have `isOfficialPlugin` static flag set to `true`', () => {
expect( BookmarkUI.isOfficialPlugin ).to.be.true;
} );

it( 'should have `isPremiumPlugin` static flag set to `false`', () => {
expect( BookmarkUI.isPremiumPlugin ).to.be.false;
} );

it( 'should load ContextualBalloon', () => {
expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<a href="#bookmark_for_image">Link to image bookmark.</a>
</p>

<p>
<a href="https://ckeditor.com">External link.</a>
</p>

<h2>Example amount of large text</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptas mollitia laudantium laboriosam, vitae molestiae velit voluptate aliquid autem nisi minima, quis maiores at iste accusamus ipsam odio facilis iusto? Explicabo!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptas mollitia laudantium laboriosam, vitae molestiae velit voluptate aliquid autem nisi minima, quis maiores at iste accusamus ipsam odio facilis iusto? Explicabo!</p>
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 @@ -8,6 +8,7 @@
"Open link in new tab": "Button opening the link in new browser tab.",
"This link has no URL": "Label explaining that a link has no URL set (the URL is empty).",
"Open in a new tab": "The label of the switch button that controls whether the edited link will open in a new tab.",
"Scroll to target": "Button scrolling to the link target.",
"Downloadable": "The label of the switch button that controls whether the edited link refers to downloadable resource.",
"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."
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "43.2.0",
"@ckeditor/ckeditor5-block-quote": "43.2.0",
"@ckeditor/ckeditor5-bookmark": "0.0.1",
"@ckeditor/ckeditor5-cloud-services": "43.2.0",
"@ckeditor/ckeditor5-code-block": "43.2.0",
"@ckeditor/ckeditor5-dev-utils": "^44.0.0",
Expand Down
16 changes: 13 additions & 3 deletions packages/ckeditor5-link/src/linkediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ import {
ensureSafeUrl,
getLocalizedDecorators,
normalizeDecorators,
openLink,
addLinkProtocolIfApplicable,
createBookmarkCallbacks,
openLink,
type NormalizedLinkDecoratorAutomaticDefinition,
type NormalizedLinkDecoratorManualDefinition
} from './utils.js';
Expand Down Expand Up @@ -260,6 +261,15 @@ export default class LinkEditing extends Plugin {
const editor = this.editor;
const view = editor.editing.view;
const viewDocument = view.document;
const bookmarkCallbacks = createBookmarkCallbacks( editor );

function handleLinkOpening( url: string ): void {
if ( bookmarkCallbacks.isScrollableToTarget( url ) ) {
bookmarkCallbacks.scrollToTarget( url );
} else {
openLink( url );
}
}

this.listenTo<ViewDocumentClickEvent>( viewDocument, 'click', ( evt, data ) => {
const shouldOpen = env.isMac ? data.domEvent.metaKey : data.domEvent.ctrlKey;
Expand Down Expand Up @@ -287,7 +297,7 @@ export default class LinkEditing extends Plugin {
evt.stop();
data.preventDefault();

openLink( url );
handleLinkOpening( url );
}, { context: '$capture' } );

// Open link on Alt+Enter.
Expand All @@ -302,7 +312,7 @@ export default class LinkEditing extends Plugin {

evt.stop();

openLink( url );
handleLinkOpening( url );
} );
}

Expand Down
13 changes: 11 additions & 2 deletions packages/ckeditor5-link/src/linkui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import LinkFormView, { type LinkFormValidatorCallback } from './ui/linkformview.
import LinkActionsView from './ui/linkactionsview.js';
import type LinkCommand from './linkcommand.js';
import type UnlinkCommand from './unlinkcommand.js';
import { addLinkProtocolIfApplicable, isLinkElement, LINK_KEYSTROKE } from './utils.js';
import {
addLinkProtocolIfApplicable,
isLinkElement,
createBookmarkCallbacks,
LINK_KEYSTROKE
} from './utils.js';

import linkIcon from '../theme/icons/link.svg';

Expand Down Expand Up @@ -171,7 +176,11 @@ export default class LinkUI extends Plugin {
*/
private _createActionsView(): LinkActionsView {
const editor = this.editor;
const actionsView = new LinkActionsView( editor.locale, editor.config.get( 'link' ) );
const actionsView = new LinkActionsView(
editor.locale,
editor.config.get( 'link' ),
createBookmarkCallbacks( editor )
);
const linkCommand: LinkCommand = editor.commands.get( 'link' )!;
const unlinkCommand: UnlinkCommand = editor.commands.get( 'unlink' )!;

Expand Down
45 changes: 40 additions & 5 deletions packages/ckeditor5-link/src/ui/linkactionsview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ButtonView, View, ViewCollection, FocusCycler, type FocusableView } fro
import { FocusTracker, KeystrokeHandler, type LocaleTranslate, type Locale } from 'ckeditor5/src/utils.js';
import { icons } from 'ckeditor5/src/core.js';

import { ensureSafeUrl } from '../utils.js';
import { ensureSafeUrl, openLink } from '../utils.js';

// See: #8833.
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
Expand Down Expand Up @@ -70,16 +70,19 @@ export default class LinkActionsView extends View {

private readonly _linkConfig: LinkConfig;

private readonly _options?: LinkActionsViewOptions;

declare public t: LocaleTranslate;

/**
* @inheritDoc
*/
constructor( locale: Locale, linkConfig: LinkConfig = {} ) {
constructor( locale: Locale, linkConfig: LinkConfig = {}, options?: LinkActionsViewOptions ) {
super( locale );

const t = locale.t;

this._options = options;
this.previewButtonView = this._createPreviewButton();
this.unlinkButtonView = this._createButton( t( 'Unlink' ), unlinkIcon, 'unlink' );
this.editButtonView = this._createButton( t( 'Edit link' ), icons.pencil, 'edit' );
Expand Down Expand Up @@ -197,8 +200,7 @@ export default class LinkActionsView extends View {
const t = this.t;

button.set( {
withText: true,
tooltip: t( 'Open link in new tab' )
withText: true
} );

button.extendTemplate( {
Expand All @@ -210,7 +212,25 @@ export default class LinkActionsView extends View {
href: bind.to( 'href', href => href && ensureSafeUrl( href, this._linkConfig.allowedProtocols ) ),
target: '_blank',
rel: 'noopener noreferrer'
},
on: {
click: bind.to( evt => {
if ( this._options && this._options.isScrollableToTarget( this.href ) ) {
evt.preventDefault();
this._options.scrollToTarget( this.href! );
} else {
openLink( this.href! );
}
} )
}
} );

button.bind( 'tooltip' ).to( this, 'href', href => {
if ( this._options && this._options.isScrollableToTarget( href ) ) {
return t( 'Scroll to target' );
}

return t( 'Open link in new tab' );
} );

button.bind( 'label' ).to( this, 'href', href => {
Expand All @@ -220,7 +240,6 @@ export default class LinkActionsView extends View {
button.bind( 'isEnabled' ).to( this, 'href', href => !!href );

button.template!.tag = 'a';
button.template!.eventListeners = {};

return button;
}
Expand All @@ -245,3 +264,19 @@ export type UnlinkEvent = {
name: 'unlink';
args: [];
};

/**
* The options that are passed to the {@link LinkActionsView} constructor.
*/
export type LinkActionsViewOptions = {

/**
* Returns `true` when bookmark `id` matches the hash from `link`.
*/
isScrollableToTarget: ( href: string | undefined ) => boolean;

/**
* Scrolls the view to the desired bookmark or open a link in new window.
*/
scrollToTarget: ( href: string ) => void;
};
46 changes: 46 additions & 0 deletions packages/ckeditor5-link/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ import type {
ViewNode,
ViewDocumentFragment
} from 'ckeditor5/src/engine.js';

import type { Editor } from 'ckeditor5/src/core.js';
import type { LocaleTranslate } from 'ckeditor5/src/utils.js';
import type { BookmarkEditing } from '@ckeditor/ckeditor5-bookmark';

import type {
LinkDecoratorAutomaticDefinition,
LinkDecoratorDefinition,
LinkDecoratorManualDefinition
} from './linkconfig.js';

import type { LinkActionsViewOptions } from './ui/linkactionsview.js';

import { upperFirst } from 'lodash-es';

const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
Expand Down Expand Up @@ -194,6 +199,47 @@ export function openLink( link: string ): void {
window.open( link, '_blank', 'noopener' );
}

/**
* Creates the bookmark callbacks for handling link opening experience.
*/
export function createBookmarkCallbacks( editor: Editor ): LinkActionsViewOptions {
const bookmarkEditing: BookmarkEditing | null = editor.plugins.has( 'BookmarkEditing' ) ?
editor.plugins.get( 'BookmarkEditing' ) :
null;

/**
* Returns `true` when bookmark `id` matches the hash from `link`.
*/
function isScrollableToTarget( link: string | undefined ): boolean {
return !!link &&
link.startsWith( '#' ) &&
!!bookmarkEditing &&
!!bookmarkEditing.getElementForBookmarkId( link.slice( 1 ) );
}

/**
* Scrolls the view to the desired bookmark or open a link in new window.
*/
function scrollToTarget( link: string ): void {
const bookmarkId = link.slice( 1 );
const modelBookmark = bookmarkEditing!.getElementForBookmarkId( bookmarkId );

editor.model.change( writer => {
writer.setSelection( modelBookmark!, 'on' );
} );

editor.editing.view.scrollToTheSelection( {
alignToTop: true,
forceScroll: true
} );
}

return {
isScrollableToTarget,
scrollToTarget
};
}

export type NormalizedLinkDecoratorAutomaticDefinition = LinkDecoratorAutomaticDefinition & { id: string };
export type NormalizedLinkDecoratorManualDefinition = LinkDecoratorManualDefinition & { id: string };
export type NormalizedLinkDecoratorDefinition = NormalizedLinkDecoratorAutomaticDefinition | NormalizedLinkDecoratorManualDefinition;
Loading