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

Create a custom link button #4836

Closed
joachimdoerr opened this issue May 4, 2018 · 17 comments
Closed

Create a custom link button #4836

joachimdoerr opened this issue May 4, 2018 · 17 comments

Comments

@joachimdoerr
Copy link

How i can create a custom link button for a CMS implementation?

@Reinmar
Copy link
Member

Reinmar commented May 4, 2018

What do you mean? What do you want it to do?

cc @oleq

@joachimdoerr
Copy link
Author

Hello @Reinmar, for the cms redaxo i want to create a extension that provide the ckeditor5. The button that I need to add must be call a cms dialog like the screenshot:
bildschirmfoto 2018-05-04 um 19 51 00

  • On click to the Button "Interner Link" a popup will be open and you can chose a Site:
    bildschirmfoto 2018-05-04 um 19 51 14
  • On click to a site in the linklist the popup close automatically and write the cms link into the link input field:
    bildschirmfoto 2018-05-04 um 19 51 20

@oleq
Copy link
Member

oleq commented May 8, 2018

Would you like to use the existing UI (see below) or build a new one for that?

image

@joachimdoerr
Copy link
Author

I want ot us the existing UI a button between the tow buttons in the UI would be the best solution.

@oleq
Copy link
Member

oleq commented May 9, 2018

You can write a simple plugin to add a button to the existing UI. Check out https://docs.ckeditor.com/ckeditor5/latest/builds/guides/development/installing-plugins.html to learn how to extend existing editor build, create own editor builds, etc.

image

import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import LinkUI from '@ckeditor/ckeditor5-link/src/linkui';

class InternalLink extends Plugin {
	init() {
		const editor = this.editor;
		const linkUI = editor.plugins.get( LinkUI );

		this.linkFormView = linkUI.formView;
		this.button = this._createButton();

		this.linkFormView.once( 'render', () => {
			// Render button's tamplate.
			this.button.render();

			// Register the button under the link form view, it will handle its destruction.
			this.linkFormView.registerChild( this.button );

			// Inject the element into DOM.
			this.linkFormView.element.insertBefore( this.button.element, this.linkFormView.saveButtonView.element );
		} );
	}

	_createButton() {
		const editor = this.editor;
		const button = new ButtonView( this.locale );
		const linkCommand = editor.commands.get( 'link' );

		button.set( {
			label: 'Internal link',
			withText: true,
			tooltip: true
		} );

		// Probably this button should be also disabled when the link command is disabled.
		// Try setting editor.isReadOnly = true to see it in action.
		button.bind( 'isEnabled' ).to( linkCommand );

		button.on( 'execute', () => {
			// Do something (like open the popup), then update the link URL field's value.
			// The line below will be probably executed inside some callback.
			this.linkFormView.urlInputView.value = 'http://some.internal.link';
		} );

		return button;
	}
}

@joachimdoerr
Copy link
Author

joachimdoerr commented May 9, 2018

@oleq thanks for your example!

  • I try to use that and create for it this repository https://github.com/basecondition/ckeditor5-rexlink and publish it as a npm package.
  • Than i add the package as devDependencies, add it to the build.config as plugins, and add it to the src/ckeditor.js.
	plugins: [ ....
		'ckeditor5-rexlink/src/rexlink'
	],
...
import RexlinkPlugin from 'ckeditor5-rexlink/src/rexlink';
...
ClassicEditor.build = {
	plugins: [
		RexlinkPlugin
	],
  • Than I try to build the editor npm run build. That works but I get this warning:
WARNING in ./src/ckeditor.js
59:2-15 "export 'default' (imported as 'RexlinkPlugin') was not found in 'ckeditor5-rexlink/src/rexlink'
 @ ./src/ckeditor.js

What is wrong?

@jodator
Copy link
Contributor

jodator commented May 9, 2018

@joachimdoerr
Copy link
Author

joachimdoerr commented May 9, 2018

@jodator thanks! Now it works. @oleq your example works absolutely perfect! Thanks a lot!
https://github.com/basecondition/ckeditor5-rexlink/blob/master/src/rexlink.js#L46

@joachimdoerr
Copy link
Author

I have still one question about that. I want add the button optional. As options like alignment

options['alignment'] = [ 'left', 'right', 'center', 'justify' ];

Also:

options['rexlink'] = [ 'internal', 'media' ];

How i can modify the plugin to ensure it?

@joachimdoerr joachimdoerr reopened this May 9, 2018
@oleq
Copy link
Member

oleq commented May 10, 2018

Your plugin should define a config option in plugin's constructor, e.g.

constructor( editor ) {
	super( editor );

	editor.config.define( 'link.rexlink', [ 'internal', 'media' ] );
}

then in the init() you can read the config's value and make the buttons conditional:

const rexlinkConfig = editor.config.get( 'link.rexlink' );

if ( something about rexlinkConfig ) {
	// create buttons or not
}
ClassicEditor
	.create( ...., {
		link: {
			rexlink: [ 'foo' ]
		}
	} )
	.then( editor => {
		window.editor = editor;
	} )
	.catch( err => {
		console.error( err );
	} );

@joachimdoerr
Copy link
Author

Thanks @oleq thats work perfekt!

  • Now i add 2 svg icons for the media and the internal button. I think that looks good
    bildschirmfoto 2018-05-10 um 14 48 27

  • The next point that i see is the link text problem. When i dont mark any words in the text and add a link the editor will be add the link with the link as text - that is ok for domain links but for internal links that look stupid
    bildschirmfoto 2018-05-10 um 14 59 23

  • The internal link widget gives the internal link site title. is it possible that the editor write an other text as the link target text in this case?

  • For this i want to use the linktext : https://github.com/basecondition/ckeditor5-rexlink/blob/master/src/rexlink.js#L75

@oleq
Copy link
Member

oleq commented May 10, 2018

@joachimdoerr Check out https://github.com/ckeditor/ckeditor5-link/issues/73 and https://github.com/ckeditor/ckeditor5-link/issues/15.

You can try listening to LinkFormView#submit event, which is fired when the user clicks the save ("tick") button. Then, if you discover that the link is internal and the current selection is collapsed (nothing is selected), you can insert a corresponding text. It could work.

@joachimdoerr
Copy link
Author

@oleq thanks for the event idee. For now I crate this solution that works for me

        button.on( 'execute', () => {
            // Do something (like open the popup), then update the link URL field's value.

            var linkMap = openLinkMap('', '&clang=1');
            const urlInputView = this.linkFormView.urlInputView;
            const thatLinkFormView = this.linkFormView.element;
    
            $(linkMap).on('rex:selectLink', function (event, linkurl, linktext) {
                event.preventDefault();
                linkMap.close();
    
                // The line below will be probably executed inside some callback.
                urlInputView.value = linkurl;
    
                $(thatLinkFormView).submit(function(){
                    var regex = '>' + linkurl + '<',
                        matches = editor.getData().match(regex);
    
                    if (matches) {
                        var result = editor.getData().replace(regex, '>' + linktext + '<');
                        editor.setData(result);
                    }
                });
            });

        } );

The next step is the image integration. I want to use the redaxo mediamanager to add images. In which repository can I open a issue about that?

@oleq
Copy link
Member

oleq commented May 11, 2018

If I were you, I'd experiment with https://github.com/ckeditor/ckeditor5-link/issues/195#issuecomment-388057470. What you did in the last snippet could fail if one decided to change the text of the link later on and then insert the same URL somewhere else because (if I got it correctly) it blindly seeks certain <a> and replaces their text. It will override existing texts upon successive insertions of the same link.

The next step is the image integration. I want to use the redaxo mediamanager to add images. In which repository can I open a issue about that?

Check out https://docs.ckeditor.com/ckeditor5/latest/api/adapter-ckfinder.html

@oleq oleq closed this as completed May 11, 2018
@mlewand mlewand transferred this issue from ckeditor/ckeditor5-link Oct 9, 2019
@JanWennrichPCSG
Copy link

Sorry for replying to this closed issue, but I'm currently facing the same problem.
It seems like the approach above, posted @oleq (#4836 (comment)), no longer works.

Is there a different way to add a custom button to the link dialog now?
I also created an issue explaining my exact situation (see: #12349)

@leevigraham
Copy link

leevigraham commented Feb 7, 2023

@JanWennrichPCSG ckeditor v36 lazily creates the form view so I took a different approach.

It's a bit hacky… I override the _createFormView method, call the original and then add children to the template:

import {Plugin} from '@ckeditor/ckeditor5-core';
import {Link, LinkUI} from "@ckeditor/ckeditor5-link";
import {addListToDropdown, ButtonView, createDropdown, Model, View} from "@ckeditor/ckeditor5-ui";
import {Collection} from "@ckeditor/ckeditor5-utils";

export default class CraftLink extends Plugin {

	constructor(editor) {
		super(editor);
		editor.config.define('craftLink', {
			assetModal: {
				sources: null,
				condition: null,
				criteria: null,
				defaultSiteId: null,
				storageKey: null,
			},
			entryModal: {
				sources: null,
				condition: null,
				criteria: null,
				defaultSiteId: null,
				storageKey: null,
			}
		});
	}

	static get requires() {
		return [Link];
	}

	static get pluginName() {
		return 'CraftLink';
	}

	init() {
		this.linkUI = this.editor.plugins.get(LinkUI);
		this._addFormViewButtons();
	}

	_addFormViewButtons() {
		const editor = this.editor;
		const t = editor.t;
		const _createFormViewOriginal = this.linkUI._createFormView;

		this.linkUI._createFormView = () => {
			const formView = _createFormViewOriginal.bind(this.linkUI).call();
			const linkToAssetButton = new ButtonView();
			const linkToEntryButton = new ButtonView();

			linkToAssetButton.set({
				label: t('Link to an Asset'),
				withText: true,
				_elementType: 'craft\\elements\\Asset',
				_refHandle: 'asset',
				_commandName: 'craftLinkElementCommand',
			});

			linkToAssetButton.on('execute', (evt) => {
				this._openModal({
					elementType: evt.source._elementType,
					refHandle: evt.source._refHandle
				})
			});

			linkToEntryButton.set({
				label: t('Link to an Entry'),
				withText: true,
				_elementType: 'craft\\elements\\Entry',
				_refHandle: 'entry',
				_commandName: 'craftLinkElementCommand',
			});
			linkToEntryButton.on('execute', (evt) => {
				this._openModal({
					elementType: evt.source._elementType,
					refHandle: evt.source._refHandle
				})
			});

			const dividerView = new View();
			dividerView.setTemplate({
				tag: 'div',
				attributes: {
					class: ['ck', 'ck-reset'],
					style: {
						margin: '4px',
						borderLeft: '1px solid var(--ck-color-base-border)'
					}
				}
			});

			const additionalButtonsView = new View();
			additionalButtonsView.setTemplate({
				tag: 'div',
				attributes: {
					class: ['ck', 'ck-reset'],
					style: {
						display: 'flex',
						alignItems: 'stretch',
						justifyContent: 'center',
						padding: 'var(--ck-spacing-small)',
						borderBottom: '1px solid var(--ck-color-base-border)'
					}
				},
				children: [linkToAssetButton, dividerView, linkToEntryButton]
			});

			formView.template.children.unshift(additionalButtonsView);
			formView.template.attributes.class.push('ck-link-form_layout-vertical');

			return formView;
		}
	}

	_openModal(args) {
		const modalSettings = this.editor.config.get(`craftImage.${args.refHandle}Modal`);

		Craft.createElementSelectorModal(args.elementType, {
			...modalSettings,
			onCancel: () => {
				this.editor.editing.view.focus();
			},
			onSelect: (elements) => {
				if (!elements.length) {
					return;
				}
				const [element] = elements;
				const url = `${element.url}#${args.refHandle}:${element.id}@${element.siteId}`;
				this.editor.editing.view.focus();
				this.linkUI._showUI(true);
				this.linkUI.formView.urlInputView.fieldView.element.value = url;
				setTimeout(() => {
					this.linkUI.formView.urlInputView.fieldView.element.focus();
				}, 100)
			},
			closeOtherModals: false
		});
	}
}

Then you get something like:

image

@niegowski
Copy link
Contributor

Sorry for replying to this closed issue, but I'm currently facing the same problem. It seems like the approach above, posted @oleq (#4836 (comment)), no longer works.

Is there a different way to add a custom button to the link dialog now? I also created an issue explaining my exact situation (see: #12349)

Here is an updated version of the solution:

import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import LinkUI from '@ckeditor/ckeditor5-link/src/linkui';

class InternalLink extends Plugin {
	init() {
		const editor = this.editor;
		const linkUI = editor.plugins.get( LinkUI );
		const contextualBalloonPlugin = editor.plugins.get( 'ContextualBalloon' );

		this.listenTo( contextualBalloonPlugin, 'change:visibleView', ( evt, name, visibleView ) => {
			if ( visibleView === linkUI.formView ) {
				// Detach the listener.
				this.stopListening( contextualBalloonPlugin, 'change:visibleView' );

				this.linkFormView = linkUI.formView;
				this.button = this._createButton();

				console.log( 'The link form view has been displayed', this.linkFormView );

				// Render button's tamplate.
				this.button.render();

				// Register the button under the link form view, it will handle its destruction.
				this.linkFormView.registerChild( this.button );

				// Inject the element into DOM.
				this.linkFormView.element.insertBefore( this.button.element, this.linkFormView.saveButtonView.element );
			}
		} );
	}

	_createButton() {
		const editor = this.editor;
		const button = new ButtonView( this.locale );
		const linkCommand = editor.commands.get( 'link' );

		button.set( {
			label: 'Internal link',
			withText: true,
			tooltip: true
		} );

		// Probably this button should be also disabled when the link command is disabled.
		// Try setting editor.isReadOnly = true to see it in action.
		button.bind( 'isEnabled' ).to( linkCommand );

		button.on( 'execute', () => {
			// Do something (like open the popup), then update the link URL field's value.
			// The line below will be probably executed inside some callback.
			this.linkFormView.urlInputView.value = 'http://some.internal.link';
		} );

		return button;
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants