diff --git a/docs/features/font.md b/docs/features/font.md index e84cace..e63fdba 100644 --- a/docs/features/font.md +++ b/docs/features/font.md @@ -21,7 +21,7 @@ The {@link module:font/font~Font} plugin enables the following features in the r ## Configuring the font family feature -It is possible to configure which font family options are supported by the WYSIWYG editor. Use the {@link module:font/fontfamily~FontFamilyConfig#options `fontFamily.options`} configuration option to do so. +It is possible to configure which font family options are supported by the WYSIWYG editor. Use the {@link module:font/fontfamily~FontFamilyConfig#options `config.fontFamily.options`} configuration option to do so. Use the special `'default'` keyword to use the default font family defined in the web page styles. It removes any custom font family. @@ -47,9 +47,28 @@ ClassicEditor {@snippet features/custom-font-family-options} +### Accept all font names + +By default, all `font-family` values that are not specified in the `config.fontFamily.options` are stripped. You can enable support for all font names by using the {@link module:font/fontfamily~FontFamilyConfig#supportAllValues `config.fontFamily.supportAllValues`} option. + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + fontFamily: { + options: [ + // ... + ], + supportAllValues: true + }, + // ... + } ) + .then( ... ) + .catch( ... ); +``` + ## Configuring the font size feature -It is possible to configure which font size options are supported by the WYSIWYG editor. Use the {@link module:font/fontsize~FontSizeConfig#options `fontSize.options`} configuration option to do so. +It is possible to configure which font size options are supported by the WYSIWYG editor. Use the {@link module:font/fontsize~FontSizeConfig#options `config.fontSize.options`} configuration option to do so. Use the special `'default'` keyword to use the default font size defined in the web page styles. It removes any custom font size. @@ -150,6 +169,26 @@ ClassicEditor {@snippet features/custom-font-size-numeric-options} +### Prevent removing non-specified values + +By default, all `font-size` values that are not specified in the `config.fontSize.options` are stripped. You can enable support for all font sizes by using the {@link module:font/fontfamily~FontSizeConfig#supportAllValues `config.fontSize.supportAllValues`} option. + + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + fontSize: { + options: [ + // ... + ], + supportAllValues: true + }, + // ... + } ) + .then( ... ) + .catch( ... ); +``` + ## Configuring the font color and font background color features Both font color and font background color features are configurable and share the same configuration format. @@ -164,7 +203,7 @@ Check out the WYSIWYG editor below with both features customized using the edito ### Specifying available colors -It is possible to configure which colors are available in the color dropdown. Use the {@link module:font/fontcolor~FontColorConfig#colors `fontColor.colors`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#colors `fontBackgroundColor.colors`} configuration options to do so. +It is possible to configure which colors are available in the color dropdown. Use the {@link module:font/fontcolor~FontColorConfig#colors `config.fontColor.colors`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#colors `config.fontBackgroundColor.colors`} configuration options to do so. ```js ClassicEditor @@ -232,7 +271,7 @@ ClassicEditor ### Changing the geometry of the color grid -You can configure the number of columns in the color dropdown by setting the {@link module:font/fontcolor~FontColorConfig#columns `fontColor.columns`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns `fontBackgroundColor.columns`} configuration options. +You can configure the number of columns in the color dropdown by setting the {@link module:font/fontcolor~FontColorConfig#columns `config.fontColor.columns`} and {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#columns `config.fontBackgroundColor.columns`} configuration options. Usually, you will want to use this option when changing the number of [available colors](#specifying-available-colors). @@ -265,7 +304,7 @@ ClassicEditor The font and font background color dropdowns contain the "Document colors" section. It lists the colors already used in the document for the users to be able to easily reuse them (for consistency purposes). -By default, the number of displayed document colors is limited to one row, but you can adjust it (or remove the whole section) by using the {@link module:font/fontcolor~FontColorConfig#documentColors `fontColor.documentColors`} or {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#documentColors `fontBackgroundColor.documentColors`} options. +By default, the number of displayed document colors is limited to one row, but you can adjust it (or remove the whole section) by using the {@link module:font/fontcolor~FontColorConfig#documentColors `config.fontColor.documentColors`} or {@link module:font/fontbackgroundcolor~FontBackgroundColorConfig#documentColors `config.fontBackgroundColor.documentColors`} options. ```js ClassicEditor @@ -340,7 +379,7 @@ The {@link module:font/fontfamily~FontFamily} plugin registers the following com * The `'fontFamily'` dropdown. * The {@link module:font/fontfamily/fontfamilycommand~FontFamilyCommand `'fontFamily'`} command. - The number of options and their names correspond to the {@link module:font/fontfamily~FontFamilyConfig#options `fontFamily.options`} configuration option. + The number of options and their names correspond to the {@link module:font/fontfamily~FontFamilyConfig#options `config.fontFamily.options`} configuration option. You can change the font family of the current selection by executing the command with a desired value: @@ -386,7 +425,7 @@ The {@link module:font/fontsize~FontSize} plugin registers the following compone * The `'fontSize'` dropdown. * The {@link module:font/fontsize/fontsizecommand~FontSizeCommand `'fontSize'`} command. - The number of options and their names correspond to the {@link module:font/fontsize~FontSizeConfig#options `fontSize.options`} configuration option. + The number of options and their names correspond to the {@link module:font/fontsize~FontSizeConfig#options `config.fontSize.options`} configuration option. You can change the font size of the current selection by executing the command with a desired value: @@ -398,7 +437,7 @@ The {@link module:font/fontsize~FontSize} plugin registers the following compone editor.execute( 'fontSize', { value: 'small' } ); ``` - Passing an empty value will remove any `fontSize` set: + Passing an empty value will remove any `config.fontSize` set: ```js editor.execute( 'fontSize' ); diff --git a/src/fontfamily.js b/src/fontfamily.js index cbea666..dc7912d 100644 --- a/src/fontfamily.js +++ b/src/fontfamily.js @@ -112,3 +112,19 @@ export default class FontFamily extends Plugin { * * @member {Array.} module:font/fontfamily~FontFamilyConfig#options */ + +/** + * By default the plugin removes any `font-family` value that does not match to the plugin's configuration. It means if you paste a content + * with font families that the editor does not understand, the font-family attribute will be removed and the content will be displayed + * with the font. + * + * You can preserve pasted font family values by switching the option: + * + * const fontSizeConfig = { + * supportAllValues: true + * }; + * + * Now, the font families, not specified in the editor's configuration, won't be removed when pasting the content. + * + * @member {Boolean} module:font/fontfamily~FontFamilyConfig#supportAllValues + */ diff --git a/src/fontfamily/fontfamilyediting.js b/src/fontfamily/fontfamilyediting.js index 39e522f..3faea51 100644 --- a/src/fontfamily/fontfamilyediting.js +++ b/src/fontfamily/fontfamilyediting.js @@ -49,7 +49,8 @@ export default class FontFamilyEditing extends Plugin { 'Times New Roman, Times, serif', 'Trebuchet MS, Helvetica, sans-serif', 'Verdana, Geneva, sans-serif' - ] + ], + supportAllValues: false } ); } @@ -71,8 +72,42 @@ export default class FontFamilyEditing extends Plugin { const definition = buildDefinition( FONT_FAMILY, options ); // Set-up the two-way conversion. - editor.conversion.attributeToElement( definition ); + if ( editor.config.get( 'fontFamily.supportAllValues' ) ) { + this._prepareAnyValueConverters(); + } else { + editor.conversion.attributeToElement( definition ); + } editor.commands.add( FONT_FAMILY, new FontFamilyCommand( editor ) ); } + + /** + * Those converters enable keeping any value found as `style="font-family: *"` as a value of an attribute on a text even + * if it isn't defined in the plugin configuration. + * + * @private + */ + _prepareAnyValueConverters() { + const editor = this.editor; + + editor.conversion.for( 'downcast' ).attributeToElement( { + model: FONT_FAMILY, + view: ( attributeValue, writer ) => { + return writer.createAttributeElement( 'span', { style: 'font-family:' + attributeValue }, { priority: 7 } ); + } + } ); + + editor.conversion.for( 'upcast' ).attributeToAttribute( { + model: { + key: FONT_FAMILY, + value: viewElement => viewElement.getStyle( 'font-family' ) + }, + view: { + name: 'span', + styles: { + 'font-family': /.*/ + } + } + } ); + } } diff --git a/src/fontfamily/fontfamilyui.js b/src/fontfamily/fontfamilyui.js index 49976b1..0c4d035 100644 --- a/src/fontfamily/fontfamilyui.js +++ b/src/fontfamily/fontfamilyui.js @@ -109,7 +109,18 @@ function _prepareListOptions( options, command ) { } ) }; - def.model.bind( 'isOn' ).to( command, 'value', value => value === option.model ); + def.model.bind( 'isOn' ).to( command, 'value', value => { + // "Default" or check in strict font-family converters mode. + if ( value === option.model ) { + return true; + } + + if ( !value || !option.model ) { + return false; + } + + return value.split( ',' )[ 0 ].replace( /'/g, '' ).toLowerCase() === option.model.toLowerCase(); + } ); // Try to set a dropdown list item style. if ( option.view && option.view.styles ) { diff --git a/src/fontsize.js b/src/fontsize.js index 5a8c86a..5047d11 100644 --- a/src/fontsize.js +++ b/src/fontsize.js @@ -105,6 +105,22 @@ export default class FontSize extends Plugin { * options: [ 9, 10, 11, 12, 13, 14, 15 ] * }; * + * Also, you can define a label in the dropdown for numerical values: + * + * const fontSizeConfig = { + * options: [ + * { + * title: 'Small', + * model: '8px + * }, + * 'default', + * { + * title: 'Big', + * model: '14px + * } + * ] + * }; + * * Font size can be applied using the command API. To do that, use the `'fontSize'` command and pass the desired font size as a `value`. * For example, the following code will apply the `fontSize` attribute with the **tiny** value to the current selection: * @@ -114,3 +130,19 @@ export default class FontSize extends Plugin { * * @member {Array.} module:font/fontsize~FontSizeConfig#options */ + +/** + * By default the plugin removes any `font-size` value that does not match to the plugin's configuration. It means if you paste a content + * with font sizes that the editor does not understand, the font-size attribute will be removed and the content will be displayed + * with the default size. + * + * You can preserve pasted font size values by switching the option: + * + * const fontSizeConfig = { + * supportAllValues: true + * }; + * + * Now, the font sizes, not specified in the editor's configuration, won't be removed when pasting the content. + * + * @member {Boolean} module:font/fontsize~FontSizeConfig#supportAllValues + */ diff --git a/src/fontsize/fontsizeediting.js b/src/fontsize/fontsizeediting.js index 6780199..b44b10b 100644 --- a/src/fontsize/fontsizeediting.js +++ b/src/fontsize/fontsizeediting.js @@ -48,31 +48,70 @@ export default class FontSizeEditing extends Plugin { 'default', 'big', 'huge' - ] + ], + supportAllValues: false } ); + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + // Allow fontSize attribute on text nodes. + editor.model.schema.extend( '$text', { allowAttributes: FONT_SIZE } ); + editor.model.schema.setAttributeProperties( FONT_SIZE, { + isFormatting: true, + copyOnEnter: true + } ); + + const supportAllValues = editor.config.get( 'fontSize.supportAllValues' ); // Define view to model conversion. - const options = normalizeOptions( this.editor.config.get( 'fontSize.options' ) ).filter( item => item.model ); + const options = normalizeOptions( this.editor.config.get( 'fontSize.options' ), { supportAllValues } ) + .filter( item => item.model ); const definition = buildDefinition( FONT_SIZE, options ); // Set-up the two-way conversion. - editor.conversion.attributeToElement( definition ); + if ( supportAllValues ) { + this._prepareAnyValueConverters(); + } else { + editor.conversion.attributeToElement( definition ); + } // Add FontSize command. editor.commands.add( FONT_SIZE, new FontSizeCommand( editor ) ); } /** - * @inheritDoc + * Those converters enable keeping any value found as `style="font-size: *"` as a value of an attribute on a text even + * if it isn't defined in the plugin configuration. + * + * @private */ - init() { + _prepareAnyValueConverters() { const editor = this.editor; - // Allow fontSize attribute on text nodes. - editor.model.schema.extend( '$text', { allowAttributes: FONT_SIZE } ); - editor.model.schema.setAttributeProperties( FONT_SIZE, { - isFormatting: true, - copyOnEnter: true + editor.conversion.for( 'downcast' ).attributeToElement( { + model: FONT_SIZE, + view: ( attributeValue, writer ) => { + if ( !attributeValue ) { + return; + } + + return writer.createAttributeElement( 'span', { style: 'font-size:' + attributeValue }, { priority: 7 } ); + } + } ); + + editor.conversion.for( 'upcast' ).attributeToAttribute( { + model: { + key: FONT_SIZE, + value: viewElement => viewElement.getStyle( 'font-size' ) + }, + view: { + name: 'span' + } } ); } } diff --git a/src/fontsize/utils.js b/src/fontsize/utils.js index d41cb21..86635d8 100644 --- a/src/fontsize/utils.js +++ b/src/fontsize/utils.js @@ -7,75 +7,105 @@ * @module font/fontsize/utils */ +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; + /** * Normalizes and translates the {@link module:font/fontsize~FontSizeConfig#options configuration options} * to the {@link module:font/fontsize~FontSizeOption} format. * * @param {Array.} configuredOptions An array of options taken from the configuration. + * @param {Object} [options={}] + * @param {Boolean} [options.supportAllValues=false] * @returns {Array.} */ -export function normalizeOptions( configuredOptions ) { +export function normalizeOptions( configuredOptions, options = {} ) { + const supportAllValues = options.supportAllValues || false; + // Convert options to objects. return configuredOptions - .map( getOptionDefinition ) + .map( item => getOptionDefinition( item, supportAllValues ) ) // Filter out undefined values that `getOptionDefinition` might return. .filter( option => !!option ); } -// Default named presets map. +// The values should be synchronized with values specified in the "/theme/fontsize.css" file. +export const FONT_SIZE_PRESET_UNITS = { + tiny: '0.7em', + small: '0.85em', + big: '1.4em', + huge: '1.8em' +}; + +// Default named presets map. Always create a new instance of the preset object in order to avoid modifying references. const namedPresets = { - tiny: { - title: 'Tiny', - model: 'tiny', - view: { - name: 'span', - classes: 'text-tiny', - priority: 7 - } + get tiny() { + return { + title: 'Tiny', + model: 'tiny', + view: { + name: 'span', + classes: 'text-tiny', + priority: 7 + } + }; }, - small: { - title: 'Small', - model: 'small', - view: { - name: 'span', - classes: 'text-small', - priority: 7 - } + get small() { + return { + title: 'Small', + model: 'small', + view: { + name: 'span', + classes: 'text-small', + priority: 7 + } + }; }, - big: { - title: 'Big', - model: 'big', - view: { - name: 'span', - classes: 'text-big', - priority: 7 - } + get big() { + return { + title: 'Big', + model: 'big', + view: { + name: 'span', + classes: 'text-big', + priority: 7 + } + }; }, - huge: { - title: 'Huge', - model: 'huge', - view: { - name: 'span', - classes: 'text-huge', - priority: 7 - } + get huge() { + return { + title: 'Huge', + model: 'huge', + view: { + name: 'span', + classes: 'text-huge', + priority: 7 + } + }; } }; // Returns an option definition either from preset or creates one from number shortcut. // If object is passed then this method will return it without alternating it. Returns undefined for item than cannot be parsed. +// If supportAllValues=true, model will be set to a specified unit value instead of text. // // @param {String|Number|Object} item +// @param {Boolean} supportAllValues // @returns {undefined|module:font/fontsize~FontSizeOption} -function getOptionDefinition( option ) { - // Treat any object as full item definition provided by user in configuration. - if ( typeof option === 'object' ) { - return option; +function getOptionDefinition( option, supportAllValues ) { + // Check whether passed option is a full item definition provided by user in configuration. + if ( isFullItemDefinition( option ) ) { + return attachPriority( option ); } + const preset = findPreset( option ); + // Item is a named preset. - if ( namedPresets[ option ] ) { - return namedPresets[ option ]; + if ( preset ) { + if ( supportAllValues ) { + preset.model = FONT_SIZE_PRESET_UNITS[ option ]; + } + + return attachPriority( preset ); } // 'Default' font size. It will be used to remove the fontSize attribute. @@ -87,33 +117,97 @@ function getOptionDefinition( option ) { } // At this stage we probably have numerical value to generate a preset so parse it's value. - const sizePreset = parseFloat( option ); - // Discard any faulty values. - if ( isNaN( sizePreset ) ) { + if ( isNumericalDefinition( option ) ) { return; } // Return font size definition from size value. - return generatePixelPreset( sizePreset ); + return generatePixelPreset( option ); } // Creates a predefined preset for pixel size. // -// @param {Number} size Font size in pixels. +// @param {Number} definition Font size in pixels. // @returns {module:font/fontsize~FontSizeOption} -function generatePixelPreset( size ) { - const sizeName = String( size ); - - return { - title: sizeName, - model: size, - view: { - name: 'span', - styles: { - 'font-size': `${ size }px` - }, - priority: 7 +function generatePixelPreset( definition ) { + // Extend a short (numeric value) definition. + if ( typeof definition === 'number' || typeof definition === 'string' ) { + definition = { + title: String( definition ), + model: `${ parseFloat( definition ) }px` + }; + } + + definition.view = { + name: 'span', + styles: { + 'font-size': definition.model } }; + + return attachPriority( definition ); +} + +// Adds the priority to the view element definition if missing. It's required due to ckeditor/ckeditor5#2291 +// +// @param {Object} definition +// @param {Object} definition.title +// @param {Object} definition.model +// @param {Object} definition.view +// @returns {Object} +function attachPriority( definition ) { + if ( !definition.view.priority ) { + definition.view.priority = 7; + } + + return definition; +} + +// Returns a prepared preset definition. If passed an object, a name of preset should be defined as `model` value. +// +// @param {String|Object} definition +// @param {String} definition.model A preset name. +// @returns {Object|undefined} +function findPreset( definition ) { + return namedPresets[ definition ] || namedPresets[ definition.model ]; +} + +// We treat `definition` as completed if it is an object that contains `title`, `model` and `view` values. +// +// @param {Object} definition +// @param {String} definition.title +// @param {String} definition.model +// @param {Object} definition.view +// @returns {Boolean} +function isFullItemDefinition( definition ) { + return typeof definition === 'object' && definition.title && definition.model && definition.view; +} + +// We treat `definition` as numerical if it is a number, number-like (string) or an object with the `title` key. +// +// @param {Object|Number|String} definition +// @param {Object} definition.title +// @returns {Boolean} +function isNumericalDefinition( definition ) { + let numberValue; + + if ( typeof definition === 'object' ) { + if ( !definition.model ) { + /** + * Provided value as an option for {@link module:font/fontsize~FontSize} seems to invalid. + * + * See valid examples described in the {@link module:font/fontsize~FontSizeConfig#options plugin configuration}. + * + * @error font-size-invalid-definition + */ + throw new CKEditorError( 'font-size-invalid-definition: Provided font size definition is invalid.', null, definition ); + } else { + numberValue = parseFloat( definition.model ); + } + } else { + numberValue = parseFloat( definition ); + } + + return isNaN( numberValue ); } diff --git a/tests/fontfamily/fontfamilyediting.js b/tests/fontfamily/fontfamilyediting.js index 72fc73f..a973996 100644 --- a/tests/fontfamily/fontfamilyediting.js +++ b/tests/fontfamily/fontfamilyediting.js @@ -66,6 +66,70 @@ describe( 'FontFamilyEditing', () => { 'Trebuchet MS, Helvetica, sans-serif', 'Verdana, Geneva, sans-serif' ] ); + + expect( editor.config.get( 'fontFamily.supportAllValues' ) ).to.equal( false ); + } ); + } ); + + describe( 'supportAllValues=true', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontFamilyEditing, Paragraph ], + fontFamily: { + options: [ + 'Arial' + ], + supportAllValues: true + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'editing pipeline conversion', () => { + it( 'should convert unknown fontFamily attribute values', () => { + setModelData( doc, 'f<$text fontFamily="foo-bar">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should convert defined fontFamily attribute values', () => { + setModelData( doc, 'f<$text fontFamily="Arial">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + it( 'should convert from an element with defined style when with other styles', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontFamily="Other">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should convert from a complex definition', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontFamily="Arial,sans-serif">oo' ); + + expect( editor.getData() ).to.equal( data ); + } ); } ); } ); } ); diff --git a/tests/fontfamily/fontfamilyui.js b/tests/fontfamily/fontfamilyui.js index ab66a2f..7d6e0b1 100644 --- a/tests/fontfamily/fontfamilyui.js +++ b/tests/fontfamily/fontfamilyui.js @@ -104,6 +104,42 @@ describe( 'FontFamilyUI', () => { .to.deep.equal( [ false, true, false, false, false, false, false, false, false ] ); } ); + it( 'should activate current option in dropdown for full font family definitions', () => { + const element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ FontFamilyEditing, FontFamilyUI ], + fontSize: { + supportAllValues: true + } + } ) + .then( editor => { + const command = editor.commands.get( 'fontFamily' ); + const dropdown = editor.ui.componentFactory.create( 'fontFamily' ); + + const listView = dropdown.listView; + + command.value = undefined; + + // The first item is 'default' font family. + expect( listView.items.map( item => item.children.first.isOn ) ) + .to.deep.equal( [ true, false, false, false, false, false, false, false, false ] ); + + command.value = '\'Courier New\', Courier, monospace'; + + // The third item is 'Courier New' font family. + expect( listView.items.map( item => item.children.first.isOn ) ) + .to.deep.equal( [ false, false, true, false, false, false, false, false, false ] ); + + return editor.destroy(); + } ) + .then( () => { + element.remove(); + } ); + } ); + describe( 'model to command binding', () => { it( 'isEnabled', () => { command.isEnabled = false; diff --git a/tests/fontsize/fontsizeediting.js b/tests/fontsize/fontsizeediting.js index 7f8e0a1..469b32b 100644 --- a/tests/fontsize/fontsizeediting.js +++ b/tests/fontsize/fontsizeediting.js @@ -56,6 +56,50 @@ describe( 'FontSizeEditing', () => { describe( 'default value', () => { it( 'should be set', () => { expect( editor.config.get( 'fontSize.options' ) ).to.deep.equal( [ 'tiny', 'small', 'default', 'big', 'huge' ] ); + expect( editor.config.get( 'fontSize.supportAllValues' ) ).to.equal( false ); + } ); + } ); + + describe( 'supportAllValues=true', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontSizeEditing, Paragraph ], + fontSize: { + supportAllValues: true + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'editing pipeline conversion', () => { + it( 'should pass fontSize to data', () => { + setModelData( doc, 'f<$text fontSize="10px">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + it( 'should convert from an element with defined style when with other styles', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18px">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); } ); } ); } ); @@ -102,7 +146,7 @@ describe( 'FontSizeEditing', () => { } ); it( 'should convert fontSize attribute to predefined pixel size preset', () => { - setModelData( doc, 'f<$text fontSize="18">oo' ); + setModelData( doc, 'f<$text fontSize="18px">oo' ); expect( editor.getData() ).to.equal( '

foo

' ); } ); @@ -188,7 +232,7 @@ describe( 'FontSizeEditing', () => { editor.setData( data ); - expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18">oo' ); + expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18px">oo' ); expect( editor.getData() ).to.equal( data ); } ); @@ -198,7 +242,7 @@ describe( 'FontSizeEditing', () => { editor.setData( data ); - expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18">oo' ); + expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18px">oo' ); expect( editor.getData() ).to.equal( '

foo

' ); } ); diff --git a/tests/fontsize/utils.js b/tests/fontsize/utils.js index e8523b0..cf60643 100644 --- a/tests/fontsize/utils.js +++ b/tests/fontsize/utils.js @@ -3,7 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import { normalizeOptions } from '../../src/fontsize/utils'; +import { normalizeOptions, FONT_SIZE_PRESET_UNITS } from '../../src/fontsize/utils'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; describe( 'FontSizeEditing Utils', () => { describe( 'normalizeOptions()', () => { @@ -15,12 +16,12 @@ describe( 'FontSizeEditing Utils', () => { expect( normalizeOptions( [ { title: 'My Size', model: 'my-size', - view: { name: 'span', styles: 'font-size: 12em;' } + view: { name: 'span', styles: 'font-size: 12em;', priority: 7 } } ] ) ).to.deep.equal( [ { title: 'My Size', model: 'my-size', - view: { name: 'span', styles: 'font-size: 12em;' } + view: { name: 'span', styles: 'font-size: 12em;', priority: 7 } } ] ); } ); @@ -35,18 +36,139 @@ describe( 'FontSizeEditing Utils', () => { { title: 'Huge', model: 'huge', view: { name: 'span', classes: 'text-huge', priority: 7 } } ] ); } ); + + it( 'should return defined presets with units in model values if supportAllValues=true', () => { + const options = normalizeOptions( [ 'tiny', 'small', 'default', 'big', 'huge' ], { supportAllValues: true } ); + + expect( options ).to.deep.equal( [ + { title: 'Tiny', model: '0.7em', view: { name: 'span', classes: 'text-tiny', priority: 7 } }, + { title: 'Small', model: '0.85em', view: { name: 'span', classes: 'text-small', priority: 7 } }, + { title: 'Default', model: undefined }, + { title: 'Big', model: '1.4em', view: { name: 'span', classes: 'text-big', priority: 7 } }, + { title: 'Huge', model: '1.8em', view: { name: 'span', classes: 'text-huge', priority: 7 } } + ] ); + } ); + + it( 'should add "view" definition if missing', () => { + const tinyOption = { + title: 'Tiny', + model: 'tiny' + }; + + expect( normalizeOptions( [ tinyOption ] ) ).to.deep.equal( [ + { title: 'Tiny', model: 'tiny', view: { name: 'span', classes: 'text-tiny', priority: 7 } } + ] ); + } ); + + it( 'should add "view.priority" to returned definition if missing', () => { + const tinyOption = { + title: 'Tiny', + model: 'tiny', + view: { + name: 'span', + classes: 'text-tiny' + } + }; + + expect( normalizeOptions( [ tinyOption ] ) ).to.deep.equal( [ + { title: 'Tiny', model: 'tiny', view: { name: 'span', classes: 'text-tiny', priority: 7 } } + ] ); + } ); + + it( 'should not modify "view.priority" if already specified', () => { + const tinyOption = { + title: 'Tiny', + model: 'tiny', + view: { + name: 'span', + classes: 'text-tiny', + priority: 10 + } + }; + + expect( normalizeOptions( [ tinyOption ] ) ).to.deep.equal( [ + { title: 'Tiny', model: 'tiny', view: { name: 'span', classes: 'text-tiny', priority: 10 } } + ] ); + } ); } ); describe( 'numerical presets', () => { it( 'should return generated presets', () => { - expect( normalizeOptions( [ '10', 12, 'default', '14.1', 18.3 ] ) ).to.deep.equal( [ - { title: '10', model: 10, view: { name: 'span', styles: { 'font-size': '10px' }, priority: 7 } }, - { title: '12', model: 12, view: { name: 'span', styles: { 'font-size': '12px' }, priority: 7 } }, + expect( normalizeOptions( [ '10', '12', 'default', '14.1', 18.3 ] ) ).to.deep.equal( [ + { title: '10', model: '10px', view: { name: 'span', styles: { 'font-size': '10px' }, priority: 7 } }, + { title: '12', model: '12px', view: { name: 'span', styles: { 'font-size': '12px' }, priority: 7 } }, { title: 'Default', model: undefined }, - { title: '14.1', model: 14.1, view: { name: 'span', styles: { 'font-size': '14.1px' }, priority: 7 } }, - { title: '18.3', model: 18.3, view: { name: 'span', styles: { 'font-size': '18.3px' }, priority: 7 } } + { title: '14.1', model: '14.1px', view: { name: 'span', styles: { 'font-size': '14.1px' }, priority: 7 } }, + { title: '18.3', model: '18.3px', view: { name: 'span', styles: { 'font-size': '18.3px' }, priority: 7 } } + ] ); + } ); + + it( 'should add "view" definition if missing', () => { + const numericOption = { + title: '18', + model: '18px' + }; + + expect( normalizeOptions( [ numericOption ] ) ).to.deep.equal( [ + { title: '18', model: '18px', view: { name: 'span', styles: { 'font-size': '18px' }, priority: 7 } } + ] ); + } ); + + it( 'should discard incomprehensible value', () => { + const numericOption = { + title: '18', + model: 'unknown' + }; + + expect( normalizeOptions( [ numericOption ] ) ).to.deep.equal( [] ); + } ); + + it( 'should add "view.priority" to returned definition if missing', () => { + const numericOption = { + title: '18', + model: '18px', + view: { + name: 'span', + styles: { 'font-size': '18px' } + } + }; + + expect( normalizeOptions( [ numericOption ] ) ).to.deep.equal( [ + { title: '18', model: '18px', view: { name: 'span', styles: { 'font-size': '18px' }, priority: 7 } } ] ); } ); + + it( 'should not modify "view.priority" if already specified', () => { + const numericOption = { + title: '18', + model: '18px', + view: { + name: 'span', + styles: { 'font-size': '18px' }, + priority: 10 + } + }; + + expect( normalizeOptions( [ numericOption ] ) ).to.deep.equal( [ + { title: '18', model: '18px', view: { name: 'span', styles: { 'font-size': '18px' }, priority: 10 } } + ] ); + } ); + + it( 'should throw an error if definition misses "model" value', () => { + const definition = { + title: '18' + }; + + expectToThrowCKEditorError( () => { + normalizeOptions( [ definition ] ); + }, /font-size-invalid-definition/, null, definition ); + } ); + } ); + } ); + + describe( 'FONT_SIZE_PRESET_UNITS', () => { + it( 'provides default values', () => { + expect( Object.keys( FONT_SIZE_PRESET_UNITS ).length ).to.equal( 4 ); } ); } ); } ); diff --git a/tests/integration.js b/tests/integration.js index 08a71c1..99828e4 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -9,7 +9,6 @@ import Font from '../src/font'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import env from '@ckeditor/ckeditor5-utils/src/env'; describe( 'Integration test Font', () => { let element, editor, model; @@ -42,28 +41,57 @@ describe( 'Integration test Font', () => { '' ); - if ( !env.isEdge ) { - expect( editor.getData() ).to.equal( - '

' + - 'foo' + - '' + - '

' - ); - } else { - // Edge sorts attributes of an element. - expect( editor.getData() ).to.equal( - '

' + - 'foo' + - '' + - '

' - ); - } + expect( editor.getData() ).to.equal( + '

' + + 'foo' + + '' + + '

' + ); + } ); + + it( 'should render one span element for all types of font features (supportAllValues=true)', () => { + const element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Font, ArticlePluginSet ], + fontFamily: { + supportAllValues: true + }, + fontSize: { + supportAllValues: true + } + } ) + .then( editor => { + const model = editor.model; + + setModelData( model, + '' + + '<$text fontColor="#123456" fontBackgroundColor="rgb(10,20,30)" ' + + 'fontSize="48px" fontFamily="docs-Roboto"' + + '>foo' + + '' + + '' + ); + + expect( editor.getData() ).to.equal( + '

' + + 'foo' + + '' + + '

' + ); + + return editor.destroy(); + } ) + .then( () => { + element.remove(); + } ); } ); } ); @@ -85,5 +113,45 @@ describe( 'Integration test Font', () => { '

' ); } ); + + it( 'should render elements wrapped in proper order (supportAllValues=true)', () => { + const element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Font, ArticlePluginSet ], + fontFamily: { + supportAllValues: true + }, + fontSize: { + supportAllValues: true + } + } ) + .then( editor => { + const model = editor.model; + + setModelData( model, + '' + + '<$text bold="true" linkHref="foo" fontColor="red" fontSize="18px">foo' + + '' + ); + + expect( editor.getData() ).to.equal( + '

' + + '' + + '' + + 'foo' + + '' + + '' + + '

' + ); + + return editor.destroy(); + } ) + .then( () => { + element.remove(); + } ); + } ); } ); } ); diff --git a/tests/manual/font-family.html b/tests/manual/font-family.html index e35a8fd..7654e1d 100644 --- a/tests/manual/font-family.html +++ b/tests/manual/font-family.html @@ -1,6 +1,17 @@ +

+ Font-family converters mode: + + +

+

Font Family feature sample.

+

Docs-Roboto, Arial:

+

+ This font will be removed after changing the font-family converters to restricted value matching. +

+

Arial, Helvetica, sans-serif:

Topping cheesecake cotton candy toffee cookie cookie lemon drops cotton candy. Carrot cake dessert jelly beans powder cake cupcake tiramisu pastry gummi bears. diff --git a/tests/manual/font-family.js b/tests/manual/font-family.js index 0e1b9e8..c4c87dc 100644 --- a/tests/manual/font-family.js +++ b/tests/manual/font-family.js @@ -3,22 +3,51 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console, window, document */ +/* globals window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import FontFamily from '../../src/fontfamily'; -ClassicEditor - .create( document.querySelector( '#editor' ), { +const restrictedModeButton = document.getElementById( 'mode-restricted-values' ); +const standardModeButton = document.getElementById( 'mode-disable-value-matching' ); + +restrictedModeButton.addEventListener( 'change', handleModeChange ); +standardModeButton.addEventListener( 'change', handleModeChange ); + +// When page loaded. +startMode( document.querySelector( 'input[name="mode"]:checked' ).value ); + +// When a user changed a mode. +async function handleModeChange( evt ) { + await startMode( evt.target.value ); +} + +// Starts the editor. +async function startMode( selectedMode ) { + if ( selectedMode === 'restricted-values' ) { + await reloadEditor(); + } else { + await reloadEditor( { supportAllValues: true } ); + } +} + +async function reloadEditor( options = {} ) { + if ( window.editor ) { + await window.editor.destroy(); + } + + const config = { plugins: [ ArticlePluginSet, FontFamily ], toolbar: [ 'heading', '|', 'fontFamily', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' - ] - } ) - .then( editor => { - window.editor = editor; - } ) - .catch( err => { - console.error( err.stack ); - } ); + ], + fontFamily: {} + }; + + if ( options.supportAllValues ) { + config.fontFamily.supportAllValues = true; + } + + window.editor = await ClassicEditor.create( document.querySelector( '#editor' ), config ); +} diff --git a/tests/manual/font-family.md b/tests/manual/font-family.md index bdd7ecb..30d8bd6 100644 --- a/tests/manual/font-family.md +++ b/tests/manual/font-family.md @@ -1,7 +1,7 @@ ### Loading The data should be loaded with paragraphs, each with different font. -Also the image caption should have "changed font" string with different font. +Also the image caption should have "changed font" string with different font. ### Testing @@ -10,3 +10,12 @@ Try to: - Change font size by selecting some text. - Change to default font size by selecting many paragraphs. - Change to default font size by selecting some text. + +### Converters mode + +The "Restricted value matching" option means that all font-family values that aren't defined in the plugin's configuration will be removed (e.g. when pasted from Google Docs). +This behaviour can be disabled by selecting the "Disabled value matching" option. + +The `Docs-Roboto, Arial` font-family is not specified in the plugin's configuration and should be restored to default font when the "Restricted value matching" option is selected. + +By default editor should load with the "Disabled value matching" option. diff --git a/tests/manual/font-size-numeric.html b/tests/manual/font-size-numeric.html index 6ef262a..49f171b 100644 --- a/tests/manual/font-size-numeric.html +++ b/tests/manual/font-size-numeric.html @@ -1,3 +1,9 @@ +

+ Font-size converters mode: + + +

+

Font Size feature sample.

@@ -8,4 +14,9 @@

Font Size feature sample.

Some text with font-size set to: 18px.

Some text with font-size set to: 20px.

Some text with font-size set to: 22px.

+

Some text with font-size set to: 36px.

+

Some text with font-size set to: 48px.

+

Some text with font-size set to: 64px.

+

Some text with font-size set to: 2em.

+

Some text with font-size set to: 15.5pt.

diff --git a/tests/manual/font-size-numeric.js b/tests/manual/font-size-numeric.js index c6015e2..c4e88d5 100644 --- a/tests/manual/font-size-numeric.js +++ b/tests/manual/font-size-numeric.js @@ -3,23 +3,51 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console, window, document */ +/* globals window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import FontSize from '../../src/fontsize'; -ClassicEditor - .create( document.querySelector( '#editor' ), { +const restrictedModeButton = document.getElementById( 'mode-restricted-values' ); +const standardModeButton = document.getElementById( 'mode-disable-value-matching' ); + +restrictedModeButton.addEventListener( 'change', handleModeChange ); +standardModeButton.addEventListener( 'change', handleModeChange ); + +// When page loaded. +startMode( document.querySelector( 'input[name="mode"]:checked' ).value ); + +// When a user changed a mode. +async function handleModeChange( evt ) { + await startMode( evt.target.value ); +} + +// Starts the editor. +async function startMode( selectedMode ) { + if ( selectedMode === 'restricted-values' ) { + await reloadEditor(); + } else { + await reloadEditor( { supportAllValues: true } ); + } +} + +async function reloadEditor( options = {} ) { + if ( window.editor ) { + await window.editor.destroy(); + } + + const config = { plugins: [ ArticlePluginSet, FontSize ], toolbar: [ 'heading', '|', 'fontSize', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], fontSize: { options: [ 10, 12, 14, 'default', 18, 20, 22 ] } - } ) - .then( editor => { - window.editor = editor; - } ) - .catch( err => { - console.error( err.stack ); - } ); + }; + + if ( options.supportAllValues ) { + config.fontSize.supportAllValues = true; + } + + window.editor = await ClassicEditor.create( document.querySelector( '#editor' ), config ); +} diff --git a/tests/manual/font-size-numeric.md b/tests/manual/font-size-numeric.md index bf60baf..e0f6605 100644 --- a/tests/manual/font-size-numeric.md +++ b/tests/manual/font-size-numeric.md @@ -10,3 +10,12 @@ Try to: - Change font by selecting some text. - Change to default font by selecting many paragraphs. - Change to default font by selecting some text. + +### Converters mode + +The "Restricted value matching" option means that all font-size values that aren't defined in the plugin's configuration will be removed (e.g. when pasted from Google Docs). +This behaviour can be disabled by selecting the "Disabled value matching" option. + +All font-size above 22 aren't specified in the plugin's configuration and should be restored to default size when the "Restricted value matching" option is selected. + +By default editor should load with the "Disabled value matching" option. diff --git a/theme/fontsize.css b/theme/fontsize.css index 677b6ef..85c9313 100644 --- a/theme/fontsize.css +++ b/theme/fontsize.css @@ -3,6 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* The values should be synchronized with the "FONT_SIZE_PRESET_UNITS" object in the "/src/fontsize/utils.js" file. */ .text-tiny { font-size: .7em; }