1/* 2Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3For licensing, see LICENSE.html or http://ckeditor.com/license 4*/ 5 6CKEDITOR.plugins.add( 'link', 7{ 8 init : function( editor ) 9 { 10 // Add the link and unlink buttons. 11 editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link' ) ); 12 editor.addCommand( 'anchor', new CKEDITOR.dialogCommand( 'anchor' ) ); 13 editor.addCommand( 'unlink', new CKEDITOR.unlinkCommand() ); 14 editor.addCommand( 'removeAnchor', new CKEDITOR.removeAnchorCommand() ); 15 editor.ui.addButton( 'Link', 16 { 17 label : editor.lang.link.toolbar, 18 command : 'link' 19 } ); 20 editor.ui.addButton( 'Unlink', 21 { 22 label : editor.lang.unlink, 23 command : 'unlink' 24 } ); 25 editor.ui.addButton( 'Anchor', 26 { 27 label : editor.lang.anchor.toolbar, 28 command : 'anchor' 29 } ); 30 CKEDITOR.dialog.add( 'link', this.path + 'dialogs/link.js' ); 31 CKEDITOR.dialog.add( 'anchor', this.path + 'dialogs/anchor.js' ); 32 33 // Add the CSS styles for anchor placeholders. 34 35 var side = ( editor.lang.dir == 'rtl' ? 'right' : 'left' ); 36 var basicCss = 37 'background:url(' + CKEDITOR.getUrl( this.path + 'images/anchor.gif' ) + ') no-repeat ' + side + ' center;' + 38 'border:1px dotted #00f;'; 39 40 editor.addCss( 41 'a.cke_anchor,a.cke_anchor_empty' + 42 // IE6 breaks with the following selectors. 43 ( ( CKEDITOR.env.ie && CKEDITOR.env.version < 7 ) ? '' : 44 ',a[name],a[data-cke-saved-name]' ) + 45 '{' + 46 basicCss + 47 'padding-' + side + ':18px;' + 48 // Show the arrow cursor for the anchor image (FF at least). 49 'cursor:auto;' + 50 '}' + 51 ( CKEDITOR.env.ie ? ( 52 'a.cke_anchor_empty' + 53 '{' + 54 // Make empty anchor selectable on IE. 55 'display:inline-block;' + 56 '}' 57 ) : '' ) + 58 'img.cke_anchor' + 59 '{' + 60 basicCss + 61 'width:16px;' + 62 'min-height:15px;' + 63 // The default line-height on IE. 64 'height:1.15em;' + 65 // Opera works better with "middle" (even if not perfect) 66 'vertical-align:' + ( CKEDITOR.env.opera ? 'middle' : 'text-bottom' ) + ';' + 67 '}'); 68 69 // Register selection change handler for the unlink button. 70 editor.on( 'selectionChange', function( evt ) 71 { 72 if ( editor.readOnly ) 73 return; 74 75 /* 76 * Despite our initial hope, document.queryCommandEnabled() does not work 77 * for this in Firefox. So we must detect the state by element paths. 78 */ 79 var command = editor.getCommand( 'unlink' ), 80 element = evt.data.path.lastElement && evt.data.path.lastElement.getAscendant( 'a', true ); 81 if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() ) 82 command.setState( CKEDITOR.TRISTATE_OFF ); 83 else 84 command.setState( CKEDITOR.TRISTATE_DISABLED ); 85 } ); 86 87 editor.on( 'doubleclick', function( evt ) 88 { 89 var element = CKEDITOR.plugins.link.getSelectedLink( editor ) || evt.data.element; 90 91 if ( !element.isReadOnly() ) 92 { 93 if ( element.is( 'a' ) ) 94 { 95 evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link'; 96 editor.getSelection().selectElement( element ); 97 } 98 else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) ) 99 evt.data.dialog = 'anchor'; 100 } 101 }); 102 103 // If the "menu" plugin is loaded, register the menu items. 104 if ( editor.addMenuItems ) 105 { 106 editor.addMenuItems( 107 { 108 anchor : 109 { 110 label : editor.lang.anchor.menu, 111 command : 'anchor', 112 group : 'anchor', 113 order : 1 114 }, 115 116 removeAnchor : 117 { 118 label : editor.lang.anchor.remove, 119 command : 'removeAnchor', 120 group : 'anchor', 121 order : 5 122 }, 123 124 link : 125 { 126 label : editor.lang.link.menu, 127 command : 'link', 128 group : 'link', 129 order : 1 130 }, 131 132 unlink : 133 { 134 label : editor.lang.unlink, 135 command : 'unlink', 136 group : 'link', 137 order : 5 138 } 139 }); 140 } 141 142 // If the "contextmenu" plugin is loaded, register the listeners. 143 if ( editor.contextMenu ) 144 { 145 editor.contextMenu.addListener( function( element, selection ) 146 { 147 if ( !element || element.isReadOnly() ) 148 return null; 149 150 var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ); 151 152 if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) 153 return null; 154 155 var menu = {}; 156 157 if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() ) 158 menu = { link : CKEDITOR.TRISTATE_OFF, unlink : CKEDITOR.TRISTATE_OFF }; 159 160 if ( anchor && anchor.hasAttribute( 'name' ) ) 161 menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF; 162 163 return menu; 164 }); 165 } 166 }, 167 168 afterInit : function( editor ) 169 { 170 // Register a filter to displaying placeholders after mode change. 171 172 var dataProcessor = editor.dataProcessor, 173 dataFilter = dataProcessor && dataProcessor.dataFilter, 174 htmlFilter = dataProcessor && dataProcessor.htmlFilter, 175 pathFilters = editor._.elementsPath && editor._.elementsPath.filters; 176 177 if ( dataFilter ) 178 { 179 dataFilter.addRules( 180 { 181 elements : 182 { 183 a : function( element ) 184 { 185 var attributes = element.attributes; 186 if ( !attributes.name ) 187 return null; 188 189 var isEmpty = !element.children.length; 190 191 if ( CKEDITOR.plugins.link.synAnchorSelector ) 192 { 193 // IE needs a specific class name to be applied 194 // to the anchors, for appropriate styling. 195 var ieClass = isEmpty ? 'cke_anchor_empty' : 'cke_anchor'; 196 var cls = attributes[ 'class' ]; 197 if ( attributes.name && ( !cls || cls.indexOf( ieClass ) < 0 ) ) 198 attributes[ 'class' ] = ( cls || '' ) + ' ' + ieClass; 199 200 if ( isEmpty && CKEDITOR.plugins.link.emptyAnchorFix ) 201 { 202 attributes.contenteditable = 'false'; 203 attributes[ 'data-cke-editable' ] = 1; 204 } 205 } 206 else if ( CKEDITOR.plugins.link.fakeAnchor && isEmpty ) 207 return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' ); 208 209 return null; 210 } 211 } 212 }); 213 } 214 215 if ( CKEDITOR.plugins.link.emptyAnchorFix && htmlFilter ) 216 { 217 htmlFilter.addRules( 218 { 219 elements : 220 { 221 a : function( element ) 222 { 223 delete element.attributes.contenteditable; 224 } 225 } 226 }); 227 } 228 229 if ( pathFilters ) 230 { 231 pathFilters.push( function( element, name ) 232 { 233 if ( name == 'a' ) 234 { 235 if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || 236 ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ) 237 { 238 return 'anchor'; 239 } 240 } 241 }); 242 } 243 }, 244 245 requires : [ 'fakeobjects' ] 246} ); 247 248CKEDITOR.plugins.link = 249{ 250 /** 251 * Get the surrounding link element of current selection. 252 * @param editor 253 * @example CKEDITOR.plugins.link.getSelectedLink( editor ); 254 * @since 3.2.1 255 * The following selection will all return the link element. 256 * <pre> 257 * <a href="#">li^nk</a> 258 * <a href="#">[link]</a> 259 * text[<a href="#">link]</a> 260 * <a href="#">li[nk</a>] 261 * [<b><a href="#">li]nk</a></b>] 262 * [<a href="#"><b>li]nk</b></a> 263 * </pre> 264 */ 265 getSelectedLink : function( editor ) 266 { 267 try 268 { 269 var selection = editor.getSelection(); 270 if ( selection.getType() == CKEDITOR.SELECTION_ELEMENT ) 271 { 272 var selectedElement = selection.getSelectedElement(); 273 if ( selectedElement.is( 'a' ) ) 274 return selectedElement; 275 } 276 277 var range = selection.getRanges( true )[ 0 ]; 278 range.shrink( CKEDITOR.SHRINK_TEXT ); 279 var root = range.getCommonAncestor(); 280 return root.getAscendant( 'a', true ); 281 } 282 catch( e ) { return null; } 283 }, 284 285 // Opera and WebKit don't make it possible to select empty anchors. Fake 286 // elements must be used for them. 287 fakeAnchor : CKEDITOR.env.opera || CKEDITOR.env.webkit, 288 289 // For browsers that don't support CSS3 a[name]:empty(), note IE9 is included because of #7783. 290 synAnchorSelector : CKEDITOR.env.ie, 291 292 // For browsers that have editing issue with empty anchor. 293 emptyAnchorFix : CKEDITOR.env.ie && CKEDITOR.env.version < 8, 294 295 tryRestoreFakeAnchor : function( editor, element ) 296 { 297 if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) 298 { 299 var link = editor.restoreRealElement( element ); 300 if ( link.data( 'cke-saved-name' ) ) 301 return link; 302 } 303 } 304}; 305 306CKEDITOR.unlinkCommand = function(){}; 307CKEDITOR.unlinkCommand.prototype = 308{ 309 /** @ignore */ 310 exec : function( editor ) 311 { 312 /* 313 * execCommand( 'unlink', ... ) in Firefox leaves behind <span> tags at where 314 * the <a> was, so again we have to remove the link ourselves. (See #430) 315 * 316 * TODO: Use the style system when it's complete. Let's use execCommand() 317 * as a stopgap solution for now. 318 */ 319 var selection = editor.getSelection(), 320 bookmarks = selection.createBookmarks(), 321 ranges = selection.getRanges(), 322 rangeRoot, 323 element; 324 325 for ( var i = 0 ; i < ranges.length ; i++ ) 326 { 327 rangeRoot = ranges[i].getCommonAncestor( true ); 328 element = rangeRoot.getAscendant( 'a', true ); 329 if ( !element ) 330 continue; 331 ranges[i].selectNodeContents( element ); 332 } 333 334 selection.selectRanges( ranges ); 335 editor.document.$.execCommand( 'unlink', false, null ); 336 selection.selectBookmarks( bookmarks ); 337 }, 338 339 startDisabled : true 340}; 341 342CKEDITOR.removeAnchorCommand = function(){}; 343CKEDITOR.removeAnchorCommand.prototype = 344{ 345 /** @ignore */ 346 exec : function( editor ) 347 { 348 var sel = editor.getSelection(), 349 bms = sel.createBookmarks(), 350 anchor; 351 if ( sel && ( anchor = sel.getSelectedElement() ) && ( CKEDITOR.plugins.link.fakeAnchor && !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) ) 352 anchor.remove( 1 ); 353 else 354 { 355 if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) 356 { 357 if ( anchor.hasAttribute( 'href' ) ) 358 { 359 anchor.removeAttributes( { name : 1, 'data-cke-saved-name' : 1 } ); 360 anchor.removeClass( 'cke_anchor' ); 361 } 362 else 363 anchor.remove( 1 ); 364 } 365 } 366 sel.selectBookmarks( bms ); 367 } 368}; 369 370CKEDITOR.tools.extend( CKEDITOR.config, 371{ 372 linkShowAdvancedTab : true, 373 linkShowTargetTab : true 374} ); 375