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