]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/link/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / link / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 'use strict';
7
8 ( function() {
9 CKEDITOR.plugins.add( 'link', {
10 requires: 'dialog,fakeobjects',
11 // jscs:disable maximumLineLength
12 lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
13 // jscs:enable maximumLineLength
14 icons: 'anchor,anchor-rtl,link,unlink', // %REMOVE_LINE_CORE%
15 hidpi: true, // %REMOVE_LINE_CORE%
16 onLoad: function() {
17 // Add the CSS styles for anchor placeholders.
18 var iconPath = CKEDITOR.getUrl( this.path + 'images' + ( CKEDITOR.env.hidpi ? '/hidpi' : '' ) + '/anchor.png' ),
19 baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
20
21 var template = '.%2 a.cke_anchor,' +
22 '.%2 a.cke_anchor_empty' +
23 ',.cke_editable.%2 a[name]' +
24 ',.cke_editable.%2 a[data-cke-saved-name]' +
25 '{' +
26 baseStyle +
27 'padding-%1:18px;' +
28 // Show the arrow cursor for the anchor image (FF at least).
29 'cursor:auto;' +
30 '}' +
31 '.%2 img.cke_anchor' +
32 '{' +
33 baseStyle +
34 'width:16px;' +
35 'min-height:15px;' +
36 // The default line-height on IE.
37 'height:1.15em;' +
38 // Opera works better with "middle" (even if not perfect)
39 'vertical-align:text-bottom;' +
40 '}';
41
42 // Styles with contents direction awareness.
43 function cssWithDir( dir ) {
44 return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
45 }
46
47 CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
48 },
49
50 init: function( editor ) {
51 var allowed = 'a[!href]',
52 required = 'a[href]';
53
54 if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'advanced' ) )
55 allowed = allowed.replace( ']', ',accesskey,charset,dir,id,lang,name,rel,tabindex,title,type,download]{*}(*)' );
56 if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'target' ) )
57 allowed = allowed.replace( ']', ',target,onclick]' );
58
59 // Add the link and unlink buttons.
60 editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link', {
61 allowedContent: allowed,
62 requiredContent: required
63 } ) );
64 editor.addCommand( 'anchor', new CKEDITOR.dialogCommand( 'anchor', {
65 allowedContent: 'a[!name,id]',
66 requiredContent: 'a[name]'
67 } ) );
68 editor.addCommand( 'unlink', new CKEDITOR.unlinkCommand() );
69 editor.addCommand( 'removeAnchor', new CKEDITOR.removeAnchorCommand() );
70
71 editor.setKeystroke( CKEDITOR.CTRL + 76 /*L*/, 'link' );
72
73 if ( editor.ui.addButton ) {
74 editor.ui.addButton( 'Link', {
75 label: editor.lang.link.toolbar,
76 command: 'link',
77 toolbar: 'links,10'
78 } );
79 editor.ui.addButton( 'Unlink', {
80 label: editor.lang.link.unlink,
81 command: 'unlink',
82 toolbar: 'links,20'
83 } );
84 editor.ui.addButton( 'Anchor', {
85 label: editor.lang.link.anchor.toolbar,
86 command: 'anchor',
87 toolbar: 'links,30'
88 } );
89 }
90
91 CKEDITOR.dialog.add( 'link', this.path + 'dialogs/link.js' );
92 CKEDITOR.dialog.add( 'anchor', this.path + 'dialogs/anchor.js' );
93
94 editor.on( 'doubleclick', function( evt ) {
95 // If the link has descendants and the last part of it is also a part of a word partially
96 // unlinked, clicked element may be a descendant of the link, not the link itself (http://dev.ckeditor.com/ticket/11956).
97 // The evt.data.element.getAscendant( 'img', 1 ) condition allows opening anchor dialog if the anchor is empty (#501).
98 var element = evt.data.element.getAscendant( { a: 1, img: 1 }, true );
99
100 if ( element && !element.isReadOnly() ) {
101 if ( element.is( 'a' ) ) {
102 evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link';
103
104 // Pass the link to be selected along with event data.
105 evt.data.link = element;
106 } else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) ) {
107 evt.data.dialog = 'anchor';
108 }
109 }
110 }, null, null, 0 );
111
112 // If event was cancelled, link passed in event data will not be selected.
113 editor.on( 'doubleclick', function( evt ) {
114 // Make sure both links and anchors are selected (http://dev.ckeditor.com/ticket/11822).
115 if ( evt.data.dialog in { link: 1, anchor: 1 } && evt.data.link )
116 editor.getSelection().selectElement( evt.data.link );
117 }, null, null, 20 );
118
119 // If the "menu" plugin is loaded, register the menu items.
120 if ( editor.addMenuItems ) {
121 editor.addMenuItems( {
122 anchor: {
123 label: editor.lang.link.anchor.menu,
124 command: 'anchor',
125 group: 'anchor',
126 order: 1
127 },
128
129 removeAnchor: {
130 label: editor.lang.link.anchor.remove,
131 command: 'removeAnchor',
132 group: 'anchor',
133 order: 5
134 },
135
136 link: {
137 label: editor.lang.link.menu,
138 command: 'link',
139 group: 'link',
140 order: 1
141 },
142
143 unlink: {
144 label: editor.lang.link.unlink,
145 command: 'unlink',
146 group: 'link',
147 order: 5
148 }
149 } );
150 }
151
152 // If the "contextmenu" plugin is loaded, register the listeners.
153 if ( editor.contextMenu ) {
154 editor.contextMenu.addListener( function( element ) {
155 if ( !element || element.isReadOnly() )
156 return null;
157
158 var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element );
159
160 if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) )
161 return null;
162
163 var menu = {};
164
165 if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() )
166 menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
167
168 if ( anchor && anchor.hasAttribute( 'name' ) )
169 menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF;
170
171 return menu;
172 } );
173 }
174
175 this.compiledProtectionFunction = getCompiledProtectionFunction( editor );
176 },
177
178 afterInit: function( editor ) {
179 // Empty anchors upcasting to fake objects.
180 editor.dataProcessor.dataFilter.addRules( {
181 elements: {
182 a: function( element ) {
183 if ( !element.attributes.name )
184 return null;
185
186 if ( !element.children.length )
187 return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' );
188
189 return null;
190 }
191 }
192 } );
193
194 var pathFilters = editor._.elementsPath && editor._.elementsPath.filters;
195 if ( pathFilters ) {
196 pathFilters.push( function( element, name ) {
197 if ( name == 'a' ) {
198 if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) )
199 return 'anchor';
200 }
201 } );
202 }
203 }
204 } );
205
206 // Loads the parameters in a selected link to the link dialog fields.
207 var javascriptProtocolRegex = /^javascript:/,
208 emailRegex = /^mailto:([^?]+)(?:\?(.+))?$/,
209 emailSubjectRegex = /subject=([^;?:@&=$,\/]*)/i,
210 emailBodyRegex = /body=([^;?:@&=$,\/]*)/i,
211 anchorRegex = /^#(.*)$/,
212 urlRegex = /^((?:http|https|ftp|news):\/\/)?(.*)$/,
213 selectableTargets = /^(_(?:self|top|parent|blank))$/,
214 encodedEmailLinkRegex = /^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/,
215 functionCallProtectedEmailLinkRegex = /^javascript:([^(]+)\(([^)]+)\)$/,
216 popupRegex = /\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/,
217 popupFeaturesRegex = /(?:^|,)([^=]+)=(\d+|yes|no)/gi;
218
219 var advAttrNames = {
220 id: 'advId',
221 dir: 'advLangDir',
222 accessKey: 'advAccessKey',
223 // 'data-cke-saved-name': 'advName',
224 name: 'advName',
225 lang: 'advLangCode',
226 tabindex: 'advTabIndex',
227 title: 'advTitle',
228 type: 'advContentType',
229 'class': 'advCSSClasses',
230 charset: 'advCharset',
231 style: 'advStyles',
232 rel: 'advRel'
233 };
234
235 function unescapeSingleQuote( str ) {
236 return str.replace( /\\'/g, '\'' );
237 }
238
239 function escapeSingleQuote( str ) {
240 return str.replace( /'/g, '\\$&' );
241 }
242
243 function protectEmailAddressAsEncodedString( address ) {
244 var charCode,
245 length = address.length,
246 encodedChars = [];
247
248 for ( var i = 0; i < length; i++ ) {
249 charCode = address.charCodeAt( i );
250 encodedChars.push( charCode );
251 }
252
253 return 'String.fromCharCode(' + encodedChars.join( ',' ) + ')';
254 }
255
256 function protectEmailLinkAsFunction( editor, email ) {
257 var plugin = editor.plugins.link,
258 name = plugin.compiledProtectionFunction.name,
259 params = plugin.compiledProtectionFunction.params,
260 paramName, paramValue, retval;
261
262 retval = [ name, '(' ];
263 for ( var i = 0; i < params.length; i++ ) {
264 paramName = params[ i ].toLowerCase();
265 paramValue = email[ paramName ];
266
267 i > 0 && retval.push( ',' );
268 retval.push( '\'', paramValue ? escapeSingleQuote( encodeURIComponent( email[ paramName ] ) ) : '', '\'' );
269 }
270 retval.push( ')' );
271 return retval.join( '' );
272 }
273
274 function getCompiledProtectionFunction( editor ) {
275 var emailProtection = editor.config.emailProtection || '',
276 compiledProtectionFunction;
277
278 // Compile the protection function pattern.
279 if ( emailProtection && emailProtection != 'encode' ) {
280 compiledProtectionFunction = {};
281
282 emailProtection.replace( /^([^(]+)\(([^)]+)\)$/, function( match, funcName, params ) {
283 compiledProtectionFunction.name = funcName;
284 compiledProtectionFunction.params = [];
285 params.replace( /[^,\s]+/g, function( param ) {
286 compiledProtectionFunction.params.push( param );
287 } );
288 } );
289 }
290
291 return compiledProtectionFunction;
292 }
293
294 /**
295 * Set of Link plugin helpers.
296 *
297 * @class
298 * @singleton
299 */
300 CKEDITOR.plugins.link = {
301 /**
302 * Get the surrounding link element of the current selection.
303 *
304 * CKEDITOR.plugins.link.getSelectedLink( editor );
305 *
306 * // The following selections will all return the link element.
307 *
308 * <a href="#">li^nk</a>
309 * <a href="#">[link]</a>
310 * text[<a href="#">link]</a>
311 * <a href="#">li[nk</a>]
312 * [<b><a href="#">li]nk</a></b>]
313 * [<a href="#"><b>li]nk</b></a>
314 *
315 * @since 3.2.1
316 * @param {CKEDITOR.editor} editor
317 * @param {Boolean} [returnMultiple=false] Indicates whether the function should return only the first selected link or all of them.
318 * @returns {CKEDITOR.dom.element/CKEDITOR.dom.element[]/null} A single link element or an array of link
319 * elements relevant to the current selection.
320 */
321 getSelectedLink: function( editor, returnMultiple ) {
322 var selection = editor.getSelection(),
323 selectedElement = selection.getSelectedElement(),
324 ranges = selection.getRanges(),
325 links = [],
326 link,
327 range,
328 i;
329
330 if ( !returnMultiple && selectedElement && selectedElement.is( 'a' ) ) {
331 return selectedElement;
332 }
333
334 for ( i = 0; i < ranges.length; i++ ) {
335 range = selection.getRanges()[ i ];
336
337 // Skip bogus to cover cases of multiple selection inside tables (#tp2245).
338 range.shrink( CKEDITOR.SHRINK_TEXT, false, { skipBogus: true } );
339 link = editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 );
340
341 if ( link && returnMultiple ) {
342 links.push( link );
343 } else if ( link ) {
344 return link;
345 }
346 }
347
348 return returnMultiple ? links : null;
349 },
350
351 /**
352 * Collects anchors available in the editor (i.e. used by the Link plugin).
353 * Note that the scope of search is different for inline (the "global" document) and
354 * classic (`iframe`-based) editors (the "inner" document).
355 *
356 * @since 4.3.3
357 * @param {CKEDITOR.editor} editor
358 * @returns {CKEDITOR.dom.element[]} An array of anchor elements.
359 */
360 getEditorAnchors: function( editor ) {
361 var editable = editor.editable(),
362
363 // The scope of search for anchors is the entire document for inline editors
364 // and editor's editable for classic editor/divarea (http://dev.ckeditor.com/ticket/11359).
365 scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable,
366
367 links = scope.getElementsByTag( 'a' ),
368 imgs = scope.getElementsByTag( 'img' ),
369 anchors = [],
370 i = 0,
371 item;
372
373 // Retrieve all anchors within the scope.
374 while ( ( item = links.getItem( i++ ) ) ) {
375 if ( item.data( 'cke-saved-name' ) || item.hasAttribute( 'name' ) ) {
376 anchors.push( {
377 name: item.data( 'cke-saved-name' ) || item.getAttribute( 'name' ),
378 id: item.getAttribute( 'id' )
379 } );
380 }
381 }
382 // Retrieve all "fake anchors" within the scope.
383 i = 0;
384
385 while ( ( item = imgs.getItem( i++ ) ) ) {
386 if ( ( item = this.tryRestoreFakeAnchor( editor, item ) ) ) {
387 anchors.push( {
388 name: item.getAttribute( 'name' ),
389 id: item.getAttribute( 'id' )
390 } );
391 }
392 }
393
394 return anchors;
395 },
396
397 /**
398 * Opera and WebKit do not make it possible to select empty anchors. Fake
399 * elements must be used for them.
400 *
401 * @readonly
402 * @deprecated 4.3.3 It is set to `true` in every browser.
403 * @property {Boolean}
404 */
405 fakeAnchor: true,
406
407 /**
408 * For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of http://dev.ckeditor.com/ticket/7783.
409 *
410 * @readonly
411 * @deprecated 4.3.3 It is set to `false` in every browser.
412 * @property {Boolean} synAnchorSelector
413 */
414
415 /**
416 * For browsers that have editing issues with an empty anchor.
417 *
418 * @readonly
419 * @deprecated 4.3.3 It is set to `false` in every browser.
420 * @property {Boolean} emptyAnchorFix
421 */
422
423 /**
424 * Returns an element representing a real anchor restored from a fake anchor.
425 *
426 * @param {CKEDITOR.editor} editor
427 * @param {CKEDITOR.dom.element} element
428 * @returns {CKEDITOR.dom.element} Restored anchor element or nothing if the
429 * passed element was not a fake anchor.
430 */
431 tryRestoreFakeAnchor: function( editor, element ) {
432 if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) {
433 var link = editor.restoreRealElement( element );
434 if ( link.data( 'cke-saved-name' ) )
435 return link;
436 }
437 },
438
439 /**
440 * Parses attributes of the link element and returns an object representing
441 * the current state (data) of the link. This data format is a plain object accepted
442 * e.g. by the Link dialog window and {@link #getLinkAttributes}.
443 *
444 * **Note:** Data model format produced by the parser must be compatible with the Link
445 * plugin dialog because it is passed directly to {@link CKEDITOR.dialog#setupContent}.
446 *
447 * @since 4.4
448 * @param {CKEDITOR.editor} editor
449 * @param {CKEDITOR.dom.element} element
450 * @returns {Object} An object of link data.
451 */
452 parseLinkAttributes: function( editor, element ) {
453 var href = ( element && ( element.data( 'cke-saved-href' ) || element.getAttribute( 'href' ) ) ) || '',
454 compiledProtectionFunction = editor.plugins.link.compiledProtectionFunction,
455 emailProtection = editor.config.emailProtection,
456 javascriptMatch, emailMatch, anchorMatch, urlMatch,
457 retval = {};
458
459 if ( ( javascriptMatch = href.match( javascriptProtocolRegex ) ) ) {
460 if ( emailProtection == 'encode' ) {
461 href = href.replace( encodedEmailLinkRegex, function( match, protectedAddress, rest ) {
462 // Without it 'undefined' is appended to e-mails without subject and body (http://dev.ckeditor.com/ticket/9192).
463 rest = rest || '';
464
465 return 'mailto:' +
466 String.fromCharCode.apply( String, protectedAddress.split( ',' ) ) +
467 unescapeSingleQuote( rest );
468 } );
469 }
470 // Protected email link as function call.
471 else if ( emailProtection ) {
472 href.replace( functionCallProtectedEmailLinkRegex, function( match, funcName, funcArgs ) {
473 if ( funcName == compiledProtectionFunction.name ) {
474 retval.type = 'email';
475 var email = retval.email = {};
476
477 var paramRegex = /[^,\s]+/g,
478 paramQuoteRegex = /(^')|('$)/g,
479 paramsMatch = funcArgs.match( paramRegex ),
480 paramsMatchLength = paramsMatch.length,
481 paramName, paramVal;
482
483 for ( var i = 0; i < paramsMatchLength; i++ ) {
484 paramVal = decodeURIComponent( unescapeSingleQuote( paramsMatch[ i ].replace( paramQuoteRegex, '' ) ) );
485 paramName = compiledProtectionFunction.params[ i ].toLowerCase();
486 email[ paramName ] = paramVal;
487 }
488 email.address = [ email.name, email.domain ].join( '@' );
489 }
490 } );
491 }
492 }
493
494 if ( !retval.type ) {
495 if ( ( anchorMatch = href.match( anchorRegex ) ) ) {
496 retval.type = 'anchor';
497 retval.anchor = {};
498 retval.anchor.name = retval.anchor.id = anchorMatch[ 1 ];
499 }
500 // Protected email link as encoded string.
501 else if ( ( emailMatch = href.match( emailRegex ) ) ) {
502 var subjectMatch = href.match( emailSubjectRegex ),
503 bodyMatch = href.match( emailBodyRegex );
504
505 retval.type = 'email';
506 var email = ( retval.email = {} );
507 email.address = emailMatch[ 1 ];
508 subjectMatch && ( email.subject = decodeURIComponent( subjectMatch[ 1 ] ) );
509 bodyMatch && ( email.body = decodeURIComponent( bodyMatch[ 1 ] ) );
510 }
511 // urlRegex matches empty strings, so need to check for href as well.
512 else if ( href && ( urlMatch = href.match( urlRegex ) ) ) {
513 retval.type = 'url';
514 retval.url = {};
515 retval.url.protocol = urlMatch[ 1 ];
516 retval.url.url = urlMatch[ 2 ];
517 }
518 }
519
520 // Load target and popup settings.
521 if ( element ) {
522 var target = element.getAttribute( 'target' );
523
524 // IE BUG: target attribute is an empty string instead of null in IE if it's not set.
525 if ( !target ) {
526 var onclick = element.data( 'cke-pa-onclick' ) || element.getAttribute( 'onclick' ),
527 onclickMatch = onclick && onclick.match( popupRegex );
528
529 if ( onclickMatch ) {
530 retval.target = {
531 type: 'popup',
532 name: onclickMatch[ 1 ]
533 };
534
535 var featureMatch;
536 while ( ( featureMatch = popupFeaturesRegex.exec( onclickMatch[ 2 ] ) ) ) {
537 // Some values should remain numbers (http://dev.ckeditor.com/ticket/7300)
538 if ( ( featureMatch[ 2 ] == 'yes' || featureMatch[ 2 ] == '1' ) && !( featureMatch[ 1 ] in { height: 1, width: 1, top: 1, left: 1 } ) )
539 retval.target[ featureMatch[ 1 ] ] = true;
540 else if ( isFinite( featureMatch[ 2 ] ) )
541 retval.target[ featureMatch[ 1 ] ] = featureMatch[ 2 ];
542 }
543 }
544 } else {
545 retval.target = {
546 type: target.match( selectableTargets ) ? target : 'frame',
547 name: target
548 };
549 }
550
551 var download = element.getAttribute( 'download' );
552 if ( download !== null ) {
553 retval.download = true;
554 }
555
556 var advanced = {};
557
558 for ( var a in advAttrNames ) {
559 var val = element.getAttribute( a );
560
561 if ( val )
562 advanced[ advAttrNames[ a ] ] = val;
563 }
564
565 var advName = element.data( 'cke-saved-name' ) || advanced.advName;
566
567 if ( advName )
568 advanced.advName = advName;
569
570 if ( !CKEDITOR.tools.isEmpty( advanced ) )
571 retval.advanced = advanced;
572 }
573
574 return retval;
575 },
576
577 /**
578 * Converts link data produced by {@link #parseLinkAttributes} into an object which consists
579 * of attributes to be set (with their values) and an array of attributes to be removed.
580 * This method can be used to compose or to update any link element with the given data.
581 *
582 * @since 4.4
583 * @param {CKEDITOR.editor} editor
584 * @param {Object} data Data in {@link #parseLinkAttributes} format.
585 * @returns {Object} An object consisting of two keys, i.e.:
586 *
587 * {
588 * // Attributes to be set.
589 * set: {
590 * href: 'http://foo.bar',
591 * target: 'bang'
592 * },
593 * // Attributes to be removed.
594 * removed: [
595 * 'id', 'style'
596 * ]
597 * }
598 *
599 */
600 getLinkAttributes: function( editor, data ) {
601 var emailProtection = editor.config.emailProtection || '',
602 set = {};
603
604 // Compose the URL.
605 switch ( data.type ) {
606 case 'url':
607 var protocol = ( data.url && data.url.protocol !== undefined ) ? data.url.protocol : 'http://',
608 url = ( data.url && CKEDITOR.tools.trim( data.url.url ) ) || '';
609
610 set[ 'data-cke-saved-href' ] = ( url.indexOf( '/' ) === 0 ) ? url : protocol + url;
611
612 break;
613 case 'anchor':
614 var name = ( data.anchor && data.anchor.name ),
615 id = ( data.anchor && data.anchor.id );
616
617 set[ 'data-cke-saved-href' ] = '#' + ( name || id || '' );
618
619 break;
620 case 'email':
621 var email = data.email,
622 address = email.address,
623 linkHref;
624
625 switch ( emailProtection ) {
626 case '':
627 case 'encode':
628 var subject = encodeURIComponent( email.subject || '' ),
629 body = encodeURIComponent( email.body || '' ),
630 argList = [];
631
632 // Build the e-mail parameters first.
633 subject && argList.push( 'subject=' + subject );
634 body && argList.push( 'body=' + body );
635 argList = argList.length ? '?' + argList.join( '&' ) : '';
636
637 if ( emailProtection == 'encode' ) {
638 linkHref = [
639 'javascript:void(location.href=\'mailto:\'+', // jshint ignore:line
640 protectEmailAddressAsEncodedString( address )
641 ];
642 // parameters are optional.
643 argList && linkHref.push( '+\'', escapeSingleQuote( argList ), '\'' );
644
645 linkHref.push( ')' );
646 } else {
647 linkHref = [ 'mailto:', address, argList ];
648 }
649
650 break;
651 default:
652 // Separating name and domain.
653 var nameAndDomain = address.split( '@', 2 );
654 email.name = nameAndDomain[ 0 ];
655 email.domain = nameAndDomain[ 1 ];
656
657 linkHref = [ 'javascript:', protectEmailLinkAsFunction( editor, email ) ]; // jshint ignore:line
658 }
659
660 set[ 'data-cke-saved-href' ] = linkHref.join( '' );
661 break;
662 }
663
664 // Popups and target.
665 if ( data.target ) {
666 if ( data.target.type == 'popup' ) {
667 var onclickList = [
668 'window.open(this.href, \'', data.target.name || '', '\', \''
669 ],
670 featureList = [
671 'resizable', 'status', 'location', 'toolbar', 'menubar', 'fullscreen', 'scrollbars', 'dependent'
672 ],
673 featureLength = featureList.length,
674 addFeature = function( featureName ) {
675 if ( data.target[ featureName ] )
676 featureList.push( featureName + '=' + data.target[ featureName ] );
677 };
678
679 for ( var i = 0; i < featureLength; i++ )
680 featureList[ i ] = featureList[ i ] + ( data.target[ featureList[ i ] ] ? '=yes' : '=no' );
681
682 addFeature( 'width' );
683 addFeature( 'left' );
684 addFeature( 'height' );
685 addFeature( 'top' );
686
687 onclickList.push( featureList.join( ',' ), '\'); return false;' );
688 set[ 'data-cke-pa-onclick' ] = onclickList.join( '' );
689 }
690 else if ( data.target.type != 'notSet' && data.target.name ) {
691 set.target = data.target.name;
692 }
693 }
694
695 // Force download attribute.
696 if ( data.download ) {
697 set.download = '';
698 }
699
700 // Advanced attributes.
701 if ( data.advanced ) {
702 for ( var a in advAttrNames ) {
703 var val = data.advanced[ advAttrNames[ a ] ];
704
705 if ( val )
706 set[ a ] = val;
707 }
708
709 if ( set.name )
710 set[ 'data-cke-saved-name' ] = set.name;
711 }
712
713 // Browser need the "href" fro copy/paste link to work. (http://dev.ckeditor.com/ticket/6641)
714 if ( set[ 'data-cke-saved-href' ] )
715 set.href = set[ 'data-cke-saved-href' ];
716
717 var removed = {
718 target: 1,
719 onclick: 1,
720 'data-cke-pa-onclick': 1,
721 'data-cke-saved-name': 1,
722 'download': 1
723 };
724
725 if ( data.advanced )
726 CKEDITOR.tools.extend( removed, advAttrNames );
727
728 // Remove all attributes which are not currently set.
729 for ( var s in set )
730 delete removed[ s ];
731
732 return {
733 set: set,
734 removed: CKEDITOR.tools.objectKeys( removed )
735 };
736 },
737
738
739 /**
740 * Determines whether an element should have a "Display Text" field in the Link dialog.
741 *
742 * @since 4.5.11
743 * @param {CKEDITOR.dom.element/null} element Selected element, `null` if none selected or if a ranged selection
744 * is made.
745 * @param {CKEDITOR.editor} editor The editor instance for which the check is performed.
746 * @returns {Boolean}
747 */
748 showDisplayTextForElement: function( element, editor ) {
749 var undesiredElements = {
750 img: 1,
751 table: 1,
752 tbody: 1,
753 thead: 1,
754 tfoot: 1,
755 input: 1,
756 select: 1,
757 textarea: 1
758 },
759 selection = editor.getSelection();
760
761 // Widget duck typing, we don't want to show display text for widgets.
762 if ( editor.widgets && editor.widgets.focused ) {
763 return false;
764 }
765
766 if ( selection && selection.getRanges().length > 1 ) {
767 return false;
768 }
769
770 return !element || !element.getName || !element.is( undesiredElements );
771 }
772 };
773
774 // TODO Much probably there's no need to expose these as public objects.
775
776 CKEDITOR.unlinkCommand = function() {};
777 CKEDITOR.unlinkCommand.prototype = {
778 exec: function( editor ) {
779 // IE/Edge removes link from selection while executing "unlink" command when cursor
780 // is right before/after link's text. Therefore whole link must be selected and the
781 // position of cursor must be restored to its initial state after unlinking. (http://dev.ckeditor.com/ticket/13062)
782 if ( CKEDITOR.env.ie ) {
783 var range = editor.getSelection().getRanges()[ 0 ],
784 link = ( range.getPreviousEditableNode() && range.getPreviousEditableNode().getAscendant( 'a', true ) ) ||
785 ( range.getNextEditableNode() && range.getNextEditableNode().getAscendant( 'a', true ) ),
786 bookmark;
787
788 if ( range.collapsed && link ) {
789 bookmark = range.createBookmark();
790 range.selectNodeContents( link );
791 range.select();
792 }
793 }
794
795 var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } );
796 editor.removeStyle( style );
797
798 if ( bookmark ) {
799 range.moveToBookmark( bookmark );
800 range.select();
801 }
802 },
803
804 refresh: function( editor, path ) {
805 // Despite our initial hope, document.queryCommandEnabled() does not work
806 // for this in Firefox. So we must detect the state by element paths.
807
808 var element = path.lastElement && path.lastElement.getAscendant( 'a', true );
809
810 if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() )
811 this.setState( CKEDITOR.TRISTATE_OFF );
812 else
813 this.setState( CKEDITOR.TRISTATE_DISABLED );
814 },
815
816 contextSensitive: 1,
817 startDisabled: 1,
818 requiredContent: 'a[href]',
819 editorFocus: 1
820 };
821
822 CKEDITOR.removeAnchorCommand = function() {};
823 CKEDITOR.removeAnchorCommand.prototype = {
824 exec: function( editor ) {
825 var sel = editor.getSelection(),
826 bms = sel.createBookmarks(),
827 anchor;
828 if ( sel && ( anchor = sel.getSelectedElement() ) && ( !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) )
829 anchor.remove( 1 );
830 else {
831 if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) {
832 if ( anchor.hasAttribute( 'href' ) ) {
833 anchor.removeAttributes( { name: 1, 'data-cke-saved-name': 1 } );
834 anchor.removeClass( 'cke_anchor' );
835 } else {
836 anchor.remove( 1 );
837 }
838 }
839 }
840 sel.selectBookmarks( bms );
841 },
842 requiredContent: 'a[name]'
843 };
844
845 CKEDITOR.tools.extend( CKEDITOR.config, {
846 /**
847 * Whether to show the Advanced tab in the Link dialog window.
848 *
849 * @cfg {Boolean} [linkShowAdvancedTab=true]
850 * @member CKEDITOR.config
851 */
852 linkShowAdvancedTab: true,
853
854 /**
855 * Whether to show the Target tab in the Link dialog window.
856 *
857 * @cfg {Boolean} [linkShowTargetTab=true]
858 * @member CKEDITOR.config
859 */
860 linkShowTargetTab: true
861
862 /**
863 * Whether JavaScript code is allowed as a `href` attribute in an anchor tag.
864 * With this option enabled it is possible to create links like:
865 *
866 * <a href="javascript:alert('Hello world!')">hello world</a>
867 *
868 * By default JavaScript links are not allowed and will not pass
869 * the Link dialog window validation.
870 *
871 * @since 4.4.1
872 * @cfg {Boolean} [linkJavaScriptLinksAllowed=false]
873 * @member CKEDITOR.config
874 */
875 } );
876 } )();