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