Dashboard sipadu mbip
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

plugin.js 60KB

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