Skip to content

Commit

Permalink
Merge pull request #17261 from ckeditor/ck/1944-bookmark-clicking-on-…
Browse files Browse the repository at this point in the history
…link-with-hash

Internal: Extended handling of clicking on link when `URL` starts with `#`.
  • Loading branch information
pszczesniak authored Oct 18, 2024
2 parents 4132b53 + ea1811b commit bafdfd3
Show file tree
Hide file tree
Showing 15 changed files with 642 additions and 29 deletions.
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

0 comments on commit bafdfd3

Please sign in to comment.