CKEditor Tutorial: Adding a ‘Link to local page from site’ field

UPDATE 4TH March ’14
The method described in this article is over 3 years old and doesn’t work properly with recent versions of CKEditor but Simon Georget has improved this implementation for CKEditor 4.2.2 and you can find it on Github here.

CKeditor is a great piece of work, not least because it’s free to use by anyone who wants to integrate it into their project. Myself, I am using it for a web content management system that I am developing and it’s definitely the best javascript WYSIWYG editor I’ve yet to come across, especially since it provides a comprehensive and well documented API framework.

One feature that is particularly important if you’re embedding an instance of CKEditor into a CMS is the ‘link’ dialog which allows your end users to select an object (image or area of text) and link to something like a URL, email address or anchor. This is a great facility and is nicely done but what happens when the user wants to link to other pages in the same web site for example the Contact page or the page for product no. 3723?

Out of the box CKEditor couldn’t support this since it has no knowledge of the other pages in your site that the user might want to link to. However, with a little customisation it is possible to add this functionality. This article will help you customise CKEditor’s link dialog to get the list of possible pages and then present them alongside the exisitng options for URL, anchor and email. You will end up seeing a dialog looking something like this:
link_to_local
… which when selected will show a drop down select box with a list of possible pages to link to, something like this:
link_to_local2

This approach is a thousand times more useable than expecting the user to browser to the target page (in another tab) copy the link and paste it into the URL field (or manually write an <a href> link tag.

So without any further ado let us begin our customisation.

  1. Write a function that generates the options in a JSON object

    In the template that holds the CKEditor instance we need to place a function that provides the list of possible options for target pages within the same site. This function should return this list in JSON format:

    [[first page, first page's link],[second page, second page's link],…]

    For example:

    [['Contact','index.php?p=4'],['About','index.php?p=5'],['Home page','index.php?p=1']]

    The important thing here is to make sure that the JSON is formatted as an array of arrays using square braces to define an array. In the above example the links are from a php site. In my case I am using CFML with the following script:

    <!--- data has to be sent via a JSON object (array of arrays)--->
    <cfset pagesArray = arraynew(1)>
     
    <!--- get the list of pages into the object --->
    <cfset pages = APPLICATION.tools.getPages(SESSION.langID)>
    <cfoutput query='pages'>
    	<cfset temp = arraynew()>
    	<cfset arrayappend(temp,'page: ' & pages.pageName)>
    	<cfset arrayappend(temp,'/page.cfm?pageID=' & pages.pageID)>
    	<cfset arrayappend(pagesArray,temp)>
    </cfoutput>
     
    <!--- convert the CFML object to JSON string for insertion into the calling template --->
    <cftry>
    	<cfset JSONstr = SerializeJSON(pagesArray)>
     
    	<cfcatch type="any">
     
    		<!--- manually convert it to JSON --->
    		<cfset JSONstr = '['>
    		<cfloop from="1" to="#arraylen(pagesArray)#" index="thisIndex">
    			<cfset thisEntry = pagesArray[thisIndex]>
    			<cfset thisName = thisEntry[1]>
    			<cfset thisURL = thisEntry[2]>	
    			<cfset JSONstr = JSONstr & '["#thisName#","#thisURL#"]'>	
    		</cfloop>			
    		<cfset JSONstr = JSONstr & ']'>
    		<cfset JSONstr = replace(JSONstr,'][','],[','ALL')>
     
    	</cfcatch>
    </cftry>

    This script uses CFML’s SerializeJSON() function to format an object (or in this case an array of arrays) into a JSON string. It should be possible to use any server side scripting language to generate the data in the correct format.

    PHP has a similar function json_encode() although for this to be natively supported you will need at least PHP version 5.2. A version less than that and you’ll have to include the json_decode() function. More info here

  2. Insert the JSON string into the calling template

    Use a hidden HTML input tag to hold the JSON string. Continuing with the CFML example this would look like this:

    <cfoutput>
    	<input type='hidden' id='pageListJSON' value='#JSONstr#'>
    </cfoutput>

    The string can then be accessed by the CKEditor using javascript to query the DOM.

  3. Add the new page selector element to CKEditor’s link definition

    Edit the link.js file, full path: <CKEditor root>/plugins/link/dialogs/link.js
    Find the ‘linkType’ selector definition which is at around line 400 and add the following line as the second item in the items section:

    [ 'localPage', 'localPage']

    Following the linkType section you should add a new section for the new page selector select field as follows:

    UPDATE: latest versions of CKEditor no longer work with the original customisation below, see comment from Eric Reynolds for how to implement this in version 4.1.1

    {
    	id : 'linkType',
    	type : 'select',
    	label : editor.lang.link.type,
    	'default' : 'url',
    	items :
    	[
    		[ editor.lang.common.url, 'url' ],
    		[ 'localPage', 'localPage'], // ADD THIS LINE
    		[ editor.lang.link.toAnchor, 'anchor' ],
    		[ editor.lang.link.toEmail, 'email' ]
    	],
    	onChange : linkTypeChanged,
    	setup : function( data )
    	{
    		if ( data.type )
    			this.setValue( data.type );
    	},
    	commit : function( data )
    	{
    		data.type = this.getValue();
    	}
    },
    // ADD THE NEW CODE BEGINNING HERE
    {
    	type : 'vbox',
    	id : 'localPageOptions',
    	children : [
    	{
    		type : 'select',
    		label : 'Page from within your site to link to',
    		id : 'localPage',
    		title : 'Select the page from within your site that you would like to link to',
    		items: eval(document.getElementById("pageListJSON").value),
    		setup : function( data )
    		{
    			if ( data.localPage )
    				this.setValue( data.localPage );
    		},
    		commit : function( data )
    		{
    			if ( !data.localPage )
    				data.localPage = {};
    				data.localPage = this.getValue();
    		}
    	}]						
    },

    This has added the new element but it will not be shown until it has been properly registered with the plugin.

    Note that I am not making use of CKEditor’s internationalization framework which allows translations of GUI elements to be added to translation files found in <CKEditor root>/lang/ and then called using editor.lang.link.elementName from within the link.js dialog definition.
    Instead I am writing my custom labels directly into the file as I only need this to work in English.

  4. Add the localPage option to the Link type selector

    In the same link.js file at around line 45 you should find the following line of code:

    partIds = [ 'urlOptions',  'anchorOptions', 'emailOptions' ],

    Just add the ‘localPageOptions’ entry so it looks like this:

    partIds = [ 'urlOptions', 'localPageOptions', 'anchorOptions', 'emailOptions' ],
  5. Add the code to form the correct URL for insertion into the HTML document

    At around line 1200 in the link.js file look for the section called //Compose the URL and add code so that it looks like this:

    // Compose the URL.
    switch ( data.type || 'url' )
    {
    	case 'url':
    		var protocol = ( data.url && data.url.protocol != undefined ) ? data.url.protocol : 'http://',
    			url = ( data.url && data.url.url ) || '';
    		attributes._cke_saved_href = ( url.indexOf( '/' ) === 0 ) ? url : protocol + url;
    		break;
     
    	case 'localPage':	// ADD THIS SECTION
    		attributes._cke_saved_href = data.localPage; 
    		break;
     
    	case 'anchor':
    		var name = ( data.anchor && data.anchor.name ),
    			id = ( data.anchor && data.anchor.id );
    		attributes._cke_saved_href = '#' + ( name || id || '' );
    		break;
  6. That’s all. Your CKEditor instance should now present your users with the option to link to the other pages in your site and then automatically generate the correct link and insert it into the HTML document.

    As I mentioned before this will not work in multiple languages yet as the english language versions of each label have been hard-coded into the dialog definition. If you require multi-lingual support then each english text should be replaced by an editor.lang.link.elementName placeholder and corresponding translations added to the language files in the CKEditor’s /lang folder.

28 thoughts on “CKEditor Tutorial: Adding a ‘Link to local page from site’ field

  1. This is really awesome! I looked around in the documentation and forums a lot for a good explanation as to how to do this and this is better than everything I found.

    Having said that, I think it is not the best approach to change the native CKE link plugin as this will mean you will have to do so for every future update of CKE you install.

    How about doing this as a separate plugin which then can be assigned it’s own button on the toolbar? this way you can save this in a separate JS file and you can continue update you CKE installation without any worries.

    I would assume making a new link plugin just for this functionality should mean just copying the existing link plugin and deleting most of it and just adding the new elements you explained in this post, but I’m not sure and would like to hear you thoughts about it. If you can do this plugin and release it to the world that would be even more awesome and I would personally see you as god :)

    Thanks,
    Amos

  2. @amosmos

    thanks for your great comment. I’m very pleased you like my article.

    I understand your reasoning for building this as its own separate plugin and I did consider this approach but decided not to for a couple of reasons.

    Reason 1: There would be separate link buttons which is a source of potential confusion for the user. From a usability perspective this functionality belongs where I put it, I believe.

    Reason 2: This customised link.js dialog dialog definition will most likely survive any update of the CKE core. Any changes made to the plugin definition framework in future updates of CKE core would then require all exisitng plugins to be re-worked, which I think very unlikely. It would just be a case of performing the update and then replacing the newly updated version of link.js with the customised version. Future enhancements to the exisitng link.js could be merged with this customisation fairly easily. In fact in that event I will be doing those merges for myself and can post them here.

  3. Thanks for the answer!

    Of course I agree with your opinion that 2 separate link buttons is a wrong thing.

    I guess the best way would just be to just copy the upgraded link plugin and make it a separate plugin and then just unload the native link plugin and load your customized one. This way in future changes to CKE you can always decide which one to use (no overwrite), and you can always make further changes to the customized plugin (in the even of any change to the native link plugin you change it again).

    Bottom line, it’s a shame that CK doesn’t provide link list option as part of it’s core features. This is a very basic and common need when using CKE in a CMS and would be very easy to implement for them. I know TinyMCE has this feature out of the box.

    How about you contact them and propose your additions to be included in the next version of CKE?

    Thanks,
    Amos

  4. Hmm tried this, but if I insert a link without selecting text first, I get the URL back instead of the title

    For example, say my JSON array has the following
    ['Happy Days', 67],
    ['Sad Days', 44],
    ['New post', 31]

    My CMS uses numbers for pages rather than fully qualified URLs, which it will then parse at run time. Using the code above, if I insert a link without selecting text to use, I get <a href=”67″ rel=”nofollow”>67</a> instead of <a href=”67″ rel=”nofollow”>Happy Days</a>

    Any ideas?

    • @mokargas

      Have you tried formatting your JSON so that the number representing the page is contained in singlequotes?
      I.e.
      ['Happy Days', '67'],
      ['Sad Days', '44'],
      ['New post', '31']

  5. @Ben

    Thankyou very much for the reply! I did indeed try that, but no luck on that front – it may be because I have to use an older version of CK on my project.

    I ended up piggy backing the title onto the data.internalPage object, and using jQuery to get the title itself from the selected option. Then when the node is created later on, I have an if statement that checks if data.internalPage has a title, and if not, creates a link using data.internalPage.title.

    Also had to change the anchor detection system, as it turns out the CMS was actually inserting #67# not 67.

    Not sure if that’s the ‘CK’ way to do it, but it works fine cross-browser. *shrug*

  6. Fantastic article, and perfect timing. I noticed one trivial bug however. On your page, you refer to the element id:’JSONpageList’ — but then in the link.js file, you provided ‘pageListJSON’ as the target. This didn’t work for me until I provided the same ID in both places.

  7. hey,

    just so you’ll know, this code breaks in the newest version of CKE.

    to fix it you need to change
    ‘attributes._cke_saved_href = data.localPage; ‘
    to
    ‘attributes[ 'data-cke-saved-href' ] = data.localPage; ‘

    this is exactly why i think this should be made a core function of CKE by the CKSource team, or at least a separate plugin that works by itself and not counting on the core “link” plugin. of course i am not blaming you for CKSource’s wrong doings… :)

    thanks anyway,
    amos

  8. Hi Amos,

    thanks for posting this fix. You’re right of course about the editor needing to support this functionality natively. I’m heading over to add my name to your new feature ticket right now :)

    Happy New Year

    Ben

  9. Pingback: Zac’s Attic Blog » CKEditor: Adding a select list of URLs for users in a CMS

  10. Thanks for this tutorial, I am not sure I could have made the necessary changes without it.

    I just had to do something similar, adding 2 more options to links to pages on a site, although I had to generate the links from the page number the user input rather than to select from a list.

    I almost have it working, just have to get the Target=”_blank” working – this is not selected it’s forced for the special links – nearly there…

  11. This is a great tutorial. I have some comments on this- first, read Amos’s comment, this is indeed helpful.

    Second thing – which I have tried to find a solution for more than two days:

    If you use any single quote/double quote inside the JSON object, then the object MUST NOT be included in the page as a hidden input field.

    Why single/double quote?:

    If you type:
    value=”___”
    then any double-quote inside the JSON will destroy the dialog
    and if you type
    value=’___’
    which is what the author mentioned, any single-quote with the JSON will destroy the dialog – the user will not be able even to insert a simple URL link.

    THE SOLUTION:
    DO NOT put the JSON as a hidden field. Just eval() the server-side output directly:

    For PHP (what I use):

    var links = eval(‘<?php echo $output');

    for ColdFusion simply replace the PHP output with coldfusion or you know, what you need…

    After that, change the data inside link.js to read this output:
    items: links,

    I build a CMS for a client who is using English+French+Hebrew in his website, and I noticed that in English and Hebrew everything works, but in every French page the link dialog simply doesn’t open.

    Good luck. and never give up -

  12. This is great. However Link.js did in fact change since you wrote this. So hopefully the updates will work. The one thing I can’t figure out is where to put the CF and json stuff in your first example. I put in on the form itself but that didn’t work. Any ideas?

  13. Well it is kind of working in Firefox. I can create a normal link. If i go to create a link to a local page (they are showing properly) it won’t let me hit the OK button. Meaning I click it and nothing happens. Strange.

  14. This does work in ckeditor 4.1.1, you have to do something like this instead:

    // BEGIN CMS PAGE LIST
    {
    type : ‘vbox’,
    id : ‘localPageOptions’,
    children : [
    {
    type : 'select',
    label : 'Page from within your site to link to',
    id : 'localPage',
    title : 'Select the page from within your site that you would like to link to',
    items: eval(document.getElementById("pageListJSON").value),
    setup : function( a )
    {
    //a.url && this.setValue( a.url.protocol || "")
    if ( a.localPage )
    this.setValue( a.localPage );
    },
    commit : function( a )
    {
    if ( !a.localPage )
    a.localPage = {};
    a.localPage = this.getValue();
    }
    }]
    },
    // END CMS PAGE LIST

    case ‘localPage’:
    a["data-cke-saved-href"] = c.localPage;
    break;

  15. HI,

    Its not working with ckeditor 4+.
    May be I am going wrong in step 2 i.e. passing JSON value.
    How can I get 1 working sample to download?

    Kind Regards,

    Hirendra

  16. Thanks Ben for this blog post, it helped me a lot.

    I’ve adapted your script to make it work with CKEditor 4.2.2, i10n ready and less intrusive (with the help of ajax call).
    I didn’t like much the hidden input to transmit data.

    Finally I packaged it as a new plugin available at : https://github.com/simogeo/ckeditor-adv_link

    Hope it helps some of you, guys.

  17. @simo thanks for your comment and sorry it took so long to approve. I’m going to update the article and put the link to your Github project up there near the top of the page now.

  18. HI,

    This is a great Article. Am new to CK editor and using it for the first time. Currently am using CK editor 4.2.2. My question is if we have 3 bookmarks say B1,B2,B3 then I need to pull this as a list in then URL Link.

    i.e I the URL link i need to pull a drop down with the available bookmarks. Can you please let me know how we can do this.

    Thanks in advance,
    Sudheer,

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>