2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
9 CKEDITOR
.dialog
.add( 'link', function( editor
) {
10 var plugin
= CKEDITOR
.plugins
.link
,
13 function createRangeForLink( editor
, link
) {
14 var range
= editor
.createRange();
16 range
.setStartBefore( link
);
17 range
.setEndAfter( link
);
22 function insertLinksIntoSelection( editor
, data
) {
23 var attributes
= plugin
.getLinkAttributes( editor
, data
),
24 ranges
= editor
.getSelection().getRanges(),
25 style
= new CKEDITOR
.style( {
27 attributes: attributes
.set
36 style
.type
= CKEDITOR
.STYLE_INLINE
; // need to override... dunno why.
38 for ( i
= 0; i
< ranges
.length
; i
++ ) {
41 // Use link URL as text with a collapsed cursor.
42 if ( range
.collapsed
) {
43 // Short mailto link text view (http://dev.ckeditor.com/ticket/5736).
44 text
= new CKEDITOR
.dom
.text( data
.linkText
|| ( data
.type
== 'email' ?
45 data
.email
.address : attributes
.set[ 'data-cke-saved-href' ] ), editor
.document
);
46 range
.insertNode( text
);
47 range
.selectNodeContents( text
);
48 } else if ( initialLinkText
!== data
.linkText
) {
49 text
= new CKEDITOR
.dom
.text( data
.linkText
, editor
.document
);
51 // Shrink range to preserve block element.
52 range
.shrink( CKEDITOR
.SHRINK_TEXT
);
54 // Use extractHtmlFromRange to remove markup within the selection. Also this method is a little
55 // smarter than range#deleteContents as it plays better e.g. with table cells.
56 editor
.editable().extractHtmlFromRange( range
);
58 range
.insertNode( text
);
61 // Editable links nested within current range should be removed, so that the link is applied to whole selection.
62 nestedLinks
= range
._find( 'a' );
64 for ( j
= 0; j
< nestedLinks
.length
; j
++ ) {
65 nestedLinks
[ j
].remove( true );
70 style
.applyToRange( range
, editor
);
72 rangesToSelect
.push( range
);
75 editor
.getSelection().selectRanges( rangesToSelect
);
78 function editLinksInSelection( editor
, selectedElements
, data
) {
79 var attributes
= plugin
.getLinkAttributes( editor
, data
),
87 for ( i
= 0; i
< selectedElements
.length
; i
++ ) {
88 // We're only editing an existing link, so just overwrite the attributes.
89 element
= selectedElements
[ i
];
90 href
= element
.data( 'cke-saved-href' );
91 textView
= element
.getHtml();
93 element
.setAttributes( attributes
.set );
94 element
.removeAttributes( attributes
.removed
);
97 if ( data
.linkText
&& initialLinkText
!= data
.linkText
) {
98 // Display text has been changed.
99 newText
= data
.linkText
;
100 } else if ( href
== textView
|| data
.type
== 'email' && textView
.indexOf( '@' ) != -1 ) {
101 // Update text view when user changes protocol (http://dev.ckeditor.com/ticket/4612).
102 // Short mailto link text view (http://dev.ckeditor.com/ticket/5736).
103 newText
= data
.type
== 'email' ? data
.email
.address : attributes
.set[ 'data-cke-saved-href' ];
107 element
.setText( newText
);
110 ranges
.push( createRangeForLink( editor
, element
) );
113 // We changed the content, so need to select it again.
114 editor
.getSelection().selectRanges( ranges
);
117 // Handles the event when the "Target" selection box is changed.
118 var targetChanged = function() {
119 var dialog
= this.getDialog(),
120 popupFeatures
= dialog
.getContentElement( 'target', 'popupFeatures' ),
121 targetName
= dialog
.getContentElement( 'target', 'linkTargetName' ),
122 value
= this.getValue();
124 if ( !popupFeatures
|| !targetName
)
127 popupFeatures
= popupFeatures
.getElement();
128 popupFeatures
.hide();
129 targetName
.setValue( '' );
133 targetName
.setLabel( editor
.lang
.link
.targetFrameName
);
134 targetName
.getElement().show();
137 popupFeatures
.show();
138 targetName
.setLabel( editor
.lang
.link
.targetPopupName
);
139 targetName
.getElement().show();
142 targetName
.setValue( value
);
143 targetName
.getElement().hide();
149 // Handles the event when the "Type" selection box is changed.
150 var linkTypeChanged = function() {
151 var dialog
= this.getDialog(),
152 partIds
= [ 'urlOptions', 'anchorOptions', 'emailOptions' ],
153 typeValue
= this.getValue(),
154 uploadTab
= dialog
.definition
.getContents( 'upload' ),
155 uploadInitiallyHidden
= uploadTab
&& uploadTab
.hidden
;
157 if ( typeValue
== 'url' ) {
158 if ( editor
.config
.linkShowTargetTab
)
159 dialog
.showPage( 'target' );
160 if ( !uploadInitiallyHidden
)
161 dialog
.showPage( 'upload' );
163 dialog
.hidePage( 'target' );
164 if ( !uploadInitiallyHidden
)
165 dialog
.hidePage( 'upload' );
168 for ( var i
= 0; i
< partIds
.length
; i
++ ) {
169 var element
= dialog
.getContentElement( 'info', partIds
[ i
] );
173 element
= element
.getElement().getParent().getParent();
174 if ( partIds
[ i
] == typeValue
+ 'Options' )
183 var setupParams = function( page
, data
) {
185 this.setValue( data
[ page
][ this.id
] || '' );
188 var setupPopupParams = function( data
) {
189 return setupParams
.call( this, 'target', data
);
192 var setupAdvParams = function( data
) {
193 return setupParams
.call( this, 'advanced', data
);
196 var commitParams = function( page
, data
) {
200 data
[ page
][ this.id
] = this.getValue() || '';
203 var commitPopupParams = function( data
) {
204 return commitParams
.call( this, 'target', data
);
207 var commitAdvParams = function( data
) {
208 return commitParams
.call( this, 'advanced', data
);
211 var commonLang
= editor
.lang
.common
,
212 linkLang
= editor
.lang
.link
,
216 title: linkLang
.title
,
217 minWidth: ( CKEDITOR
.skinName
|| editor
.config
.skin
) == 'moono-lisa' ? 450 : 350,
221 label: linkLang
.info
,
222 title: linkLang
.info
,
225 id: 'linkDisplayText',
226 label: linkLang
.displayText
,
230 this.setValue( editor
.getSelection().getSelectedText() );
232 // Keep inner text so that it can be compared in commit function. By obtaining value from getData()
233 // we get value stripped from new line chars which is important when comparing the value later on.
234 initialLinkText
= this.getValue();
236 commit: function( data
) {
237 data
.linkText
= this.isEnabled() ? this.getValue() : '';
243 label: linkLang
.type
,
246 [ linkLang
.toUrl
, 'url' ],
247 [ linkLang
.toAnchor
, 'anchor' ],
248 [ linkLang
.toEmail
, 'email' ]
250 onChange: linkTypeChanged
,
251 setup: function( data
) {
252 this.setValue( data
.type
|| 'url' );
254 commit: function( data
) {
255 data
.type
= this.getValue();
263 widths: [ '25%', '75%' ],
267 label: commonLang
.protocol
,
268 'default': 'http://',
270 // Force 'ltr' for protocol names in BIDI. (http://dev.ckeditor.com/ticket/5433)
271 [ 'http://\u200E', 'http://' ],
272 [ 'https://\u200E', 'https://' ],
273 [ 'ftp://\u200E', 'ftp://' ],
274 [ 'news://\u200E', 'news://' ],
275 [ linkLang
.other
, '' ]
277 setup: function( data
) {
279 this.setValue( data
.url
.protocol
|| '' );
281 commit: function( data
) {
285 data
.url
.protocol
= this.getValue();
291 label: commonLang
.url
,
294 this.allowOnChange
= true;
296 onKeyUp: function() {
297 this.allowOnChange
= false;
298 var protocolCmb
= this.getDialog().getContentElement( 'info', 'protocol' ),
299 url
= this.getValue(),
300 urlOnChangeProtocol
= /^(http|https|ftp|news):\/\/(?=.)/i,
301 urlOnChangeTestOther
= /^((javascript:)|[#\/\.\?])/i;
303 var protocol
= urlOnChangeProtocol
.exec( url
);
305 this.setValue( url
.substr( protocol
[ 0 ].length
) );
306 protocolCmb
.setValue( protocol
[ 0 ].toLowerCase() );
307 } else if ( urlOnChangeTestOther
.test( url
) ) {
308 protocolCmb
.setValue( '' );
311 this.allowOnChange
= true;
313 onChange: function() {
314 if ( this.allowOnChange
) // Dont't call on dialog load.
317 validate: function() {
318 var dialog
= this.getDialog();
320 if ( dialog
.getContentElement( 'info', 'linkType' ) && dialog
.getValueOf( 'info', 'linkType' ) != 'url' )
323 if ( !editor
.config
.linkJavaScriptLinksAllowed
&& ( /javascript\:/ ).test( this.getValue() ) ) {
324 alert( commonLang
.invalidValue
); // jshint ignore:line
328 if ( this.getDialog().fakeObj
) // Edit Anchor.
331 var func
= CKEDITOR
.dialog
.validate
.notEmpty( linkLang
.noUrl
);
332 return func
.apply( this );
334 setup: function( data
) {
335 this.allowOnChange
= false;
337 this.setValue( data
.url
.url
);
338 this.allowOnChange
= true;
341 commit: function( data
) {
342 // IE will not trigger the onChange event if the mouse has been used
343 // to carry all the operations http://dev.ckeditor.com/ticket/4724
349 data
.url
.url
= this.getValue();
350 this.allowOnChange
= false;
354 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
355 this.getElement().show();
362 filebrowser: 'info:url',
363 label: commonLang
.browseServer
374 id: 'selectAnchorText',
375 label: linkLang
.selectAnchor
,
377 anchors
= plugin
.getEditorAnchors( editor
);
379 this.getElement()[ anchors
&& anchors
.length
? 'show' : 'hide' ]();
388 label: linkLang
.anchorName
,
389 style: 'width: 100%;',
393 setup: function( data
) {
398 for ( var i
= 0; i
< anchors
.length
; i
++ ) {
399 if ( anchors
[ i
].name
)
400 this.add( anchors
[ i
].name
);
405 this.setValue( data
.anchor
.name
);
407 var linkType
= this.getDialog().getContentElement( 'info', 'linkType' );
408 if ( linkType
&& linkType
.getValue() == 'email' )
411 commit: function( data
) {
415 data
.anchor
.name
= this.getValue();
422 label: linkLang
.anchorId
,
423 style: 'width: 100%;',
427 setup: function( data
) {
432 for ( var i
= 0; i
< anchors
.length
; i
++ ) {
433 if ( anchors
[ i
].id
)
434 this.add( anchors
[ i
].id
);
439 this.setValue( data
.anchor
.id
);
441 commit: function( data
) {
445 data
.anchor
.id
= this.getValue();
449 this.getElement()[ anchors
&& anchors
.length
? 'show' : 'hide' ]();
456 style: 'text-align: center;',
457 html: '<div role="note" tabIndex="-1">' + CKEDITOR
.tools
.htmlEncode( linkLang
.noAnchors
) + '</div>',
458 // Focus the first element defined in above html.
461 this.getElement()[ anchors
&& anchors
.length
? 'hide' : 'show' ]();
465 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
466 this.getElement().hide();
476 label: linkLang
.emailAddress
,
478 validate: function() {
479 var dialog
= this.getDialog();
481 if ( !dialog
.getContentElement( 'info', 'linkType' ) || dialog
.getValueOf( 'info', 'linkType' ) != 'email' )
484 var func
= CKEDITOR
.dialog
.validate
.notEmpty( linkLang
.noEmail
);
485 return func
.apply( this );
487 setup: function( data
) {
489 this.setValue( data
.email
.address
);
491 var linkType
= this.getDialog().getContentElement( 'info', 'linkType' );
492 if ( linkType
&& linkType
.getValue() == 'email' )
495 commit: function( data
) {
499 data
.email
.address
= this.getValue();
505 label: linkLang
.emailSubject
,
506 setup: function( data
) {
508 this.setValue( data
.email
.subject
);
510 commit: function( data
) {
514 data
.email
.subject
= this.getValue();
520 label: linkLang
.emailBody
,
523 setup: function( data
) {
525 this.setValue( data
.email
.body
);
527 commit: function( data
) {
531 data
.email
.body
= this.getValue();
535 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
536 this.getElement().hide();
542 requiredContent: 'a[target]', // This is not fully correct, because some target option requires JS.
543 label: linkLang
.target
,
544 title: linkLang
.target
,
547 widths: [ '50%', '50%' ],
550 id: 'linkTargetType',
551 label: commonLang
.target
,
553 style: 'width : 100%;',
555 [ commonLang
.notSet
, 'notSet' ],
556 [ linkLang
.targetFrame
, 'frame' ],
557 [ linkLang
.targetPopup
, 'popup' ],
558 [ commonLang
.targetNew
, '_blank' ],
559 [ commonLang
.targetTop
, '_top' ],
560 [ commonLang
.targetSelf
, '_self' ],
561 [ commonLang
.targetParent
, '_parent' ]
563 onChange: targetChanged
,
564 setup: function( data
) {
566 this.setValue( data
.target
.type
|| 'notSet' );
567 targetChanged
.call( this );
569 commit: function( data
) {
573 data
.target
.type
= this.getValue();
578 id: 'linkTargetName',
579 label: linkLang
.targetFrameName
,
581 setup: function( data
) {
583 this.setValue( data
.target
.name
);
585 commit: function( data
) {
589 data
.target
.name
= this.getValue().replace( /([^\x00-\x7F]|\s)/gi, '' );
601 label: linkLang
.popupFeatures
,
607 label: linkLang
.popupResizable
,
608 setup: setupPopupParams
,
609 commit: commitPopupParams
614 label: linkLang
.popupStatusBar
,
615 setup: setupPopupParams
,
616 commit: commitPopupParams
625 label: linkLang
.popupLocationBar
,
626 setup: setupPopupParams
,
627 commit: commitPopupParams
633 label: linkLang
.popupToolbar
,
634 setup: setupPopupParams
,
635 commit: commitPopupParams
644 label: linkLang
.popupMenuBar
,
645 setup: setupPopupParams
,
646 commit: commitPopupParams
652 label: linkLang
.popupFullScreen
,
653 setup: setupPopupParams
,
654 commit: commitPopupParams
663 label: linkLang
.popupScrollBars
,
664 setup: setupPopupParams
,
665 commit: commitPopupParams
671 label: linkLang
.popupDependent
,
672 setup: setupPopupParams
,
673 commit: commitPopupParams
681 widths: [ '50%', '50%' ],
682 labelLayout: 'horizontal',
683 label: commonLang
.width
,
685 setup: setupPopupParams
,
686 commit: commitPopupParams
691 labelLayout: 'horizontal',
692 widths: [ '50%', '50%' ],
693 label: linkLang
.popupLeft
,
695 setup: setupPopupParams
,
696 commit: commitPopupParams
704 labelLayout: 'horizontal',
705 widths: [ '50%', '50%' ],
706 label: commonLang
.height
,
708 setup: setupPopupParams
,
709 commit: commitPopupParams
714 labelLayout: 'horizontal',
715 label: linkLang
.popupTop
,
716 widths: [ '50%', '50%' ],
718 setup: setupPopupParams
,
719 commit: commitPopupParams
728 label: linkLang
.upload
,
729 title: linkLang
.upload
,
731 filebrowser: 'uploadButton',
735 label: commonLang
.upload
,
736 style: 'height:40px',
742 label: commonLang
.uploadSubmit
,
743 filebrowser: 'info:url',
744 'for': [ 'upload', 'upload' ]
749 label: linkLang
.advanced
,
750 title: linkLang
.advanced
,
756 widths: [ '45%', '35%', '20%' ],
760 requiredContent: 'a[id]',
762 setup: setupAdvParams
,
763 commit: commitAdvParams
768 requiredContent: 'a[dir]',
769 label: linkLang
.langDir
,
771 style: 'width:110px',
773 [ commonLang
.notSet
, '' ],
774 [ linkLang
.langDirLTR
, 'ltr' ],
775 [ linkLang
.langDirRTL
, 'rtl' ]
777 setup: setupAdvParams
,
778 commit: commitAdvParams
783 requiredContent: 'a[accesskey]',
785 label: linkLang
.acccessKey
,
787 setup: setupAdvParams
,
788 commit: commitAdvParams
794 widths: [ '45%', '35%', '20%' ],
797 label: linkLang
.name
,
799 requiredContent: 'a[name]',
800 setup: setupAdvParams
,
801 commit: commitAdvParams
806 label: linkLang
.langCode
,
808 requiredContent: 'a[lang]',
811 setup: setupAdvParams
,
812 commit: commitAdvParams
817 label: linkLang
.tabIndex
,
819 requiredContent: 'a[tabindex]',
822 setup: setupAdvParams
,
823 commit: commitAdvParams
833 widths: [ '45%', '55%' ],
836 label: linkLang
.advisoryTitle
,
837 requiredContent: 'a[title]',
840 setup: setupAdvParams
,
841 commit: commitAdvParams
846 label: linkLang
.advisoryContentType
,
847 requiredContent: 'a[type]',
849 id: 'advContentType',
850 setup: setupAdvParams
,
851 commit: commitAdvParams
857 widths: [ '45%', '55%' ],
860 label: linkLang
.cssClasses
,
861 requiredContent: 'a(cke-xyz)', // Random text like 'xyz' will check if all are allowed.
864 setup: setupAdvParams
,
865 commit: commitAdvParams
870 label: linkLang
.charset
,
871 requiredContent: 'a[charset]',
874 setup: setupAdvParams
,
875 commit: commitAdvParams
881 widths: [ '45%', '55%' ],
885 requiredContent: 'a[rel]',
888 setup: setupAdvParams
,
889 commit: commitAdvParams
893 label: linkLang
.styles
,
894 requiredContent: 'a{cke-xyz}', // Random text like 'xyz' will check if all are allowed.
897 validate: CKEDITOR
.dialog
.validate
.inlineStyle( editor
.lang
.common
.invalidInlineStyle
),
898 setup: setupAdvParams
,
899 commit: commitAdvParams
904 widths: [ '45%', '55%' ],
908 requiredContent: 'a[download]',
909 label: linkLang
.download
,
910 setup: function( data
) {
911 if ( data
.download
!== undefined )
912 this.setValue( 'checked', 'checked' );
914 commit: function( data
) {
915 if ( this.getValue() ) {
916 data
.download
= this.getValue();
924 var editor
= this.getParentEditor(),
925 selection
= editor
.getSelection(),
926 displayTextField
= this.getContentElement( 'info', 'linkDisplayText' ).getElement().getParent().getParent(),
927 elements
= plugin
.getSelectedLink( editor
, true ),
928 firstLink
= elements
[ 0 ] || null;
930 // Fill in all the relevant fields if there's already one link selected.
931 if ( firstLink
&& firstLink
.hasAttribute( 'href' ) ) {
932 // Don't change selection if some element is already selected.
933 // For example - don't destroy fake selection.
934 if ( !selection
.getSelectedElement() && !selection
.isInTable() ) {
935 selection
.selectElement( firstLink
);
939 var data
= plugin
.parseLinkAttributes( editor
, firstLink
);
941 // Here we'll decide whether or not we want to show Display Text field.
942 if ( elements
.length
<= 1 && plugin
.showDisplayTextForElement( firstLink
, editor
) ) {
943 displayTextField
.show();
945 displayTextField
.hide();
948 // Record down the selected element in the dialog.
949 this._
.selectedElements
= elements
;
951 this.setupContent( data
);
956 // Collect data from fields.
957 this.commitContent( data
);
959 if ( !this._
.selectedElements
.length
) {
960 insertLinksIntoSelection( editor
, data
);
962 editLinksInSelection( editor
, this._
.selectedElements
, data
);
964 delete this._
.selectedElements
;
968 if ( !editor
.config
.linkShowAdvancedTab
)
969 this.hidePage( 'advanced' ); //Hide Advanded tab.
971 if ( !editor
.config
.linkShowTargetTab
)
972 this.hidePage( 'target' ); //Hide Target tab.
974 // Inital focus on 'url' field if link is of type URL.
975 onFocus: function() {
976 var linkType
= this.getContentElement( 'info', 'linkType' ),
979 if ( linkType
&& linkType
.getValue() == 'url' ) {
980 urlField
= this.getContentElement( 'info', 'url' );
987 // jscs:disable maximumLineLength
989 * The e-mail address anti-spam protection option. The protection will be
990 * applied when creating or modifying e-mail links through the editor interface.
992 * Two methods of protection can be chosen:
994 * 1. The e-mail parts (name, domain, and any other query string) are
995 * assembled into a function call pattern. Such function must be
996 * provided by the developer in the pages that will use the contents.
997 * 2. Only the e-mail address is obfuscated into a special string that
998 * has no meaning for humans or spam bots, but which is properly
999 * rendered and accepted by the browser.
1001 * Both approaches require JavaScript to be enabled.
1003 * // href="mailto:tester@ckeditor.com?subject=subject&body=body"
1004 * config.emailProtection = '';
1006 * // href="<a href=\"javascript:void(location.href=\'mailto:\'+String.fromCharCode(116,101,115,116,101,114,64,99,107,101,100,105,116,111,114,46,99,111,109)+\'?subject=subject&body=body\')\">e-mail</a>"
1007 * config.emailProtection = 'encode';
1009 * // href="javascript:mt('tester','ckeditor.com','subject','body')"
1010 * config.emailProtection = 'mt(NAME,DOMAIN,SUBJECT,BODY)';
1013 * @cfg {String} [emailProtection='' (empty string = disabled)]
1014 * @member CKEDITOR.config