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