plugin.js 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. 'use strict';
  6. ( function() {
  7. var template = '<img alt="" src="" />',
  8. templateBlock = new CKEDITOR.template(
  9. '<figure class="{captionedClass}">' +
  10. template +
  11. '<figcaption>{captionPlaceholder}</figcaption>' +
  12. '</figure>' ),
  13. alignmentsObj = { left: 0, center: 1, right: 2 },
  14. regexPercent = /^\s*(\d+\%)\s*$/i;
  15. CKEDITOR.plugins.add( 'image2', {
  16. // jscs:disable maximumLineLength
  17. lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
  18. // jscs:enable maximumLineLength
  19. requires: 'widget,dialog',
  20. icons: 'image',
  21. hidpi: true,
  22. onLoad: function() {
  23. CKEDITOR.addCss(
  24. '.cke_image_nocaption{' +
  25. // This is to remove unwanted space so resize
  26. // wrapper is displayed property.
  27. 'line-height:0' +
  28. '}' +
  29. '.cke_editable.cke_image_sw, .cke_editable.cke_image_sw *{cursor:sw-resize !important}' +
  30. '.cke_editable.cke_image_se, .cke_editable.cke_image_se *{cursor:se-resize !important}' +
  31. '.cke_image_resizer{' +
  32. 'display:none;' +
  33. 'position:absolute;' +
  34. 'width:10px;' +
  35. 'height:10px;' +
  36. 'bottom:-5px;' +
  37. 'right:-5px;' +
  38. 'background:#000;' +
  39. 'outline:1px solid #fff;' +
  40. // Prevent drag handler from being misplaced (https://dev.ckeditor.com/ticket/11207).
  41. 'line-height:0;' +
  42. 'cursor:se-resize;' +
  43. '}' +
  44. '.cke_image_resizer_wrapper{' +
  45. 'position:relative;' +
  46. 'display:inline-block;' +
  47. 'line-height:0;' +
  48. '}' +
  49. // Bottom-left corner style of the resizer.
  50. '.cke_image_resizer.cke_image_resizer_left{' +
  51. 'right:auto;' +
  52. 'left:-5px;' +
  53. 'cursor:sw-resize;' +
  54. '}' +
  55. '.cke_widget_wrapper:hover .cke_image_resizer,' +
  56. '.cke_image_resizer.cke_image_resizing{' +
  57. 'display:block' +
  58. '}' +
  59. // Hide resizer in read only mode (#2816).
  60. '.cke_editable[contenteditable="false"] .cke_image_resizer{' +
  61. 'display:none;' +
  62. '}' +
  63. // Expand widget wrapper when linked inline image.
  64. '.cke_widget_wrapper>a{' +
  65. 'display:inline-block' +
  66. '}' );
  67. },
  68. init: function( editor ) {
  69. // Abort when Easyimage is to be loaded since this plugins
  70. // share the same functionality (#1791).
  71. if ( editor.plugins.detectConflict( 'image2', [ 'easyimage' ] ) ) {
  72. return;
  73. }
  74. // Adapts configuration from original image plugin. Should be removed
  75. // when we'll rename image2 to image.
  76. var config = editor.config,
  77. lang = editor.lang.image2,
  78. image = widgetDef( editor );
  79. // Since filebrowser plugin discovers config properties by dialog (plugin?)
  80. // names (sic!), this hack will be necessary as long as Image2 is not named
  81. // Image. And since Image2 will never be Image, for sure some filebrowser logic
  82. // got to be refined.
  83. config.filebrowserImage2BrowseUrl = config.filebrowserImageBrowseUrl;
  84. config.filebrowserImage2UploadUrl = config.filebrowserImageUploadUrl;
  85. // Add custom elementspath names to widget definition.
  86. image.pathName = lang.pathName;
  87. image.editables.caption.pathName = lang.pathNameCaption;
  88. // Register the widget.
  89. editor.widgets.add( 'image', image );
  90. // Add toolbar button for this plugin.
  91. editor.ui.addButton && editor.ui.addButton( 'Image', {
  92. label: editor.lang.common.image,
  93. command: 'image',
  94. toolbar: 'insert,10'
  95. } );
  96. // Register context menu option for editing widget.
  97. if ( editor.contextMenu ) {
  98. editor.addMenuGroup( 'image', 10 );
  99. editor.addMenuItem( 'image', {
  100. label: lang.menu,
  101. command: 'image',
  102. group: 'image'
  103. } );
  104. }
  105. CKEDITOR.dialog.add( 'image2', this.path + 'dialogs/image2.js' );
  106. },
  107. afterInit: function( editor ) {
  108. // Integrate with align commands (justify plugin).
  109. var align = { left: 1, right: 1, center: 1, block: 1 },
  110. integrate = alignCommandIntegrator( editor );
  111. for ( var value in align )
  112. integrate( value );
  113. // Integrate with link commands (link plugin).
  114. linkCommandIntegrator( editor );
  115. }
  116. } );
  117. // Wiget states (forms) depending on alignment and configuration.
  118. //
  119. // Non-captioned widget (inline styles)
  120. // ┌──────┬───────────────────────────────┬─────────────────────────────┐
  121. // │Align │Internal form │Data │
  122. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  123. // │none │<wrapper> │<img /> │
  124. // │ │ <img /> │ │
  125. // │ │</wrapper> │ │
  126. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  127. // │left │<wrapper style=”float:left”> │<img style=”float:left” /> │
  128. // │ │ <img /> │ │
  129. // │ │</wrapper> │ │
  130. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  131. // │center│<wrapper> │<p style=”text-align:center”>│
  132. // │ │ <p style=”text-align:center”> │ <img /> │
  133. // │ │ <img /> │</p> │
  134. // │ │ </p> │ │
  135. // │ │</wrapper> │ │
  136. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  137. // │right │<wrapper style=”float:right”> │<img style=”float:right” /> │
  138. // │ │ <img /> │ │
  139. // │ │</wrapper> │ │
  140. // └──────┴───────────────────────────────┴─────────────────────────────┘
  141. //
  142. // Non-captioned widget (config.image2_alignClasses defined)
  143. // ┌──────┬───────────────────────────────┬─────────────────────────────┐
  144. // │Align │Internal form │Data │
  145. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  146. // │none │<wrapper> │<img /> │
  147. // │ │ <img /> │ │
  148. // │ │</wrapper> │ │
  149. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  150. // │left │<wrapper class=”left”> │<img class=”left” /> │
  151. // │ │ <img /> │ │
  152. // │ │</wrapper> │ │
  153. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  154. // │center│<wrapper> │<p class=”center”> │
  155. // │ │ <p class=”center”> │ <img /> │
  156. // │ │ <img /> │</p> │
  157. // │ │ </p> │ │
  158. // │ │</wrapper> │ │
  159. // ├──────┼───────────────────────────────┼─────────────────────────────┤
  160. // │right │<wrapper class=”right”> │<img class=”right” /> │
  161. // │ │ <img /> │ │
  162. // │ │</wrapper> │ │
  163. // └──────┴───────────────────────────────┴─────────────────────────────┘
  164. //
  165. // Captioned widget (inline styles)
  166. // ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐
  167. // │Align │Internal form │Data │
  168. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  169. // │none │<wrapper> │<figure /> │
  170. // │ │ <figure /> │ │
  171. // │ │</wrapper> │ │
  172. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  173. // │left │<wrapper style=”float:left”> │<figure style=”float:left” /> │
  174. // │ │ <figure /> │ │
  175. // │ │</wrapper> │ │
  176. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  177. // │center│<wrapper style=”text-align:center”> │<div style=”text-align:center”> │
  178. // │ │ <figure style=”display:inline-block” />│ <figure style=”display:inline-block” />│
  179. // │ │</wrapper> │</p> │
  180. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  181. // │right │<wrapper style=”float:right”> │<figure style=”float:right” /> │
  182. // │ │ <figure /> │ │
  183. // │ │</wrapper> │ │
  184. // └──────┴────────────────────────────────────────┴────────────────────────────────────────┘
  185. //
  186. // Captioned widget (config.image2_alignClasses defined)
  187. // ┌──────┬────────────────────────────────────────┬────────────────────────────────────────┐
  188. // │Align │Internal form │Data │
  189. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  190. // │none │<wrapper> │<figure /> │
  191. // │ │ <figure /> │ │
  192. // │ │</wrapper> │ │
  193. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  194. // │left │<wrapper class=”left”> │<figure class=”left” /> │
  195. // │ │ <figure /> │ │
  196. // │ │</wrapper> │ │
  197. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  198. // │center│<wrapper class=”center”> │<div class=”center”> │
  199. // │ │ <figure /> │ <figure /> │
  200. // │ │</wrapper> │</p> │
  201. // ├──────┼────────────────────────────────────────┼────────────────────────────────────────┤
  202. // │right │<wrapper class=”right”> │<figure class=”right” /> │
  203. // │ │ <figure /> │ │
  204. // │ │</wrapper> │ │
  205. // └──────┴────────────────────────────────────────┴────────────────────────────────────────┘
  206. //
  207. // @param {CKEDITOR.editor}
  208. // @returns {Object}
  209. function widgetDef( editor ) {
  210. var alignClasses = editor.config.image2_alignClasses,
  211. captionedClass = editor.config.image2_captionedClass;
  212. function deflate() {
  213. if ( this.deflated )
  214. return;
  215. // Remember whether widget was focused before destroyed.
  216. if ( editor.widgets.focused == this.widget )
  217. this.focused = true;
  218. editor.widgets.destroy( this.widget );
  219. // Mark widget was destroyed.
  220. this.deflated = true;
  221. }
  222. function inflate() {
  223. var editable = editor.editable(),
  224. doc = editor.document;
  225. // Create a new widget. This widget will be either captioned
  226. // non-captioned, block or inline according to what is the
  227. // new state of the widget.
  228. if ( this.deflated ) {
  229. this.widget = editor.widgets.initOn( this.element, 'image', this.widget.data );
  230. // Once widget was re-created, it may become an inline element without
  231. // block wrapper (i.e. when unaligned, end not captioned). Let's do some
  232. // sort of autoparagraphing here (https://dev.ckeditor.com/ticket/10853).
  233. if ( this.widget.inline && !( new CKEDITOR.dom.elementPath( this.widget.wrapper, editable ).block ) ) {
  234. var block = doc.createElement( editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
  235. block.replace( this.widget.wrapper );
  236. this.widget.wrapper.move( block );
  237. }
  238. // The focus must be transferred from the old one (destroyed)
  239. // to the new one (just created).
  240. if ( this.focused ) {
  241. this.widget.focus();
  242. delete this.focused;
  243. }
  244. delete this.deflated;
  245. }
  246. // If now widget was destroyed just update wrapper's alignment.
  247. // According to the new state.
  248. else {
  249. setWrapperAlign( this.widget, alignClasses );
  250. }
  251. }
  252. return {
  253. allowedContent: getWidgetAllowedContent( editor ),
  254. requiredContent: 'img[src,alt]',
  255. features: getWidgetFeatures( editor ),
  256. styleableElements: 'img figure',
  257. // This widget converts style-driven dimensions to attributes.
  258. contentTransformations: [
  259. [ 'img[width]: sizeToAttribute' ]
  260. ],
  261. // This widget has an editable caption.
  262. editables: {
  263. caption: {
  264. selector: 'figcaption',
  265. allowedContent: 'br em strong sub sup u s; a[!href,target]'
  266. }
  267. },
  268. parts: {
  269. image: 'img',
  270. caption: 'figcaption'
  271. // parts#link defined in widget#init
  272. },
  273. // The name of this widget's dialog.
  274. dialog: 'image2',
  275. // Template of the widget: plain image.
  276. template: template,
  277. data: function() {
  278. var features = this.features;
  279. // Image can't be captioned when figcaption is disallowed (https://dev.ckeditor.com/ticket/11004).
  280. if ( this.data.hasCaption && !editor.filter.checkFeature( features.caption ) )
  281. this.data.hasCaption = false;
  282. // Image can't be aligned when floating is disallowed (https://dev.ckeditor.com/ticket/11004).
  283. if ( this.data.align != 'none' && !editor.filter.checkFeature( features.align ) )
  284. this.data.align = 'none';
  285. // Convert the internal form of the widget from the old state to the new one.
  286. this.shiftState( {
  287. widget: this,
  288. element: this.element,
  289. oldData: this.oldData,
  290. newData: this.data,
  291. deflate: deflate,
  292. inflate: inflate
  293. } );
  294. // Update widget.parts.link since it will not auto-update unless widget
  295. // is destroyed and re-inited.
  296. if ( !this.data.link ) {
  297. if ( this.parts.link )
  298. delete this.parts.link;
  299. } else {
  300. if ( !this.parts.link )
  301. this.parts.link = this.parts.image.getParent();
  302. }
  303. this.parts.image.setAttributes( {
  304. src: this.data.src,
  305. // This internal is required by the editor.
  306. 'data-cke-saved-src': this.data.src,
  307. alt: this.data.alt
  308. } );
  309. // If shifting non-captioned -> captioned, remove classes
  310. // related to styles from <img/>.
  311. if ( this.oldData && !this.oldData.hasCaption && this.data.hasCaption ) {
  312. for ( var c in this.data.classes )
  313. this.parts.image.removeClass( c );
  314. }
  315. // Set dimensions of the image according to gathered data.
  316. // Do it only when the attributes are allowed (https://dev.ckeditor.com/ticket/11004).
  317. if ( editor.filter.checkFeature( features.dimension ) )
  318. setDimensions( this );
  319. // Cache current data.
  320. this.oldData = CKEDITOR.tools.extend( {}, this.data );
  321. },
  322. init: function() {
  323. var helpers = CKEDITOR.plugins.image2,
  324. image = this.parts.image,
  325. data = {
  326. hasCaption: !!this.parts.caption,
  327. src: image.getAttribute( 'src' ),
  328. alt: image.getAttribute( 'alt' ) || '',
  329. width: image.getAttribute( 'width' ) || '',
  330. height: image.getAttribute( 'height' ) || '',
  331. // Lock ratio is on by default (https://dev.ckeditor.com/ticket/10833).
  332. lock: this.ready ? helpers.checkHasNaturalRatio( image ) : true
  333. };
  334. // If we used 'a' in widget#parts definition, it could happen that
  335. // selected element is a child of widget.parts#caption. Since there's no clever
  336. // way to solve it with CSS selectors, it's done like that. (https://dev.ckeditor.com/ticket/11783).
  337. var link = image.getAscendant( 'a' );
  338. if ( link && this.wrapper.contains( link ) )
  339. this.parts.link = link;
  340. // Depending on configuration, read style/class from element and
  341. // then remove it. Removed style/class will be set on wrapper in #data listener.
  342. // Note: Center alignment is detected during upcast, so only left/right cases
  343. // are checked below.
  344. if ( !data.align ) {
  345. var alignElement = data.hasCaption ? this.element : image;
  346. // Read the initial left/right alignment from the class set on element.
  347. if ( alignClasses ) {
  348. if ( alignElement.hasClass( alignClasses[ 0 ] ) ) {
  349. data.align = 'left';
  350. } else if ( alignElement.hasClass( alignClasses[ 2 ] ) ) {
  351. data.align = 'right';
  352. }
  353. if ( data.align ) {
  354. alignElement.removeClass( alignClasses[ alignmentsObj[ data.align ] ] );
  355. } else {
  356. data.align = 'none';
  357. }
  358. }
  359. // Read initial float style from figure/image and then remove it.
  360. else {
  361. data.align = alignElement.getStyle( 'float' ) || 'none';
  362. alignElement.removeStyle( 'float' );
  363. }
  364. }
  365. // Update data.link object with attributes if the link has been discovered.
  366. if ( editor.plugins.link && this.parts.link ) {
  367. data.link = helpers.getLinkAttributesParser()( editor, this.parts.link );
  368. // Get rid of cke_widget_* classes in data. Otherwise
  369. // they might appear in link dialog.
  370. var advanced = data.link.advanced;
  371. if ( advanced && advanced.advCSSClasses ) {
  372. advanced.advCSSClasses = CKEDITOR.tools.trim( advanced.advCSSClasses.replace( /cke_\S+/, '' ) );
  373. }
  374. }
  375. // Get rid of extra vertical space when there's no caption.
  376. // It will improve the look of the resizer.
  377. this.wrapper[ ( data.hasCaption ? 'remove' : 'add' ) + 'Class' ]( 'cke_image_nocaption' );
  378. this.setData( data );
  379. // Setup dynamic image resizing with mouse.
  380. // Don't initialize resizer when dimensions are disallowed (https://dev.ckeditor.com/ticket/11004).
  381. if ( editor.filter.checkFeature( this.features.dimension ) && editor.config.image2_disableResizer !== true ) {
  382. setupResizer( this );
  383. }
  384. this.shiftState = helpers.stateShifter( this.editor );
  385. // Add widget editing option to its context menu.
  386. this.on( 'contextMenu', function( evt ) {
  387. evt.data.image = CKEDITOR.TRISTATE_OFF;
  388. // Integrate context menu items for link.
  389. // Note that widget may be wrapped in a link, which
  390. // does not belong to that widget (https://dev.ckeditor.com/ticket/11814).
  391. if ( this.parts.link || this.wrapper.getAscendant( 'a' ) )
  392. evt.data.link = evt.data.unlink = CKEDITOR.TRISTATE_OFF;
  393. } );
  394. },
  395. // Overrides default method to handle internal mutability of Image2.
  396. // @see CKEDITOR.plugins.widget#addClass
  397. addClass: function( className ) {
  398. getStyleableElement( this ).addClass( className );
  399. },
  400. // Overrides default method to handle internal mutability of Image2.
  401. // @see CKEDITOR.plugins.widget#hasClass
  402. hasClass: function( className ) {
  403. return getStyleableElement( this ).hasClass( className );
  404. },
  405. // Overrides default method to handle internal mutability of Image2.
  406. // @see CKEDITOR.plugins.widget#removeClass
  407. removeClass: function( className ) {
  408. getStyleableElement( this ).removeClass( className );
  409. },
  410. // Overrides default method to handle internal mutability of Image2.
  411. // @see CKEDITOR.plugins.widget#getClasses
  412. getClasses: ( function() {
  413. var classRegex = new RegExp( '^(' + [].concat( captionedClass, alignClasses ).join( '|' ) + ')$' );
  414. return function() {
  415. var classes = this.repository.parseElementClasses( getStyleableElement( this ).getAttribute( 'class' ) );
  416. // Neither config.image2_captionedClass nor config.image2_alignClasses
  417. // do not belong to style classes.
  418. for ( var c in classes ) {
  419. if ( classRegex.test( c ) )
  420. delete classes[ c ];
  421. }
  422. return classes;
  423. };
  424. } )(),
  425. upcast: upcastWidgetElement( editor ),
  426. downcast: downcastWidgetElement( editor ),
  427. getLabel: function() {
  428. var label = ( this.data.alt || '' ) + ' ' + this.pathName;
  429. return this.editor.lang.widget.label.replace( /%1/, label );
  430. }
  431. };
  432. }
  433. /**
  434. * A set of Enhanced Image (image2) plugin helpers.
  435. *
  436. * @class
  437. * @singleton
  438. */
  439. CKEDITOR.plugins.image2 = {
  440. stateShifter: function( editor ) {
  441. // Tag name used for centering non-captioned widgets.
  442. var doc = editor.document,
  443. alignClasses = editor.config.image2_alignClasses,
  444. captionedClass = editor.config.image2_captionedClass,
  445. editable = editor.editable(),
  446. // The order that stateActions get executed. It matters!
  447. shiftables = [ 'hasCaption', 'align', 'link' ];
  448. // Atomic procedures, one per state variable.
  449. var stateActions = {
  450. align: function( shift, oldValue, newValue ) {
  451. var el = shift.element;
  452. // Alignment changed.
  453. if ( shift.changed.align ) {
  454. // No caption in the new state.
  455. if ( !shift.newData.hasCaption ) {
  456. // Changed to "center" (non-captioned).
  457. if ( newValue == 'center' ) {
  458. shift.deflate();
  459. shift.element = wrapInCentering( editor, el );
  460. }
  461. // Changed to "non-center" from "center" while caption removed.
  462. if ( !shift.changed.hasCaption && oldValue == 'center' && newValue != 'center' ) {
  463. shift.deflate();
  464. shift.element = unwrapFromCentering( el );
  465. }
  466. }
  467. }
  468. // Alignment remains and "center" removed caption.
  469. else if ( newValue == 'center' && shift.changed.hasCaption && !shift.newData.hasCaption ) {
  470. shift.deflate();
  471. shift.element = wrapInCentering( editor, el );
  472. }
  473. // Finally set display for figure.
  474. if ( !alignClasses && el.is( 'figure' ) ) {
  475. if ( newValue == 'center' )
  476. el.setStyle( 'display', 'inline-block' );
  477. else
  478. el.removeStyle( 'display' );
  479. }
  480. },
  481. hasCaption: function( shift, oldValue, newValue ) {
  482. // This action is for real state change only.
  483. if ( !shift.changed.hasCaption )
  484. return;
  485. // Get <img/> or <a><img/></a> from widget. Note that widget element might itself
  486. // be what we're looking for. Also element can be <p style="text-align:center"><a>...</a></p>.
  487. var imageOrLink;
  488. if ( shift.element.is( { img: 1, a: 1 } ) )
  489. imageOrLink = shift.element;
  490. else
  491. imageOrLink = shift.element.findOne( 'a,img' );
  492. // Switching hasCaption always destroys the widget.
  493. shift.deflate();
  494. // There was no caption, but the caption is to be added.
  495. if ( newValue ) {
  496. // Create new <figure> from widget template.
  497. var figure = CKEDITOR.dom.element.createFromHtml( templateBlock.output( {
  498. captionedClass: captionedClass,
  499. captionPlaceholder: editor.lang.image2.captionPlaceholder
  500. } ), doc );
  501. // Replace element with <figure>.
  502. replaceSafely( figure, shift.element );
  503. // Use old <img/> or <a><img/></a> instead of the one from the template,
  504. // so we won't lose additional attributes.
  505. imageOrLink.replace( figure.findOne( 'img' ) );
  506. // Update widget's element.
  507. shift.element = figure;
  508. }
  509. // The caption was present, but now it's to be removed.
  510. else {
  511. // Unwrap <img/> or <a><img/></a> from figure.
  512. imageOrLink.replace( shift.element );
  513. // Update widget's element.
  514. shift.element = imageOrLink;
  515. }
  516. },
  517. link: function( shift, oldValue, newValue ) {
  518. if ( shift.changed.link ) {
  519. var img = shift.element.is( 'img' ) ?
  520. shift.element : shift.element.findOne( 'img' ),
  521. link = shift.element.is( 'a' ) ?
  522. shift.element : shift.element.findOne( 'a' ),
  523. // Why deflate:
  524. // If element is <img/>, it will be wrapped into <a>,
  525. // which becomes a new widget.element.
  526. // If element is <a><img/></a>, it will be unlinked
  527. // so <img/> becomes a new widget.element.
  528. needsDeflate = ( shift.element.is( 'a' ) && !newValue ) || ( shift.element.is( 'img' ) && newValue ),
  529. newEl;
  530. if ( needsDeflate )
  531. shift.deflate();
  532. // If unlinked the image, returned element is <img>.
  533. if ( !newValue )
  534. newEl = unwrapFromLink( link );
  535. else {
  536. // If linked the image, returned element is <a>.
  537. if ( !oldValue )
  538. newEl = wrapInLink( img, shift.newData.link );
  539. // Set and remove all attributes associated with this state.
  540. var attributes = CKEDITOR.plugins.image2.getLinkAttributesGetter()( editor, newValue );
  541. if ( !CKEDITOR.tools.isEmpty( attributes.set ) )
  542. ( newEl || link ).setAttributes( attributes.set );
  543. if ( attributes.removed.length )
  544. ( newEl || link ).removeAttributes( attributes.removed );
  545. }
  546. if ( needsDeflate )
  547. shift.element = newEl;
  548. }
  549. }
  550. };
  551. function wrapInCentering( editor, element ) {
  552. var attribsAndStyles = {};
  553. if ( alignClasses )
  554. attribsAndStyles.attributes = { 'class': alignClasses[ 1 ] };
  555. else
  556. attribsAndStyles.styles = { 'text-align': 'center' };
  557. // There's no gentle way to center inline element with CSS, so create p/div
  558. // that wraps widget contents and does the trick either with style or class.
  559. var center = doc.createElement(
  560. editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div', attribsAndStyles );
  561. // Replace element with centering wrapper.
  562. replaceSafely( center, element );
  563. element.move( center );
  564. return center;
  565. }
  566. function unwrapFromCentering( element ) {
  567. var imageOrLink = element.findOne( 'a,img' );
  568. imageOrLink.replace( element );
  569. return imageOrLink;
  570. }
  571. // Wraps <img/> -> <a><img/></a>.
  572. // Returns reference to <a>.
  573. //
  574. // @param {CKEDITOR.dom.element} img
  575. // @param {Object} linkData
  576. // @returns {CKEDITOR.dom.element}
  577. function wrapInLink( img, linkData ) {
  578. var link = doc.createElement( 'a', {
  579. attributes: {
  580. href: linkData.url
  581. }
  582. } );
  583. link.replace( img );
  584. img.move( link );
  585. return link;
  586. }
  587. // De-wraps <a><img/></a> -> <img/>.
  588. // Returns the reference to <img/>
  589. //
  590. // @param {CKEDITOR.dom.element} link
  591. // @returns {CKEDITOR.dom.element}
  592. function unwrapFromLink( link ) {
  593. var img = link.findOne( 'img' );
  594. img.replace( link );
  595. return img;
  596. }
  597. function replaceSafely( replacing, replaced ) {
  598. if ( replaced.getParent() ) {
  599. var range = editor.createRange();
  600. range.moveToPosition( replaced, CKEDITOR.POSITION_BEFORE_START );
  601. // Remove old element. Do it before insertion to avoid a case when
  602. // element is moved from 'replaced' element before it, what creates
  603. // a tricky case which insertElementIntorRange does not handle.
  604. replaced.remove();
  605. editable.insertElementIntoRange( replacing, range );
  606. }
  607. else {
  608. replacing.replace( replaced );
  609. }
  610. }
  611. return function( shift ) {
  612. var name, i;
  613. shift.changed = {};
  614. for ( i = 0; i < shiftables.length; i++ ) {
  615. name = shiftables[ i ];
  616. shift.changed[ name ] = shift.oldData ?
  617. shift.oldData[ name ] !== shift.newData[ name ] : false;
  618. }
  619. // Iterate over possible state variables.
  620. for ( i = 0; i < shiftables.length; i++ ) {
  621. name = shiftables[ i ];
  622. stateActions[ name ]( shift,
  623. shift.oldData ? shift.oldData[ name ] : null,
  624. shift.newData[ name ] );
  625. }
  626. shift.inflate();
  627. };
  628. },
  629. /**
  630. * Checks whether the current image ratio matches the natural one
  631. * by comparing dimensions.
  632. *
  633. * @param {CKEDITOR.dom.element} image
  634. * @returns {Boolean}
  635. */
  636. checkHasNaturalRatio: function( image ) {
  637. var $ = image.$,
  638. natural = this.getNatural( image );
  639. // The reason for two alternative comparisons is that the rounding can come from
  640. // both dimensions, e.g. there are two cases:
  641. // 1. height is computed as a rounded relation of the real height and the value of width,
  642. // 2. width is computed as a rounded relation of the real width and the value of heigh.
  643. return Math.round( $.clientWidth / natural.width * natural.height ) == $.clientHeight ||
  644. Math.round( $.clientHeight / natural.height * natural.width ) == $.clientWidth;
  645. },
  646. /**
  647. * Returns natural dimensions of the image. For modern browsers
  648. * it uses natural(Width|Height). For old ones (IE8) it creates
  649. * a new image and reads the dimensions.
  650. *
  651. * @param {CKEDITOR.dom.element} image
  652. * @returns {Object}
  653. */
  654. getNatural: function( image ) {
  655. var dimensions;
  656. if ( image.$.naturalWidth ) {
  657. dimensions = {
  658. width: image.$.naturalWidth,
  659. height: image.$.naturalHeight
  660. };
  661. } else {
  662. var img = new Image();
  663. img.src = image.getAttribute( 'src' );
  664. dimensions = {
  665. width: img.width,
  666. height: img.height
  667. };
  668. }
  669. return dimensions;
  670. },
  671. /**
  672. * Returns an attribute getter function. Default getter comes from the Link plugin
  673. * and is documented by {@link CKEDITOR.plugins.link#getLinkAttributes}.
  674. *
  675. * **Note:** It is possible to override this method and use a custom getter e.g.
  676. * in the absence of the Link plugin.
  677. *
  678. * **Note:** If a custom getter is used, a data model format it produces
  679. * must be compatible with {@link CKEDITOR.plugins.link#getLinkAttributes}.
  680. *
  681. * **Note:** A custom getter must understand the data model format produced by
  682. * {@link #getLinkAttributesParser} to work correctly.
  683. *
  684. * @returns {Function} A function that gets (composes) link attributes.
  685. * @since 4.5.5
  686. */
  687. getLinkAttributesGetter: function() {
  688. // https://dev.ckeditor.com/ticket/13885
  689. return CKEDITOR.plugins.link.getLinkAttributes;
  690. },
  691. /**
  692. * Returns an attribute parser function. Default parser comes from the Link plugin
  693. * and is documented by {@link CKEDITOR.plugins.link#parseLinkAttributes}.
  694. *
  695. * **Note:** It is possible to override this method and use a custom parser e.g.
  696. * in the absence of the Link plugin.
  697. *
  698. * **Note:** If a custom parser is used, a data model format produced by the parser
  699. * must be compatible with {@link #getLinkAttributesGetter}.
  700. *
  701. * **Note:** If a custom parser is used, it should be compatible with the
  702. * {@link CKEDITOR.plugins.link#parseLinkAttributes} data model format. Otherwise the
  703. * Link plugin dialog may not be populated correctly with parsed data. However
  704. * as long as Enhanced Image is **not** used with the Link plugin dialog, any custom data model
  705. * will work, being stored as an internal property of Enhanced Image widget's data only.
  706. *
  707. * @returns {Function} A function that parses attributes.
  708. * @since 4.5.5
  709. */
  710. getLinkAttributesParser: function() {
  711. // https://dev.ckeditor.com/ticket/13885
  712. return CKEDITOR.plugins.link.parseLinkAttributes;
  713. }
  714. };
  715. function setWrapperAlign( widget, alignClasses ) {
  716. var wrapper = widget.wrapper,
  717. align = widget.data.align,
  718. hasCaption = widget.data.hasCaption;
  719. if ( alignClasses ) {
  720. // Remove all align classes first.
  721. for ( var i = 3; i--; )
  722. wrapper.removeClass( alignClasses[ i ] );
  723. if ( align == 'center' ) {
  724. // Avoid touching non-captioned, centered widgets because
  725. // they have the class set on the element instead of wrapper:
  726. //
  727. // <div class="cke_widget_wrapper">
  728. // <p class="center-class">
  729. // <img />
  730. // </p>
  731. // </div>
  732. if ( hasCaption ) {
  733. wrapper.addClass( alignClasses[ 1 ] );
  734. }
  735. } else if ( align != 'none' ) {
  736. wrapper.addClass( alignClasses[ alignmentsObj[ align ] ] );
  737. }
  738. } else {
  739. if ( align == 'center' ) {
  740. if ( hasCaption )
  741. wrapper.setStyle( 'text-align', 'center' );
  742. else
  743. wrapper.removeStyle( 'text-align' );
  744. wrapper.removeStyle( 'float' );
  745. }
  746. else {
  747. if ( align == 'none' )
  748. wrapper.removeStyle( 'float' );
  749. else
  750. wrapper.setStyle( 'float', align );
  751. wrapper.removeStyle( 'text-align' );
  752. }
  753. }
  754. }
  755. // Returns a function that creates widgets from all <img> and
  756. // <figure class="{config.image2_captionedClass}"> elements.
  757. //
  758. // @param {CKEDITOR.editor} editor
  759. // @returns {Function}
  760. function upcastWidgetElement( editor ) {
  761. var isCenterWrapper = centerWrapperChecker( editor ),
  762. captionedClass = editor.config.image2_captionedClass;
  763. // @param {CKEDITOR.htmlParser.element} el
  764. // @param {Object} data
  765. return function( el, data ) {
  766. var dimensions = { width: 1, height: 1 },
  767. name = el.name,
  768. image;
  769. // https://dev.ckeditor.com/ticket/11110 Don't initialize on pasted fake objects.
  770. if ( el.attributes[ 'data-cke-realelement' ] )
  771. return;
  772. // If a center wrapper is found, there are 3 possible cases:
  773. //
  774. // 1. <div style="text-align:center"><figure>...</figure></div>.
  775. // In this case centering is done with a class set on widget.wrapper.
  776. // Simply replace centering wrapper with figure (it's no longer necessary).
  777. //
  778. // 2. <p style="text-align:center"><img/></p>.
  779. // Nothing to do here: <p> remains for styling purposes.
  780. //
  781. // 3. <div style="text-align:center"><img/></div>.
  782. // Nothing to do here (2.) but that case is only possible in enterMode different
  783. // than ENTER_P.
  784. if ( isCenterWrapper( el ) ) {
  785. if ( name == 'div' ) {
  786. var figure = el.getFirst( 'figure' );
  787. // Case #1.
  788. if ( figure ) {
  789. el.replaceWith( figure );
  790. el = figure;
  791. }
  792. }
  793. // Cases #2 and #3 (handled transparently)
  794. // If there's a centering wrapper, save it in data.
  795. data.align = 'center';
  796. // Image can be wrapped in link <a><img/></a>.
  797. image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' );
  798. }
  799. // No center wrapper has been found.
  800. else if ( name == 'figure' && el.hasClass( captionedClass ) ) {
  801. image = el.find( function( child ) {
  802. return child.name === 'img' &&
  803. CKEDITOR.tools.array.indexOf( [ 'figure', 'a' ], child.parent.name ) !== -1;
  804. }, true )[ 0 ];
  805. // Upcast linked image like <a><img/></a>.
  806. } else if ( isLinkedOrStandaloneImage( el ) ) {
  807. image = el.name == 'a' ? el.children[ 0 ] : el;
  808. }
  809. if ( !image )
  810. return;
  811. // If there's an image, then cool, we got a widget.
  812. // Now just remove dimension attributes expressed with %.
  813. for ( var d in dimensions ) {
  814. var dimension = image.attributes[ d ];
  815. if ( dimension && dimension.match( regexPercent ) )
  816. delete image.attributes[ d ];
  817. }
  818. return el;
  819. };
  820. }
  821. // Returns a function which transforms the widget to the external format
  822. // according to the current configuration.
  823. //
  824. // @param {CKEDITOR.editor}
  825. function downcastWidgetElement( editor ) {
  826. var alignClasses = editor.config.image2_alignClasses;
  827. // @param {CKEDITOR.htmlParser.element} el
  828. return function( el ) {
  829. // In case of <a><img/></a>, <img/> is the element to hold
  830. // inline styles or classes (image2_alignClasses).
  831. var attrsHolder = el.name == 'a' ? el.getFirst() : el,
  832. attrs = attrsHolder.attributes,
  833. align = this.data.align;
  834. // De-wrap the image from resize handle wrapper.
  835. // Only block widgets have one.
  836. if ( !this.inline ) {
  837. var resizeWrapper = el.getFirst( 'span' );
  838. if ( resizeWrapper )
  839. resizeWrapper.replaceWith( resizeWrapper.getFirst( { img: 1, a: 1 } ) );
  840. }
  841. if ( align && align != 'none' ) {
  842. var styles = CKEDITOR.tools.parseCssText( attrs.style || '' );
  843. // When the widget is captioned (<figure>) and internally centering is done
  844. // with widget's wrapper style/class, in the external data representation,
  845. // <figure> must be wrapped with an element holding an style/class:
  846. //
  847. // <div style="text-align:center">
  848. // <figure class="image" style="display:inline-block">...</figure>
  849. // </div>
  850. // or
  851. // <div class="some-center-class">
  852. // <figure class="image">...</figure>
  853. // </div>
  854. //
  855. if ( align == 'center' && el.name == 'figure' ) {
  856. el = el.wrapWith( new CKEDITOR.htmlParser.element( 'div',
  857. alignClasses ? { 'class': alignClasses[ 1 ] } : { style: 'text-align:center' } ) );
  858. }
  859. // If left/right, add float style to the downcasted element.
  860. else if ( align in { left: 1, right: 1 } ) {
  861. if ( alignClasses )
  862. attrsHolder.addClass( alignClasses[ alignmentsObj[ align ] ] );
  863. else
  864. styles[ 'float' ] = align;
  865. }
  866. // Update element styles.
  867. if ( !alignClasses && !CKEDITOR.tools.isEmpty( styles ) )
  868. attrs.style = CKEDITOR.tools.writeCssText( styles );
  869. }
  870. return el;
  871. };
  872. }
  873. // Returns a function that checks if an element is a centering wrapper.
  874. //
  875. // @param {CKEDITOR.editor} editor
  876. // @returns {Function}
  877. function centerWrapperChecker( editor ) {
  878. var captionedClass = editor.config.image2_captionedClass,
  879. alignClasses = editor.config.image2_alignClasses,
  880. validChildren = { figure: 1, a: 1, img: 1 };
  881. return function( el ) {
  882. // Wrapper must be either <div> or <p>.
  883. if ( !( el.name in { div: 1, p: 1 } ) )
  884. return false;
  885. var children = el.children;
  886. // Centering wrapper can have only one child.
  887. if ( children.length !== 1 )
  888. return false;
  889. var child = children[ 0 ];
  890. // Only <figure> or <img /> can be first (only) child of centering wrapper,
  891. // regardless of its type.
  892. if ( !( child.name in validChildren ) )
  893. return false;
  894. // If centering wrapper is <p>, only <img /> can be the child.
  895. // <p style="text-align:center"><img /></p>
  896. if ( el.name == 'p' ) {
  897. if ( !isLinkedOrStandaloneImage( child ) )
  898. return false;
  899. }
  900. // Centering <div> can hold <img/> or <figure>, depending on enterMode.
  901. else {
  902. // If a <figure> is the first (only) child, it must have a class.
  903. // <div style="text-align:center"><figure>...</figure><div>
  904. if ( child.name == 'figure' ) {
  905. if ( !child.hasClass( captionedClass ) )
  906. return false;
  907. } else {
  908. // Centering <div> can hold <img/> or <a><img/></a> only when enterMode
  909. // is ENTER_(BR|DIV).
  910. // <div style="text-align:center"><img /></div>
  911. // <div style="text-align:center"><a><img /></a></div>
  912. if ( editor.enterMode == CKEDITOR.ENTER_P )
  913. return false;
  914. // Regardless of enterMode, a child which is not <figure> must be
  915. // either <img/> or <a><img/></a>.
  916. if ( !isLinkedOrStandaloneImage( child ) )
  917. return false;
  918. }
  919. }
  920. // Centering wrapper got to be... centering. If image2_alignClasses are defined,
  921. // check for centering class. Otherwise, check the style.
  922. if ( alignClasses ? el.hasClass( alignClasses[ 1 ] ) :
  923. CKEDITOR.tools.parseCssText( el.attributes.style || '', true )[ 'text-align' ] == 'center' )
  924. return true;
  925. return false;
  926. };
  927. }
  928. // Checks whether element is <img/> or <a><img/></a>.
  929. //
  930. // @param {CKEDITOR.htmlParser.element}
  931. function isLinkedOrStandaloneImage( el ) {
  932. if ( el.name == 'img' )
  933. return true;
  934. else if ( el.name == 'a' )
  935. return el.children.length == 1 && el.getFirst( 'img' );
  936. return false;
  937. }
  938. // Sets width and height of the widget image according to current widget data.
  939. //
  940. // @param {CKEDITOR.plugins.widget} widget
  941. function setDimensions( widget ) {
  942. var data = widget.data,
  943. dimensions = { width: data.width, height: data.height },
  944. image = widget.parts.image;
  945. for ( var d in dimensions ) {
  946. if ( dimensions[ d ] )
  947. image.setAttribute( d, dimensions[ d ] );
  948. else
  949. image.removeAttribute( d );
  950. }
  951. }
  952. // Defines all features related to drag-driven image resizing.
  953. //
  954. // @param {CKEDITOR.plugins.widget} widget
  955. function setupResizer( widget ) {
  956. var editor = widget.editor,
  957. editable = editor.editable(),
  958. doc = editor.document,
  959. // Store the resizer in a widget for testing (https://dev.ckeditor.com/ticket/11004).
  960. resizer = widget.resizer = doc.createElement( 'span' );
  961. resizer.addClass( 'cke_image_resizer' );
  962. resizer.setAttribute( 'title', editor.lang.image2.resizer );
  963. resizer.append( new CKEDITOR.dom.text( '\u200b', doc ) );
  964. // Inline widgets don't need a resizer wrapper as an image spans the entire widget.
  965. if ( !widget.inline ) {
  966. var imageOrLink = widget.parts.link || widget.parts.image,
  967. oldResizeWrapper = imageOrLink.getParent(),
  968. resizeWrapper = doc.createElement( 'span' );
  969. resizeWrapper.addClass( 'cke_image_resizer_wrapper' );
  970. resizeWrapper.append( imageOrLink );
  971. resizeWrapper.append( resizer );
  972. widget.element.append( resizeWrapper, true );
  973. // Remove the old wrapper which could came from e.g. pasted HTML
  974. // and which could be corrupted (e.g. resizer span has been lost).
  975. if ( oldResizeWrapper.is( 'span' ) )
  976. oldResizeWrapper.remove();
  977. } else {
  978. widget.wrapper.append( resizer );
  979. }
  980. // Calculate values of size variables and mouse offsets.
  981. resizer.on( 'mousedown', function( evt ) {
  982. var image = widget.parts.image,
  983. // Don't update attributes if less than 15.
  984. // This is to prevent images to visually disappear.
  985. min = {
  986. width: 15,
  987. height: 15
  988. },
  989. max = getMaxSize(),
  990. // "factor" can be either 1 or -1. I.e.: For right-aligned images, we need to
  991. // subtract the difference to get proper width, etc. Without "factor",
  992. // resizer starts working the opposite way.
  993. factor = widget.data.align == 'right' ? -1 : 1,
  994. // The x-coordinate of the mouse relative to the screen
  995. // when button gets pressed.
  996. startX = evt.data.$.screenX,
  997. startY = evt.data.$.screenY,
  998. // The initial dimensions and aspect ratio of the image.
  999. startWidth = image.$.clientWidth,
  1000. startHeight = image.$.clientHeight,
  1001. ratio = startWidth / startHeight,
  1002. listeners = [],
  1003. // A class applied to editable during resizing.
  1004. cursorClass = 'cke_image_s' + ( !~factor ? 'w' : 'e' ),
  1005. nativeEvt, newWidth, newHeight, updateData,
  1006. moveDiffX, moveDiffY, moveRatio;
  1007. // Save the undo snapshot first: before resizing.
  1008. editor.fire( 'saveSnapshot' );
  1009. // Mousemove listeners are removed on mouseup.
  1010. attachToDocuments( 'mousemove', onMouseMove, listeners );
  1011. // Clean up the mousemove listener. Update widget data if valid.
  1012. attachToDocuments( 'mouseup', onMouseUp, listeners );
  1013. // The entire editable will have the special cursor while resizing goes on.
  1014. editable.addClass( cursorClass );
  1015. // This is to always keep the resizer element visible while resizing.
  1016. resizer.addClass( 'cke_image_resizing' );
  1017. // Attaches an event to a global document if inline editor.
  1018. // Additionally, if classic (`iframe`-based) editor, also attaches the same event to `iframe`'s document.
  1019. function attachToDocuments( name, callback, collection ) {
  1020. var globalDoc = CKEDITOR.document,
  1021. listeners = [];
  1022. if ( !doc.equals( globalDoc ) )
  1023. listeners.push( globalDoc.on( name, callback ) );
  1024. listeners.push( doc.on( name, callback ) );
  1025. if ( collection ) {
  1026. for ( var i = listeners.length; i--; )
  1027. collection.push( listeners.pop() );
  1028. }
  1029. }
  1030. // Calculate with first, and then adjust height, preserving ratio.
  1031. function adjustToX() {
  1032. newWidth = startWidth + factor * moveDiffX;
  1033. newHeight = Math.round( newWidth / ratio );
  1034. }
  1035. // Calculate height first, and then adjust width, preserving ratio.
  1036. function adjustToY() {
  1037. newHeight = startHeight - moveDiffY;
  1038. newWidth = Math.round( newHeight * ratio );
  1039. }
  1040. // This is how variables refer to the geometry.
  1041. // Note: x corresponds to moveOffset, this is the position of mouse
  1042. // Note: o corresponds to [startX, startY].
  1043. //
  1044. // +--------------+--------------+
  1045. // | | |
  1046. // | I | II |
  1047. // | | |
  1048. // +------------- o -------------+ _ _ _
  1049. // | | | ^
  1050. // | VI | III | | moveDiffY
  1051. // | | x _ _ _ _ _ v
  1052. // +--------------+---------|----+
  1053. // | |
  1054. // <------->
  1055. // moveDiffX
  1056. function onMouseMove( evt ) {
  1057. nativeEvt = evt.data.$;
  1058. // This is how far the mouse is from the point the button was pressed.
  1059. moveDiffX = nativeEvt.screenX - startX;
  1060. moveDiffY = startY - nativeEvt.screenY;
  1061. // This is the aspect ratio of the move difference.
  1062. moveRatio = Math.abs( moveDiffX / moveDiffY );
  1063. // Left, center or none-aligned widget.
  1064. if ( factor == 1 ) {
  1065. if ( moveDiffX <= 0 ) {
  1066. // Case: IV.
  1067. if ( moveDiffY <= 0 )
  1068. adjustToX();
  1069. // Case: I.
  1070. else {
  1071. if ( moveRatio >= ratio )
  1072. adjustToX();
  1073. else
  1074. adjustToY();
  1075. }
  1076. } else {
  1077. // Case: III.
  1078. if ( moveDiffY <= 0 ) {
  1079. if ( moveRatio >= ratio )
  1080. adjustToY();
  1081. else
  1082. adjustToX();
  1083. }
  1084. // Case: II.
  1085. else {
  1086. adjustToY();
  1087. }
  1088. }
  1089. }
  1090. // Right-aligned widget. It mirrors behaviours, so I becomes II,
  1091. // IV becomes III and vice-versa.
  1092. else {
  1093. if ( moveDiffX <= 0 ) {
  1094. // Case: IV.
  1095. if ( moveDiffY <= 0 ) {
  1096. if ( moveRatio >= ratio )
  1097. adjustToY();
  1098. else
  1099. adjustToX();
  1100. }
  1101. // Case: I.
  1102. else {
  1103. adjustToY();
  1104. }
  1105. } else {
  1106. // Case: III.
  1107. if ( moveDiffY <= 0 )
  1108. adjustToX();
  1109. // Case: II.
  1110. else {
  1111. if ( moveRatio >= ratio ) {
  1112. adjustToX();
  1113. } else {
  1114. adjustToY();
  1115. }
  1116. }
  1117. }
  1118. }
  1119. if ( isAllowedSize( newWidth, newHeight ) ) {
  1120. updateData = { width: newWidth, height: newHeight };
  1121. image.setAttributes( updateData );
  1122. }
  1123. }
  1124. function onMouseUp() {
  1125. var l;
  1126. while ( ( l = listeners.pop() ) )
  1127. l.removeListener();
  1128. // Restore default cursor by removing special class.
  1129. editable.removeClass( cursorClass );
  1130. // This is to bring back the regular behaviour of the resizer.
  1131. resizer.removeClass( 'cke_image_resizing' );
  1132. if ( updateData ) {
  1133. widget.setData( updateData );
  1134. // Save another undo snapshot: after resizing.
  1135. editor.fire( 'saveSnapshot' );
  1136. }
  1137. // Don't update data twice or more.
  1138. updateData = false;
  1139. }
  1140. function getMaxSize() {
  1141. var maxSize = editor.config.image2_maxSize,
  1142. natural;
  1143. if ( !maxSize ) {
  1144. return null;
  1145. }
  1146. maxSize = CKEDITOR.tools.copy( maxSize );
  1147. natural = CKEDITOR.plugins.image2.getNatural( image );
  1148. maxSize.width = Math.max( maxSize.width === 'natural' ? natural.width : maxSize.width, min.width );
  1149. maxSize.height = Math.max( maxSize.height === 'natural' ? natural.height : maxSize.height, min.width );
  1150. return maxSize;
  1151. }
  1152. function isAllowedSize( width, height ) {
  1153. var isTooSmall = width < min.width || height < min.height,
  1154. isTooBig = max && ( width > max.width || height > max.height );
  1155. return !isTooSmall && !isTooBig;
  1156. }
  1157. } );
  1158. // Change the position of the widget resizer when data changes.
  1159. widget.on( 'data', function() {
  1160. resizer[ widget.data.align == 'right' ? 'addClass' : 'removeClass' ]( 'cke_image_resizer_left' );
  1161. } );
  1162. }
  1163. // Integrates widget alignment setting with justify
  1164. // plugin's commands (execution and refreshment).
  1165. // @param {CKEDITOR.editor} editor
  1166. // @param {String} value 'left', 'right', 'center' or 'block'
  1167. function alignCommandIntegrator( editor ) {
  1168. var execCallbacks = [],
  1169. enabled;
  1170. return function( value ) {
  1171. var command = editor.getCommand( 'justify' + value );
  1172. // Most likely, the justify plugin isn't loaded.
  1173. if ( !command )
  1174. return;
  1175. // This command will be manually refreshed along with
  1176. // other commands after exec.
  1177. execCallbacks.push( function() {
  1178. command.refresh( editor, editor.elementPath() );
  1179. } );
  1180. if ( value in { right: 1, left: 1, center: 1 } ) {
  1181. command.on( 'exec', function( evt ) {
  1182. var widget = getFocusedWidget( editor );
  1183. if ( widget ) {
  1184. widget.setData( 'align', value );
  1185. // Once the widget changed its align, all the align commands
  1186. // must be refreshed: the event is to be cancelled.
  1187. for ( var i = execCallbacks.length; i--; )
  1188. execCallbacks[ i ]();
  1189. evt.cancel();
  1190. }
  1191. } );
  1192. }
  1193. command.on( 'refresh', function( evt ) {
  1194. var widget = getFocusedWidget( editor ),
  1195. allowed = { right: 1, left: 1, center: 1 };
  1196. if ( !widget )
  1197. return;
  1198. // Cache "enabled" on first use. This is because filter#checkFeature may
  1199. // not be available during plugin's afterInit in the future — a moment when
  1200. // alignCommandIntegrator is called.
  1201. if ( enabled === undefined )
  1202. enabled = editor.filter.checkFeature( editor.widgets.registered.image.features.align );
  1203. // Don't allow justify commands when widget alignment is disabled (https://dev.ckeditor.com/ticket/11004).
  1204. if ( !enabled )
  1205. this.setState( CKEDITOR.TRISTATE_DISABLED );
  1206. else {
  1207. this.setState(
  1208. ( widget.data.align == value ) ? (
  1209. CKEDITOR.TRISTATE_ON
  1210. ) : (
  1211. ( value in allowed ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
  1212. )
  1213. );
  1214. }
  1215. evt.cancel();
  1216. } );
  1217. };
  1218. }
  1219. function linkCommandIntegrator( editor ) {
  1220. // Nothing to integrate with if link is not loaded.
  1221. if ( !editor.plugins.link )
  1222. return;
  1223. var listener = CKEDITOR.on( 'dialogDefinition', function( evt ) {
  1224. var dialog = evt.data;
  1225. if ( dialog.name == 'link' ) {
  1226. var def = dialog.definition;
  1227. var onShow = def.onShow,
  1228. onOk = def.onOk;
  1229. def.onShow = function() {
  1230. var widget = getFocusedWidget( editor ),
  1231. displayTextField = this.getContentElement( 'info', 'linkDisplayText' ).getElement().getParent().getParent();
  1232. // Widget cannot be enclosed in a link, i.e.
  1233. // <a>foo<inline widget/>bar</a>
  1234. if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) {
  1235. this.setupContent( widget.data.link || {} );
  1236. // Hide the display text in case of linking image2 widget.
  1237. displayTextField.hide();
  1238. } else {
  1239. // Make sure that display text is visible, as it might be hidden by image2 integration
  1240. // before.
  1241. displayTextField.show();
  1242. onShow.apply( this, arguments );
  1243. }
  1244. };
  1245. // Set widget data if linking the widget using
  1246. // link dialog (instead of default action).
  1247. // State shifter handles data change and takes
  1248. // care of internal DOM structure of linked widget.
  1249. def.onOk = function() {
  1250. var widget = getFocusedWidget( editor );
  1251. // Widget cannot be enclosed in a link, i.e.
  1252. // <a>foo<inline widget/>bar</a>
  1253. if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) {
  1254. var data = {};
  1255. // Collect data from fields.
  1256. this.commitContent( data );
  1257. // Set collected data to widget.
  1258. widget.setData( 'link', data );
  1259. } else {
  1260. onOk.apply( this, arguments );
  1261. }
  1262. };
  1263. }
  1264. } );
  1265. // Listener has to be removed due to leaking the editor reference (#589).
  1266. editor.on( 'destroy', function() {
  1267. listener.removeListener();
  1268. } );
  1269. // Overwrite the default behavior of unlink command.
  1270. editor.getCommand( 'unlink' ).on( 'exec', function( evt ) {
  1271. var widget = getFocusedWidget( editor );
  1272. // Override unlink only when link truly belongs to the widget.
  1273. // If wrapped inline widget in a link, let default unlink work (https://dev.ckeditor.com/ticket/11814).
  1274. if ( !widget || !widget.parts.link )
  1275. return;
  1276. widget.setData( 'link', null );
  1277. // Selection (which is fake) may not change if unlinked image in focused widget,
  1278. // i.e. if captioned image. Let's refresh command state manually here.
  1279. this.refresh( editor, editor.elementPath() );
  1280. evt.cancel();
  1281. } );
  1282. // Overwrite default refresh of unlink command.
  1283. editor.getCommand( 'unlink' ).on( 'refresh', function( evt ) {
  1284. var widget = getFocusedWidget( editor );
  1285. if ( !widget )
  1286. return;
  1287. // Note that widget may be wrapped in a link, which
  1288. // does not belong to that widget (https://dev.ckeditor.com/ticket/11814).
  1289. this.setState( widget.data.link || widget.wrapper.getAscendant( 'a' ) ?
  1290. CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
  1291. evt.cancel();
  1292. } );
  1293. }
  1294. // Returns the focused widget, if of the type specific for this plugin.
  1295. // If no widget is focused, `null` is returned.
  1296. //
  1297. // @param {CKEDITOR.editor}
  1298. // @returns {CKEDITOR.plugins.widget}
  1299. function getFocusedWidget( editor ) {
  1300. var widget = editor.widgets.focused;
  1301. if ( widget && widget.name == 'image' )
  1302. return widget;
  1303. return null;
  1304. }
  1305. // Returns a set of widget allowedContent rules, depending
  1306. // on configurations like config#image2_alignClasses or
  1307. // config#image2_captionedClass.
  1308. //
  1309. // @param {CKEDITOR.editor}
  1310. // @returns {Object}
  1311. function getWidgetAllowedContent( editor ) {
  1312. var alignClasses = editor.config.image2_alignClasses,
  1313. rules = {
  1314. // Widget may need <div> or <p> centering wrapper.
  1315. div: {
  1316. match: centerWrapperChecker( editor )
  1317. },
  1318. p: {
  1319. match: centerWrapperChecker( editor )
  1320. },
  1321. img: {
  1322. attributes: '!src,alt,width,height'
  1323. },
  1324. figure: {
  1325. classes: '!' + editor.config.image2_captionedClass
  1326. },
  1327. figcaption: true
  1328. };
  1329. if ( alignClasses ) {
  1330. // Centering class from the config.
  1331. rules.div.classes = alignClasses[ 1 ];
  1332. rules.p.classes = rules.div.classes;
  1333. // Left/right classes from the config.
  1334. rules.img.classes = alignClasses[ 0 ] + ',' + alignClasses[ 2 ];
  1335. rules.figure.classes += ',' + rules.img.classes;
  1336. } else {
  1337. // Centering with text-align.
  1338. rules.div.styles = 'text-align';
  1339. rules.p.styles = 'text-align';
  1340. rules.img.styles = 'float';
  1341. rules.figure.styles = 'float,display';
  1342. }
  1343. return rules;
  1344. }
  1345. // Returns a set of widget feature rules, depending
  1346. // on editor configuration. Note that the following may not cover
  1347. // all the possible cases since requiredContent supports a single
  1348. // tag only.
  1349. //
  1350. // @param {CKEDITOR.editor}
  1351. // @returns {Object}
  1352. function getWidgetFeatures( editor ) {
  1353. var alignClasses = editor.config.image2_alignClasses,
  1354. features = {
  1355. dimension: {
  1356. requiredContent: 'img[width,height]'
  1357. },
  1358. align: {
  1359. requiredContent: 'img' +
  1360. ( alignClasses ? '(' + alignClasses[ 0 ] + ')' : '{float}' )
  1361. },
  1362. caption: {
  1363. requiredContent: 'figcaption'
  1364. }
  1365. };
  1366. return features;
  1367. }
  1368. // Returns element which is styled, considering current
  1369. // state of the widget.
  1370. //
  1371. // @see CKEDITOR.plugins.widget#applyStyle
  1372. // @param {CKEDITOR.plugins.widget} widget
  1373. // @returns {CKEDITOR.dom.element}
  1374. function getStyleableElement( widget ) {
  1375. return widget.data.hasCaption ? widget.element : widget.parts.image;
  1376. }
  1377. } )();
  1378. /**
  1379. * A CSS class applied to the `<figure>` element of a captioned image.
  1380. *
  1381. * Read more in the {@glink features/image2 documentation} and see the
  1382. * {@glink examples/image2 example}.
  1383. *
  1384. * // Changes the class to "captionedImage".
  1385. * config.image2_captionedClass = 'captionedImage';
  1386. *
  1387. * @cfg {String} [image2_captionedClass='image']
  1388. * @member CKEDITOR.config
  1389. */
  1390. CKEDITOR.config.image2_captionedClass = 'image';
  1391. /**
  1392. * Determines whether dimension inputs should be automatically filled when the image URL changes in the Enhanced Image
  1393. * plugin dialog window.
  1394. *
  1395. * Read more in the {@glink features/image2 documentation} and see the
  1396. * {@glink examples/image2 example}.
  1397. *
  1398. * config.image2_prefillDimensions = false;
  1399. *
  1400. * @since 4.5.0
  1401. * @cfg {Boolean} [image2_prefillDimensions=true]
  1402. * @member CKEDITOR.config
  1403. */
  1404. /**
  1405. * Disables the image resizer. By default the resizer is enabled.
  1406. *
  1407. * Read more in the {@glink features/image2 documentation} and see the
  1408. * {@glink examples/image2 example}.
  1409. *
  1410. * config.image2_disableResizer = true;
  1411. *
  1412. * @since 4.5.0
  1413. * @cfg {Boolean} [image2_disableResizer=false]
  1414. * @member CKEDITOR.config
  1415. */
  1416. /**
  1417. * CSS classes applied to aligned images. Useful to take control over the way
  1418. * the images are aligned, i.e. to customize output HTML and integrate external stylesheets.
  1419. *
  1420. * Classes should be defined in an array of three elements, containing left, center, and right
  1421. * alignment classes, respectively. For example:
  1422. *
  1423. * config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ];
  1424. *
  1425. * **Note**: Once this configuration option is set, the plugin will no longer produce inline
  1426. * styles for alignment. It means that e.g. the following HTML will be produced:
  1427. *
  1428. * <img alt="My image" class="custom-center-class" src="foo.png" />
  1429. *
  1430. * instead of:
  1431. *
  1432. * <img alt="My image" style="float:left" src="foo.png" />
  1433. *
  1434. * **Note**: Once this configuration option is set, corresponding style definitions
  1435. * must be supplied to the editor:
  1436. *
  1437. * * For {@glink guide/dev_framed classic editor} it can be done by defining additional
  1438. * styles in the {@link CKEDITOR.config#contentsCss stylesheets loaded by the editor}. The same
  1439. * styles must be provided on the target page where the content will be loaded.
  1440. * * For {@glink guide/dev_inline inline editor} the styles can be defined directly
  1441. * with `<style> ... <style>` or `<link href="..." rel="stylesheet">`, i.e. within the `<head>`
  1442. * of the page.
  1443. *
  1444. * For example, considering the following configuration:
  1445. *
  1446. * config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ];
  1447. *
  1448. * CSS rules can be defined as follows:
  1449. *
  1450. * .align-left {
  1451. * float: left;
  1452. * }
  1453. *
  1454. * .align-right {
  1455. * float: right;
  1456. * }
  1457. *
  1458. * .align-center {
  1459. * text-align: center;
  1460. * }
  1461. *
  1462. * .align-center > figure {
  1463. * display: inline-block;
  1464. * }
  1465. *
  1466. * Read more in the {@glink features/image2 documentation} and see the
  1467. * {@glink examples/image2 example}.
  1468. *
  1469. * @since 4.4.0
  1470. * @cfg {String[]} [image2_alignClasses=null]
  1471. * @member CKEDITOR.config
  1472. */
  1473. /**
  1474. * Determines whether alternative text is required for the captioned image.
  1475. *
  1476. * config.image2_altRequired = true;
  1477. *
  1478. * Read more in the {@glink features/image2 documentation} and see the
  1479. * {@glink examples/image2 example}.
  1480. *
  1481. * @since 4.6.0
  1482. * @cfg {Boolean} [image2_altRequired=false]
  1483. * @member CKEDITOR.config
  1484. */
  1485. /**
  1486. * Determines the maximum size that an image can be resized to with the resize handle.
  1487. *
  1488. * It stores two properties: `width` and `height`. They can be set with one of the two types:
  1489. *
  1490. * * A number representing a value that limits the maximum size in pixel units:
  1491. *
  1492. * ```js
  1493. * config.image2_maxSize = {
  1494. * height: 300,
  1495. * width: 250
  1496. * };
  1497. * ```
  1498. *
  1499. * * A string representing the natural image size, so each image resize operation is limited to its own natural height or width:
  1500. *
  1501. * ```js
  1502. * config.image2_maxSize = {
  1503. * height: 'natural',
  1504. * width: 'natural'
  1505. * }
  1506. * ```
  1507. *
  1508. * Note: An image can still be resized to bigger dimensions when using the image dialog.
  1509. *
  1510. * @since 4.12.0
  1511. * @cfg {Object.<String, Number/String>} [image2_maxSize]
  1512. * @member CKEDITOR.config
  1513. */