tablestyle.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module table/tablestyle
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  10. import { findAncestor } from './commands/utils';
  11. function downcastToStyle( conversion, modelAttribute, viewStyleName ) {
  12. conversion.for( 'downcast' ).attributeToAttribute( {
  13. model: modelAttribute,
  14. view: modelAttributeValue => ( {
  15. key: 'style',
  16. value: {
  17. [ viewStyleName ]: modelAttributeValue
  18. }
  19. } )
  20. } );
  21. }
  22. /**
  23. * The table editing feature.
  24. *
  25. * @extends module:core/plugin~Plugin
  26. */
  27. export default class TableStyle extends Plugin {
  28. /**
  29. * @inheritDoc
  30. */
  31. init() {
  32. const editor = this.editor;
  33. const model = editor.model;
  34. const schema = model.schema;
  35. const conversion = editor.conversion;
  36. schema.extend( 'table', {
  37. allowAttributes: [ 'borderWidth', 'borderColor', 'borderStyle', 'background-color', 'width', 'height' ]
  38. } );
  39. schema.extend( 'tableRow', {
  40. allowAttributes: [ 'height' ]
  41. } );
  42. schema.extend( 'tableCell', {
  43. allowAttributes: [
  44. 'borderWidth', 'borderColor', 'borderStyle',
  45. 'background-color', 'padding', 'vertical-align', 'width', 'height' ]
  46. } );
  47. // Table attributes.
  48. setupTableConversion( conversion, 'background-color' );
  49. setupTableConversion( conversion, 'width' );
  50. setupTableConversion( conversion, 'height' );
  51. // Table row attributes.
  52. setupConversion( conversion, 'height', 'tableRow' );
  53. upcastBorderStyles( conversion, 'td' );
  54. upcastBorderStyles( conversion, 'th' );
  55. upcastBorderStyles( conversion, 'table' );
  56. downcastToStyle( conversion, 'borderStyle', 'border-style' );
  57. downcastToStyle( conversion, 'borderColor', 'border-color' );
  58. downcastToStyle( conversion, 'borderWidth', 'border-width' );
  59. setupConversion( conversion, 'background-color', 'tableCell' );
  60. setupConversion( conversion, 'padding', 'tableCell' );
  61. setupConversion( conversion, 'vertical-align', 'tableCell' );
  62. setupConversion( conversion, 'width', 'tableCell' );
  63. editor.ui.componentFactory.add( 'borderWidth', locale => {
  64. const button = new ButtonView( locale );
  65. button.set( {
  66. label: 'Border width',
  67. icon: false,
  68. tooltip: true,
  69. withText: true
  70. } );
  71. button.on( 'execute', () => {
  72. const model = editor.model;
  73. const document = model.document;
  74. const selection = document.selection;
  75. const firstPosition = selection.getFirstPosition();
  76. const tableCell = findAncestor( 'tableCell', firstPosition );
  77. const border = tableCell.getAttribute( 'border' );
  78. let currentWidth;
  79. if ( border ) {
  80. const borderWidth = ( border && border.width ) || {};
  81. // Unify width to one value. If different values are set default to top (or right, etc).
  82. currentWidth = borderWidth.top || borderWidth.right || borderWidth.bottom || borderWidth.left;
  83. }
  84. // eslint-disable-next-line no-undef,no-alert
  85. const newWidth = prompt( 'Set border width:', currentWidth || '' );
  86. const parts = /^([0-9]*[.]?[0-9]*)([a-z]{2,4})?/.exec( newWidth );
  87. const unit = parts[ 2 ];
  88. const borderWidthToSet = parts[ 1 ] + ( unit || 'px' );
  89. // TODO: Command, setting new value is dumb on `border` object.
  90. editor.model.change( writer => {
  91. writer.setAttribute( 'borderWidth', {
  92. top: borderWidthToSet,
  93. right: borderWidthToSet,
  94. bottom: borderWidthToSet,
  95. left: borderWidthToSet
  96. }, tableCell );
  97. } );
  98. } );
  99. return button;
  100. } );
  101. editor.ui.componentFactory.add( 'borderColor', locale => {
  102. const button = new ButtonView( locale );
  103. button.set( {
  104. label: 'Border color',
  105. icon: false,
  106. tooltip: true,
  107. withText: true
  108. } );
  109. button.on( 'execute', () => {
  110. const model = editor.model;
  111. const document = model.document;
  112. const selection = document.selection;
  113. const firstPosition = selection.getFirstPosition();
  114. const tableCell = findAncestor( 'tableCell', firstPosition );
  115. const border = tableCell.getAttribute( 'border' );
  116. let currentColor;
  117. if ( border ) {
  118. const borderColor = ( border && border.color ) || {};
  119. // Unify width to one value. If different values are set default to top (or right, etc).
  120. currentColor = borderColor.top || borderColor.right || borderColor.bottom || borderColor.left;
  121. }
  122. // eslint-disable-next-line no-undef,no-alert
  123. const newColor = prompt( 'Set new border color:', currentColor || '' );
  124. // TODO: Command, setting new value is dumb on `border` object.
  125. editor.model.change( writer => {
  126. writer.setAttribute( 'borderColor', {
  127. top: newColor,
  128. right: newColor,
  129. bottom: newColor,
  130. left: newColor
  131. }, tableCell );
  132. } );
  133. } );
  134. return button;
  135. } );
  136. editor.ui.componentFactory.add( 'borderStyle', locale => {
  137. const button = new ButtonView( locale );
  138. button.set( {
  139. label: 'Border style',
  140. icon: false,
  141. tooltip: true,
  142. withText: true
  143. } );
  144. button.on( 'execute', () => {
  145. const model = editor.model;
  146. const document = model.document;
  147. const selection = document.selection;
  148. const firstPosition = selection.getFirstPosition();
  149. const tableCell = findAncestor( 'tableCell', firstPosition );
  150. const border = tableCell.getAttribute( 'border' );
  151. let currentStyle;
  152. if ( border ) {
  153. const borderStyle = ( border && border.style ) || {};
  154. // Unify width to one value. If different values are set default to top (or right, etc).
  155. currentStyle = borderStyle.top || borderStyle.right || borderStyle.bottom || borderStyle.left;
  156. }
  157. // eslint-disable-next-line no-undef,no-alert
  158. const newStyle = prompt( 'Set new border style:', currentStyle || '' );
  159. // TODO: Command, setting new value is dumb on `border` object.
  160. editor.model.change( writer => {
  161. writer.setAttribute( 'borderStyle', {
  162. top: newStyle,
  163. right: newStyle,
  164. bottom: newStyle,
  165. left: newStyle
  166. }, tableCell );
  167. } );
  168. } );
  169. return button;
  170. } );
  171. editor.ui.componentFactory.add( 'backgroundColor', locale => {
  172. const button = new ButtonView( locale );
  173. button.set( {
  174. label: 'Background color',
  175. icon: false,
  176. tooltip: true,
  177. withText: true
  178. } );
  179. button.on( 'execute', () => {
  180. const model = editor.model;
  181. const document = model.document;
  182. const selection = document.selection;
  183. const firstPosition = selection.getFirstPosition();
  184. const tableCell = findAncestor( 'tableCell', firstPosition );
  185. const backgroundColor = tableCell.getAttribute( 'background-color' );
  186. // eslint-disable-next-line no-undef,no-alert
  187. const newColor = prompt( 'Set new background color:', backgroundColor || '' );
  188. editor.model.change( writer => {
  189. writer.setAttribute( 'background-color', newColor, tableCell );
  190. } );
  191. } );
  192. return button;
  193. } );
  194. editor.ui.componentFactory.add( 'padding', locale => {
  195. const button = new ButtonView( locale );
  196. button.set( {
  197. label: 'Cell padding',
  198. icon: false,
  199. tooltip: true,
  200. withText: true
  201. } );
  202. button.on( 'execute', () => {
  203. const model = editor.model;
  204. const document = model.document;
  205. const selection = document.selection;
  206. const firstPosition = selection.getFirstPosition();
  207. const tableCell = findAncestor( 'tableCell', firstPosition );
  208. const padding = tableCell.getAttribute( 'padding' );
  209. let currentPadding;
  210. if ( padding ) {
  211. // Unify width to one value. If different values are set default to top (or right, etc).
  212. currentPadding = padding.top || padding.right || padding.bottom || padding.left;
  213. }
  214. // eslint-disable-next-line no-undef,no-alert
  215. const newPadding = prompt( 'Set new padding:', currentPadding || '' );
  216. editor.model.change( writer => {
  217. writer.setAttribute( 'padding', newPadding, tableCell );
  218. } );
  219. } );
  220. return button;
  221. } );
  222. }
  223. }
  224. function setupConversion( conversion, styleName, modelName ) {
  225. // General upcast 'border' attribute (requires model border attribute to be allowed).
  226. upcastAttribute( conversion, styleName, modelName );
  227. // Downcast table cell only (table has own downcast converter).
  228. conversion.for( 'downcast' ).attributeToAttribute( {
  229. model: {
  230. name: modelName,
  231. key: styleName
  232. },
  233. view: modelAttributeValue => ( {
  234. key: 'style',
  235. value: {
  236. [ styleName ]: modelAttributeValue
  237. }
  238. } )
  239. } );
  240. }
  241. function upcastAttribute( conversion, styleName, modelName ) {
  242. conversion.for( 'upcast' ).attributeToAttribute( {
  243. view: {
  244. styles: {
  245. [ styleName ]: /[\s\S]+/
  246. }
  247. },
  248. model: {
  249. name: modelName,
  250. key: styleName,
  251. value: viewElement => viewElement.getNormalizedStyle( styleName )
  252. }
  253. } );
  254. }
  255. function upcastBorderStyles( conversion, viewElement ) {
  256. conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:' + viewElement, ( evt, data, conversionApi ) => {
  257. let matcherPattern;
  258. if ( data.viewItem.hasStyle( 'border' ) ) {
  259. matcherPattern = {
  260. styles: [ 'border' ]
  261. };
  262. } else {
  263. const stylesToConsume = [
  264. 'border-top',
  265. 'border-right',
  266. 'border-bottom',
  267. 'border-left'
  268. ].filter( styleName => data.viewItem.hasStyle( styleName ) );
  269. if ( stylesToConsume.length ) {
  270. matcherPattern = {
  271. styles: stylesToConsume
  272. };
  273. } else {
  274. return;
  275. }
  276. }
  277. // Try to consume appropriate values from consumable values list.
  278. const toMatch = matcherPattern;
  279. if ( !conversionApi.consumable.test( data.viewItem, toMatch ) ) {
  280. return;
  281. }
  282. const modelElement = [ ...data.modelRange.getItems( { shallow: true } ) ].pop();
  283. conversionApi.consumable.consume( data.viewItem, toMatch );
  284. if ( conversionApi.schema.checkAttribute( modelElement, 'borderStyle' ) ) {
  285. conversionApi.writer.setAttribute( 'borderStyle', data.viewItem.getNormalizedStyle( 'border-style' ), modelElement );
  286. }
  287. if ( conversionApi.schema.checkAttribute( modelElement, 'borderColor' ) ) {
  288. conversionApi.writer.setAttribute( 'borderColor', data.viewItem.getNormalizedStyle( 'border-color' ), modelElement );
  289. }
  290. if ( conversionApi.schema.checkAttribute( modelElement, 'borderWidth' ) ) {
  291. conversionApi.writer.setAttribute( 'borderWidth', data.viewItem.getNormalizedStyle( 'border-width' ), modelElement );
  292. }
  293. } ) );
  294. }
  295. function setupTableConversion( conversion, styleName ) {
  296. upcastAttribute( conversion, styleName, 'table' );
  297. // Properly downcast table border attribute on <table> and not on <figure>.
  298. conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( `attribute:${ styleName }:table`, ( evt, data, conversionApi ) => {
  299. const { item, attributeNewValue } = data;
  300. const { mapper, writer } = conversionApi;
  301. const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'table' ) );
  302. writer.setStyle( styleName, attributeNewValue, table );
  303. } ) );
  304. }