8
0
Quellcode durchsuchen

Implemented validation of table cell properties form fields.

Aleksander Nowodzinski vor 5 Jahren
Ursprung
Commit
e73ab85bac

+ 79 - 6
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesui.js

@@ -14,11 +14,20 @@ import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsid
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import TableCellPropertiesView from './ui/tablecellpropertiesview';
 import tableCellProperties from './../../theme/icons/table-cell-properties.svg';
-import { repositionContextualBalloon, getBalloonCellPositionData } from '../ui/utils';
+import {
+	repositionContextualBalloon,
+	getBalloonCellPositionData,
+	getLocalizedColorErrorText,
+	getLocalizedLengthErrorText,
+	colorFieldValidator,
+	lengthFieldValidator
+} from '../ui/utils';
+import { debounce } from 'lodash-es';
 
 const DEFAULT_BORDER_STYLE = 'none';
 const DEFAULT_HORIZONTAL_ALIGNMENT = 'left';
 const DEFAULT_VERTICAL_ALIGNMENT = 'middle';
+const ERROR_TEXT_TIMEOUT = 500;
 
 /**
  * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
@@ -112,6 +121,7 @@ export default class TableCellPropertiesUI extends Plugin {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
 		const view = new TableCellPropertiesView( editor.locale );
+		const t = editor.t;
 
 		// Render the view so its #element is available for the clickOutsideHandler.
 		view.render();
@@ -148,15 +158,44 @@ export default class TableCellPropertiesUI extends Plugin {
 			callback: () => this._hideView()
 		} );
 
+		const colorErrorText = getLocalizedColorErrorText( t );
+		const lengthErrorText = getLocalizedLengthErrorText( t );
+
 		// Create the "UI -> editor data" binding.
 		// These listeners update the editor data (via table commands) when any observable
-		// property of the view has changed. This makes the view live, which means the changes are
+		// property of the view has changed. They also validate the value and display errors in the UI
+		// when necessary. This makes the view live, which means the changes are
 		// visible in the editing as soon as the user types or changes fields' values.
 		view.on( 'change:borderStyle', this._getPropertyChangeCallback( 'tableCellBorderStyle' ) );
-		view.on( 'change:borderColor', this._getPropertyChangeCallback( 'tableCellBorderColor' ) );
-		view.on( 'change:borderWidth', this._getPropertyChangeCallback( 'tableCellBorderWidth' ) );
-		view.on( 'change:padding', this._getPropertyChangeCallback( 'tableCellPadding' ) );
-		view.on( 'change:backgroundColor', this._getPropertyChangeCallback( 'tableCellBackgroundColor' ) );
+
+		view.on( 'change:borderColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderColorInput,
+			commandName: 'tableCellBorderColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
+		view.on( 'change:borderWidth', this._getValidatedPropertyChangeCallback( {
+			viewField: view.borderWidthInput,
+			commandName: 'tableCellBorderWidth',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:padding', this._getValidatedPropertyChangeCallback( {
+			viewField: view.paddingInput,
+			commandName: 'tableCellPadding',
+			errorText: lengthErrorText,
+			validator: lengthFieldValidator
+		} ) );
+
+		view.on( 'change:backgroundColor', this._getValidatedPropertyChangeCallback( {
+			viewField: view.backgroundInput,
+			commandName: 'tableCellBackgroundColor',
+			errorText: colorErrorText,
+			validator: colorFieldValidator
+		} ) );
+
 		view.on( 'change:horizontalAlignment', this._getPropertyChangeCallback( 'tableCellHorizontalAlignment' ) );
 		view.on( 'change:verticalAlignment', this._getPropertyChangeCallback( 'tableCellVerticalAlignment' ) );
 
@@ -275,4 +314,38 @@ export default class TableCellPropertiesUI extends Plugin {
 			} );
 		};
 	}
+
+	/**
+	 * Creates a callback that when executed upon {@link #view view's} property change:
+	 * * executes a related editor command with the new property value if the value is valid,
+	 * * or sets the error text next to the invalid field, if the value did not pass the validation.
+	 *
+	 * @private
+	 * @param {Object} options
+	 * @param {String} options.commandName
+	 * @param {module:ui/view~View} options.viewField
+	 * @param {Function} options.validator
+	 * @param {String} options.errorText
+	 * @returns {Function}
+	 */
+	_getValidatedPropertyChangeCallback( { commandName, viewField, validator, errorText } ) {
+		const setErrorTextDebounced = debounce( () => {
+			viewField.errorText = errorText;
+		}, ERROR_TEXT_TIMEOUT );
+
+		return ( evt, propertyName, newValue ) => {
+			setErrorTextDebounced.cancel();
+
+			if ( validator( newValue ) ) {
+				this.editor.execute( commandName, {
+					value: newValue,
+					batch: this._undoStepBatch
+				} );
+
+				viewField.errorText = null;
+			} else {
+				setErrorTextDebounced();
+			}
+		};
+	}
 }

+ 15 - 2
packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js

@@ -64,7 +64,6 @@ export default class TableCellPropertiesView extends View {
 
 		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
 		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
-		const { saveButtonView, cancelButtonView } = this._createActionButtons();
 
 		/**
 		 * Tracks information about the DOM focus in the form.
@@ -211,6 +210,11 @@ export default class TableCellPropertiesView extends View {
 		 */
 		this.verticalAlignmentToolbar = verticalAlignmentToolbar;
 
+		// Defer creating to make sure other fields are present and the Save button can
+		// bind its #isEnabled to their error messages so there's no way to save unless all
+		// fields are valid.
+		const { saveButtonView, cancelButtonView } = this._createActionButtons();
+
 		/**
 		 * The "Save" button view.
 		 *
@@ -562,9 +566,14 @@ export default class TableCellPropertiesView extends View {
 	_createActionButtons() {
 		const locale = this.locale;
 		const t = this.t;
-
 		const saveButtonView = new ButtonView( locale );
 		const cancelButtonView = new ButtonView( locale );
+		const fieldsThatShouldValidateToSave = [
+			this.borderWidthInput,
+			this.borderColorInput,
+			this.backgroundInput,
+			this.paddingInput
+		];
 
 		saveButtonView.set( {
 			label: t( 'Save' ),
@@ -574,6 +583,10 @@ export default class TableCellPropertiesView extends View {
 			withText: true,
 		} );
 
+		saveButtonView.bind( 'isEnabled' ).toMany( fieldsThatShouldValidateToSave, 'errorText', ( ...errorTexts ) => {
+			return errorTexts.every( errorText => !errorText );
+		} );
+
 		cancelButtonView.set( {
 			label: t( 'Cancel' ),
 			icon: cancelIcon,

+ 53 - 0
packages/ckeditor5-table/src/ui/utils.js

@@ -11,6 +11,7 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
+import { isColor, isLength } from '@ckeditor/ckeditor5-engine/src/view/styles/utils';
 import { getTableWidgetAncestor } from '../utils';
 import { findAncestor } from '../commands/utils';
 
@@ -24,6 +25,8 @@ const BALLOON_POSITIONS = [
 	DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
 ];
 
+const isEmpty = val => val.trim() === '';
+
 /**
  * A helper utility that positions the
  * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
@@ -111,6 +114,56 @@ export function getBorderStyleLabels( t ) {
 }
 
 /**
+ * Returns a localized error string that can be displayed next to color (background, border)
+ * fields that have an invalid value.
+ *
+ * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
+ * that is used to localize strings.
+ * @returns {String}
+ */
+export function getLocalizedColorErrorText( t ) {
+	return t( 'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".' );
+}
+
+/**
+ * Returns a localized error string that can be displayed next to length (padding, border width)
+ * fields that have an invalid value.
+ *
+ * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
+ * that is used to localize strings.
+ * @returns {String}
+ */
+export function getLocalizedLengthErrorText( t ) {
+	return t( 'The value is invalid. Try "10px" or "2em" or simply "2".' );
+}
+
+/**
+ * Returns `true` when the passed value is an empty string or a valid CSS color expression.
+ * Otherwise, `false` is returned.
+ *
+ * See {@link module:engine/view/styles/utils~isColor}.
+ *
+ * @param {String} value
+ * @returns {Boolean}
+ */
+export function colorFieldValidator( value ) {
+	return isEmpty( value ) || isColor( value );
+}
+
+/**
+ * Returns `true` when the passed value is an empty string or a valid CSS length expression.
+ * Otherwise, `false` is returned.
+ *
+ * See {@link module:engine/view/styles/utils~isLength}.
+ *
+ * @param {String} value
+ * @returns {Boolean}
+ */
+export function lengthFieldValidator( value ) {
+	return isEmpty( value ) || isLength( value );
+}
+
+/**
  * Generates item definitions for a UI dropdown that allows changing the border style of a table or a table cell.
  *
  * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|

+ 24 - 0
packages/ckeditor5-table/theme/tableform.css

@@ -33,4 +33,28 @@
 			flex-grow: 0;
 		}
 	}
+
+	& .ck.ck-labeled-view {
+		/* Allow absolute positioning of the status (error) balloons. */
+		position: relative;
+
+		& .ck.ck-labeled-view__status {
+			position: absolute;
+			left: 50%;
+			bottom: calc( -1 * var(--ck-table-properties-error-arrow-size) );
+			transform: translate(-50%,100%);
+
+			/* Make sure the balloon status stays on top of other form elements. */
+			z-index: 1;
+
+			/* The arrow pointing towards the field. */
+			&::after {
+				content: "";
+				position: absolute;
+				top: calc( -1 * var(--ck-table-properties-error-arrow-size) );
+				left: 50%;
+				transform: translateX( -50% );
+			}
+		}
+	}
 }