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 // Handles the event when the "Target" selection box is changed.
14 var targetChanged = function() {
15 var dialog
= this.getDialog(),
16 popupFeatures
= dialog
.getContentElement( 'target', 'popupFeatures' ),
17 targetName
= dialog
.getContentElement( 'target', 'linkTargetName' ),
18 value
= this.getValue();
20 if ( !popupFeatures
|| !targetName
)
23 popupFeatures
= popupFeatures
.getElement();
25 targetName
.setValue( '' );
29 targetName
.setLabel( editor
.lang
.link
.targetFrameName
);
30 targetName
.getElement().show();
34 targetName
.setLabel( editor
.lang
.link
.targetPopupName
);
35 targetName
.getElement().show();
38 targetName
.setValue( value
);
39 targetName
.getElement().hide();
45 // Handles the event when the "Type" selection box is changed.
46 var linkTypeChanged = function() {
47 var dialog
= this.getDialog(),
48 partIds
= [ 'urlOptions', 'anchorOptions', 'emailOptions' ],
49 typeValue
= this.getValue(),
50 uploadTab
= dialog
.definition
.getContents( 'upload' ),
51 uploadInitiallyHidden
= uploadTab
&& uploadTab
.hidden
;
53 if ( typeValue
== 'url' ) {
54 if ( editor
.config
.linkShowTargetTab
)
55 dialog
.showPage( 'target' );
56 if ( !uploadInitiallyHidden
)
57 dialog
.showPage( 'upload' );
59 dialog
.hidePage( 'target' );
60 if ( !uploadInitiallyHidden
)
61 dialog
.hidePage( 'upload' );
64 for ( var i
= 0; i
< partIds
.length
; i
++ ) {
65 var element
= dialog
.getContentElement( 'info', partIds
[ i
] );
69 element
= element
.getElement().getParent().getParent();
70 if ( partIds
[ i
] == typeValue
+ 'Options' )
79 var setupParams = function( page
, data
) {
81 this.setValue( data
[ page
][ this.id
] || '' );
84 var setupPopupParams = function( data
) {
85 return setupParams
.call( this, 'target', data
);
88 var setupAdvParams = function( data
) {
89 return setupParams
.call( this, 'advanced', data
);
92 var commitParams = function( page
, data
) {
96 data
[ page
][ this.id
] = this.getValue() || '';
99 var commitPopupParams = function( data
) {
100 return commitParams
.call( this, 'target', data
);
103 var commitAdvParams = function( data
) {
104 return commitParams
.call( this, 'advanced', data
);
107 var commonLang
= editor
.lang
.common
,
108 linkLang
= editor
.lang
.link
,
112 title: linkLang
.title
,
113 minWidth: ( CKEDITOR
.skinName
|| editor
.config
.skin
) == 'moono-lisa' ? 450 : 350,
117 label: linkLang
.info
,
118 title: linkLang
.info
,
121 id: 'linkDisplayText',
122 label: linkLang
.displayText
,
126 this.setValue( editor
.getSelection().getSelectedText() );
128 // Keep inner text so that it can be compared in commit function. By obtaining value from getData()
129 // we get value stripped from new line chars which is important when comparing the value later on.
130 initialLinkText
= this.getValue();
132 commit: function( data
) {
133 data
.linkText
= this.isEnabled() ? this.getValue() : '';
139 label: linkLang
.type
,
142 [ linkLang
.toUrl
, 'url' ],
143 [ linkLang
.toAnchor
, 'anchor' ],
144 [ linkLang
.toEmail
, 'email' ]
146 onChange: linkTypeChanged
,
147 setup: function( data
) {
148 this.setValue( data
.type
|| 'url' );
150 commit: function( data
) {
151 data
.type
= this.getValue();
159 widths: [ '25%', '75%' ],
163 label: commonLang
.protocol
,
164 'default': 'http://',
166 // Force 'ltr' for protocol names in BIDI. (#5433)
167 [ 'http://\u200E', 'http://' ],
168 [ 'https://\u200E', 'https://' ],
169 [ 'ftp://\u200E', 'ftp://' ],
170 [ 'news://\u200E', 'news://' ],
171 [ linkLang
.other
, '' ]
173 setup: function( data
) {
175 this.setValue( data
.url
.protocol
|| '' );
177 commit: function( data
) {
181 data
.url
.protocol
= this.getValue();
187 label: commonLang
.url
,
190 this.allowOnChange
= true;
192 onKeyUp: function() {
193 this.allowOnChange
= false;
194 var protocolCmb
= this.getDialog().getContentElement( 'info', 'protocol' ),
195 url
= this.getValue(),
196 urlOnChangeProtocol
= /^(http|https|ftp|news):\/\/(?=.)/i,
197 urlOnChangeTestOther
= /^((javascript:)|[#\/\.\?])/i;
199 var protocol
= urlOnChangeProtocol
.exec( url
);
201 this.setValue( url
.substr( protocol
[ 0 ].length
) );
202 protocolCmb
.setValue( protocol
[ 0 ].toLowerCase() );
203 } else if ( urlOnChangeTestOther
.test( url
) ) {
204 protocolCmb
.setValue( '' );
207 this.allowOnChange
= true;
209 onChange: function() {
210 if ( this.allowOnChange
) // Dont't call on dialog load.
213 validate: function() {
214 var dialog
= this.getDialog();
216 if ( dialog
.getContentElement( 'info', 'linkType' ) && dialog
.getValueOf( 'info', 'linkType' ) != 'url' )
219 if ( !editor
.config
.linkJavaScriptLinksAllowed
&& ( /javascript\:/ ).test( this.getValue() ) ) {
220 alert( commonLang
.invalidValue
); // jshint ignore:line
224 if ( this.getDialog().fakeObj
) // Edit Anchor.
227 var func
= CKEDITOR
.dialog
.validate
.notEmpty( linkLang
.noUrl
);
228 return func
.apply( this );
230 setup: function( data
) {
231 this.allowOnChange
= false;
233 this.setValue( data
.url
.url
);
234 this.allowOnChange
= true;
237 commit: function( data
) {
238 // IE will not trigger the onChange event if the mouse has been used
239 // to carry all the operations #4724
245 data
.url
.url
= this.getValue();
246 this.allowOnChange
= false;
250 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
251 this.getElement().show();
258 filebrowser: 'info:url',
259 label: commonLang
.browseServer
270 id: 'selectAnchorText',
271 label: linkLang
.selectAnchor
,
273 anchors
= plugin
.getEditorAnchors( editor
);
275 this.getElement()[ anchors
&& anchors
.length
? 'show' : 'hide' ]();
284 label: linkLang
.anchorName
,
285 style: 'width: 100%;',
289 setup: function( data
) {
294 for ( var i
= 0; i
< anchors
.length
; i
++ ) {
295 if ( anchors
[ i
].name
)
296 this.add( anchors
[ i
].name
);
301 this.setValue( data
.anchor
.name
);
303 var linkType
= this.getDialog().getContentElement( 'info', 'linkType' );
304 if ( linkType
&& linkType
.getValue() == 'email' )
307 commit: function( data
) {
311 data
.anchor
.name
= this.getValue();
318 label: linkLang
.anchorId
,
319 style: 'width: 100%;',
323 setup: function( data
) {
328 for ( var i
= 0; i
< anchors
.length
; i
++ ) {
329 if ( anchors
[ i
].id
)
330 this.add( anchors
[ i
].id
);
335 this.setValue( data
.anchor
.id
);
337 commit: function( data
) {
341 data
.anchor
.id
= this.getValue();
345 this.getElement()[ anchors
&& anchors
.length
? 'show' : 'hide' ]();
352 style: 'text-align: center;',
353 html: '<div role="note" tabIndex="-1">' + CKEDITOR
.tools
.htmlEncode( linkLang
.noAnchors
) + '</div>',
354 // Focus the first element defined in above html.
357 this.getElement()[ anchors
&& anchors
.length
? 'hide' : 'show' ]();
361 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
362 this.getElement().hide();
372 label: linkLang
.emailAddress
,
374 validate: function() {
375 var dialog
= this.getDialog();
377 if ( !dialog
.getContentElement( 'info', 'linkType' ) || dialog
.getValueOf( 'info', 'linkType' ) != 'email' )
380 var func
= CKEDITOR
.dialog
.validate
.notEmpty( linkLang
.noEmail
);
381 return func
.apply( this );
383 setup: function( data
) {
385 this.setValue( data
.email
.address
);
387 var linkType
= this.getDialog().getContentElement( 'info', 'linkType' );
388 if ( linkType
&& linkType
.getValue() == 'email' )
391 commit: function( data
) {
395 data
.email
.address
= this.getValue();
401 label: linkLang
.emailSubject
,
402 setup: function( data
) {
404 this.setValue( data
.email
.subject
);
406 commit: function( data
) {
410 data
.email
.subject
= this.getValue();
416 label: linkLang
.emailBody
,
419 setup: function( data
) {
421 this.setValue( data
.email
.body
);
423 commit: function( data
) {
427 data
.email
.body
= this.getValue();
431 if ( !this.getDialog().getContentElement( 'info', 'linkType' ) )
432 this.getElement().hide();
438 requiredContent: 'a[target]', // This is not fully correct, because some target option requires JS.
439 label: linkLang
.target
,
440 title: linkLang
.target
,
443 widths: [ '50%', '50%' ],
446 id: 'linkTargetType',
447 label: commonLang
.target
,
449 style: 'width : 100%;',
451 [ commonLang
.notSet
, 'notSet' ],
452 [ linkLang
.targetFrame
, 'frame' ],
453 [ linkLang
.targetPopup
, 'popup' ],
454 [ commonLang
.targetNew
, '_blank' ],
455 [ commonLang
.targetTop
, '_top' ],
456 [ commonLang
.targetSelf
, '_self' ],
457 [ commonLang
.targetParent
, '_parent' ]
459 onChange: targetChanged
,
460 setup: function( data
) {
462 this.setValue( data
.target
.type
|| 'notSet' );
463 targetChanged
.call( this );
465 commit: function( data
) {
469 data
.target
.type
= this.getValue();
474 id: 'linkTargetName',
475 label: linkLang
.targetFrameName
,
477 setup: function( data
) {
479 this.setValue( data
.target
.name
);
481 commit: function( data
) {
485 data
.target
.name
= this.getValue().replace( /([^\x00-\x7F]|\s)/gi, '' );
497 label: linkLang
.popupFeatures
,
503 label: linkLang
.popupResizable
,
504 setup: setupPopupParams
,
505 commit: commitPopupParams
510 label: linkLang
.popupStatusBar
,
511 setup: setupPopupParams
,
512 commit: commitPopupParams
521 label: linkLang
.popupLocationBar
,
522 setup: setupPopupParams
,
523 commit: commitPopupParams
529 label: linkLang
.popupToolbar
,
530 setup: setupPopupParams
,
531 commit: commitPopupParams
540 label: linkLang
.popupMenuBar
,
541 setup: setupPopupParams
,
542 commit: commitPopupParams
548 label: linkLang
.popupFullScreen
,
549 setup: setupPopupParams
,
550 commit: commitPopupParams
559 label: linkLang
.popupScrollBars
,
560 setup: setupPopupParams
,
561 commit: commitPopupParams
567 label: linkLang
.popupDependent
,
568 setup: setupPopupParams
,
569 commit: commitPopupParams
577 widths: [ '50%', '50%' ],
578 labelLayout: 'horizontal',
579 label: commonLang
.width
,
581 setup: setupPopupParams
,
582 commit: commitPopupParams
587 labelLayout: 'horizontal',
588 widths: [ '50%', '50%' ],
589 label: linkLang
.popupLeft
,
591 setup: setupPopupParams
,
592 commit: commitPopupParams
600 labelLayout: 'horizontal',
601 widths: [ '50%', '50%' ],
602 label: commonLang
.height
,
604 setup: setupPopupParams
,
605 commit: commitPopupParams
610 labelLayout: 'horizontal',
611 label: linkLang
.popupTop
,
612 widths: [ '50%', '50%' ],
614 setup: setupPopupParams
,
615 commit: commitPopupParams
624 label: linkLang
.upload
,
625 title: linkLang
.upload
,
627 filebrowser: 'uploadButton',
631 label: commonLang
.upload
,
632 style: 'height:40px',
638 label: commonLang
.uploadSubmit
,
639 filebrowser: 'info:url',
640 'for': [ 'upload', 'upload' ]
645 label: linkLang
.advanced
,
646 title: linkLang
.advanced
,
652 widths: [ '45%', '35%', '20%' ],
656 requiredContent: 'a[id]',
658 setup: setupAdvParams
,
659 commit: commitAdvParams
664 requiredContent: 'a[dir]',
665 label: linkLang
.langDir
,
667 style: 'width:110px',
669 [ commonLang
.notSet
, '' ],
670 [ linkLang
.langDirLTR
, 'ltr' ],
671 [ linkLang
.langDirRTL
, 'rtl' ]
673 setup: setupAdvParams
,
674 commit: commitAdvParams
679 requiredContent: 'a[accesskey]',
681 label: linkLang
.acccessKey
,
683 setup: setupAdvParams
,
684 commit: commitAdvParams
690 widths: [ '45%', '35%', '20%' ],
693 label: linkLang
.name
,
695 requiredContent: 'a[name]',
696 setup: setupAdvParams
,
697 commit: commitAdvParams
702 label: linkLang
.langCode
,
704 requiredContent: 'a[lang]',
707 setup: setupAdvParams
,
708 commit: commitAdvParams
713 label: linkLang
.tabIndex
,
715 requiredContent: 'a[tabindex]',
718 setup: setupAdvParams
,
719 commit: commitAdvParams
729 widths: [ '45%', '55%' ],
732 label: linkLang
.advisoryTitle
,
733 requiredContent: 'a[title]',
736 setup: setupAdvParams
,
737 commit: commitAdvParams
742 label: linkLang
.advisoryContentType
,
743 requiredContent: 'a[type]',
745 id: 'advContentType',
746 setup: setupAdvParams
,
747 commit: commitAdvParams
753 widths: [ '45%', '55%' ],
756 label: linkLang
.cssClasses
,
757 requiredContent: 'a(cke-xyz)', // Random text like 'xyz' will check if all are allowed.
760 setup: setupAdvParams
,
761 commit: commitAdvParams
766 label: linkLang
.charset
,
767 requiredContent: 'a[charset]',
770 setup: setupAdvParams
,
771 commit: commitAdvParams
777 widths: [ '45%', '55%' ],
781 requiredContent: 'a[rel]',
784 setup: setupAdvParams
,
785 commit: commitAdvParams
789 label: linkLang
.styles
,
790 requiredContent: 'a{cke-xyz}', // Random text like 'xyz' will check if all are allowed.
793 validate: CKEDITOR
.dialog
.validate
.inlineStyle( editor
.lang
.common
.invalidInlineStyle
),
794 setup: setupAdvParams
,
795 commit: commitAdvParams
800 widths: [ '45%', '55%' ],
804 requiredContent: 'a[download]',
805 label: linkLang
.download
,
806 setup: function( data
) {
807 if ( data
.download
!== undefined )
808 this.setValue( 'checked', 'checked' );
810 commit: function( data
) {
811 if ( this.getValue() ) {
812 data
.download
= this.getValue();
820 var editor
= this.getParentEditor(),
821 selection
= editor
.getSelection(),
822 selectedElement
= selection
.getSelectedElement(),
823 displayTextField
= this.getContentElement( 'info', 'linkDisplayText' ).getElement().getParent().getParent(),
826 // Fill in all the relevant fields if there's already one link selected.
827 if ( ( element
= plugin
.getSelectedLink( editor
) ) && element
.hasAttribute( 'href' ) ) {
828 // Don't change selection if some element is already selected.
829 // For example - don't destroy fake selection.
830 if ( !selectedElement
) {
831 selection
.selectElement( element
);
832 selectedElement
= element
;
838 // Here we'll decide whether or not we want to show Display Text field.
839 if ( plugin
.showDisplayTextForElement( selectedElement
, editor
) ) {
840 displayTextField
.show();
842 displayTextField
.hide();
845 var data
= plugin
.parseLinkAttributes( editor
, element
);
847 // Record down the selected element in the dialog.
848 this._
.selectedElement
= element
;
850 this.setupContent( data
);
855 // Collect data from fields.
856 this.commitContent( data
);
858 var selection
= editor
.getSelection(),
859 attributes
= plugin
.getLinkAttributes( editor
, data
),
863 if ( !this._
.selectedElement
) {
864 var range
= selection
.getRanges()[ 0 ],
867 // Use link URL as text with a collapsed cursor.
868 if ( range
.collapsed
) {
869 // Short mailto link text view (#5736).
870 text
= new CKEDITOR
.dom
.text( data
.linkText
|| ( data
.type
== 'email' ?
871 data
.email
.address : attributes
.set[ 'data-cke-saved-href' ] ), editor
.document
);
872 range
.insertNode( text
);
873 range
.selectNodeContents( text
);
874 } else if ( initialLinkText
!== data
.linkText
) {
875 text
= new CKEDITOR
.dom
.text( data
.linkText
, editor
.document
);
877 // Shrink range to preserve block element.
878 range
.shrink( CKEDITOR
.SHRINK_TEXT
);
880 // Use extractHtmlFromRange to remove markup within the selection. Also this method is a little
881 // smarter than range#deleteContents as it plays better e.g. with table cells.
882 editor
.editable().extractHtmlFromRange( range
);
884 range
.insertNode( text
);
887 // Editable links nested within current range should be removed, so that the link is applied to whole selection.
888 nestedLinks
= range
._find( 'a' );
890 for ( var i
= 0; i
< nestedLinks
.length
; i
++ ) {
891 nestedLinks
[ i
].remove( true );
895 var style
= new CKEDITOR
.style( {
897 attributes: attributes
.set
900 style
.type
= CKEDITOR
.STYLE_INLINE
; // need to override... dunno why.
901 style
.applyToRange( range
, editor
);
904 // We're only editing an existing link, so just overwrite the attributes.
905 var element
= this._
.selectedElement
,
906 href
= element
.data( 'cke-saved-href' ),
907 textView
= element
.getHtml(),
910 element
.setAttributes( attributes
.set );
911 element
.removeAttributes( attributes
.removed
);
913 if ( data
.linkText
&& initialLinkText
!= data
.linkText
) {
914 // Display text has been changed.
915 newText
= data
.linkText
;
916 } else if ( href
== textView
|| data
.type
== 'email' && textView
.indexOf( '@' ) != -1 ) {
917 // Update text view when user changes protocol (#4612).
918 // Short mailto link text view (#5736).
919 newText
= data
.type
== 'email' ? data
.email
.address : attributes
.set[ 'data-cke-saved-href' ];
923 element
.setText( newText
);
924 // We changed the content, so need to select it again.
925 selection
.selectElement( element
);
928 delete this._
.selectedElement
;
932 if ( !editor
.config
.linkShowAdvancedTab
)
933 this.hidePage( 'advanced' ); //Hide Advanded tab.
935 if ( !editor
.config
.linkShowTargetTab
)
936 this.hidePage( 'target' ); //Hide Target tab.
938 // Inital focus on 'url' field if link is of type URL.
939 onFocus: function() {
940 var linkType
= this.getContentElement( 'info', 'linkType' ),
943 if ( linkType
&& linkType
.getValue() == 'url' ) {
944 urlField
= this.getContentElement( 'info', 'url' );
951 // jscs:disable maximumLineLength
953 * The e-mail address anti-spam protection option. The protection will be
954 * applied when creating or modifying e-mail links through the editor interface.
956 * Two methods of protection can be chosen:
958 * 1. The e-mail parts (name, domain, and any other query string) are
959 * assembled into a function call pattern. Such function must be
960 * provided by the developer in the pages that will use the contents.
961 * 2. Only the e-mail address is obfuscated into a special string that
962 * has no meaning for humans or spam bots, but which is properly
963 * rendered and accepted by the browser.
965 * Both approaches require JavaScript to be enabled.
967 * // href="mailto:tester@ckeditor.com?subject=subject&body=body"
968 * config.emailProtection = '';
970 * // 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>"
971 * config.emailProtection = 'encode';
973 * // href="javascript:mt('tester','ckeditor.com','subject','body')"
974 * config.emailProtection = 'mt(NAME,DOMAIN,SUBJECT,BODY)';
977 * @cfg {String} [emailProtection='' (empty string = disabled)]
978 * @member CKEDITOR.config