diff options
Diffstat (limited to 'sources/plugins/link/plugin.js')
-rw-r--r-- | sources/plugins/link/plugin.js | 828 |
1 files changed, 828 insertions, 0 deletions
diff --git a/sources/plugins/link/plugin.js b/sources/plugins/link/plugin.js new file mode 100644 index 0000000..94d582b --- /dev/null +++ b/sources/plugins/link/plugin.js | |||
@@ -0,0 +1,828 @@ | |||
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,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 | var element = CKEDITOR.plugins.link.getSelectedLink( editor ) || evt.data.element; | ||
96 | |||
97 | if ( !element.isReadOnly() ) { | ||
98 | if ( element.is( 'a' ) ) { | ||
99 | evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link'; | ||
100 | |||
101 | // Pass the link to be selected along with event data. | ||
102 | evt.data.link = element; | ||
103 | } else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) ) { | ||
104 | evt.data.dialog = 'anchor'; | ||
105 | } | ||
106 | } | ||
107 | }, null, null, 0 ); | ||
108 | |||
109 | // If event was cancelled, link passed in event data will not be selected. | ||
110 | editor.on( 'doubleclick', function( evt ) { | ||
111 | // Make sure both links and anchors are selected (#11822). | ||
112 | if ( evt.data.dialog in { link: 1, anchor: 1 } && evt.data.link ) | ||
113 | editor.getSelection().selectElement( evt.data.link ); | ||
114 | }, null, null, 20 ); | ||
115 | |||
116 | // If the "menu" plugin is loaded, register the menu items. | ||
117 | if ( editor.addMenuItems ) { | ||
118 | editor.addMenuItems( { | ||
119 | anchor: { | ||
120 | label: editor.lang.link.anchor.menu, | ||
121 | command: 'anchor', | ||
122 | group: 'anchor', | ||
123 | order: 1 | ||
124 | }, | ||
125 | |||
126 | removeAnchor: { | ||
127 | label: editor.lang.link.anchor.remove, | ||
128 | command: 'removeAnchor', | ||
129 | group: 'anchor', | ||
130 | order: 5 | ||
131 | }, | ||
132 | |||
133 | link: { | ||
134 | label: editor.lang.link.menu, | ||
135 | command: 'link', | ||
136 | group: 'link', | ||
137 | order: 1 | ||
138 | }, | ||
139 | |||
140 | unlink: { | ||
141 | label: editor.lang.link.unlink, | ||
142 | command: 'unlink', | ||
143 | group: 'link', | ||
144 | order: 5 | ||
145 | } | ||
146 | } ); | ||
147 | } | ||
148 | |||
149 | // If the "contextmenu" plugin is loaded, register the listeners. | ||
150 | if ( editor.contextMenu ) { | ||
151 | editor.contextMenu.addListener( function( element ) { | ||
152 | if ( !element || element.isReadOnly() ) | ||
153 | return null; | ||
154 | |||
155 | var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ); | ||
156 | |||
157 | if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) | ||
158 | return null; | ||
159 | |||
160 | var menu = {}; | ||
161 | |||
162 | if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() ) | ||
163 | menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF }; | ||
164 | |||
165 | if ( anchor && anchor.hasAttribute( 'name' ) ) | ||
166 | menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF; | ||
167 | |||
168 | return menu; | ||
169 | } ); | ||
170 | } | ||
171 | |||
172 | this.compiledProtectionFunction = getCompiledProtectionFunction( editor ); | ||
173 | }, | ||
174 | |||
175 | afterInit: function( editor ) { | ||
176 | // Empty anchors upcasting to fake objects. | ||
177 | editor.dataProcessor.dataFilter.addRules( { | ||
178 | elements: { | ||
179 | a: function( element ) { | ||
180 | if ( !element.attributes.name ) | ||
181 | return null; | ||
182 | |||
183 | if ( !element.children.length ) | ||
184 | return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' ); | ||
185 | |||
186 | return null; | ||
187 | } | ||
188 | } | ||
189 | } ); | ||
190 | |||
191 | var pathFilters = editor._.elementsPath && editor._.elementsPath.filters; | ||
192 | if ( pathFilters ) { | ||
193 | pathFilters.push( function( element, name ) { | ||
194 | if ( name == 'a' ) { | ||
195 | if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ) | ||
196 | return 'anchor'; | ||
197 | } | ||
198 | } ); | ||
199 | } | ||
200 | } | ||
201 | } ); | ||
202 | |||
203 | // Loads the parameters in a selected link to the link dialog fields. | ||
204 | var javascriptProtocolRegex = /^javascript:/, | ||
205 | emailRegex = /^mailto:([^?]+)(?:\?(.+))?$/, | ||
206 | emailSubjectRegex = /subject=([^;?:@&=$,\/]*)/i, | ||
207 | emailBodyRegex = /body=([^;?:@&=$,\/]*)/i, | ||
208 | anchorRegex = /^#(.*)$/, | ||
209 | urlRegex = /^((?:http|https|ftp|news):\/\/)?(.*)$/, | ||
210 | selectableTargets = /^(_(?:self|top|parent|blank))$/, | ||
211 | encodedEmailLinkRegex = /^javascript:void\(location\.href='mailto:'\+String\.fromCharCode\(([^)]+)\)(?:\+'(.*)')?\)$/, | ||
212 | functionCallProtectedEmailLinkRegex = /^javascript:([^(]+)\(([^)]+)\)$/, | ||
213 | popupRegex = /\s*window.open\(\s*this\.href\s*,\s*(?:'([^']*)'|null)\s*,\s*'([^']*)'\s*\)\s*;\s*return\s*false;*\s*/, | ||
214 | popupFeaturesRegex = /(?:^|,)([^=]+)=(\d+|yes|no)/gi; | ||
215 | |||
216 | var advAttrNames = { | ||
217 | id: 'advId', | ||
218 | dir: 'advLangDir', | ||
219 | accessKey: 'advAccessKey', | ||
220 | // 'data-cke-saved-name': 'advName', | ||
221 | name: 'advName', | ||
222 | lang: 'advLangCode', | ||
223 | tabindex: 'advTabIndex', | ||
224 | title: 'advTitle', | ||
225 | type: 'advContentType', | ||
226 | 'class': 'advCSSClasses', | ||
227 | charset: 'advCharset', | ||
228 | style: 'advStyles', | ||
229 | rel: 'advRel' | ||
230 | }; | ||
231 | |||
232 | function unescapeSingleQuote( str ) { | ||
233 | return str.replace( /\\'/g, '\'' ); | ||
234 | } | ||
235 | |||
236 | function escapeSingleQuote( str ) { | ||
237 | return str.replace( /'/g, '\\$&' ); | ||
238 | } | ||
239 | |||
240 | function protectEmailAddressAsEncodedString( address ) { | ||
241 | var charCode, | ||
242 | length = address.length, | ||
243 | encodedChars = []; | ||
244 | |||
245 | for ( var i = 0; i < length; i++ ) { | ||
246 | charCode = address.charCodeAt( i ); | ||
247 | encodedChars.push( charCode ); | ||
248 | } | ||
249 | |||
250 | return 'String.fromCharCode(' + encodedChars.join( ',' ) + ')'; | ||
251 | } | ||
252 | |||
253 | function protectEmailLinkAsFunction( editor, email ) { | ||
254 | var plugin = editor.plugins.link, | ||
255 | name = plugin.compiledProtectionFunction.name, | ||
256 | params = plugin.compiledProtectionFunction.params, | ||
257 | paramName, paramValue, retval; | ||
258 | |||
259 | retval = [ name, '(' ]; | ||
260 | for ( var i = 0; i < params.length; i++ ) { | ||
261 | paramName = params[ i ].toLowerCase(); | ||
262 | paramValue = email[ paramName ]; | ||
263 | |||
264 | i > 0 && retval.push( ',' ); | ||
265 | retval.push( '\'', paramValue ? escapeSingleQuote( encodeURIComponent( email[ paramName ] ) ) : '', '\'' ); | ||
266 | } | ||
267 | retval.push( ')' ); | ||
268 | return retval.join( '' ); | ||
269 | } | ||
270 | |||
271 | function getCompiledProtectionFunction( editor ) { | ||
272 | var emailProtection = editor.config.emailProtection || '', | ||
273 | compiledProtectionFunction; | ||
274 | |||
275 | // Compile the protection function pattern. | ||
276 | if ( emailProtection && emailProtection != 'encode' ) { | ||
277 | compiledProtectionFunction = {}; | ||
278 | |||
279 | emailProtection.replace( /^([^(]+)\(([^)]+)\)$/, function( match, funcName, params ) { | ||
280 | compiledProtectionFunction.name = funcName; | ||
281 | compiledProtectionFunction.params = []; | ||
282 | params.replace( /[^,\s]+/g, function( param ) { | ||
283 | compiledProtectionFunction.params.push( param ); | ||
284 | } ); | ||
285 | } ); | ||
286 | } | ||
287 | |||
288 | return compiledProtectionFunction; | ||
289 | } | ||
290 | |||
291 | /** | ||
292 | * Set of Link plugin helpers. | ||
293 | * | ||
294 | * @class | ||
295 | * @singleton | ||
296 | */ | ||
297 | CKEDITOR.plugins.link = { | ||
298 | /** | ||
299 | * Get the surrounding link element of the current selection. | ||
300 | * | ||
301 | * CKEDITOR.plugins.link.getSelectedLink( editor ); | ||
302 | * | ||
303 | * // The following selections will all return the link element. | ||
304 | * | ||
305 | * <a href="#">li^nk</a> | ||
306 | * <a href="#">[link]</a> | ||
307 | * text[<a href="#">link]</a> | ||
308 | * <a href="#">li[nk</a>] | ||
309 | * [<b><a href="#">li]nk</a></b>] | ||
310 | * [<a href="#"><b>li]nk</b></a> | ||
311 | * | ||
312 | * @since 3.2.1 | ||
313 | * @param {CKEDITOR.editor} editor | ||
314 | */ | ||
315 | getSelectedLink: function( editor ) { | ||
316 | var selection = editor.getSelection(); | ||
317 | var selectedElement = selection.getSelectedElement(); | ||
318 | if ( selectedElement && selectedElement.is( 'a' ) ) | ||
319 | return selectedElement; | ||
320 | |||
321 | var range = selection.getRanges()[ 0 ]; | ||
322 | |||
323 | if ( range ) { | ||
324 | range.shrink( CKEDITOR.SHRINK_TEXT ); | ||
325 | return editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 ); | ||
326 | } | ||
327 | return null; | ||
328 | }, | ||
329 | |||
330 | /** | ||
331 | * Collects anchors available in the editor (i.e. used by the Link plugin). | ||
332 | * Note that the scope of search is different for inline (the "global" document) and | ||
333 | * classic (`iframe`-based) editors (the "inner" document). | ||
334 | * | ||
335 | * @since 4.3.3 | ||
336 | * @param {CKEDITOR.editor} editor | ||
337 | * @returns {CKEDITOR.dom.element[]} An array of anchor elements. | ||
338 | */ | ||
339 | getEditorAnchors: function( editor ) { | ||
340 | var editable = editor.editable(), | ||
341 | |||
342 | // The scope of search for anchors is the entire document for inline editors | ||
343 | // and editor's editable for classic editor/divarea (#11359). | ||
344 | scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable, | ||
345 | |||
346 | links = scope.getElementsByTag( 'a' ), | ||
347 | imgs = scope.getElementsByTag( 'img' ), | ||
348 | anchors = [], | ||
349 | i = 0, | ||
350 | item; | ||
351 | |||
352 | // Retrieve all anchors within the scope. | ||
353 | while ( ( item = links.getItem( i++ ) ) ) { | ||
354 | if ( item.data( 'cke-saved-name' ) || item.hasAttribute( 'name' ) ) { | ||
355 | anchors.push( { | ||
356 | name: item.data( 'cke-saved-name' ) || item.getAttribute( 'name' ), | ||
357 | id: item.getAttribute( 'id' ) | ||
358 | } ); | ||
359 | } | ||
360 | } | ||
361 | // Retrieve all "fake anchors" within the scope. | ||
362 | i = 0; | ||
363 | |||
364 | while ( ( item = imgs.getItem( i++ ) ) ) { | ||
365 | if ( ( item = this.tryRestoreFakeAnchor( editor, item ) ) ) { | ||
366 | anchors.push( { | ||
367 | name: item.getAttribute( 'name' ), | ||
368 | id: item.getAttribute( 'id' ) | ||
369 | } ); | ||
370 | } | ||
371 | } | ||
372 | |||
373 | return anchors; | ||
374 | }, | ||
375 | |||
376 | /** | ||
377 | * Opera and WebKit do not make it possible to select empty anchors. Fake | ||
378 | * elements must be used for them. | ||
379 | * | ||
380 | * @readonly | ||
381 | * @deprecated 4.3.3 It is set to `true` in every browser. | ||
382 | * @property {Boolean} | ||
383 | */ | ||
384 | fakeAnchor: true, | ||
385 | |||
386 | /** | ||
387 | * For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of #7783. | ||
388 | * | ||
389 | * @readonly | ||
390 | * @deprecated 4.3.3 It is set to `false` in every browser. | ||
391 | * @property {Boolean} synAnchorSelector | ||
392 | */ | ||
393 | |||
394 | /** | ||
395 | * For browsers that have editing issues with an empty anchor. | ||
396 | * | ||
397 | * @readonly | ||
398 | * @deprecated 4.3.3 It is set to `false` in every browser. | ||
399 | * @property {Boolean} emptyAnchorFix | ||
400 | */ | ||
401 | |||
402 | /** | ||
403 | * Returns an element representing a real anchor restored from a fake anchor. | ||
404 | * | ||
405 | * @param {CKEDITOR.editor} editor | ||
406 | * @param {CKEDITOR.dom.element} element | ||
407 | * @returns {CKEDITOR.dom.element} Restored anchor element or nothing if the | ||
408 | * passed element was not a fake anchor. | ||
409 | */ | ||
410 | tryRestoreFakeAnchor: function( editor, element ) { | ||
411 | if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) { | ||
412 | var link = editor.restoreRealElement( element ); | ||
413 | if ( link.data( 'cke-saved-name' ) ) | ||
414 | return link; | ||
415 | } | ||
416 | }, | ||
417 | |||
418 | /** | ||
419 | * Parses attributes of the link element and returns an object representing | ||
420 | * the current state (data) of the link. This data format is a plain object accepted | ||
421 | * e.g. by the Link dialog window and {@link #getLinkAttributes}. | ||
422 | * | ||
423 | * **Note:** Data model format produced by the parser must be compatible with the Link | ||
424 | * plugin dialog because it is passed directly to {@link CKEDITOR.dialog#setupContent}. | ||
425 | * | ||
426 | * @since 4.4 | ||
427 | * @param {CKEDITOR.editor} editor | ||
428 | * @param {CKEDITOR.dom.element} element | ||
429 | * @returns {Object} An object of link data. | ||
430 | */ | ||
431 | parseLinkAttributes: function( editor, element ) { | ||
432 | var href = ( element && ( element.data( 'cke-saved-href' ) || element.getAttribute( 'href' ) ) ) || '', | ||
433 | compiledProtectionFunction = editor.plugins.link.compiledProtectionFunction, | ||
434 | emailProtection = editor.config.emailProtection, | ||
435 | javascriptMatch, emailMatch, anchorMatch, urlMatch, | ||
436 | retval = {}; | ||
437 | |||
438 | if ( ( javascriptMatch = href.match( javascriptProtocolRegex ) ) ) { | ||
439 | if ( emailProtection == 'encode' ) { | ||
440 | href = href.replace( encodedEmailLinkRegex, function( match, protectedAddress, rest ) { | ||
441 | // Without it 'undefined' is appended to e-mails without subject and body (#9192). | ||
442 | rest = rest || ''; | ||
443 | |||
444 | return 'mailto:' + | ||
445 | String.fromCharCode.apply( String, protectedAddress.split( ',' ) ) + | ||
446 | unescapeSingleQuote( rest ); | ||
447 | } ); | ||
448 | } | ||
449 | // Protected email link as function call. | ||
450 | else if ( emailProtection ) { | ||
451 | href.replace( functionCallProtectedEmailLinkRegex, function( match, funcName, funcArgs ) { | ||
452 | if ( funcName == compiledProtectionFunction.name ) { | ||
453 | retval.type = 'email'; | ||
454 | var email = retval.email = {}; | ||
455 | |||
456 | var paramRegex = /[^,\s]+/g, | ||
457 | paramQuoteRegex = /(^')|('$)/g, | ||
458 | paramsMatch = funcArgs.match( paramRegex ), | ||
459 | paramsMatchLength = paramsMatch.length, | ||
460 | paramName, paramVal; | ||
461 | |||
462 | for ( var i = 0; i < paramsMatchLength; i++ ) { | ||
463 | paramVal = decodeURIComponent( unescapeSingleQuote( paramsMatch[ i ].replace( paramQuoteRegex, '' ) ) ); | ||
464 | paramName = compiledProtectionFunction.params[ i ].toLowerCase(); | ||
465 | email[ paramName ] = paramVal; | ||
466 | } | ||
467 | email.address = [ email.name, email.domain ].join( '@' ); | ||
468 | } | ||
469 | } ); | ||
470 | } | ||
471 | } | ||
472 | |||
473 | if ( !retval.type ) { | ||
474 | if ( ( anchorMatch = href.match( anchorRegex ) ) ) { | ||
475 | retval.type = 'anchor'; | ||
476 | retval.anchor = {}; | ||
477 | retval.anchor.name = retval.anchor.id = anchorMatch[ 1 ]; | ||
478 | } | ||
479 | // Protected email link as encoded string. | ||
480 | else if ( ( emailMatch = href.match( emailRegex ) ) ) { | ||
481 | var subjectMatch = href.match( emailSubjectRegex ), | ||
482 | bodyMatch = href.match( emailBodyRegex ); | ||
483 | |||
484 | retval.type = 'email'; | ||
485 | var email = ( retval.email = {} ); | ||
486 | email.address = emailMatch[ 1 ]; | ||
487 | subjectMatch && ( email.subject = decodeURIComponent( subjectMatch[ 1 ] ) ); | ||
488 | bodyMatch && ( email.body = decodeURIComponent( bodyMatch[ 1 ] ) ); | ||
489 | } | ||
490 | // urlRegex matches empty strings, so need to check for href as well. | ||
491 | else if ( href && ( urlMatch = href.match( urlRegex ) ) ) { | ||
492 | retval.type = 'url'; | ||
493 | retval.url = {}; | ||
494 | retval.url.protocol = urlMatch[ 1 ]; | ||
495 | retval.url.url = urlMatch[ 2 ]; | ||
496 | } | ||
497 | } | ||
498 | |||
499 | // Load target and popup settings. | ||
500 | if ( element ) { | ||
501 | var target = element.getAttribute( 'target' ); | ||
502 | |||
503 | // IE BUG: target attribute is an empty string instead of null in IE if it's not set. | ||
504 | if ( !target ) { | ||
505 | var onclick = element.data( 'cke-pa-onclick' ) || element.getAttribute( 'onclick' ), | ||
506 | onclickMatch = onclick && onclick.match( popupRegex ); | ||
507 | |||
508 | if ( onclickMatch ) { | ||
509 | retval.target = { | ||
510 | type: 'popup', | ||
511 | name: onclickMatch[ 1 ] | ||
512 | }; | ||
513 | |||
514 | var featureMatch; | ||
515 | while ( ( featureMatch = popupFeaturesRegex.exec( onclickMatch[ 2 ] ) ) ) { | ||
516 | // Some values should remain numbers (#7300) | ||
517 | if ( ( featureMatch[ 2 ] == 'yes' || featureMatch[ 2 ] == '1' ) && !( featureMatch[ 1 ] in { height: 1, width: 1, top: 1, left: 1 } ) ) | ||
518 | retval.target[ featureMatch[ 1 ] ] = true; | ||
519 | else if ( isFinite( featureMatch[ 2 ] ) ) | ||
520 | retval.target[ featureMatch[ 1 ] ] = featureMatch[ 2 ]; | ||
521 | } | ||
522 | } | ||
523 | } else { | ||
524 | retval.target = { | ||
525 | type: target.match( selectableTargets ) ? target : 'frame', | ||
526 | name: target | ||
527 | }; | ||
528 | } | ||
529 | |||
530 | var download = element.getAttribute( 'download' ); | ||
531 | if ( download !== null ) { | ||
532 | retval.download = true; | ||
533 | } | ||
534 | |||
535 | var advanced = {}; | ||
536 | |||
537 | for ( var a in advAttrNames ) { | ||
538 | var val = element.getAttribute( a ); | ||
539 | |||
540 | if ( val ) | ||
541 | advanced[ advAttrNames[ a ] ] = val; | ||
542 | } | ||
543 | |||
544 | var advName = element.data( 'cke-saved-name' ) || advanced.advName; | ||
545 | |||
546 | if ( advName ) | ||
547 | advanced.advName = advName; | ||
548 | |||
549 | if ( !CKEDITOR.tools.isEmpty( advanced ) ) | ||
550 | retval.advanced = advanced; | ||
551 | } | ||
552 | |||
553 | return retval; | ||
554 | }, | ||
555 | |||
556 | /** | ||
557 | * Converts link data produced by {@link #parseLinkAttributes} into an object which consists | ||
558 | * of attributes to be set (with their values) and an array of attributes to be removed. | ||
559 | * This method can be used to compose or to update any link element with the given data. | ||
560 | * | ||
561 | * @since 4.4 | ||
562 | * @param {CKEDITOR.editor} editor | ||
563 | * @param {Object} data Data in {@link #parseLinkAttributes} format. | ||
564 | * @returns {Object} An object consisting of two keys, i.e.: | ||
565 | * | ||
566 | * { | ||
567 | * // Attributes to be set. | ||
568 | * set: { | ||
569 | * href: 'http://foo.bar', | ||
570 | * target: 'bang' | ||
571 | * }, | ||
572 | * // Attributes to be removed. | ||
573 | * removed: [ | ||
574 | * 'id', 'style' | ||
575 | * ] | ||
576 | * } | ||
577 | * | ||
578 | */ | ||
579 | getLinkAttributes: function( editor, data ) { | ||
580 | var emailProtection = editor.config.emailProtection || '', | ||
581 | set = {}; | ||
582 | |||
583 | // Compose the URL. | ||
584 | switch ( data.type ) { | ||
585 | case 'url': | ||
586 | var protocol = ( data.url && data.url.protocol !== undefined ) ? data.url.protocol : 'http://', | ||
587 | url = ( data.url && CKEDITOR.tools.trim( data.url.url ) ) || ''; | ||
588 | |||
589 | set[ 'data-cke-saved-href' ] = ( url.indexOf( '/' ) === 0 ) ? url : protocol + url; | ||
590 | |||
591 | break; | ||
592 | case 'anchor': | ||
593 | var name = ( data.anchor && data.anchor.name ), | ||
594 | id = ( data.anchor && data.anchor.id ); | ||
595 | |||
596 | set[ 'data-cke-saved-href' ] = '#' + ( name || id || '' ); | ||
597 | |||
598 | break; | ||
599 | case 'email': | ||
600 | var email = data.email, | ||
601 | address = email.address, | ||
602 | linkHref; | ||
603 | |||
604 | switch ( emailProtection ) { | ||
605 | case '': | ||
606 | case 'encode': | ||
607 | var subject = encodeURIComponent( email.subject || '' ), | ||
608 | body = encodeURIComponent( email.body || '' ), | ||
609 | argList = []; | ||
610 | |||
611 | // Build the e-mail parameters first. | ||
612 | subject && argList.push( 'subject=' + subject ); | ||
613 | body && argList.push( 'body=' + body ); | ||
614 | argList = argList.length ? '?' + argList.join( '&' ) : ''; | ||
615 | |||
616 | if ( emailProtection == 'encode' ) { | ||
617 | linkHref = [ | ||
618 | 'javascript:void(location.href=\'mailto:\'+', // jshint ignore:line | ||
619 | protectEmailAddressAsEncodedString( address ) | ||
620 | ]; | ||
621 | // parameters are optional. | ||
622 | argList && linkHref.push( '+\'', escapeSingleQuote( argList ), '\'' ); | ||
623 | |||
624 | linkHref.push( ')' ); | ||
625 | } else { | ||
626 | linkHref = [ 'mailto:', address, argList ]; | ||
627 | } | ||
628 | |||
629 | break; | ||
630 | default: | ||
631 | // Separating name and domain. | ||
632 | var nameAndDomain = address.split( '@', 2 ); | ||
633 | email.name = nameAndDomain[ 0 ]; | ||
634 | email.domain = nameAndDomain[ 1 ]; | ||
635 | |||
636 | linkHref = [ 'javascript:', protectEmailLinkAsFunction( editor, email ) ]; // jshint ignore:line | ||
637 | } | ||
638 | |||
639 | set[ 'data-cke-saved-href' ] = linkHref.join( '' ); | ||
640 | break; | ||
641 | } | ||
642 | |||
643 | // Popups and target. | ||
644 | if ( data.target ) { | ||
645 | if ( data.target.type == 'popup' ) { | ||
646 | var onclickList = [ | ||
647 | 'window.open(this.href, \'', data.target.name || '', '\', \'' | ||
648 | ], | ||
649 | featureList = [ | ||
650 | 'resizable', 'status', 'location', 'toolbar', 'menubar', 'fullscreen', 'scrollbars', 'dependent' | ||
651 | ], | ||
652 | featureLength = featureList.length, | ||
653 | addFeature = function( featureName ) { | ||
654 | if ( data.target[ featureName ] ) | ||
655 | featureList.push( featureName + '=' + data.target[ featureName ] ); | ||
656 | }; | ||
657 | |||
658 | for ( var i = 0; i < featureLength; i++ ) | ||
659 | featureList[ i ] = featureList[ i ] + ( data.target[ featureList[ i ] ] ? '=yes' : '=no' ); | ||
660 | |||
661 | addFeature( 'width' ); | ||
662 | addFeature( 'left' ); | ||
663 | addFeature( 'height' ); | ||
664 | addFeature( 'top' ); | ||
665 | |||
666 | onclickList.push( featureList.join( ',' ), '\'); return false;' ); | ||
667 | set[ 'data-cke-pa-onclick' ] = onclickList.join( '' ); | ||
668 | } | ||
669 | else if ( data.target.type != 'notSet' && data.target.name ) { | ||
670 | set.target = data.target.name; | ||
671 | } | ||
672 | } | ||
673 | |||
674 | // Force download attribute. | ||
675 | if ( data.download ) { | ||
676 | set.download = ''; | ||
677 | } | ||
678 | |||
679 | // Advanced attributes. | ||
680 | if ( data.advanced ) { | ||
681 | for ( var a in advAttrNames ) { | ||
682 | var val = data.advanced[ advAttrNames[ a ] ]; | ||
683 | |||
684 | if ( val ) | ||
685 | set[ a ] = val; | ||
686 | } | ||
687 | |||
688 | if ( set.name ) | ||
689 | set[ 'data-cke-saved-name' ] = set.name; | ||
690 | } | ||
691 | |||
692 | // Browser need the "href" fro copy/paste link to work. (#6641) | ||
693 | if ( set[ 'data-cke-saved-href' ] ) | ||
694 | set.href = set[ 'data-cke-saved-href' ]; | ||
695 | |||
696 | var removed = { | ||
697 | target: 1, | ||
698 | onclick: 1, | ||
699 | 'data-cke-pa-onclick': 1, | ||
700 | 'data-cke-saved-name': 1, | ||
701 | 'download': 1 | ||
702 | }; | ||
703 | |||
704 | if ( data.advanced ) | ||
705 | CKEDITOR.tools.extend( removed, advAttrNames ); | ||
706 | |||
707 | // Remove all attributes which are not currently set. | ||
708 | for ( var s in set ) | ||
709 | delete removed[ s ]; | ||
710 | |||
711 | return { | ||
712 | set: set, | ||
713 | removed: CKEDITOR.tools.objectKeys( removed ) | ||
714 | }; | ||
715 | }, | ||
716 | |||
717 | |||
718 | /** | ||
719 | * Determines whether an element should have a "Display Text" field in the Link dialog. | ||
720 | * | ||
721 | * @since 4.5.11 | ||
722 | * @param {CKEDITOR.dom.element/null} element Selected element, `null` if none selected or if a ranged selection | ||
723 | * is made. | ||
724 | * @param {CKEDITOR.editor} editor The editor instance for which the check is performed. | ||
725 | * @returns {Boolean} | ||
726 | */ | ||
727 | showDisplayTextForElement: function( element, editor ) { | ||
728 | var undesiredElements = { | ||
729 | img: 1, | ||
730 | table: 1, | ||
731 | tbody: 1, | ||
732 | thead: 1, | ||
733 | tfoot: 1, | ||
734 | input: 1, | ||
735 | select: 1, | ||
736 | textarea: 1 | ||
737 | }; | ||
738 | |||
739 | // Widget duck typing, we don't want to show display text for widgets. | ||
740 | if ( editor.widgets && editor.widgets.focused ) { | ||
741 | return false; | ||
742 | } | ||
743 | |||
744 | return !element || !element.getName || !element.is( undesiredElements ); | ||
745 | } | ||
746 | }; | ||
747 | |||
748 | // TODO Much probably there's no need to expose these as public objects. | ||
749 | |||
750 | CKEDITOR.unlinkCommand = function() {}; | ||
751 | CKEDITOR.unlinkCommand.prototype = { | ||
752 | exec: function( editor ) { | ||
753 | var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } ); | ||
754 | editor.removeStyle( style ); | ||
755 | }, | ||
756 | |||
757 | refresh: function( editor, path ) { | ||
758 | // Despite our initial hope, document.queryCommandEnabled() does not work | ||
759 | // for this in Firefox. So we must detect the state by element paths. | ||
760 | |||
761 | var element = path.lastElement && path.lastElement.getAscendant( 'a', true ); | ||
762 | |||
763 | if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() ) | ||
764 | this.setState( CKEDITOR.TRISTATE_OFF ); | ||
765 | else | ||
766 | this.setState( CKEDITOR.TRISTATE_DISABLED ); | ||
767 | }, | ||
768 | |||
769 | contextSensitive: 1, | ||
770 | startDisabled: 1, | ||
771 | requiredContent: 'a[href]' | ||
772 | }; | ||
773 | |||
774 | CKEDITOR.removeAnchorCommand = function() {}; | ||
775 | CKEDITOR.removeAnchorCommand.prototype = { | ||
776 | exec: function( editor ) { | ||
777 | var sel = editor.getSelection(), | ||
778 | bms = sel.createBookmarks(), | ||
779 | anchor; | ||
780 | if ( sel && ( anchor = sel.getSelectedElement() ) && ( !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) ) | ||
781 | anchor.remove( 1 ); | ||
782 | else { | ||
783 | if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) { | ||
784 | if ( anchor.hasAttribute( 'href' ) ) { | ||
785 | anchor.removeAttributes( { name: 1, 'data-cke-saved-name': 1 } ); | ||
786 | anchor.removeClass( 'cke_anchor' ); | ||
787 | } else { | ||
788 | anchor.remove( 1 ); | ||
789 | } | ||
790 | } | ||
791 | } | ||
792 | sel.selectBookmarks( bms ); | ||
793 | }, | ||
794 | requiredContent: 'a[name]' | ||
795 | }; | ||
796 | |||
797 | CKEDITOR.tools.extend( CKEDITOR.config, { | ||
798 | /** | ||
799 | * Whether to show the Advanced tab in the Link dialog window. | ||
800 | * | ||
801 | * @cfg {Boolean} [linkShowAdvancedTab=true] | ||
802 | * @member CKEDITOR.config | ||
803 | */ | ||
804 | linkShowAdvancedTab: true, | ||
805 | |||
806 | /** | ||
807 | * Whether to show the Target tab in the Link dialog window. | ||
808 | * | ||
809 | * @cfg {Boolean} [linkShowTargetTab=true] | ||
810 | * @member CKEDITOR.config | ||
811 | */ | ||
812 | linkShowTargetTab: true | ||
813 | |||
814 | /** | ||
815 | * Whether JavaScript code is allowed as a `href` attribute in an anchor tag. | ||
816 | * With this option enabled it is possible to create links like: | ||
817 | * | ||
818 | * <a href="javascript:alert('Hello world!')">hello world</a> | ||
819 | * | ||
820 | * By default JavaScript links are not allowed and will not pass | ||
821 | * the Link dialog window validation. | ||
822 | * | ||
823 | * @since 4.4.1 | ||
824 | * @cfg {Boolean} [linkJavaScriptLinksAllowed=false] | ||
825 | * @member CKEDITOR.config | ||
826 | */ | ||
827 | } ); | ||
828 | } )(); | ||