8
0

utils.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * @license Copyright (c) 2003-2020, 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/ui/utils
  7. */
  8. import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
  9. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  10. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  11. import Model from '@ckeditor/ckeditor5-ui/src/model';
  12. import ColorInputView from './colorinputview';
  13. import { isColor, isLength, isPercentage } from '@ckeditor/ckeditor5-engine/src/view/styles/utils';
  14. import { getTableWidgetAncestor } from '../utils';
  15. import { findAncestor } from '../commands/utils';
  16. const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;
  17. const BALLOON_POSITIONS = [
  18. DEFAULT_BALLOON_POSITIONS.northArrowSouth,
  19. DEFAULT_BALLOON_POSITIONS.northArrowSouthWest,
  20. DEFAULT_BALLOON_POSITIONS.northArrowSouthEast,
  21. DEFAULT_BALLOON_POSITIONS.southArrowNorth,
  22. DEFAULT_BALLOON_POSITIONS.southArrowNorthWest,
  23. DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
  24. ];
  25. const isEmpty = val => val === '';
  26. /**
  27. * A helper utility that positions the
  28. * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
  29. * with respect to the table in the editor content, if one is selected.
  30. *
  31. * @param {module:core/editor/editor~Editor} editor The editor instance.
  32. * @param {String} target Either "cell" or "table". Determines the target the balloon will
  33. * be attached to.
  34. */
  35. export function repositionContextualBalloon( editor, target ) {
  36. const balloon = editor.plugins.get( 'ContextualBalloon' );
  37. if ( getTableWidgetAncestor( editor.editing.view.document.selection ) ) {
  38. let position;
  39. if ( target === 'cell' ) {
  40. position = getBalloonCellPositionData( editor );
  41. } else {
  42. position = getBalloonTablePositionData( editor );
  43. }
  44. balloon.updatePosition( position );
  45. }
  46. }
  47. /**
  48. * Returns the positioning options that control the geometry of the
  49. * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
  50. * to the selected table in the editor content.
  51. *
  52. * @param {module:core/editor/editor~Editor} editor The editor instance.
  53. * @returns {module:utils/dom/position~Options}
  54. */
  55. export function getBalloonTablePositionData( editor ) {
  56. const firstPosition = editor.model.document.selection.getFirstPosition();
  57. const modelTable = findAncestor( 'table', firstPosition );
  58. const viewTable = editor.editing.mapper.toViewElement( modelTable );
  59. return {
  60. target: editor.editing.view.domConverter.viewToDom( viewTable ),
  61. positions: BALLOON_POSITIONS
  62. };
  63. }
  64. /**
  65. * Returns the positioning options that control the geometry of the
  66. * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
  67. * to the selected table cell in the editor content.
  68. *
  69. * @param {module:core/editor/editor~Editor} editor The editor instance.
  70. * @returns {module:utils/dom/position~Options}
  71. */
  72. export function getBalloonCellPositionData( editor ) {
  73. // This is a bit naive. See https://github.com/ckeditor/ckeditor5/issues/6357.
  74. const modelTableCell = getTableCellAtPosition( editor.model.document.selection.getFirstPosition() );
  75. const viewTableCell = editor.editing.mapper.toViewElement( modelTableCell );
  76. return {
  77. target: editor.editing.view.domConverter.viewToDom( viewTableCell ),
  78. positions: BALLOON_POSITIONS
  79. };
  80. }
  81. /**
  82. * Returns an object containing pairs of CSS border style values and their localized UI
  83. * labels. Used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}
  84. * and {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}.
  85. *
  86. * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
  87. * that is used to localize strings.
  88. * @returns {Object.<String,String>}
  89. */
  90. export function getBorderStyleLabels( t ) {
  91. return {
  92. none: t( 'None' ),
  93. solid: t( 'Solid' ),
  94. dotted: t( 'Dotted' ),
  95. dashed: t( 'Dashed' ),
  96. double: t( 'Double' ),
  97. groove: t( 'Groove' ),
  98. ridge: t( 'Ridge' ),
  99. inset: t( 'Inset' ),
  100. outset: t( 'Outset' )
  101. };
  102. }
  103. /**
  104. * Returns a localized error string that can be displayed next to color (background, border)
  105. * fields that have an invalid value.
  106. *
  107. * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
  108. * that is used to localize strings.
  109. * @returns {String}
  110. */
  111. export function getLocalizedColorErrorText( t ) {
  112. return t( 'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".' );
  113. }
  114. /**
  115. * Returns a localized error string that can be displayed next to length (padding, border width)
  116. * fields that have an invalid value.
  117. *
  118. * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
  119. * that is used to localize strings.
  120. * @returns {String}
  121. */
  122. export function getLocalizedLengthErrorText( t ) {
  123. return t( 'The value is invalid. Try "10px" or "2em" or simply "2".' );
  124. }
  125. /**
  126. * Returns `true` when the passed value is an empty string or a valid CSS color expression.
  127. * Otherwise, `false` is returned.
  128. *
  129. * See {@link module:engine/view/styles/utils~isColor}.
  130. *
  131. * @param {String} value
  132. * @returns {Boolean}
  133. */
  134. export function colorFieldValidator( value ) {
  135. value = value.trim();
  136. return isEmpty( value ) || isColor( value );
  137. }
  138. /**
  139. * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
  140. * Otherwise, `false` is returned.
  141. *
  142. * See {@link module:engine/view/styles/utils~isLength}.
  143. * See {@link module:engine/view/styles/utils~isPercentage}.
  144. *
  145. * @param {String} value
  146. * @returns {Boolean}
  147. */
  148. export function lengthFieldValidator( value ) {
  149. value = value.trim();
  150. return isEmpty( value ) || isNumberString( value ) || isLength( value ) || isPercentage( value );
  151. }
  152. /**
  153. * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
  154. * Otherwise, `false` is returned.
  155. *
  156. * See {@link module:engine/view/styles/utils~isLength}.
  157. *
  158. * @param {String} value
  159. * @returns {Boolean}
  160. */
  161. export function lineWidthFieldValidator( value ) {
  162. value = value.trim();
  163. return isEmpty( value ) || isNumberString( value ) || isLength( value );
  164. }
  165. /**
  166. * Generates item definitions for a UI dropdown that allows changing the border style of a table or a table cell.
  167. *
  168. * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|
  169. * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} view
  170. * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
  171. */
  172. export function getBorderStyleDefinitions( view ) {
  173. const itemDefinitions = new Collection();
  174. const styleLabels = getBorderStyleLabels( view.t );
  175. for ( const style in styleLabels ) {
  176. const definition = {
  177. type: 'button',
  178. model: new Model( {
  179. _borderStyleValue: style === 'none' ? '' : style,
  180. label: styleLabels[ style ],
  181. withText: true
  182. } )
  183. };
  184. if ( style === 'none' ) {
  185. definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => !value );
  186. } else {
  187. definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
  188. return value === style;
  189. } );
  190. }
  191. itemDefinitions.add( definition );
  192. }
  193. return itemDefinitions;
  194. }
  195. /**
  196. * A helper that fills a toolbar with buttons that:
  197. *
  198. * * have some labels,
  199. * * have some icons,
  200. * * set a certain UI view property value upon execution.
  201. *
  202. * @param {Object} options
  203. * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|
  204. * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} options.view
  205. * @param {Array.<String>} options.icons
  206. * @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar
  207. * @param {Object.<String,String>} labels
  208. * @param {String} propertyName
  209. * @param {Function} nameToValue A function that maps a button name to a value. By default names are the same as values.
  210. */
  211. export function fillToolbar( { view, icons, toolbar, labels, propertyName, nameToValue } ) {
  212. for ( const name in labels ) {
  213. const button = new ButtonView( view.locale );
  214. button.set( {
  215. label: labels[ name ],
  216. icon: icons[ name ]
  217. } );
  218. button.bind( 'isOn' ).to( view, propertyName, value => {
  219. return value === nameToValue( name );
  220. } );
  221. button.on( 'execute', () => {
  222. view[ propertyName ] = nameToValue( name );
  223. } );
  224. toolbar.items.add( button );
  225. }
  226. }
  227. /**
  228. * A default color palette used by various user interfaces related to tables, for instance,
  229. * by {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI} or
  230. * {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI}.
  231. *
  232. * The color palette follows the {@link module:table/table~TableColorConfig table color configuration format}
  233. * and contains the following color definitions:
  234. *
  235. * const defaultColors = [
  236. * {
  237. * color: 'hsl(0, 0%, 0%)',
  238. * label: 'Black'
  239. * },
  240. * {
  241. * color: 'hsl(0, 0%, 30%)',
  242. * label: 'Dim grey'
  243. * },
  244. * {
  245. * color: 'hsl(0, 0%, 60%)',
  246. * label: 'Grey'
  247. * },
  248. * {
  249. * color: 'hsl(0, 0%, 90%)',
  250. * label: 'Light grey'
  251. * },
  252. * {
  253. * color: 'hsl(0, 0%, 100%)',
  254. * label: 'White',
  255. * hasBorder: true
  256. * },
  257. * {
  258. * color: 'hsl(0, 75%, 60%)',
  259. * label: 'Red'
  260. * },
  261. * {
  262. * color: 'hsl(30, 75%, 60%)',
  263. * label: 'Orange'
  264. * },
  265. * {
  266. * color: 'hsl(60, 75%, 60%)',
  267. * label: 'Yellow'
  268. * },
  269. * {
  270. * color: 'hsl(90, 75%, 60%)',
  271. * label: 'Light green'
  272. * },
  273. * {
  274. * color: 'hsl(120, 75%, 60%)',
  275. * label: 'Green'
  276. * },
  277. * {
  278. * color: 'hsl(150, 75%, 60%)',
  279. * label: 'Aquamarine'
  280. * },
  281. * {
  282. * color: 'hsl(180, 75%, 60%)',
  283. * label: 'Turquoise'
  284. * },
  285. * {
  286. * color: 'hsl(210, 75%, 60%)',
  287. * label: 'Light blue'
  288. * },
  289. * {
  290. * color: 'hsl(240, 75%, 60%)',
  291. * label: 'Blue'
  292. * },
  293. * {
  294. * color: 'hsl(270, 75%, 60%)',
  295. * label: 'Purple'
  296. * }
  297. * ];
  298. */
  299. export const defaultColors = [
  300. {
  301. color: 'hsl(0, 0%, 0%)',
  302. label: 'Black'
  303. },
  304. {
  305. color: 'hsl(0, 0%, 30%)',
  306. label: 'Dim grey'
  307. },
  308. {
  309. color: 'hsl(0, 0%, 60%)',
  310. label: 'Grey'
  311. },
  312. {
  313. color: 'hsl(0, 0%, 90%)',
  314. label: 'Light grey'
  315. },
  316. {
  317. color: 'hsl(0, 0%, 100%)',
  318. label: 'White',
  319. hasBorder: true
  320. },
  321. {
  322. color: 'hsl(0, 75%, 60%)',
  323. label: 'Red'
  324. },
  325. {
  326. color: 'hsl(30, 75%, 60%)',
  327. label: 'Orange'
  328. },
  329. {
  330. color: 'hsl(60, 75%, 60%)',
  331. label: 'Yellow'
  332. },
  333. {
  334. color: 'hsl(90, 75%, 60%)',
  335. label: 'Light green'
  336. },
  337. {
  338. color: 'hsl(120, 75%, 60%)',
  339. label: 'Green'
  340. },
  341. {
  342. color: 'hsl(150, 75%, 60%)',
  343. label: 'Aquamarine'
  344. },
  345. {
  346. color: 'hsl(180, 75%, 60%)',
  347. label: 'Turquoise'
  348. },
  349. {
  350. color: 'hsl(210, 75%, 60%)',
  351. label: 'Light blue'
  352. },
  353. {
  354. color: 'hsl(240, 75%, 60%)',
  355. label: 'Blue'
  356. },
  357. {
  358. color: 'hsl(270, 75%, 60%)',
  359. label: 'Purple'
  360. }
  361. ];
  362. /**
  363. * Returns a creator for a color input with a label.
  364. *
  365. * For given options, it returns a function that creates an instance of a
  366. * {@link module:table/ui/colorinputview~ColorInputView color input} logically related to
  367. * a {@link module:ui/labeledview/labeledview~LabeledView labeled view} in the DOM.
  368. *
  369. * The helper does the following:
  370. *
  371. * * It sets the color input `id` and `ariaDescribedById` attributes.
  372. * * It binds the color input `isReadOnly` to the labeled view.
  373. * * It binds the color input `hasError` to the labeled view.
  374. * * It enables a logic that cleans up the error when the user starts typing in the color input.
  375. *
  376. * Usage:
  377. *
  378. * const colorInputCreator = getLabeledColorInputCreator( {
  379. * colorConfig: [ ... ],
  380. * columns: 3,
  381. * } );
  382. *
  383. * const labeledInputView = new LabeledView( locale, colorInputCreator );
  384. * console.log( labeledInputView.view ); // A color input instance.
  385. *
  386. * @private
  387. * @param options Color input options.
  388. * @param {module:table/table~TableColorConfig} options.colorConfig The configuration of the color palette
  389. * displayed in the input's dropdown.
  390. * @param {Number} options.columns The configuration of the number of columns the color palette consists of
  391. * in the input's dropdown.
  392. * @returns {Function}
  393. */
  394. export function getLabeledColorInputCreator( options ) {
  395. return ( labeledView, viewUid, statusUid ) => {
  396. const inputView = new ColorInputView( labeledView.locale, {
  397. colorDefinitions: colorConfigToColorGridDefinitions( options.colorConfig ),
  398. columns: options.columns
  399. } );
  400. inputView.set( {
  401. id: viewUid,
  402. ariaDescribedById: statusUid
  403. } );
  404. inputView.bind( 'isReadOnly' ).to( labeledView, 'isEnabled', value => !value );
  405. inputView.bind( 'errorText' ).to( labeledView );
  406. inputView.on( 'input', () => {
  407. // UX: Make the error text disappear and disable the error indicator as the user
  408. // starts fixing the errors.
  409. labeledView.errorText = null;
  410. } );
  411. return inputView;
  412. };
  413. }
  414. // A simple helper method to detect number strings.
  415. // I allows full number notation, so omitting 0 is not allowed:
  416. function isNumberString( value ) {
  417. const parsedValue = parseFloat( value );
  418. return !Number.isNaN( parsedValue ) && value === String( parsedValue );
  419. }
  420. // @param {Array.<Object>} colorConfig
  421. // @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}
  422. function colorConfigToColorGridDefinitions( colorConfig ) {
  423. return colorConfig.map( item => ( {
  424. color: item.model,
  425. label: item.label,
  426. options: {
  427. hasBorder: item.hasBorder
  428. }
  429. } ) );
  430. }
  431. // Returns the first selected table cell from a multi-cell or in-cell selection.
  432. //
  433. // @param {module:engine/model/position~Position} position Document position.
  434. // @returns {module:engine/model/element~Element}
  435. function getTableCellAtPosition( position ) {
  436. const isTableCellSelected = position.nodeAfter && position.nodeAfter.is( 'tableCell' );
  437. return isTableCellSelected ? position.nodeAfter : findAncestor( 'tableCell', position );
  438. }