星火管控前端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plugin.js 63KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798
  1. /**
  2. * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
  3. * CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
  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. legacyLockBehavior = this.ready ? helpers.checkHasNaturalRatio( image ) : true,
  326. data = {
  327. hasCaption: !!this.parts.caption,
  328. src: image.getAttribute( 'src' ),
  329. alt: image.getAttribute( 'alt' ) || '',
  330. width: image.getAttribute( 'width' ) || '',
  331. height: image.getAttribute( 'height' ) || '',
  332. // Lock ratio should respect the value of the config.image2_defaultLockRatio.
  333. // If the variable is not set, then it fallback to the legacy one
  334. // (#5219, https://dev.ckeditor.com/ticket/10833).
  335. lock: editor.config.image2_defaultLockRatio !== undefined ?
  336. editor.config.image2_defaultLockRatio : legacyLockBehavior
  337. };
  338. // If we used 'a' in widget#parts definition, it could happen that
  339. // selected element is a child of widget.parts#caption. Since there's no clever
  340. // way to solve it with CSS selectors, it's done like that. (https://dev.ckeditor.com/ticket/11783).
  341. var link = image.getAscendant( 'a' );
  342. if ( link && this.wrapper.contains( link ) )
  343. this.parts.link = link;
  344. // Depending on configuration, read style/class from element and
  345. // then remove it. Removed style/class will be set on wrapper in #data listener.
  346. // Note: Center alignment is detected during upcast, so only left/right cases
  347. // are checked below.
  348. if ( !data.align ) {
  349. var alignElement = data.hasCaption ? this.element : image;
  350. // Read the initial left/right alignment from the class set on element.
  351. if ( alignClasses ) {
  352. if ( alignElement.hasClass( alignClasses[ 0 ] ) ) {
  353. data.align = 'left';
  354. } else if ( alignElement.hasClass( alignClasses[ 2 ] ) ) {
  355. data.align = 'right';
  356. }
  357. if ( data.align ) {
  358. alignElement.removeClass( alignClasses[ alignmentsObj[ data.align ] ] );
  359. } else {
  360. data.align = 'none';
  361. }
  362. }
  363. // Read initial float style from figure/image and then remove it.
  364. else {
  365. data.align = alignElement.getStyle( 'float' ) || 'none';
  366. alignElement.removeStyle( 'float' );
  367. }
  368. }
  369. // Update data.link object with attributes if the link has been discovered.
  370. if ( editor.plugins.link && this.parts.link ) {
  371. data.link = helpers.getLinkAttributesParser()( editor, this.parts.link );
  372. // Get rid of cke_widget_* classes in data. Otherwise
  373. // they might appear in link dialog.
  374. var advanced = data.link.advanced;
  375. if ( advanced && advanced.advCSSClasses ) {
  376. advanced.advCSSClasses = CKEDITOR.tools.trim( advanced.advCSSClasses.replace( /cke_\S+/, '' ) );
  377. }
  378. }
  379. // Get rid of extra vertical space when there's no caption.
  380. // It will improve the look of the resizer.
  381. this.wrapper[ ( data.hasCaption ? 'remove' : 'add' ) + 'Class' ]( 'cke_image_nocaption' );
  382. this.setData( data );
  383. // Setup dynamic image resizing with mouse.
  384. // Don't initialize resizer when dimensions are disallowed (https://dev.ckeditor.com/ticket/11004).
  385. if ( editor.filter.checkFeature( this.features.dimension ) && editor.config.image2_disableResizer !== true ) {
  386. setupResizer( this );
  387. }
  388. this.shiftState = helpers.stateShifter( this.editor );
  389. // Add widget editing option to its context menu.
  390. this.on( 'contextMenu', function( evt ) {
  391. evt.data.image = CKEDITOR.TRISTATE_OFF;
  392. // Integrate context menu items for link.
  393. // Note that widget may be wrapped in a link, which
  394. // does not belong to that widget (https://dev.ckeditor.com/ticket/11814).
  395. if ( this.parts.link || this.wrapper.getAscendant( 'a' ) )
  396. evt.data.link = evt.data.unlink = CKEDITOR.TRISTATE_OFF;
  397. } );
  398. },
  399. // Overrides default method to handle internal mutability of Image2.
  400. // @see CKEDITOR.plugins.widget#addClass
  401. addClass: function( className ) {
  402. getStyleableElement( this ).addClass( className );
  403. },
  404. // Overrides default method to handle internal mutability of Image2.
  405. // @see CKEDITOR.plugins.widget#hasClass
  406. hasClass: function( className ) {
  407. return getStyleableElement( this ).hasClass( className );
  408. },
  409. // Overrides default method to handle internal mutability of Image2.
  410. // @see CKEDITOR.plugins.widget#removeClass
  411. removeClass: function( className ) {
  412. getStyleableElement( this ).removeClass( className );
  413. },
  414. // Overrides default method to handle internal mutability of Image2.
  415. // @see CKEDITOR.plugins.widget#getClasses
  416. getClasses: ( function() {
  417. var classRegex = new RegExp( '^(' + [].concat( captionedClass, alignClasses ).join( '|' ) + ')$' );
  418. return function() {
  419. var classes = this.repository.parseElementClasses( getStyleableElement( this ).getAttribute( 'class' ) );
  420. // Neither config.image2_captionedClass nor config.image2_alignClasses
  421. // do not belong to style classes.
  422. for ( var c in classes ) {
  423. if ( classRegex.test( c ) )
  424. delete classes[ c ];
  425. }
  426. return classes;
  427. };
  428. } )(),
  429. upcast: upcastWidgetElement( editor ),
  430. downcast: downcastWidgetElement( editor ),
  431. getLabel: function() {
  432. var label = ( this.data.alt || '' ) + ' ' + this.pathName;
  433. return this.editor.lang.widget.label.replace( /%1/, label );
  434. }
  435. };
  436. }
  437. /**
  438. * A set of Enhanced Image (image2) plugin helpers.
  439. *
  440. * @class
  441. * @singleton
  442. */
  443. CKEDITOR.plugins.image2 = {
  444. stateShifter: function( editor ) {
  445. // Tag name used for centering non-captioned widgets.
  446. var doc = editor.document,
  447. alignClasses = editor.config.image2_alignClasses,
  448. captionedClass = editor.config.image2_captionedClass,
  449. editable = editor.editable(),
  450. // The order that stateActions get executed. It matters!
  451. shiftables = [ 'hasCaption', 'align', 'link' ];
  452. // Atomic procedures, one per state variable.
  453. var stateActions = {
  454. align: function( shift, oldValue, newValue ) {
  455. var el = shift.element;
  456. // Alignment changed.
  457. if ( shift.changed.align ) {
  458. // No caption in the new state.
  459. if ( !shift.newData.hasCaption ) {
  460. // Changed to "center" (non-captioned).
  461. if ( newValue == 'center' ) {
  462. shift.deflate();
  463. shift.element = wrapInCentering( editor, el );
  464. }
  465. // Changed to "non-center" from "center" while caption removed.
  466. if ( !shift.changed.hasCaption && oldValue == 'center' && newValue != 'center' ) {
  467. shift.deflate();
  468. shift.element = unwrapFromCentering( el );
  469. }
  470. }
  471. }
  472. // Alignment remains and "center" removed caption.
  473. else if ( newValue == 'center' && shift.changed.hasCaption && !shift.newData.hasCaption ) {
  474. shift.deflate();
  475. shift.element = wrapInCentering( editor, el );
  476. }
  477. // Finally set display for figure.
  478. if ( !alignClasses && el.is( 'figure' ) ) {
  479. if ( newValue == 'center' )
  480. el.setStyle( 'display', 'inline-block' );
  481. else
  482. el.removeStyle( 'display' );
  483. }
  484. },
  485. hasCaption: function( shift, oldValue, newValue ) {
  486. // This action is for real state change only.
  487. if ( !shift.changed.hasCaption )
  488. return;
  489. // Get <img/> or <a><img/></a> from widget. Note that widget element might itself
  490. // be what we're looking for. Also element can be <p style="text-align:center"><a>...</a></p>.
  491. var imageOrLink;
  492. if ( shift.element.is( { img: 1, a: 1 } ) )
  493. imageOrLink = shift.element;
  494. else
  495. imageOrLink = shift.element.findOne( 'a,img' );
  496. // Switching hasCaption always destroys the widget.
  497. shift.deflate();
  498. // There was no caption, but the caption is to be added.
  499. if ( newValue ) {
  500. // Create new <figure> from widget template.
  501. var figure = CKEDITOR.dom.element.createFromHtml( templateBlock.output( {
  502. captionedClass: captionedClass,
  503. captionPlaceholder: editor.lang.image2.captionPlaceholder
  504. } ), doc );
  505. // Replace element with <figure>.
  506. replaceSafely( figure, shift.element );
  507. // Use old <img/> or <a><img/></a> instead of the one from the template,
  508. // so we won't lose additional attributes.
  509. imageOrLink.replace( figure.findOne( 'img' ) );
  510. // Update widget's element.
  511. shift.element = figure;
  512. }
  513. // The caption was present, but now it's to be removed.
  514. else {
  515. // Unwrap <img/> or <a><img/></a> from figure.
  516. imageOrLink.replace( shift.element );
  517. // Update widget's element.
  518. shift.element = imageOrLink;
  519. }
  520. },
  521. link: function( shift, oldValue, newValue ) {
  522. if ( shift.changed.link ) {
  523. var img = shift.element.is( 'img' ) ?
  524. shift.element : shift.element.findOne( 'img' ),
  525. link = shift.element.is( 'a' ) ?
  526. shift.element : shift.element.findOne( 'a' ),
  527. // Why deflate:
  528. // If element is <img/>, it will be wrapped into <a>,
  529. // which becomes a new widget.element.
  530. // If element is <a><img/></a>, it will be unlinked
  531. // so <img/> becomes a new widget.element.
  532. needsDeflate = ( shift.element.is( 'a' ) && !newValue ) || ( shift.element.is( 'img' ) && newValue ),
  533. newEl;
  534. if ( needsDeflate )
  535. shift.deflate();
  536. // If unlinked the image, returned element is <img>.
  537. if ( !newValue )
  538. newEl = unwrapFromLink( link );
  539. else {
  540. // If linked the image, returned element is <a>.
  541. if ( !oldValue )
  542. newEl = wrapInLink( img, shift.newData.link );
  543. // Set and remove all attributes associated with this state.
  544. var attributes = CKEDITOR.plugins.image2.getLinkAttributesGetter()( editor, newValue );
  545. if ( !CKEDITOR.tools.isEmpty( attributes.set ) )
  546. ( newEl || link ).setAttributes( attributes.set );
  547. if ( attributes.removed.length )
  548. ( newEl || link ).removeAttributes( attributes.removed );
  549. }
  550. if ( needsDeflate )
  551. shift.element = newEl;
  552. }
  553. }
  554. };
  555. function wrapInCentering( editor, element ) {
  556. var attribsAndStyles = {};
  557. if ( alignClasses )
  558. attribsAndStyles.attributes = { 'class': alignClasses[ 1 ] };
  559. else
  560. attribsAndStyles.styles = { 'text-align': 'center' };
  561. // There's no gentle way to center inline element with CSS, so create p/div
  562. // that wraps widget contents and does the trick either with style or class.
  563. var center = doc.createElement(
  564. editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div', attribsAndStyles );
  565. // Replace element with centering wrapper.
  566. replaceSafely( center, element );
  567. element.move( center );
  568. return center;
  569. }
  570. function unwrapFromCentering( element ) {
  571. var imageOrLink = element.findOne( 'a,img' );
  572. imageOrLink.replace( element );
  573. return imageOrLink;
  574. }
  575. // Wraps <img/> -> <a><img/></a>.
  576. // Returns reference to <a>.
  577. //
  578. // @param {CKEDITOR.dom.element} img
  579. // @param {Object} linkData
  580. // @returns {CKEDITOR.dom.element}
  581. function wrapInLink( img, linkData ) {
  582. var link = doc.createElement( 'a', {
  583. attributes: {
  584. href: linkData.url
  585. }
  586. } );
  587. link.replace( img );
  588. img.move( link );
  589. return link;
  590. }
  591. // De-wraps <a><img/></a> -> <img/>.
  592. // Returns the reference to <img/>
  593. //
  594. // @param {CKEDITOR.dom.element} link
  595. // @returns {CKEDITOR.dom.element}
  596. function unwrapFromLink( link ) {
  597. var img = link.findOne( 'img' );
  598. img.replace( link );
  599. return img;
  600. }
  601. function replaceSafely( replacing, replaced ) {
  602. if ( replaced.getParent() ) {
  603. var range = editor.createRange();
  604. range.moveToPosition( replaced, CKEDITOR.POSITION_BEFORE_START );
  605. // Remove old element. Do it before insertion to avoid a case when
  606. // element is moved from 'replaced' element before it, what creates
  607. // a tricky case which insertElementIntorRange does not handle.
  608. replaced.remove();
  609. editable.insertElementIntoRange( replacing, range );
  610. }
  611. else {
  612. replacing.replace( replaced );
  613. }
  614. }
  615. return function( shift ) {
  616. var name, i;
  617. shift.changed = {};
  618. for ( i = 0; i < shiftables.length; i++ ) {
  619. name = shiftables[ i ];
  620. shift.changed[ name ] = shift.oldData ?
  621. shift.oldData[ name ] !== shift.newData[ name ] : false;
  622. }
  623. // Iterate over possible state variables.
  624. for ( i = 0; i < shiftables.length; i++ ) {
  625. name = shiftables[ i ];
  626. stateActions[ name ]( shift,
  627. shift.oldData ? shift.oldData[ name ] : null,
  628. shift.newData[ name ] );
  629. }
  630. shift.inflate();
  631. };
  632. },
  633. /**
  634. * Checks whether the current image ratio matches the natural one
  635. * by comparing dimensions.
  636. *
  637. * @param {CKEDITOR.dom.element} image
  638. * @returns {Boolean}
  639. */
  640. checkHasNaturalRatio: function( image ) {
  641. var $ = image.$,
  642. natural = this.getNatural( image );
  643. // The reason for two alternative comparisons is that the rounding can come from
  644. // both dimensions, e.g. there are two cases:
  645. // 1. height is computed as a rounded relation of the real height and the value of width,
  646. // 2. width is computed as a rounded relation of the real width and the value of heigh.
  647. return Math.round( $.clientWidth / natural.width * natural.height ) == $.clientHeight ||
  648. Math.round( $.clientHeight / natural.height * natural.width ) == $.clientWidth;
  649. },
  650. /**
  651. * Returns natural dimensions of the image. For modern browsers
  652. * it uses natural(Width|Height). For old ones (IE8) it creates
  653. * a new image and reads the dimensions.
  654. *
  655. * @param {CKEDITOR.dom.element} image
  656. * @returns {Object}
  657. */
  658. getNatural: function( image ) {
  659. var dimensions;
  660. if ( image.$.naturalWidth ) {
  661. dimensions = {
  662. width: image.$.naturalWidth,
  663. height: image.$.naturalHeight
  664. };
  665. } else {
  666. var img = new Image();
  667. img.src = image.getAttribute( 'src' );
  668. dimensions = {
  669. width: img.width,
  670. height: img.height
  671. };
  672. }
  673. return dimensions;
  674. },
  675. /**
  676. * Returns an attribute getter function. Default getter comes from the Link plugin
  677. * and is documented by {@link CKEDITOR.plugins.link#getLinkAttributes}.
  678. *
  679. * **Note:** It is possible to override this method and use a custom getter e.g.
  680. * in the absence of the Link plugin.
  681. *
  682. * **Note:** If a custom getter is used, a data model format it produces
  683. * must be compatible with {@link CKEDITOR.plugins.link#getLinkAttributes}.
  684. *
  685. * **Note:** A custom getter must understand the data model format produced by
  686. * {@link #getLinkAttributesParser} to work correctly.
  687. *
  688. * @returns {Function} A function that gets (composes) link attributes.
  689. * @since 4.5.5
  690. */
  691. getLinkAttributesGetter: function() {
  692. // https://dev.ckeditor.com/ticket/13885
  693. return CKEDITOR.plugins.link.getLinkAttributes;
  694. },
  695. /**
  696. * Returns an attribute parser function. Default parser comes from the Link plugin
  697. * and is documented by {@link CKEDITOR.plugins.link#parseLinkAttributes}.
  698. *
  699. * **Note:** It is possible to override this method and use a custom parser e.g.
  700. * in the absence of the Link plugin.
  701. *
  702. * **Note:** If a custom parser is used, a data model format produced by the parser
  703. * must be compatible with {@link #getLinkAttributesGetter}.
  704. *
  705. * **Note:** If a custom parser is used, it should be compatible with the
  706. * {@link CKEDITOR.plugins.link#parseLinkAttributes} data model format. Otherwise the
  707. * Link plugin dialog may not be populated correctly with parsed data. However
  708. * as long as Enhanced Image is **not** used with the Link plugin dialog, any custom data model
  709. * will work, being stored as an internal property of Enhanced Image widget's data only.
  710. *
  711. * @returns {Function} A function that parses attributes.
  712. * @since 4.5.5
  713. */
  714. getLinkAttributesParser: function() {
  715. // https://dev.ckeditor.com/ticket/13885
  716. return CKEDITOR.plugins.link.parseLinkAttributes;
  717. }
  718. };
  719. function setWrapperAlign( widget, alignClasses ) {
  720. var wrapper = widget.wrapper,
  721. align = widget.data.align,
  722. hasCaption = widget.data.hasCaption;
  723. if ( alignClasses ) {
  724. // Remove all align classes first.
  725. for ( var i = 3; i--; )
  726. wrapper.removeClass( alignClasses[ i ] );
  727. if ( align == 'center' ) {
  728. // Avoid touching non-captioned, centered widgets because
  729. // they have the class set on the element instead of wrapper:
  730. //
  731. // <div class="cke_widget_wrapper">
  732. // <p class="center-class">
  733. // <img />
  734. // </p>
  735. // </div>
  736. if ( hasCaption ) {
  737. wrapper.addClass( alignClasses[ 1 ] );
  738. }
  739. } else if ( align != 'none' ) {
  740. wrapper.addClass( alignClasses[ alignmentsObj[ align ] ] );
  741. }
  742. } else {
  743. if ( align == 'center' ) {
  744. if ( hasCaption )
  745. wrapper.setStyle( 'text-align', 'center' );
  746. else
  747. wrapper.removeStyle( 'text-align' );
  748. wrapper.removeStyle( 'float' );
  749. }
  750. else {
  751. if ( align == 'none' )
  752. wrapper.removeStyle( 'float' );
  753. else
  754. wrapper.setStyle( 'float', align );
  755. wrapper.removeStyle( 'text-align' );
  756. }
  757. }
  758. }
  759. // Returns a function that creates widgets from all <img> and
  760. // <figure class="{config.image2_captionedClass}"> elements.
  761. //
  762. // @param {CKEDITOR.editor} editor
  763. // @returns {Function}
  764. function upcastWidgetElement( editor ) {
  765. var isCenterWrapper = centerWrapperChecker( editor ),
  766. captionedClass = editor.config.image2_captionedClass;
  767. // @param {CKEDITOR.htmlParser.element} el
  768. // @param {Object} data
  769. return function( el, data ) {
  770. var dimensions = { width: 1, height: 1 },
  771. name = el.name,
  772. image;
  773. // https://dev.ckeditor.com/ticket/11110 Don't initialize on pasted fake objects.
  774. if ( el.attributes[ 'data-cke-realelement' ] )
  775. return;
  776. // If a center wrapper is found, there are 3 possible cases:
  777. //
  778. // 1. <div style="text-align:center"><figure>...</figure></div>.
  779. // In this case centering is done with a class set on widget.wrapper.
  780. // Simply replace centering wrapper with figure (it's no longer necessary).
  781. //
  782. // 2. <p style="text-align:center"><img/></p>.
  783. // Nothing to do here: <p> remains for styling purposes.
  784. //
  785. // 3. <div style="text-align:center"><img/></div>.
  786. // Nothing to do here (2.) but that case is only possible in enterMode different
  787. // than ENTER_P.
  788. if ( isCenterWrapper( el ) ) {
  789. if ( name == 'div' ) {
  790. var figure = el.getFirst( 'figure' );
  791. // Case #1.
  792. if ( figure ) {
  793. el.replaceWith( figure );
  794. el = figure;
  795. }
  796. }
  797. // Cases #2 and #3 (handled transparently)
  798. // If there's a centering wrapper, save it in data.
  799. data.align = 'center';
  800. // Image can be wrapped in link <a><img/></a>.
  801. image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' );
  802. }
  803. // No center wrapper has been found.
  804. else if ( name == 'figure' && el.hasClass( captionedClass ) ) {
  805. image = el.find( function( child ) {
  806. return child.name === 'img' &&
  807. CKEDITOR.tools.array.indexOf( [ 'figure', 'a' ], child.parent.name ) !== -1;
  808. }, true )[ 0 ];
  809. // Upcast linked image like <a><img/></a>.
  810. } else if ( isLinkedOrStandaloneImage( el ) ) {
  811. image = el.name == 'a' ? el.children[ 0 ] : el;
  812. }
  813. if ( !image )
  814. return;
  815. // If there's an image, then cool, we got a widget.
  816. // Now just remove dimension attributes expressed with %.
  817. for ( var d in dimensions ) {
  818. var dimension = image.attributes[ d ];
  819. if ( dimension && dimension.match( regexPercent ) )
  820. delete image.attributes[ d ];
  821. }
  822. return el;
  823. };
  824. }
  825. // Returns a function which transforms the widget to the external format
  826. // according to the current configuration.
  827. //
  828. // @param {CKEDITOR.editor}
  829. function downcastWidgetElement( editor ) {
  830. var alignClasses = editor.config.image2_alignClasses;
  831. // @param {CKEDITOR.htmlParser.element} el
  832. return function( el ) {
  833. // In case of <a><img/></a>, <img/> is the element to hold
  834. // inline styles or classes (image2_alignClasses).
  835. var attrsHolder = el.name == 'a' ? el.getFirst() : el,
  836. attrs = attrsHolder.attributes,
  837. align = this.data.align;
  838. // De-wrap the image from resize handle wrapper.
  839. // Only block widgets have one.
  840. if ( !this.inline ) {
  841. var resizeWrapper = el.getFirst( 'span' );
  842. if ( resizeWrapper )
  843. resizeWrapper.replaceWith( resizeWrapper.getFirst( { img: 1, a: 1 } ) );
  844. }
  845. if ( align && align != 'none' ) {
  846. var styles = CKEDITOR.tools.parseCssText( attrs.style || '' );
  847. // When the widget is captioned (<figure>) and internally centering is done
  848. // with widget's wrapper style/class, in the external data representation,
  849. // <figure> must be wrapped with an element holding an style/class:
  850. //
  851. // <div style="text-align:center">
  852. // <figure class="image" style="display:inline-block">...</figure>
  853. // </div>
  854. // or
  855. // <div class="some-center-class">
  856. // <figure class="image">...</figure>
  857. // </div>
  858. //
  859. if ( align == 'center' && el.name == 'figure' ) {
  860. el = el.wrapWith( new CKEDITOR.htmlParser.element( 'div',
  861. alignClasses ? { 'class': alignClasses[ 1 ] } : { style: 'text-align:center' } ) );
  862. }
  863. // If left/right, add float style to the downcasted element.
  864. else if ( align in { left: 1, right: 1 } ) {
  865. if ( alignClasses )
  866. attrsHolder.addClass( alignClasses[ alignmentsObj[ align ] ] );
  867. else
  868. styles[ 'float' ] = align;
  869. }
  870. // Update element styles.
  871. if ( !alignClasses && !CKEDITOR.tools.isEmpty( styles ) )
  872. attrs.style = CKEDITOR.tools.writeCssText( styles );
  873. }
  874. return el;
  875. };
  876. }
  877. // Returns a function that checks if an element is a centering wrapper.
  878. //
  879. // @param {CKEDITOR.editor} editor
  880. // @returns {Function}
  881. function centerWrapperChecker( editor ) {
  882. var captionedClass = editor.config.image2_captionedClass,
  883. alignClasses = editor.config.image2_alignClasses,
  884. validChildren = { figure: 1, a: 1, img: 1 };
  885. return function( el ) {
  886. // Wrapper must be either <div> or <p>.
  887. if ( !( el.name in { div: 1, p: 1 } ) )
  888. return false;
  889. var children = el.children;
  890. // Centering wrapper can have only one child.
  891. if ( children.length !== 1 )
  892. return false;
  893. var child = children[ 0 ];
  894. // Only <figure> or <img /> can be first (only) child of centering wrapper,
  895. // regardless of its type.
  896. if ( !( child.name in validChildren ) )
  897. return false;
  898. // If centering wrapper is <p>, only <img /> can be the child.
  899. // <p style="text-align:center"><img /></p>
  900. if ( el.name == 'p' ) {
  901. if ( !isLinkedOrStandaloneImage( child ) )
  902. return false;
  903. }
  904. // Centering <div> can hold <img/> or <figure>, depending on enterMode.
  905. else {
  906. // If a <figure> is the first (only) child, it must have a class.
  907. // <div style="text-align:center"><figure>...</figure><div>
  908. if ( child.name == 'figure' ) {
  909. if ( !child.hasClass( captionedClass ) )
  910. return false;
  911. } else {
  912. // Centering <div> can hold <img/> or <a><img/></a> only when enterMode
  913. // is ENTER_(BR|DIV).
  914. // <div style="text-align:center"><img /></div>
  915. // <div style="text-align:center"><a><img /></a></div>
  916. if ( editor.enterMode == CKEDITOR.ENTER_P )
  917. return false;
  918. // Regardless of enterMode, a child which is not <figure> must be
  919. // either <img/> or <a><img/></a>.
  920. if ( !isLinkedOrStandaloneImage( child ) )
  921. return false;
  922. }
  923. }
  924. // Centering wrapper got to be... centering. If image2_alignClasses are defined,
  925. // check for centering class. Otherwise, check the style.
  926. if ( alignClasses ? el.hasClass( alignClasses[ 1 ] ) :
  927. CKEDITOR.tools.parseCssText( el.attributes.style || '', true )[ 'text-align' ] == 'center' )
  928. return true;
  929. return false;
  930. };
  931. }
  932. // Checks whether element is <img/> or <a><img/></a>.
  933. //
  934. // @param {CKEDITOR.htmlParser.element}
  935. function isLinkedOrStandaloneImage( el ) {
  936. if ( el.name == 'img' )
  937. return true;
  938. else if ( el.name == 'a' )
  939. return el.children.length == 1 && el.getFirst( 'img' );
  940. return false;
  941. }
  942. // Sets width and height of the widget image according to current widget data.
  943. //
  944. // @param {CKEDITOR.plugins.widget} widget
  945. function setDimensions( widget ) {
  946. var data = widget.data,
  947. dimensions = { width: data.width, height: data.height },
  948. image = widget.parts.image;
  949. for ( var d in dimensions ) {
  950. if ( dimensions[ d ] )
  951. image.setAttribute( d, dimensions[ d ] );
  952. else
  953. image.removeAttribute( d );
  954. }
  955. }
  956. // Defines all features related to drag-driven image resizing.
  957. //
  958. // @param {CKEDITOR.plugins.widget} widget
  959. function setupResizer( widget ) {
  960. var editor = widget.editor,
  961. editable = editor.editable(),
  962. doc = editor.document,
  963. // Store the resizer in a widget for testing (https://dev.ckeditor.com/ticket/11004).
  964. resizer = widget.resizer = doc.createElement( 'span' );
  965. resizer.addClass( 'cke_image_resizer' );
  966. resizer.setAttribute( 'title', editor.lang.image2.resizer );
  967. resizer.append( new CKEDITOR.dom.text( '\u200b', doc ) );
  968. // Inline widgets don't need a resizer wrapper as an image spans the entire widget.
  969. if ( !widget.inline ) {
  970. var imageOrLink = widget.parts.link || widget.parts.image,
  971. oldResizeWrapper = imageOrLink.getParent(),
  972. resizeWrapper = doc.createElement( 'span' );
  973. resizeWrapper.addClass( 'cke_image_resizer_wrapper' );
  974. resizeWrapper.append( imageOrLink );
  975. resizeWrapper.append( resizer );
  976. widget.element.append( resizeWrapper, true );
  977. // Remove the old wrapper which could came from e.g. pasted HTML
  978. // and which could be corrupted (e.g. resizer span has been lost).
  979. if ( oldResizeWrapper.is( 'span' ) )
  980. oldResizeWrapper.remove();
  981. } else {
  982. widget.wrapper.append( resizer );
  983. }
  984. // Calculate values of size variables and mouse offsets.
  985. resizer.on( 'mousedown', function( evt ) {
  986. var image = widget.parts.image,
  987. // Don't update attributes if less than 15.
  988. // This is to prevent images to visually disappear.
  989. min = {
  990. width: 15,
  991. height: 15
  992. },
  993. max = getMaxSize(),
  994. // "factor" can be either 1 or -1. I.e.: For right-aligned images, we need to
  995. // subtract the difference to get proper width, etc. Without "factor",
  996. // resizer starts working the opposite way.
  997. factor = widget.data.align == 'right' ? -1 : 1,
  998. // The x-coordinate of the mouse relative to the screen
  999. // when button gets pressed.
  1000. startX = evt.data.$.screenX,
  1001. startY = evt.data.$.screenY,
  1002. // The initial dimensions and aspect ratio of the image.
  1003. startWidth = image.$.clientWidth,
  1004. startHeight = image.$.clientHeight,
  1005. ratio = startWidth / startHeight,
  1006. listeners = [],
  1007. // A class applied to editable during resizing.
  1008. cursorClass = 'cke_image_s' + ( !~factor ? 'w' : 'e' ),
  1009. nativeEvt, newWidth, newHeight, updateData,
  1010. moveDiffX, moveDiffY, moveRatio;
  1011. // Save the undo snapshot first: before resizing.
  1012. editor.fire( 'saveSnapshot' );
  1013. // Mousemove listeners are removed on mouseup.
  1014. attachToDocuments( 'mousemove', onMouseMove, listeners );
  1015. // Clean up the mousemove listener. Update widget data if valid.
  1016. attachToDocuments( 'mouseup', onMouseUp, listeners );
  1017. // The entire editable will have the special cursor while resizing goes on.
  1018. editable.addClass( cursorClass );
  1019. // This is to always keep the resizer element visible while resizing.
  1020. resizer.addClass( 'cke_image_resizing' );
  1021. // Attaches an event to a global document if inline editor.
  1022. // Additionally, if classic (`iframe`-based) editor, also attaches the same event to `iframe`'s document.
  1023. function attachToDocuments( name, callback, collection ) {
  1024. var globalDoc = CKEDITOR.document,
  1025. listeners = [];
  1026. if ( !doc.equals( globalDoc ) )
  1027. listeners.push( globalDoc.on( name, callback ) );
  1028. listeners.push( doc.on( name, callback ) );
  1029. if ( collection ) {
  1030. for ( var i = listeners.length; i--; )
  1031. collection.push( listeners.pop() );
  1032. }
  1033. }
  1034. // Calculate with first, and then adjust height, preserving ratio.
  1035. function adjustToX() {
  1036. newWidth = startWidth + factor * moveDiffX;
  1037. newHeight = Math.round( newWidth / ratio );
  1038. }
  1039. // Calculate height first, and then adjust width, preserving ratio.
  1040. function adjustToY() {
  1041. newHeight = startHeight - moveDiffY;
  1042. newWidth = Math.round( newHeight * ratio );
  1043. }
  1044. // This is how variables refer to the geometry.
  1045. // Note: x corresponds to moveOffset, this is the position of mouse
  1046. // Note: o corresponds to [startX, startY].
  1047. //
  1048. // +--------------+--------------+
  1049. // | | |
  1050. // | I | II |
  1051. // | | |
  1052. // +------------- o -------------+ _ _ _
  1053. // | | | ^
  1054. // | VI | III | | moveDiffY
  1055. // | | x _ _ _ _ _ v
  1056. // +--------------+---------|----+
  1057. // | |
  1058. // <------->
  1059. // moveDiffX
  1060. function onMouseMove( evt ) {
  1061. nativeEvt = evt.data.$;
  1062. // This is how far the mouse is from the point the button was pressed.
  1063. moveDiffX = nativeEvt.screenX - startX;
  1064. moveDiffY = startY - nativeEvt.screenY;
  1065. // This is the aspect ratio of the move difference.
  1066. moveRatio = Math.abs( moveDiffX / moveDiffY );
  1067. // Left, center or none-aligned widget.
  1068. if ( factor == 1 ) {
  1069. if ( moveDiffX <= 0 ) {
  1070. // Case: IV.
  1071. if ( moveDiffY <= 0 )
  1072. adjustToX();
  1073. // Case: I.
  1074. else {
  1075. if ( moveRatio >= ratio )
  1076. adjustToX();
  1077. else
  1078. adjustToY();
  1079. }
  1080. } else {
  1081. // Case: III.
  1082. if ( moveDiffY <= 0 ) {
  1083. if ( moveRatio >= ratio )
  1084. adjustToY();
  1085. else
  1086. adjustToX();
  1087. }
  1088. // Case: II.
  1089. else {
  1090. adjustToY();
  1091. }
  1092. }
  1093. }
  1094. // Right-aligned widget. It mirrors behaviours, so I becomes II,
  1095. // IV becomes III and vice-versa.
  1096. else {
  1097. if ( moveDiffX <= 0 ) {
  1098. // Case: IV.
  1099. if ( moveDiffY <= 0 ) {
  1100. if ( moveRatio >= ratio )
  1101. adjustToY();
  1102. else
  1103. adjustToX();
  1104. }
  1105. // Case: I.
  1106. else {
  1107. adjustToY();
  1108. }
  1109. } else {
  1110. // Case: III.
  1111. if ( moveDiffY <= 0 )
  1112. adjustToX();
  1113. // Case: II.
  1114. else {
  1115. if ( moveRatio >= ratio ) {
  1116. adjustToX();
  1117. } else {
  1118. adjustToY();
  1119. }
  1120. }
  1121. }
  1122. }
  1123. if ( isAllowedSize( newWidth, newHeight ) ) {
  1124. updateData = { width: newWidth, height: newHeight };
  1125. image.setAttributes( updateData );
  1126. }
  1127. }
  1128. function onMouseUp() {
  1129. var l;
  1130. while ( ( l = listeners.pop() ) )
  1131. l.removeListener();
  1132. // Restore default cursor by removing special class.
  1133. editable.removeClass( cursorClass );
  1134. // This is to bring back the regular behaviour of the resizer.
  1135. resizer.removeClass( 'cke_image_resizing' );
  1136. if ( updateData ) {
  1137. widget.setData( updateData );
  1138. // Save another undo snapshot: after resizing.
  1139. editor.fire( 'saveSnapshot' );
  1140. }
  1141. // Don't update data twice or more.
  1142. updateData = false;
  1143. }
  1144. function getMaxSize() {
  1145. var maxSize = editor.config.image2_maxSize,
  1146. natural;
  1147. if ( !maxSize ) {
  1148. return null;
  1149. }
  1150. maxSize = CKEDITOR.tools.copy( maxSize );
  1151. natural = CKEDITOR.plugins.image2.getNatural( image );
  1152. maxSize.width = Math.max( maxSize.width === 'natural' ? natural.width : maxSize.width, min.width );
  1153. maxSize.height = Math.max( maxSize.height === 'natural' ? natural.height : maxSize.height, min.width );
  1154. return maxSize;
  1155. }
  1156. function isAllowedSize( width, height ) {
  1157. var isTooSmall = width < min.width || height < min.height,
  1158. isTooBig = max && ( width > max.width || height > max.height );
  1159. return !isTooSmall && !isTooBig;
  1160. }
  1161. } );
  1162. // Change the position of the widget resizer when data changes.
  1163. widget.on( 'data', function() {
  1164. resizer[ widget.data.align == 'right' ? 'addClass' : 'removeClass' ]( 'cke_image_resizer_left' );
  1165. } );
  1166. }
  1167. // Integrates widget alignment setting with justify
  1168. // plugin's commands (execution and refreshment).
  1169. // @param {CKEDITOR.editor} editor
  1170. // @param {String} value 'left', 'right', 'center' or 'block'
  1171. function alignCommandIntegrator( editor ) {
  1172. var execCallbacks = [],
  1173. enabled;
  1174. return function( value ) {
  1175. var command = editor.getCommand( 'justify' + value );
  1176. // Most likely, the justify plugin isn't loaded.
  1177. if ( !command )
  1178. return;
  1179. // This command will be manually refreshed along with
  1180. // other commands after exec.
  1181. execCallbacks.push( function() {
  1182. command.refresh( editor, editor.elementPath() );
  1183. } );
  1184. if ( value in { right: 1, left: 1, center: 1 } ) {
  1185. command.on( 'exec', function( evt ) {
  1186. var widget = getFocusedWidget( editor );
  1187. if ( widget ) {
  1188. widget.setData( 'align', value );
  1189. // Once the widget changed its align, all the align commands
  1190. // must be refreshed: the event is to be cancelled.
  1191. for ( var i = execCallbacks.length; i--; )
  1192. execCallbacks[ i ]();
  1193. evt.cancel();
  1194. }
  1195. } );
  1196. }
  1197. command.on( 'refresh', function( evt ) {
  1198. var widget = getFocusedWidget( editor ),
  1199. allowed = { right: 1, left: 1, center: 1 };
  1200. if ( !widget )
  1201. return;
  1202. // Cache "enabled" on first use. This is because filter#checkFeature may
  1203. // not be available during plugin's afterInit in the future — a moment when
  1204. // alignCommandIntegrator is called.
  1205. if ( enabled === undefined )
  1206. enabled = editor.filter.checkFeature( editor.widgets.registered.image.features.align );
  1207. // Don't allow justify commands when widget alignment is disabled (https://dev.ckeditor.com/ticket/11004).
  1208. if ( !enabled )
  1209. this.setState( CKEDITOR.TRISTATE_DISABLED );
  1210. else {
  1211. this.setState(
  1212. ( widget.data.align == value ) ? (
  1213. CKEDITOR.TRISTATE_ON
  1214. ) : (
  1215. ( value in allowed ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
  1216. )
  1217. );
  1218. }
  1219. evt.cancel();
  1220. } );
  1221. };
  1222. }
  1223. function linkCommandIntegrator( editor ) {
  1224. // Nothing to integrate with if link is not loaded.
  1225. if ( !editor.plugins.link )
  1226. return;
  1227. var listener = CKEDITOR.on( 'dialogDefinition', function( evt ) {
  1228. var dialog = evt.data;
  1229. if ( dialog.name == 'link' ) {
  1230. var def = dialog.definition;
  1231. var onShow = def.onShow,
  1232. onOk = def.onOk;
  1233. def.onShow = function() {
  1234. var widget = getFocusedWidget( editor ),
  1235. displayTextField = this.getContentElement( 'info', 'linkDisplayText' ).getElement().getParent().getParent();
  1236. // Widget cannot be enclosed in a link, i.e.
  1237. // <a>foo<inline widget/>bar</a>
  1238. if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) {
  1239. this.setupContent( widget.data.link || {} );
  1240. // Hide the display text in case of linking image2 widget.
  1241. displayTextField.hide();
  1242. } else {
  1243. // Make sure that display text is visible, as it might be hidden by image2 integration
  1244. // before.
  1245. displayTextField.show();
  1246. onShow.apply( this, arguments );
  1247. }
  1248. };
  1249. // Set widget data if linking the widget using
  1250. // link dialog (instead of default action).
  1251. // State shifter handles data change and takes
  1252. // care of internal DOM structure of linked widget.
  1253. def.onOk = function() {
  1254. var widget = getFocusedWidget( editor );
  1255. // Widget cannot be enclosed in a link, i.e.
  1256. // <a>foo<inline widget/>bar</a>
  1257. if ( widget && ( widget.inline ? !widget.wrapper.getAscendant( 'a' ) : 1 ) ) {
  1258. var data = {};
  1259. // Collect data from fields.
  1260. this.commitContent( data );
  1261. // Set collected data to widget.
  1262. widget.setData( 'link', data );
  1263. } else {
  1264. onOk.apply( this, arguments );
  1265. }
  1266. };
  1267. }
  1268. } );
  1269. // Listener has to be removed due to leaking the editor reference (#589).
  1270. editor.on( 'destroy', function() {
  1271. listener.removeListener();
  1272. } );
  1273. // Overwrite the default behavior of unlink command.
  1274. editor.getCommand( 'unlink' ).on( 'exec', function( evt ) {
  1275. var widget = getFocusedWidget( editor );
  1276. // Override unlink only when link truly belongs to the widget.
  1277. // If wrapped inline widget in a link, let default unlink work (https://dev.ckeditor.com/ticket/11814).
  1278. if ( !widget || !widget.parts.link )
  1279. return;
  1280. widget.setData( 'link', null );
  1281. // Selection (which is fake) may not change if unlinked image in focused widget,
  1282. // i.e. if captioned image. Let's refresh command state manually here.
  1283. this.refresh( editor, editor.elementPath() );
  1284. evt.cancel();
  1285. } );
  1286. // Overwrite default refresh of unlink command.
  1287. editor.getCommand( 'unlink' ).on( 'refresh', function( evt ) {
  1288. var widget = getFocusedWidget( editor );
  1289. if ( !widget )
  1290. return;
  1291. // Note that widget may be wrapped in a link, which
  1292. // does not belong to that widget (https://dev.ckeditor.com/ticket/11814).
  1293. this.setState( widget.data.link || widget.wrapper.getAscendant( 'a' ) ?
  1294. CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
  1295. evt.cancel();
  1296. } );
  1297. }
  1298. // Returns the focused widget, if of the type specific for this plugin.
  1299. // If no widget is focused, `null` is returned.
  1300. //
  1301. // @param {CKEDITOR.editor}
  1302. // @returns {CKEDITOR.plugins.widget}
  1303. function getFocusedWidget( editor ) {
  1304. var widget = editor.widgets.focused;
  1305. if ( widget && widget.name == 'image' )
  1306. return widget;
  1307. return null;
  1308. }
  1309. // Returns a set of widget allowedContent rules, depending
  1310. // on configurations like config#image2_alignClasses or
  1311. // config#image2_captionedClass.
  1312. //
  1313. // @param {CKEDITOR.editor}
  1314. // @returns {Object}
  1315. function getWidgetAllowedContent( editor ) {
  1316. var alignClasses = editor.config.image2_alignClasses,
  1317. rules = {
  1318. // Widget may need <div> or <p> centering wrapper.
  1319. div: {
  1320. match: centerWrapperChecker( editor )
  1321. },
  1322. p: {
  1323. match: centerWrapperChecker( editor )
  1324. },
  1325. img: {
  1326. attributes: '!src,alt,width,height'
  1327. },
  1328. figure: {
  1329. classes: '!' + editor.config.image2_captionedClass
  1330. },
  1331. figcaption: true
  1332. };
  1333. if ( alignClasses ) {
  1334. // Centering class from the config.
  1335. rules.div.classes = alignClasses[ 1 ];
  1336. rules.p.classes = rules.div.classes;
  1337. // Left/right classes from the config.
  1338. rules.img.classes = alignClasses[ 0 ] + ',' + alignClasses[ 2 ];
  1339. rules.figure.classes += ',' + rules.img.classes;
  1340. } else {
  1341. // Centering with text-align.
  1342. rules.div.styles = 'text-align';
  1343. rules.p.styles = 'text-align';
  1344. rules.img.styles = 'float';
  1345. rules.figure.styles = 'float,display';
  1346. }
  1347. return rules;
  1348. }
  1349. // Returns a set of widget feature rules, depending
  1350. // on editor configuration. Note that the following may not cover
  1351. // all the possible cases since requiredContent supports a single
  1352. // tag only.
  1353. //
  1354. // @param {CKEDITOR.editor}
  1355. // @returns {Object}
  1356. function getWidgetFeatures( editor ) {
  1357. var alignClasses = editor.config.image2_alignClasses,
  1358. features = {
  1359. dimension: {
  1360. requiredContent: 'img[width,height]'
  1361. },
  1362. align: {
  1363. requiredContent: 'img' +
  1364. ( alignClasses ? '(' + alignClasses[ 0 ] + ')' : '{float}' )
  1365. },
  1366. caption: {
  1367. requiredContent: 'figcaption'
  1368. }
  1369. };
  1370. return features;
  1371. }
  1372. // Returns element which is styled, considering current
  1373. // state of the widget.
  1374. //
  1375. // @see CKEDITOR.plugins.widget#applyStyle
  1376. // @param {CKEDITOR.plugins.widget} widget
  1377. // @returns {CKEDITOR.dom.element}
  1378. function getStyleableElement( widget ) {
  1379. return widget.data.hasCaption ? widget.element : widget.parts.image;
  1380. }
  1381. } )();
  1382. /**
  1383. * A CSS class applied to the `<figure>` element of a captioned image.
  1384. *
  1385. * Read more in the {@glink features/image2 documentation} and see the
  1386. * {@glink examples/image2 example}.
  1387. *
  1388. * // Changes the class to "captionedImage".
  1389. * config.image2_captionedClass = 'captionedImage';
  1390. *
  1391. * @cfg {String} [image2_captionedClass='image']
  1392. * @member CKEDITOR.config
  1393. */
  1394. CKEDITOR.config.image2_captionedClass = 'image';
  1395. /**
  1396. * Determines whether dimension inputs should be automatically filled when the image URL changes in the Enhanced Image
  1397. * plugin dialog window.
  1398. *
  1399. * Read more in the {@glink features/image2 documentation} and see the
  1400. * {@glink examples/image2 example}.
  1401. *
  1402. * config.image2_prefillDimensions = false;
  1403. *
  1404. * @since 4.5.0
  1405. * @cfg {Boolean} [image2_prefillDimensions=true]
  1406. * @member CKEDITOR.config
  1407. */
  1408. /**
  1409. * Disables the image resizer. By default the resizer is enabled.
  1410. *
  1411. * Read more in the {@glink features/image2 documentation} and see the
  1412. * {@glink examples/image2 example}.
  1413. *
  1414. * config.image2_disableResizer = true;
  1415. *
  1416. * @since 4.5.0
  1417. * @cfg {Boolean} [image2_disableResizer=false]
  1418. * @member CKEDITOR.config
  1419. */
  1420. /**
  1421. * CSS classes applied to aligned images. Useful to take control over the way
  1422. * the images are aligned, i.e. to customize output HTML and integrate external stylesheets.
  1423. *
  1424. * Classes should be defined in an array of three elements, containing left, center, and right
  1425. * alignment classes, respectively. For example:
  1426. *
  1427. * config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ];
  1428. *
  1429. * **Note**: Once this configuration option is set, the plugin will no longer produce inline
  1430. * styles for alignment. It means that e.g. the following HTML will be produced:
  1431. *
  1432. * <img alt="My image" class="custom-center-class" src="foo.png" />
  1433. *
  1434. * instead of:
  1435. *
  1436. * <img alt="My image" style="float:left" src="foo.png" />
  1437. *
  1438. * **Note**: Once this configuration option is set, corresponding style definitions
  1439. * must be supplied to the editor:
  1440. *
  1441. * * For {@glink guide/dev_framed classic editor} it can be done by defining additional
  1442. * styles in the {@link CKEDITOR.config#contentsCss stylesheets loaded by the editor}. The same
  1443. * styles must be provided on the target page where the content will be loaded.
  1444. * * For {@glink guide/dev_inline inline editor} the styles can be defined directly
  1445. * with `<style> ... <style>` or `<link href="..." rel="stylesheet">`, i.e. within the `<head>`
  1446. * of the page.
  1447. *
  1448. * For example, considering the following configuration:
  1449. *
  1450. * config.image2_alignClasses = [ 'align-left', 'align-center', 'align-right' ];
  1451. *
  1452. * CSS rules can be defined as follows:
  1453. *
  1454. * .align-left {
  1455. * float: left;
  1456. * }
  1457. *
  1458. * .align-right {
  1459. * float: right;
  1460. * }
  1461. *
  1462. * .align-center {
  1463. * text-align: center;
  1464. * }
  1465. *
  1466. * .align-center > figure {
  1467. * display: inline-block;
  1468. * }
  1469. *
  1470. * Read more in the {@glink features/image2 documentation} and see the
  1471. * {@glink examples/image2 example}.
  1472. *
  1473. * @since 4.4.0
  1474. * @cfg {String[]} [image2_alignClasses=null]
  1475. * @member CKEDITOR.config
  1476. */
  1477. /**
  1478. * Determines whether alternative text is required for the captioned image.
  1479. *
  1480. * config.image2_altRequired = true;
  1481. *
  1482. * Read more in the {@glink features/image2 documentation} and see the
  1483. * {@glink examples/image2 example}.
  1484. *
  1485. * @since 4.6.0
  1486. * @cfg {Boolean} [image2_altRequired=false]
  1487. * @member CKEDITOR.config
  1488. */
  1489. /**
  1490. * Determines the maximum size that an image can be resized to with the resize handle.
  1491. *
  1492. * It stores two properties: `width` and `height`. They can be set with one of the two types:
  1493. *
  1494. * * A number representing a value that limits the maximum size in pixel units:
  1495. *
  1496. * ```js
  1497. * config.image2_maxSize = {
  1498. * height: 300,
  1499. * width: 250
  1500. * };
  1501. * ```
  1502. *
  1503. * * A string representing the natural image size, so each image resize operation is limited to its own natural height or width:
  1504. *
  1505. * ```js
  1506. * config.image2_maxSize = {
  1507. * height: 'natural',
  1508. * width: 'natural'
  1509. * }
  1510. * ```
  1511. *
  1512. * Note: An image can still be resized to bigger dimensions when using the image dialog.
  1513. *
  1514. * @since 4.12.0
  1515. * @cfg {Object.<String, Number/String>} [image2_maxSize]
  1516. * @member CKEDITOR.config
  1517. */
  1518. /**
  1519. * Indicates the default state of the "Lock ratio" switch in the image dialog.
  1520. * If set to `true`, the ratio will be locked. If set to `false`, it will be unlocked.
  1521. * If the value is not set at all, the "Lock ratio" switch will indicate
  1522. * if the image has preserved aspect ratio upon loading.
  1523. *
  1524. * @since 4.20.0
  1525. * @cfg {Boolean} [image2_defaultLockRatio]
  1526. * @member CKEDITOR.config
  1527. */