div.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*
  2. * Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. ( function() {
  6. // Add to collection with DUP examination.
  7. // @param {Object} collection
  8. // @param {Object} element
  9. // @param {Object} database
  10. function addSafely( collection, element, database ) {
  11. // 1. IE doesn't support customData on text nodes;
  12. // 2. Text nodes never get chance to appear twice;
  13. if ( !element.is || !element.getCustomData( 'block_processed' ) ) {
  14. element.is && CKEDITOR.dom.element.setMarker( database, element, 'block_processed', true );
  15. collection.push( element );
  16. }
  17. }
  18. // Dialog reused by both 'creatediv' and 'editdiv' commands.
  19. // @param {Object} editor
  20. // @param {String} command The command name which indicate what the current command is.
  21. function divDialog( editor, command ) {
  22. // Definition of elements at which div operation should stopped.
  23. var divLimitDefinition = ( function() {
  24. // Customzie from specialize blockLimit elements
  25. var definition = CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$blockLimit );
  26. if ( editor.config.div_wrapTable ) {
  27. delete definition.td;
  28. delete definition.th;
  29. }
  30. return definition;
  31. } )();
  32. // DTD of 'div' element
  33. var dtd = CKEDITOR.dtd.div;
  34. // Get the first div limit element on the element's path.
  35. // @param {Object} element
  36. function getDivContainer( element ) {
  37. var container = editor.elementPath( element ).blockLimit;
  38. // Never consider read-only (i.e. contenteditable=false) element as
  39. // a first div limit (#11083).
  40. if ( container.isReadOnly() )
  41. container = container.getParent();
  42. // Dont stop at 'td' and 'th' when div should wrap entire table.
  43. if ( editor.config.div_wrapTable && container.is( [ 'td', 'th' ] ) ) {
  44. var parentPath = editor.elementPath( container.getParent() );
  45. container = parentPath.blockLimit;
  46. }
  47. return container;
  48. }
  49. // Init all fields' setup/commit function.
  50. // @memberof divDialog
  51. function setupFields() {
  52. this.foreach( function( field ) {
  53. // Exclude layout container elements
  54. if ( /^(?!vbox|hbox)/.test( field.type ) ) {
  55. if ( !field.setup ) {
  56. // Read the dialog fields values from the specified
  57. // element attributes.
  58. field.setup = function( element ) {
  59. field.setValue( element.getAttribute( field.id ) || '', 1 );
  60. };
  61. }
  62. if ( !field.commit ) {
  63. // Set element attributes assigned by the dialog
  64. // fields.
  65. field.commit = function( element ) {
  66. var fieldValue = this.getValue();
  67. // ignore default element attribute values
  68. if ( field.id == 'dir' && element.getComputedStyle( 'direction' ) == fieldValue ) {
  69. return;
  70. }
  71. if ( fieldValue )
  72. element.setAttribute( field.id, fieldValue );
  73. else
  74. element.removeAttribute( field.id );
  75. };
  76. }
  77. }
  78. } );
  79. }
  80. // Wrapping 'div' element around appropriate blocks among the selected ranges.
  81. // @param {Object} editor
  82. function createDiv( editor ) {
  83. // new adding containers OR detected pre-existed containers.
  84. var containers = [];
  85. // node markers store.
  86. var database = {};
  87. // All block level elements which contained by the ranges.
  88. var containedBlocks = [],
  89. block;
  90. // Get all ranges from the selection.
  91. var selection = editor.getSelection(),
  92. ranges = selection.getRanges();
  93. var bookmarks = selection.createBookmarks();
  94. var i, iterator;
  95. // collect all included elements from dom-iterator
  96. for ( i = 0; i < ranges.length; i++ ) {
  97. iterator = ranges[ i ].createIterator();
  98. while ( ( block = iterator.getNextParagraph() ) ) {
  99. // include contents of blockLimit elements.
  100. if ( block.getName() in divLimitDefinition && !block.isReadOnly() ) {
  101. var j,
  102. childNodes = block.getChildren();
  103. for ( j = 0; j < childNodes.count(); j++ )
  104. addSafely( containedBlocks, childNodes.getItem( j ), database );
  105. } else {
  106. while ( !dtd[ block.getName() ] && !block.equals( ranges[ i ].root ) )
  107. block = block.getParent();
  108. addSafely( containedBlocks, block, database );
  109. }
  110. }
  111. }
  112. CKEDITOR.dom.element.clearAllMarkers( database );
  113. var blockGroups = groupByDivLimit( containedBlocks );
  114. var ancestor, divElement;
  115. for ( i = 0; i < blockGroups.length; i++ ) {
  116. var currentNode = blockGroups[ i ][ 0 ];
  117. // Calculate the common parent node of all contained elements.
  118. ancestor = currentNode.getParent();
  119. for ( j = 1; j < blockGroups[ i ].length; j++ )
  120. ancestor = ancestor.getCommonAncestor( blockGroups[ i ][ j ] );
  121. divElement = new CKEDITOR.dom.element( 'div', editor.document );
  122. // Normalize the blocks in each group to a common parent.
  123. for ( j = 0; j < blockGroups[ i ].length; j++ ) {
  124. currentNode = blockGroups[ i ][ j ];
  125. while ( !currentNode.getParent().equals( ancestor ) )
  126. currentNode = currentNode.getParent();
  127. // This could introduce some duplicated elements in array.
  128. blockGroups[ i ][ j ] = currentNode;
  129. }
  130. // Wrapped blocks counting
  131. for ( j = 0; j < blockGroups[ i ].length; j++ ) {
  132. currentNode = blockGroups[ i ][ j ];
  133. // Avoid DUP elements introduced by grouping.
  134. if ( !( currentNode.getCustomData && currentNode.getCustomData( 'block_processed' ) ) ) {
  135. currentNode.is && CKEDITOR.dom.element.setMarker( database, currentNode, 'block_processed', true );
  136. // Establish new container, wrapping all elements in this group.
  137. if ( !j )
  138. divElement.insertBefore( currentNode );
  139. divElement.append( currentNode );
  140. }
  141. }
  142. CKEDITOR.dom.element.clearAllMarkers( database );
  143. containers.push( divElement );
  144. }
  145. selection.selectBookmarks( bookmarks );
  146. return containers;
  147. }
  148. // Divide a set of nodes to different groups by their path's blocklimit element.
  149. // Note: the specified nodes should be in source order naturally, which mean they are supposed to producea by following class:
  150. // * CKEDITOR.dom.range.Iterator
  151. // * CKEDITOR.dom.domWalker
  152. // @returns {Array[]} the grouped nodes
  153. function groupByDivLimit( nodes ) {
  154. var groups = [],
  155. lastDivLimit = null,
  156. block;
  157. for ( var i = 0; i < nodes.length; i++ ) {
  158. block = nodes[ i ];
  159. var limit = getDivContainer( block );
  160. if ( !limit.equals( lastDivLimit ) ) {
  161. lastDivLimit = limit;
  162. groups.push( [] );
  163. }
  164. groups[ groups.length - 1 ].push( block );
  165. }
  166. return groups;
  167. }
  168. // Synchronous field values to other impacted fields is required, e.g. div styles
  169. // change should also alter inline-style text.
  170. function commitInternally( targetFields ) {
  171. var dialog = this.getDialog(),
  172. element = dialog._element && dialog._element.clone() || new CKEDITOR.dom.element( 'div', editor.document );
  173. // Commit this field and broadcast to target fields.
  174. this.commit( element, true );
  175. targetFields = [].concat( targetFields );
  176. var length = targetFields.length,
  177. field;
  178. for ( var i = 0; i < length; i++ ) {
  179. field = dialog.getContentElement.apply( dialog, targetFields[ i ].split( ':' ) );
  180. field && field.setup && field.setup( element, true );
  181. }
  182. }
  183. // Registered 'CKEDITOR.style' instances.
  184. var styles = {};
  185. // Hold a collection of created block container elements.
  186. var containers = [];
  187. // @type divDialog
  188. return {
  189. title: editor.lang.div.title,
  190. minWidth: 400,
  191. minHeight: 165,
  192. contents: [ {
  193. id: 'info',
  194. label: editor.lang.common.generalTab,
  195. title: editor.lang.common.generalTab,
  196. elements: [ {
  197. type: 'hbox',
  198. widths: [ '50%', '50%' ],
  199. children: [ {
  200. id: 'elementStyle',
  201. type: 'select',
  202. style: 'width: 100%;',
  203. label: editor.lang.div.styleSelectLabel,
  204. 'default': '',
  205. // Options are loaded dynamically.
  206. items: [
  207. [ editor.lang.common.notSet, '' ]
  208. ],
  209. onChange: function() {
  210. commitInternally.call( this, [ 'info:elementStyle', 'info:class', 'advanced:dir', 'advanced:style' ] );
  211. },
  212. setup: function( element ) {
  213. for ( var name in styles )
  214. styles[ name ].checkElementRemovable( element, true, editor ) && this.setValue( name, 1 );
  215. },
  216. commit: function( element ) {
  217. var styleName;
  218. if ( ( styleName = this.getValue() ) ) {
  219. var style = styles[ styleName ];
  220. style.applyToObject( element, editor );
  221. }
  222. else {
  223. element.removeAttribute( 'style' );
  224. }
  225. }
  226. },
  227. {
  228. id: 'class',
  229. type: 'text',
  230. requiredContent: 'div(cke-xyz)', // Random text like 'xyz' will check if all are allowed.
  231. label: editor.lang.common.cssClass,
  232. 'default': ''
  233. } ]
  234. } ]
  235. },
  236. {
  237. id: 'advanced',
  238. label: editor.lang.common.advancedTab,
  239. title: editor.lang.common.advancedTab,
  240. elements: [ {
  241. type: 'vbox',
  242. padding: 1,
  243. children: [ {
  244. type: 'hbox',
  245. widths: [ '50%', '50%' ],
  246. children: [ {
  247. type: 'text',
  248. id: 'id',
  249. requiredContent: 'div[id]',
  250. label: editor.lang.common.id,
  251. 'default': ''
  252. },
  253. {
  254. type: 'text',
  255. id: 'lang',
  256. requiredContent: 'div[lang]',
  257. label: editor.lang.common.langCode,
  258. 'default': ''
  259. } ]
  260. },
  261. {
  262. type: 'hbox',
  263. children: [ {
  264. type: 'text',
  265. id: 'style',
  266. requiredContent: 'div{cke-xyz}', // Random text like 'xyz' will check if all are allowed.
  267. style: 'width: 100%;',
  268. label: editor.lang.common.cssStyle,
  269. 'default': '',
  270. commit: function( element ) {
  271. element.setAttribute( 'style', this.getValue() );
  272. }
  273. } ]
  274. },
  275. {
  276. type: 'hbox',
  277. children: [ {
  278. type: 'text',
  279. id: 'title',
  280. requiredContent: 'div[title]',
  281. style: 'width: 100%;',
  282. label: editor.lang.common.advisoryTitle,
  283. 'default': ''
  284. } ]
  285. },
  286. {
  287. type: 'select',
  288. id: 'dir',
  289. requiredContent: 'div[dir]',
  290. style: 'width: 100%;',
  291. label: editor.lang.common.langDir,
  292. 'default': '',
  293. items: [
  294. [ editor.lang.common.notSet, '' ],
  295. [ editor.lang.common.langDirLtr, 'ltr' ],
  296. [ editor.lang.common.langDirRtl, 'rtl' ]
  297. ]
  298. } ] }
  299. ]
  300. } ],
  301. onLoad: function() {
  302. setupFields.call( this );
  303. // Preparing for the 'elementStyle' field.
  304. var dialog = this,
  305. stylesField = this.getContentElement( 'info', 'elementStyle' );
  306. // Reuse the 'stylescombo' plugin's styles definition.
  307. editor.getStylesSet( function( stylesDefinitions ) {
  308. var styleName, style;
  309. if ( stylesDefinitions ) {
  310. // Digg only those styles that apply to 'div'.
  311. for ( var i = 0; i < stylesDefinitions.length; i++ ) {
  312. var styleDefinition = stylesDefinitions[ i ];
  313. if ( styleDefinition.element && styleDefinition.element == 'div' ) {
  314. styleName = styleDefinition.name;
  315. styles[ styleName ] = style = new CKEDITOR.style( styleDefinition );
  316. if ( editor.filter.check( style ) ) {
  317. // Populate the styles field options with style name.
  318. stylesField.items.push( [ styleName, styleName ] );
  319. stylesField.add( styleName, styleName );
  320. }
  321. }
  322. }
  323. }
  324. // We should disable the content element
  325. // it if no options are available at all.
  326. stylesField[ stylesField.items.length > 1 ? 'enable' : 'disable' ]();
  327. // Now setup the field value manually if dialog was opened on element. (#9689)
  328. setTimeout( function() {
  329. dialog._element && stylesField.setup( dialog._element );
  330. }, 0 );
  331. } );
  332. },
  333. onShow: function() {
  334. // Whether always create new container regardless of existed
  335. // ones.
  336. if ( command == 'editdiv' ) {
  337. // Try to discover the containers that already existed in
  338. // ranges
  339. // update dialog field values
  340. this.setupContent( this._element = CKEDITOR.plugins.div.getSurroundDiv( editor ) );
  341. }
  342. },
  343. onOk: function() {
  344. if ( command == 'editdiv' )
  345. containers = [ this._element ];
  346. else
  347. containers = createDiv( editor, true );
  348. // Update elements attributes
  349. var size = containers.length;
  350. for ( var i = 0; i < size; i++ ) {
  351. this.commitContent( containers[ i ] );
  352. // Remove empty 'style' attribute.
  353. !containers[ i ].getAttribute( 'style' ) && containers[ i ].removeAttribute( 'style' );
  354. }
  355. this.hide();
  356. },
  357. onHide: function() {
  358. // Remove style only when editing existing DIV. (#6315)
  359. if ( command == 'editdiv' )
  360. this._element.removeCustomData( 'elementStyle' );
  361. delete this._element;
  362. }
  363. };
  364. }
  365. CKEDITOR.dialog.add( 'creatediv', function( editor ) {
  366. return divDialog( editor, 'creatediv' );
  367. } );
  368. CKEDITOR.dialog.add( 'editdiv', function( editor ) {
  369. return divDialog( editor, 'editdiv' );
  370. } );
  371. } )();
  372. /**
  373. * Whether to wrap the whole table instead of individual cells when created `<div>` in table cell.
  374. *
  375. * config.div_wrapTable = true;
  376. *
  377. * @cfg {Boolean} [div_wrapTable=false]
  378. * @member CKEDITOR.config
  379. */