diff --git a/.eslintrc.js b/.eslintrc.js index b5a22b0..88dfc81 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ diff --git a/.travis.yml b/.travis.yml index 29678a9..cd72612 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,24 @@ sudo: required dist: trusty addons: + firefox: "latest" apt: sources: - - google-chrome + - google-chrome packages: - - google-chrome-stable + - google-chrome-stable language: node_js node_js: - - "6" +- '6' cache: - - node_modules +- node_modules before_install: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start +- export DISPLAY=:99.0 +- sh -e /etc/init.d/xvfb start install: - - npm install @ckeditor/ckeditor5-dev-tests - - ckeditor5-dev-tests-install-dependencies +- npm install @ckeditor/ckeditor5-dev-tests +- ckeditor5-dev-tests-install-dependencies script: - - ckeditor5-dev-tests-travis +- ckeditor5-dev-tests-travis after_success: - - codeclimate-test-reporter < coverage/lcov.info - - ckeditor5-dev-tests-save-revision +- ckeditor5-dev-tests-save-revision diff --git a/LICENSE.md b/LICENSE.md index 0cfa151..f8483c4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ Software License Agreement ========================== **CKEditor 5 Font Feature** – https://github.com/ckeditor/ckeditor5-font
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. +Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. Licensed under the terms of any of the following licenses at your choice: diff --git a/docs/_snippets/features/build-font-source.html b/docs/_snippets/features/build-font-source.html new file mode 100644 index 0000000..c4a4bca --- /dev/null +++ b/docs/_snippets/features/build-font-source.html @@ -0,0 +1,17 @@ + diff --git a/docs/_snippets/features/build-font-source.js b/docs/_snippets/features/build-font-source.js new file mode 100644 index 0000000..dd313b1 --- /dev/null +++ b/docs/_snippets/features/build-font-source.js @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals window */ + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor'; + +import Font from '@ckeditor/ckeditor5-font/src/font'; + +ClassicEditor.build.plugins.push( Font ); + +window.ClassicEditor = ClassicEditor; diff --git a/docs/_snippets/features/custom-font-family-options.html b/docs/_snippets/features/custom-font-family-options.html new file mode 100644 index 0000000..250aaa7 --- /dev/null +++ b/docs/_snippets/features/custom-font-family-options.html @@ -0,0 +1,13 @@ + + +
+

Font Family feature sample.

+ +

+ This text has "Ubuntu, Arial, Helvetica, sans-serif" font family set. +

+ +

+ This text has "Ubuntu Mono, Courier New, Courier, monospace" font family set. +

+
diff --git a/docs/_snippets/features/custom-font-family-options.js b/docs/_snippets/features/custom-font-family-options.js new file mode 100644 index 0000000..117859b --- /dev/null +++ b/docs/_snippets/features/custom-font-family-options.js @@ -0,0 +1,28 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ +ClassicEditor + .create( document.querySelector( '#snippet-custom-font-family-options' ), { + toolbar: { + items: [ + 'headings', '|', 'fontFamily', 'bulletedList', 'numberedList', 'undo', 'redo' + ], + viewportTopOffset: 60 + }, + fontFamily: { + options: [ + 'default', + 'Ubuntu, Arial, sans-serif', + 'Ubuntu Mono, Courier New, Courier, monospace' + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/custom-font-size-named-options.html b/docs/_snippets/features/custom-font-size-named-options.html new file mode 100644 index 0000000..7120e1b --- /dev/null +++ b/docs/_snippets/features/custom-font-size-named-options.html @@ -0,0 +1,9 @@ +
+

Font Size named options sample.

+ +

+ This is a mixed text with different sizes of text: + small and + big +

+
diff --git a/docs/_snippets/features/custom-font-size-named-options.js b/docs/_snippets/features/custom-font-size-named-options.js new file mode 100644 index 0000000..7fc515a --- /dev/null +++ b/docs/_snippets/features/custom-font-size-named-options.js @@ -0,0 +1,28 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ +ClassicEditor + .create( document.querySelector( '#snippet-custom-font-size-named-options' ), { + toolbar: { + items: [ + 'headings', '|', 'fontSize', 'bulletedList', 'numberedList', 'undo', 'redo' + ], + viewportTopOffset: 60 + }, + fontSize: { + options: [ + 'small', + 'normal', + 'big' + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/custom-font-size-numeric-options.html b/docs/_snippets/features/custom-font-size-numeric-options.html new file mode 100644 index 0000000..6bd000e --- /dev/null +++ b/docs/_snippets/features/custom-font-size-numeric-options.html @@ -0,0 +1,11 @@ +
+

Font Size feature numerical options sample.

+ +

9px

+

11px

+

13px

+

Normal

+

17px

+

19px

+

21px

+
diff --git a/docs/_snippets/features/custom-font-size-numeric-options.js b/docs/_snippets/features/custom-font-size-numeric-options.js new file mode 100644 index 0000000..6764cf5 --- /dev/null +++ b/docs/_snippets/features/custom-font-size-numeric-options.js @@ -0,0 +1,32 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ +ClassicEditor + .create( document.querySelector( '#snippet-custom-font-size-numeric-options' ), { + toolbar: { + items: [ + 'headings', '|', 'fontSize', 'bulletedList', 'numberedList', 'undo', 'redo' + ], + viewportTopOffset: 60 + }, + fontSize: { + options: [ + 9, + 11, + 13, + 'normal', + 17, + 19, + 21 + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/font.html b/docs/_snippets/features/font.html new file mode 100644 index 0000000..1b85e09 --- /dev/null +++ b/docs/_snippets/features/font.html @@ -0,0 +1,20 @@ +
+

Font feature sample.

+ +

+ This is a mixed text with different sizes of text: + tiny, + small, + big and + huge. +

+ +

+ This text has "Arial, Helvetica, sans-serif" family set. +

+ +

+ This text has "Courier New, Courier, monospace" family set. +

+ +
diff --git a/docs/_snippets/features/font.js b/docs/_snippets/features/font.js new file mode 100644 index 0000000..d90e39d --- /dev/null +++ b/docs/_snippets/features/font.js @@ -0,0 +1,22 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ + +ClassicEditor + .create( document.querySelector( '#snippet-font' ), { + toolbar: { + items: [ + 'headings', '|', 'fontSize', 'fontFamily', '|', 'bulletedList', 'numberedList', 'undo', 'redo' + ], + viewportTopOffset: 60 + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/api/font.md b/docs/api/font.md new file mode 100644 index 0000000..e035e9e --- /dev/null +++ b/docs/api/font.md @@ -0,0 +1,31 @@ +--- +category: api-reference +--- + +# CKEditor 5 font feature + +[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-font.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-font) + +This package implements the font family and font size features for CKEditor 5. + +## Documentation + +See the {@link features/font Font feature} guide +with corresponding {@link module:font/fontfamily~FontFamily} and {@link module:font/fontsize~FontSize} plugin documentation. + +## Installation + +```bash +npm install --save @ckeditor/ckeditor5-font +``` + +## Contribute + +The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-font. + +## External links + +* [`@ckeditor/ckeditor5-font` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-font) +* [`ckeditor/ckeditor5-font` on GitHub](https://github.com/ckeditor/ckeditor5-font) +* [Issue tracker](https://github.com/ckeditor/ckeditor5-font/issues) +* [Changelog](https://github.com/ckeditor/ckeditor5-font/blob/master/CHANGELOG.md) diff --git a/docs/features/font.md b/docs/features/font.md new file mode 100644 index 0000000..417239f --- /dev/null +++ b/docs/features/font.md @@ -0,0 +1,258 @@ +--- +title: Font +category: features +--- + +{@snippet features/build-font-source} + +The {@link module:font/font~Font} plugin enables the following features in the editor: +* {@link module:font/fontfamily~FontFamily} – allows to change the font family by applying inline `` elements with a `font-family` in the `style` attribute, +* {@link module:font/fontsize~FontSize} – allows to control size by applying inline `` elements that either have a CSS class or a `font-size` in the `style` attribute. + +## Demo + +{@snippet features/font} + +## Configuring the font family + +It is possible to configure which font family options are supported by the editor. Use the {@link module:font/fontfamily~FontFamilyConfig#options `fontFamily.options`} configuration option to do so. + +Use the special `'default'` keyword to use the default `font-family` defined in the web page styles (removes any custom font family). + +For example, the following editor supports only two font families besides the "default" one: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + fontFamily: { + options: [ + 'default', + 'Ubuntu, Arial, sans-serif', + 'Ubuntu Mono, Courier New, Courier, monospace' + ] + }, + toolbar: [ + 'headings', 'bulletedList', 'numberedList', 'fontFamily', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/custom-font-family-options} + +## Configuring the font size + +It is possible to configure which font size options are supported by the editor. Use the {@link module:font/fontsize~FontSizeConfig#options `fontSize.options`} configuration option to do so. + +Use the special `'normal'` keyword to use the default font size defined in the web page styles (removes any custom size). + +The font size feature supports two ways of defining configuration: using predefined (named) presets or simple numeric values. + +### Configuration using the predefined named presets + +The font size feature defines 4 named presets: +- `'tiny'` +- `'small'` +- `'big'` +- `'huge'` + +Each size is represented in the view as a `` element with the `text-*` class. For example, the `'tiny'` preset looks as follows in the editor data: + +```html +... +``` + +The CSS definition for the classes (presets) must be included: +- in the web page's styles where the editor runs (backend), +- in the web page's styles where the edited content is rendered (frontend) + +Here's an example of the font size CSS classes: + +```css +.text-tiny { + font-size: 0.7em; +} + +.text-small { + font-size: 0.85em; +} + +.text-big { + font-size: 1.4em; +} + +.text-huge { + font-size: 1.8em; +} +``` + +An example of the editor that supports only two font sizes: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + fontSize: { + options: [ + 'tiny', + 'normal', + 'big' + ] + }, + toolbar: [ + 'headings', 'bulletedList', 'numberedList', 'fontSize', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/custom-font-size-named-options} + +### Configuration using numerical values + +The font feature also supports numerical values. + +In this case, each size is represented in the view as a `` element with the `font-size` style set in `px`. +For example, `14` will be represented in the editor data as: + +```html +... +``` + +Here's an example of the editor that supports numerical font sizes. Note that `'normal'` is controlled by the default styles of the web page: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + fontSize: { + options: [ + 9, + 11, + 13, + 'normal', + 17, + 19, + 21 + ] + }, + toolbar: [ + 'headings', 'bulletedList', 'numberedList', 'fontSize', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/custom-font-size-numeric-options} + +## Installation + +To add this feature to your editor install the [`@ckeditor/ckeditor5-font`](https://www.npmjs.com/package/@ckeditor/ckeditor5-font) package: + +``` +npm install --save @ckeditor/ckeditor5-font +``` + +And add it to your plugin list and the toolbar configuration: + +```js +import Font from '@ckeditor/ckeditor5-font/src/font'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Font, ... ], + toolbar: [ 'fontSize', 'fontFamily', ... ] + } ) + .then( ... ) + .catch( ... ); +``` + +or add one of the font features to your plugin list and the toolbar configuration: + +```js +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ FontFamily, ... ], + toolbar: [ 'fontFamily', ... ] + } ) + .then( ... ) + .catch( ... ); +``` + + + Read more about {@link builds/guides/development/installing-plugins installing plugins}. + + +## Common API + +The {@link module:font/fontfamily~FontFamily} plugin registers: + +* 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. + + You can change the font family of the current selection by executing the command with a desired value: + + ```js + editor.execute( 'fontFamily', { value: 'Arial' } ); + ``` + + The `value` must correspond to the first font name in the configuration string. For the following default configuration: + ```js + fontFamily.options = [ + 'default', + 'Arial, Helvetica, sans-serif', + 'Courier New, Courier, monospace', + 'Georgia, serif', + 'Lucida Sans Unicode, Lucida Grande, sans-serif', + 'Tahoma, Geneva, sans-serif', + 'Times New Roman, Times, serif', + 'Trebuchet MS, Helvetica, sans-serif', + 'Verdana, Geneva, sans-serif' + ] + ``` + + the `fontFamily` command will accept the corresponding strings as values: + * `'Arial'` + * `'Courier New'` + * `'Georgia'` + * `'Lucida Sans Unicode'` + * `'Tahoma'` + * `'Times New Roman'` + * `'Trebuchet MS'` + * `'Verdana'` + + Note that passing an empty value will remove the `fontFamily` from the selection (`default`): + + ```js + editor.execute( 'fontFamily' ); + ``` + +The {@link module:font/fontsize~FontSize} plugin registers the following components: + +* 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. + + You can change the font size of the current selection by executing the command with a desired value: + + ```js + // For numerical values: + editor.execute( 'fontSize', { value: 10 } ); + + // For named presets: + editor.execute( 'fontSize', { value: 'small' } ); + ``` + passing an empty value will remove any `fontSize` set: + + ```js + editor.execute( 'fontSize' ); + ``` +## Contribute + +The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-font. diff --git a/lang/contexts.json b/lang/contexts.json new file mode 100644 index 0000000..5d6f2a0 --- /dev/null +++ b/lang/contexts.json @@ -0,0 +1,10 @@ +{ + "Font Size": "Tooltip for the font size drop-down.", + "Normal": "Drop-down option label for the normal font size.", + "Tiny": "Drop-down option label for the 'tiny' font size preset.", + "Small": "Drop-down option label for the 'small' font size preset.", + "Big": "Drop-down option label for the 'big' font size preset.", + "Huge": "Drop-down option label for the 'huge' font size preset.", + "Font Family": "Tooltip for the font family drop-down.", + "Default": "Drop-down option label for the default font family." +} diff --git a/package.json b/package.json index 57566e1..b0cffef 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "@ckeditor/ckeditor5-ui": "^1.0.0-alpha.2" }, "devDependencies": { - "eslint": "^4.8.0", - "eslint-config-ckeditor5": "^1.0.6", + "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2", + "eslint": "^4.15.0", + "eslint-config-ckeditor5": "^1.0.7", "husky": "^0.14.3", - "lint-staged": "^4.2.3" + "lint-staged": "^6.0.0" }, "engines": { "node": ">=6.0.0", diff --git a/src/font.js b/src/font.js new file mode 100644 index 0000000..4b89a37 --- /dev/null +++ b/src/font.js @@ -0,0 +1,38 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/font + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +import FontFamily from './fontfamily'; +import FontSize from './fontsize'; + +/** + * A plugin that enables (aggregates) a set of text styling features: + * * {@link module:font/fontsize~FontSize}, + * * {@link module:font/fontfamily~FontFamily}. + * + * Read more about the feature in the {@glink api/font font package} page. + * + * @extends module:core/plugin~Plugin + */ +export default class Font extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ FontFamily, FontSize ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'Font'; + } +} diff --git a/src/fontcommand.js b/src/fontcommand.js new file mode 100644 index 0000000..2f023f6 --- /dev/null +++ b/src/fontcommand.js @@ -0,0 +1,91 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontcommand + */ + +import Command from '@ckeditor/ckeditor5-core/src/command'; + +/** + * The base font command. + * + * @extends module:core/command~Command + */ +export default class FontCommand extends Command { + /** + * Creates an instance of the command. + * + * @param {module:core/editor/editor~Editor} editor Editor instance. + * @param {String} attributeKey Name of an model attribute on which this command operates. + */ + constructor( editor, attributeKey ) { + super( editor ); + + /** + * When set, it reflects the {@link #attributeKey} value of the selection. + * + * @observable + * @readonly + * @member {Boolean} module:font/fontcommand~FontCommand#value + */ + + /** + * A model attribute on which this command operates. + * + * @readonly + * @member {Boolean} module:font/fontcommand~FontCommand#attributeKey + */ + this.attributeKey = attributeKey; + } + + /** + * @inheritDoc + */ + refresh() { + const model = this.editor.model; + const doc = model.document; + + this.value = doc.selection.getAttribute( this.attributeKey ); + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, this.attributeKey ); + } + + /** + * Executes the command. Applies the `value` of the {@link #attributeKey} to the selection. + * If no `value` is passed, it removes the attribute from the selection. + * + * @protected + * @param {Object} [options] Options for the executed command. + * @param {String} [options.value] a value to apply. + * @fires execute + */ + execute( options = {} ) { + const model = this.editor.model; + const document = model.document; + const selection = document.selection; + + const value = options.value; + + model.change( writer => { + if ( selection.isCollapsed ) { + if ( value ) { + writer.setSelectionAttribute( this.attributeKey, value ); + } else { + writer.removeSelectionAttribute( this.attributeKey ); + } + } else { + const ranges = model.schema.getValidRanges( selection.getRanges(), this.attributeKey ); + + for ( const range of ranges ) { + if ( value ) { + writer.setAttribute( this.attributeKey, value, range ); + } else { + writer.removeAttribute( this.attributeKey, range ); + } + } + } + } ); + } +} diff --git a/src/fontfamily.js b/src/fontfamily.js new file mode 100644 index 0000000..d87d05b --- /dev/null +++ b/src/fontfamily.js @@ -0,0 +1,111 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontfamily + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import FontFamilyEditing from './fontfamily/fontfamilyediting'; +import FontFamilyUI from './fontfamily/fontfamilyui'; + +/** + * The font family plugin. + * + * It enables {@link module:font/fontfamily/fontfamilyediting~FontFamilyEditing} and + * {@link module:font/fontfamily/fontfamilyui~FontFamilyUI} features in the editor. + * + * @extends module:core/plugin~Plugin + */ +export default class FontFamily extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ FontFamilyEditing, FontFamilyUI ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'FontFamily'; + } +} + +/** + * Font family option descriptor. + * + * @typedef {Object} module:font/fontfamily~FontFamilyOption + * + * @property {String} title The user-readable title of the option. + * @property {String} model Attribute's unique value in the model. + * @property {module:engine/view/elementdefinition~ElementDefinition} view View element configuration. + * @property {Array.} [upcastAlso] An array with all matched elements that + * view to model conversion should also accept. + */ + +/** + * The configuration of the font family feature. + * Introduced by the {@link module:font/fontfamily/fontfamilyediting~FontFamilyEditing} feature. + * + * Read more in {@link module:font/fontfamily~FontFamilyConfig}. + * + * @member {module:font/fontfamily~FontFamilyConfig} module:core/editor/editorconfig~EditorConfig#fontFamily + */ + +/** + * The configuration of the font family feature. + * The option is used by the {@link module:font/fontfamily/fontfamilyediting~FontFamilyEditing} feature. + * + * ClassicEditor + * .create( { + * fontFamily: ... // Font family feature config. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface module:font/fontfamily~FontFamilyConfig + */ + +/** + * Available font family options defined as an array of strings. The default value is: + * + * const fontFamilyConfig = { + * options: [ + * 'default', + * 'Arial, Helvetica, sans-serif', + * 'Courier New, Courier, monospace', + * 'Georgia, serif', + * 'Lucida Sans Unicode, Lucida Grande, sans-serif', + * 'Tahoma, Geneva, sans-serif', + * 'Times New Roman, Times, serif', + * 'Trebuchet MS, Helvetica, sans-serif', + * 'Verdana, Geneva, sans-serif' + * ] + * }; + * + * which configures 8 font family options. Each option consist of one or more comma–separated font-family names. The first font name is + * used as a dropdown item description in the UI. + * + * **Note:** The family names that consist of spaces should not have quotes (as opposed to the CSS standard). The necessary quotes + * will be added automatically in the view. For example, the `"Lucida Sans Unicode"` will render as follows: + * + * ... + * + * The "default" option removes the `fontFamily` attribute from the selection. In such case, the text will + * be rendered in the view using the default font family defined in the styles of the web page. + * + * Font family can be applied using the command API. To do that, use the `fontFamily` command and pass the desired family as a `value`. + * For example, the following code will apply the `fontFamily` attribute with the `'Arial'` `value` to the current selection: + * + * editor.execute( 'fontFamily', { value: 'Arial' } ); + * + * Executing `fontFamily` command without any value will remove `fontFamily` attribute from the current selection. + * + * @member {Array.} module:font/fontfamily~FontFamilyConfig#options + */ diff --git a/src/fontfamily/fontfamilycommand.js b/src/fontfamily/fontfamilycommand.js new file mode 100644 index 0000000..91a5aab --- /dev/null +++ b/src/fontfamily/fontfamilycommand.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontfamily/fontfamilycommand + */ + +import FontCommand from '../fontcommand'; + +/** + * The font family command. It is used by the {@link module:font/fontfamily/fontfamilyediting~FontFamilyEditing} + * to apply the font family. + * + * editor.execute( 'fontFamily', { value: 'Arial' } ); + * + * **Note**: Executing the command without the value removes the attribute from the model. + * + * @extends module:font/fontcommand~FontCommand + */ +export default class FontFamilyCommand extends FontCommand { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor, 'fontFamily' ); + } +} diff --git a/src/fontfamily/fontfamilyediting.js b/src/fontfamily/fontfamilyediting.js new file mode 100644 index 0000000..a936ff5 --- /dev/null +++ b/src/fontfamily/fontfamilyediting.js @@ -0,0 +1,68 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontfamily/fontfamilyediting + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/two-way-converters'; + +import FontFamilyCommand from './fontfamilycommand'; +import { normalizeOptions } from './utils'; + +const FONT_FAMILY = 'fontFamily'; + +/** + * The font family editing feature. + * + * It introduces the {@link module:font/fontfamily/fontfamilycommand~FontFamilyCommand command} and + * the `fontFamily` attribute in the {@link module:engine/model/model~Model model} which renders + * in the {@link module:engine/view/view view} as an inline span (``), + * depending on the {@link module:font/fontfamily~FontFamilyConfig configuration}. + * + * @extends module:core/plugin~Plugin + */ +export default class FontFamilyEditing extends Plugin { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + // Define default configuration using font families shortcuts. + editor.config.define( FONT_FAMILY, { + options: [ + 'default', + 'Arial, Helvetica, sans-serif', + 'Courier New, Courier, monospace', + 'Georgia, serif', + 'Lucida Sans Unicode, Lucida Grande, sans-serif', + 'Tahoma, Geneva, sans-serif', + 'Times New Roman, Times, serif', + 'Trebuchet MS, Helvetica, sans-serif', + 'Verdana, Geneva, sans-serif' + ] + } ); + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + // Allow fontFamily attribute on text nodes. + editor.model.schema.extend( '$text', { allowAttributes: FONT_FAMILY } ); + + // Get configured font family options without "default" option. + const options = normalizeOptions( editor.config.get( 'fontFamily.options' ) ).filter( item => item.model ); + + // Set-up the two-way conversion. + attributeToElement( editor.conversion, FONT_FAMILY, options ); + + editor.commands.add( FONT_FAMILY, new FontFamilyCommand( editor ) ); + } +} diff --git a/src/fontfamily/fontfamilyui.js b/src/fontfamily/fontfamilyui.js new file mode 100644 index 0000000..abef119 --- /dev/null +++ b/src/fontfamily/fontfamilyui.js @@ -0,0 +1,117 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontfamily/fontfamilyui + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; +import Collection from '@ckeditor/ckeditor5-utils/src/collection'; + +import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import { normalizeOptions } from './utils'; +import fontFamilyIcon from '../../theme/icons/font-family.svg'; + +/** + * The font family UI plugin. It introduces the `'fontFamily'` drop-down. + * + * @extends module:core/plugin~Plugin + */ +export default class FontFamilyUI extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + + const options = this._getLocalizedOptions(); + + const command = editor.commands.get( 'fontFamily' ); + + // Register UI component. + editor.ui.componentFactory.add( 'fontFamily', locale => { + const dropdownView = createDropdown( locale ); + addListToDropdown( dropdownView, _prepareListOptions( options, command ) ); + + dropdownView.buttonView.set( { + label: t( 'Font Family' ), + icon: fontFamilyIcon, + tooltip: true + } ); + + dropdownView.extendTemplate( { + attributes: { + class: 'ck-font-family-dropdown' + } + } ); + + dropdownView.bind( 'isEnabled' ).to( command ); + + // Execute command when an item from the dropdown is selected. + this.listenTo( dropdownView, 'execute', evt => { + editor.execute( evt.source.commandName, { value: evt.source.commandParam } ); + editor.editing.view.focus(); + } ); + + return dropdownView; + } ); + } + + /** + * Returns options as defined in `config.fontFamily.options` but processed to consider + * editor localization, i.e. to display {@link module:font/fontfamily~FontFamilyOption} + * in the correct language. + * + * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t} + * when the user config is defined because the editor does not exist yet. + * + * @private + * @returns {Array.}. + */ + _getLocalizedOptions() { + const editor = this.editor; + const t = editor.t; + + const options = normalizeOptions( editor.config.get( 'fontFamily.options' ) ); + + return options.map( option => { + // The only title to localize is "Default" others are font names. + if ( option.title === 'Default' ) { + option.title = t( 'Default' ); + } + + return option; + } ); + } +} + +// Prepares FontFamily dropdown items. +// @private +// @param {Array.} options +// @param {module:font/fontsize/fontsizecommand~FontSizeCommand} command +function _prepareListOptions( options, command ) { + const dropdownItems = new Collection(); + + // Create dropdown items. + for ( const option of options ) { + const itemModel = new Model( { + commandName: 'fontFamily', + commandParam: option.model, + label: option.title + } ); + + itemModel.bind( 'isActive' ).to( command, 'value', value => value === option.model ); + + // Try to set a dropdown list item style. + if ( option.view && option.view.style ) { + itemModel.set( 'style', `font-family: ${ option.view.style[ 'font-family' ] }` ); + } + + dropdownItems.add( itemModel ); + } + return dropdownItems; +} diff --git a/src/fontfamily/utils.js b/src/fontfamily/utils.js new file mode 100644 index 0000000..3acdf5a --- /dev/null +++ b/src/fontfamily/utils.js @@ -0,0 +1,92 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontfamily/utils + */ + +/** + * Normalizes the {@link module:font/fontfamily~FontFamilyConfig#options config options} + * to the {@link module:font/fontfamily~FontFamilyOption} format. + * + * @param {Array.} configuredOptions An array of options taken from configuration. + * @returns {Array.} + */ +export function normalizeOptions( configuredOptions ) { + // Convert options to objects. + return configuredOptions + .map( getOptionDefinition ) + // Filter out undefined values that `getOptionDefinition` might return. + .filter( option => !!option ); +} + +// Returns an option definition either created from string shortcut. +// If object is passed then this method will return it without alternating it. Returns undefined for item than cannot be parsed. +// +// @param {String|Object} option +// @returns {undefined|module:font/fontfamily~FontFamilyOption} +function getOptionDefinition( option ) { + // Treat any object as full item definition provided by user in configuration. + if ( typeof option === 'object' ) { + return option; + } + + // Handle 'default' string as a special case. It will be used to remove the fontFamily attribute. + if ( option === 'default' ) { + return { + title: 'Default', + model: undefined + }; + } + + // Ignore values that we cannot parse to a definition. + if ( typeof option !== 'string' ) { + return; + } + + // Return font family definition from font string. + return generateFontPreset( option ); +} + +// Creates a predefined preset for pixel size. It deconstructs font-family like string into full configuration option. +// A font definition is passed as coma delimited set of font family names. Font names might be quoted. +// +// @param {String} A font definition form configuration. +function generateFontPreset( fontDefinition ) { + // Remove quotes from font names. They will be normalized later. + const fontNames = fontDefinition.replace( /"|'/g, '' ).split( ',' ); + + // The first matched font name will be used as dropdown list item title and as model value. + const firstFontName = fontNames[ 0 ]; + + // CSS-compatible font names. + const cssFontNames = fontNames.map( normalizeFontNameForCSS ).join( ', ' ); + + return { + title: firstFontName, + model: firstFontName, + view: { + name: 'span', + style: { + 'font-family': cssFontNames + } + } + }; +} + +// Normalizes font name for the style attribute. It adds braces (') if font name contains spaces. +// +// @param {String} fontName +// @returns {String} +function normalizeFontNameForCSS( fontName ) { + fontName = fontName.trim(); + + // Compound font names should be quoted. + if ( fontName.indexOf( ' ' ) > 0 ) { + fontName = `'${ fontName }'`; + } + + return fontName; +} diff --git a/src/fontsize.js b/src/fontsize.js new file mode 100644 index 0000000..fc2dbe1 --- /dev/null +++ b/src/fontsize.js @@ -0,0 +1,113 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontsize + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import FontSizeEditing from './fontsize/fontsizeediting'; +import FontSizeUI from './fontsize/fontsizeui'; + +/** + * The font size plugin. + * + * It enables {@link module:font/fontsize/fontsizeediting~FontSizeEditing} and + * {@link module:font/fontsize/fontsizeui~FontSizeUI} features in the editor. + * + * @extends module:core/plugin~Plugin + */ +export default class FontSize extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ FontSizeEditing, FontSizeUI ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'FontSize'; + } +} + +/** + * Font size option descriptor. + * + * @typedef {Object} module:font/fontsize~FontSizeOption + * + * @property {String} title The user-readable title of the option. + * @property {String} model Attribute's unique value in the model. + * @property {module:engine/view/elementdefinition~ElementDefinition} view View element configuration. + * @property {Array.} [upcastAlso] An array with all matched elements that + * view to model conversion should also accept. + */ + +/** + * The configuration of the font size feature. + * Introduced by the {@link module:font/fontsize/fontsizeediting~FontSizeEditing} feature. + * + * Read more in {@link module:font/fontsize~FontSizeConfig}. + * + * @member {module:font/fontsize~FontSizeConfig} module:core/editor/editorconfig~EditorConfig#fontSize + */ + +/** + * The configuration of the font size feature. + * The option is used by the {@link module:font/fontsize/fontsizeediting~FontSizeEditing} feature. + * + * ClassicEditor + * .create( { + * fontSize: ... // Font size feature config. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface module:font/fontsize~FontSizeConfig + */ + +/** + * Available font size options. Expressed as predefined presets, numerical "pixel" values + * or the {@link module:font/fontsize~FontSizeOption}. + * + * The default value is: + * + * const fontSizeConfig = { + * options: [ + * 'tiny', + * 'small', + * 'default', + * 'big', + * 'huge' + * ] + * }; + * + * It defines 4 sizes: **tiny**, **small**, **big**, and **huge**. These values will be rendered as `span` elements in view. + * The **default** defines a text without the `fontSize` attribute. + * + * Each `span` has the `class` attribute set to the corresponding size name. For instance, this is what the **small** size looks + * like in the view: + * + * ... + * + * As an alternative, the font size might be defined using the numerical values (either as a `Number` or as a `String`): + * + * const fontSizeConfig = { + * options: [ 9, 10, 11, 12, 13, 14, 15 ] + * }; + * + * 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 below code will apply the `fontSize` attribute with the **tiny** value to the current selection: + * + * editor.execute( 'fontSize', { value: 'tiny' } ); + * + * Executing `fontSize` command without value will remove `fontSize` attribute from the current selection. + * + * @member {Array.} module:font/fontsize~FontSizeConfig#options + */ diff --git a/src/fontsize/fontsizecommand.js b/src/fontsize/fontsizecommand.js new file mode 100644 index 0000000..293a8d9 --- /dev/null +++ b/src/fontsize/fontsizecommand.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontsize/fontsizecommand + */ + +import FontCommand from '../fontcommand'; + +/** + * The font size command. It is used by the {@link module:font/fontsize/fontsizeediting~FontSizeEditing} + * to apply the font size. + * + * editor.execute( 'fontSize', { value: 'small' } ); + * + * **Note**: Executing the command without the value removes the attribute from the model. + * + * @extends module:font/fontcommand~FontCommand + */ +export default class FontSizeCommand extends FontCommand { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor, 'fontSize' ); + } +} diff --git a/src/fontsize/fontsizeediting.js b/src/fontsize/fontsizeediting.js new file mode 100644 index 0000000..b2719f4 --- /dev/null +++ b/src/fontsize/fontsizeediting.js @@ -0,0 +1,68 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontsize/fontsizeediting + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/two-way-converters'; + +import FontSizeCommand from './fontsizecommand'; +import { normalizeOptions } from './utils'; + +const FONT_SIZE = 'fontSize'; + +/** + * The font size editing feature. + * + * It introduces the {@link module:font/fontsize/fontsizecommand~FontSizeCommand command} and the `fontSize` + * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view} + * as a `` element with either: + * * a style attribute (`...`), + * * or a class attribute (`...`) + * + * depending on the {@link module:font/fontsize~FontSizeConfig configuration}. + * + * @extends module:core/plugin~Plugin + */ +export default class FontSizeEditing extends Plugin { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + // Define default configuration using named presets. + editor.config.define( FONT_SIZE, { + options: [ + 'tiny', + 'small', + 'default', + 'big', + 'huge' + ] + } ); + + // Define view to model conversion. + const options = normalizeOptions( this.editor.config.get( 'fontSize.options' ) ).filter( item => item.model ); + + // Set-up the two-way conversion. + attributeToElement( editor.conversion, FONT_SIZE, options ); + + // Add FontSize command. + editor.commands.add( FONT_SIZE, new FontSizeCommand( editor ) ); + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + // Allow fontSize attribute on text nodes. + editor.model.schema.extend( '$text', { allowAttributes: FONT_SIZE } ); + } +} diff --git a/src/fontsize/fontsizeui.js b/src/fontsize/fontsizeui.js new file mode 100644 index 0000000..5af87dd --- /dev/null +++ b/src/fontsize/fontsizeui.js @@ -0,0 +1,135 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontsize/fontsizeui + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; +import Collection from '@ckeditor/ckeditor5-utils/src/collection'; + +import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import { normalizeOptions } from '../fontsize/utils'; +import fontSizeIcon from '../../theme/icons/font-size.svg'; + +/** + * The font family UI plugin. It introduces the `'fontSize'` drop-down. + * + * @extends module:core/plugin~Plugin + */ +export default class FontSizeUI extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + + const options = this._getLocalizedOptions(); + + const command = editor.commands.get( 'fontSize' ); + + // Register UI component. + editor.ui.componentFactory.add( 'fontSize', locale => { + const dropdownView = createDropdown( locale ); + addListToDropdown( dropdownView, _prepareListOptions( options, command ) ); + + // Create dropdown model. + dropdownView.buttonView.set( { + label: t( 'Font Size' ), + icon: fontSizeIcon, + tooltip: true + } ); + + dropdownView.extendTemplate( { + attributes: { + class: [ + 'ck-font-size-dropdown' + ] + } + } ); + + dropdownView.bind( 'isEnabled' ).to( command ); + + // Execute command when an item from the dropdown is selected. + this.listenTo( dropdownView, 'execute', evt => { + editor.execute( evt.source.commandName, { value: evt.source.commandParam } ); + editor.editing.view.focus(); + } ); + + return dropdownView; + } ); + } + + /** + * Returns options as defined in `config.fontSize.options` but processed to consider + * editor localization, i.e. to display {@link module:font/fontsize~FontSizeOption} + * in the correct language. + * + * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t} + * when the user config is defined because the editor does not exist yet. + * + * @private + * @returns {Array.}. + */ + _getLocalizedOptions() { + const editor = this.editor; + const t = editor.t; + + const localizedTitles = { + Default: t( 'Default' ), + Tiny: t( 'Tiny' ), + Small: t( 'Small' ), + Big: t( 'Big' ), + Huge: t( 'Huge' ) + }; + + const options = normalizeOptions( editor.config.get( 'fontSize.options' ) ); + + return options.map( option => { + const title = localizedTitles[ option.title ]; + + if ( title && title != option.title ) { + // Clone the option to avoid altering the original `namedPresets` from `./utils.js`. + option = Object.assign( {}, option, { title } ); + } + + return option; + } ); + } +} + +// Prepares FontSize dropdown items. +// @private +// @param {Array.} options +// @param {module:font/fontsize/fontsizecommand~FontSizeCommand} command +function _prepareListOptions( options, command ) { + const dropdownItems = new Collection(); + + for ( const option of options ) { + const itemModel = new Model( { + commandName: 'fontSize', + commandParam: option.model, + label: option.title, + class: 'ck-fontsize-option' + } ); + + if ( option.view && option.view.style ) { + itemModel.set( 'style', `font-size:${ option.view.style[ 'font-size' ] }` ); + } + + if ( option.view && option.view.class ) { + itemModel.set( 'class', `${ itemModel.class } ${ option.view.class }` ); + } + + itemModel.bind( 'isActive' ).to( command, 'value', value => value === option.model ); + + // Add the option to the collection. + dropdownItems.add( itemModel ); + } + + return dropdownItems; +} diff --git a/src/fontsize/utils.js b/src/fontsize/utils.js new file mode 100644 index 0000000..9472b6e --- /dev/null +++ b/src/fontsize/utils.js @@ -0,0 +1,114 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module font/fontsize/utils + */ + +/** + * Normalizes and translates the {@link module:font/fontsize~FontSizeConfig#options config options} + * to the {@link module:font/fontsize~FontSizeOption} format. + * + * @param {Array.} configuredOptions An array of options taken from configuration. + * @returns {Array.} + */ +export function normalizeOptions( configuredOptions ) { + // Convert options to objects. + return configuredOptions + .map( getOptionDefinition ) + // Filter out undefined values that `getOptionDefinition` might return. + .filter( option => !!option ); +} + +// Default named presets map. +const namedPresets = { + tiny: { + title: 'Tiny', + model: 'tiny', + view: { + name: 'span', + class: 'text-tiny' + } + }, + small: { + title: 'Small', + model: 'small', + view: { + name: 'span', + class: 'text-small' + } + }, + big: { + title: 'Big', + model: 'big', + view: { + name: 'span', + class: 'text-big' + } + }, + huge: { + title: 'Huge', + model: 'huge', + view: { + name: 'span', + class: 'text-huge' + } + } +}; + +// 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. +// +// @param {String|Number|Object} item +// @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; + } + + // Item is a named preset. + if ( namedPresets[ option ] ) { + return namedPresets[ option ]; + } + + // 'Default' font size. It will be used to remove the fontSize attribute. + if ( option === 'default' ) { + return { + model: undefined, + title: 'Default' + }; + } + + // 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 ) ) { + return; + } + + // Return font size definition from size value. + return generatePixelPreset( sizePreset ); +} + +// Creates a predefined preset for pixel size. +// +// @param {Number} size Font size in pixels. +// @returns {module:font/fontsize~FontSizeOption} +function generatePixelPreset( size ) { + const sizeName = String( size ); + + return { + title: sizeName, + model: size, + view: { + name: 'span', + style: { + 'font-size': `${ size }px` + } + } + }; +} diff --git a/tests/font.js b/tests/font.js new file mode 100644 index 0000000..07e02ed --- /dev/null +++ b/tests/font.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import Font from './../src/font'; +import FontFamily from './../src/fontfamily'; +import FontSize from './../src/fontsize'; + +describe( 'Font', () => { + it( 'requires FontSize', () => { + expect( Font.requires ).to.deep.equal( [ FontFamily, FontSize ] ); + } ); + + it( 'defines plugin name', () => { + expect( Font.pluginName ).to.equal( 'Font' ); + } ); +} ); diff --git a/tests/fontcommand.js b/tests/fontcommand.js new file mode 100644 index 0000000..3eff014 --- /dev/null +++ b/tests/fontcommand.js @@ -0,0 +1,279 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontCommand from '../src/fontcommand'; + +import Command from '@ckeditor/ckeditor5-core/src/command'; +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; + +describe( 'FontCommand', () => { + let editor, model, doc, root, command; + + beforeEach( () => { + return ModelTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot(); + + command = new FontCommand( editor, 'font' ); + editor.commands.add( 'font', command ); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'img', { + allowWhere: [ '$block', '$text' ], + isObject: true + } ); + + model.schema.extend( '$text', { allowAttributes: 'font' } ); + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'is a command', () => { + expect( FontCommand.prototype ).to.be.instanceOf( Command ); + expect( command ).to.be.instanceOf( Command ); + } ); + + describe( 'value', () => { + it( 'is set to font value when selection is in text with font attribute', () => { + setData( model, '<$text font="foo">fo[]o' ); + + expect( command ).to.have.property( 'value', 'foo' ); + } ); + + it( 'is undefined when selection is not in text with font attribute', () => { + setData( model, 'fo[]o' ); + + expect( command ).to.have.property( 'value', undefined ); + } ); + } ); + + describe( 'isEnabled', () => { + it( 'is true when selection is on text which can have font added', () => { + setData( model, 'fo[]o' ); + + expect( command ).to.have.property( 'isEnabled', true ); + } ); + } ); + + describe( 'execute()', () => { + it( 'should do nothing if the command is disabled', () => { + setData( model, 'fo[ob]ar' ); + + command.isEnabled = false; + + command.execute( { value: 'foo' } ); + + expect( getData( model ) ).to.equal( 'fo[ob]ar' ); + } ); + + it( 'should add font attribute on selected text', () => { + setData( model, 'a[bc<$text font="foo">fo]obarxyz' ); + + expect( command.value ).to.be.undefined; + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + + expect( getData( model ) ).to.equal( 'a[<$text font="foo">bcfo]obarxyz' ); + } ); + + it( 'should add font attribute on selected nodes (multiple nodes)', () => { + setData( + model, + 'abcabc[abc' + + 'foofoofoo' + + 'barbar]bar' + ); + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + + expect( getData( model ) ).to.equal( + 'abcabc[<$text font="foo">abc' + + '<$text font="foo">foofoofoo' + + '<$text font="foo">barbar]bar' + ); + } ); + + it( 'should change font attribute on selected nodes', () => { + setData( + model, + 'abc[abc<$text font="text-small">abc' + + '<$text font="text-small">foofoofoo' + + '<$text font="text-small">bar]barbar' + ); + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + + expect( getData( model ) ).to.equal( + 'abc[<$text font="foo">abcabc' + + '<$text font="foo">foofoofoo' + + '<$text font="foo">bar]<$text font="text-small">barbar' + ); + } ); + + it( 'should remove font attribute on selected nodes when passing undefined value', () => { + setData( + model, + 'abcabc[<$text font="foo">abc' + + '<$text font="foo">foofoofoo' + + '<$text font="foo">barbar]bar' + ); + expect( command.value ).to.equal( 'foo' ); + + command.execute(); + + expect( command.value ).to.be.undefined; + + expect( getData( model ) ).to.equal( + 'abcabc[abc' + + 'foofoofoo' + + 'barbar]bar' + ); + } ); + + it( 'should change selection attribute if selection is collapsed in non-empty parent', () => { + setData( model, 'a[]bc<$text font="foo">foobarxyz' ); + + expect( command.value ).to.be.undefined; + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + expect( doc.selection.hasAttribute( 'font' ) ).to.be.true; + + command.execute(); + + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'font' ) ).to.be.false; + } ); + + it( 'should not store attribute change on selection if selection is collapsed in non-empty parent', () => { + setData( model, 'a[]bc<$text font="foo">foobarxyz' ); + + command.execute( { value: 'foo' } ); + + // It should not save that bold was executed at position ( root, [ 0, 1 ] ). + + model.change( writer => { + // Simulate clicking right arrow key by changing selection ranges. + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + + // Get back to previous selection. + writer.setSelection( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] ); + } ); + + expect( command.value ).to.be.undefined; + } ); + + it( 'should change selection attribute and store it if selection is collapsed in empty parent', () => { + setData( model, 'abc<$text font="foo">foobarxyz[]' ); + + expect( command.value ).to.be.undefined; + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + expect( doc.selection.hasAttribute( 'font' ) ).to.be.true; + + // Attribute should be stored. + // Simulate clicking somewhere else in the editor. + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + } ); + + expect( command.value ).to.be.undefined; + + // Go back to where attribute was stored. + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] ); + } ); + + // Attribute should be restored. + expect( command.value ).to.equal( 'foo' ); + + command.execute(); + + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'font' ) ).to.be.false; + } ); + + it( 'should not apply attribute change where it would invalid schema', () => { + model.schema.register( 'image', { inheritAllFrom: '$block' } ); + setData( model, 'ab[c<$text font="foo">foobarxy]z' ); + + expect( command.isEnabled ).to.be.true; + + command.execute( { value: 'foo' } ); + + expect( getData( model ) ).to.equal( + 'ab[<$text font="foo">c<$text font="foo">foobarxy]z' + ); + } ); + + it( 'should use parent batch for storing undo steps', () => { + setData( model, 'a[bc<$text font="foo">fo]obarxyz' ); + + model.change( writer => { + expect( writer.batch.deltas.length ).to.equal( 0 ); + command.execute( { value: 'foo' } ); + expect( writer.batch.deltas.length ).to.equal( 1 ); + } ); + + expect( getData( model ) ).to.equal( 'a[<$text font="foo">bcfo]obarxyz' ); + } ); + + describe( 'should cause firing model change event', () => { + let spy; + + beforeEach( () => { + spy = sinon.spy(); + } ); + + it( 'collapsed selection in non-empty parent', () => { + setData( model, 'x[]y' ); + + model.document.on( 'change', spy ); + + command.execute( { value: 'foo' } ); + + expect( spy.called ).to.be.true; + } ); + + it( 'non-collapsed selection', () => { + setData( model, '[xy]' ); + + model.document.on( 'change', spy ); + + command.execute( { value: 'foo' } ); + + expect( spy.called ).to.be.true; + } ); + + it( 'in empty parent', () => { + setData( model, '[]' ); + + model.document.on( 'change', spy ); + + command.execute( { value: 'foo' } ); + + expect( spy.called ).to.be.true; + } ); + } ); + } ); +} ); diff --git a/tests/fontfamily.js b/tests/fontfamily.js new file mode 100644 index 0000000..b8b37d5 --- /dev/null +++ b/tests/fontfamily.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontFamily from './../src/fontfamily'; +import FontFamilyEditing from './../src/fontfamily/fontfamilyediting'; +import FontFamilyUI from '../src/fontfamily/fontfamilyui'; + +describe( 'FontFamily', () => { + it( 'requires FontFamilyEditing and FontFamilyUI', () => { + expect( FontFamily.requires ).to.deep.equal( [ FontFamilyEditing, FontFamilyUI ] ); + } ); + + it( 'defines plugin name', () => { + expect( FontFamily.pluginName ).to.equal( 'FontFamily' ); + } ); +} ); diff --git a/tests/fontfamily/fontfamilycommand.js b/tests/fontfamily/fontfamilycommand.js new file mode 100644 index 0000000..f5974f3 --- /dev/null +++ b/tests/fontfamily/fontfamilycommand.js @@ -0,0 +1,35 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontFamilyCommand from '../../src/fontfamily/fontfamilycommand'; +import FontCommand from '../../src/fontcommand'; + +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; + +describe( 'FontFamilyCommand', () => { + let editor, command; + + beforeEach( () => { + return ModelTestEditor.create() + .then( newEditor => { + editor = newEditor; + + command = new FontFamilyCommand( editor ); + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'is a FontCommand', () => { + expect( FontFamilyCommand.prototype ).to.be.instanceOf( FontCommand ); + expect( command ).to.be.instanceOf( FontCommand ); + } ); + + it( 'operates on fontFamily attribute', () => { + expect( command ).to.have.property( 'attributeKey', 'fontFamily' ); + } ); +} ); diff --git a/tests/fontfamily/fontfamilyediting.js b/tests/fontfamily/fontfamilyediting.js new file mode 100644 index 0000000..7669fbc --- /dev/null +++ b/tests/fontfamily/fontfamilyediting.js @@ -0,0 +1,201 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontFamilyEditing from './../../src/fontfamily/fontfamilyediting'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'FontFamilyEditing', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontFamilyEditing, Paragraph ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'should set proper schema rules', () => { + expect( editor.model.schema.checkAttribute( [ '$block', '$text' ], 'fontFamily' ) ).to.be.true; + expect( editor.model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'fontFamily' ) ).to.be.true; + + expect( editor.model.schema.checkAttribute( [ '$block' ], 'fontFamily' ) ).to.be.false; + } ); + + describe( 'config', () => { + describe( 'default value', () => { + it( 'should be set', () => { + expect( editor.config.get( 'fontFamily.options' ) ).to.deep.equal( [ + 'default', + 'Arial, Helvetica, sans-serif', + 'Courier New, Courier, monospace', + 'Georgia, serif', + 'Lucida Sans Unicode, Lucida Grande, sans-serif', + 'Tahoma, Geneva, sans-serif', + 'Times New Roman, Times, serif', + 'Trebuchet MS, Helvetica, sans-serif', + 'Verdana, Geneva, sans-serif' + ] ); + } ); + } ); + } ); + + describe( 'editing pipeline conversion', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontFamilyEditing, Paragraph ], + fontFamily: { + options: [ + 'Arial', + 'Lucida Sans Unicode, Lucida Grande, sans-serif', + { + title: 'My font', + model: 'my', + view: { + name: 'mark', + class: 'my-style' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + it( 'should discard unknown fontFamily attribute values', () => { + setModelData( doc, 'f<$text fontFamily="foo-bar">oo' ); + + expect( editor.getData() ).to.equal( '

foo

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

foo

' ); + } ); + + it( 'should convert fontFamily attribute to configured complex preset', () => { + setModelData( doc, 'f<$text fontFamily="Lucida Sans Unicode">oo' ); + + expect( editor.getData() ) + .to.equal( '

foo

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

foo

' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontFamilyEditing, Paragraph ], + fontFamily: { + options: [ + 'Lucida Sans Unicode, Lucida Grande, sans-serif', + { + title: 'My other setting', + model: 'my-other', + view: { + name: 'span', + style: { 'font-family': 'Other' } + } + }, + { + title: 'My setting', + model: 'my', + view: { + name: 'mark', + style: { 'font-family': 'Verdana' }, + class: 'my-style' + } + }, + { + title: 'Hybrid', + model: 'complex', + view: { + name: 'span', + class: [ 'text-complex' ] + }, + upcastAlso: [ + { name: 'span', style: { 'font-family': 'Arial' } }, + { name: 'span', style: { 'font-family': 'Arial,sans-serif' } }, + { name: 'span', attribute: { 'data-font': 'Arial' } } + ] + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + it( 'should convert from element with defined style when with other styles', () => { + const data = '

foo

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

foo

' ); + } ); + + it( 'should convert from user defined element', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontFamily="my">oo' ); + + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'should convert from complex definitions', () => { + editor.setData( + '

foo

' + + '

foo

' + + '

bar

' + + '

baz

' + ); + + expect( getModelData( doc ) ).to.equal( + '[]f<$text fontFamily="complex">oo' + + 'f<$text fontFamily="complex">oo' + + 'b<$text fontFamily="complex">ar' + + 'b<$text fontFamily="complex">az' + ); + + expect( editor.getData() ).to.equal( + '

foo

' + + '

foo

' + + '

bar

' + + '

baz

' + ); + } ); + } ); +} ); diff --git a/tests/fontfamily/fontfamilyui.js b/tests/fontfamily/fontfamilyui.js new file mode 100644 index 0000000..37e27d2 --- /dev/null +++ b/tests/fontfamily/fontfamilyui.js @@ -0,0 +1,163 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import FontFamilyEditing from '../../src/fontfamily/fontfamilyediting'; +import FontFamilyUI from '../../src/fontfamily/fontfamilyui'; + +import fontFamilyIcon from '../../theme/icons/font-family.svg'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; + +testUtils.createSinonSandbox(); + +describe( 'FontFamilyUI', () => { + let editor, command, element; + + before( () => { + addTranslations( 'en', { + 'Font Family': 'Font Family', + 'Default': 'Default' + } ); + + addTranslations( 'pl', { + 'Font Family': 'Czcionka', + 'Default': 'Domyślna' + } ); + } ); + + after( () => { + clearTranslations(); + } ); + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ FontFamilyEditing, FontFamilyUI ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + element.remove(); + + return editor.destroy(); + } ); + + describe( 'fontFamily Dropdown', () => { + let dropdown; + + beforeEach( () => { + command = editor.commands.get( 'fontFamily' ); + dropdown = editor.ui.componentFactory.create( 'fontFamily' ); + } ); + + it( 'button has the base properties', () => { + const button = dropdown.buttonView; + + expect( button ).to.have.property( 'label', 'Font Family' ); + expect( button ).to.have.property( 'tooltip', true ); + expect( button ).to.have.property( 'icon', fontFamilyIcon ); + } ); + + it( 'should add custom CSS class to dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'fontFamily' ); + + dropdown.render(); + + expect( dropdown.element.classList.contains( 'ck-font-family-dropdown' ) ).to.be.true; + } ); + + it( 'should focus view after command execution', () => { + const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); + const dropdown = editor.ui.componentFactory.create( 'fontFamily' ); + + dropdown.commandName = 'fontFamily'; + dropdown.fire( 'execute' ); + + sinon.assert.calledOnce( focusSpy ); + } ); + + it( 'should activate current option in dropdown', () => { + const listView = dropdown.listView; + + command.value = undefined; + + // The first item is 'default' font family. + expect( listView.items.map( item => item.isActive ) ) + .to.deep.equal( [ true, false, false, false, false, false, false, false, false ] ); + + command.value = 'Arial'; + + // The second item is 'Arial' font family. + expect( listView.items.map( item => item.isActive ) ) + .to.deep.equal( [ false, true, false, false, false, false, false, false, false ] ); + } ); + + describe( 'model to command binding', () => { + it( 'isEnabled', () => { + command.isEnabled = false; + + expect( dropdown.buttonView.isEnabled ).to.be.false; + + command.isEnabled = true; + expect( dropdown.buttonView.isEnabled ).to.be.true; + } ); + } ); + + describe( 'localization', () => { + beforeEach( () => { + return localizedEditor( [ 'default', 'Arial' ] ); + } ); + + it( 'works for the #buttonView', () => { + const buttonView = dropdown.buttonView; + + expect( buttonView.label ).to.equal( 'Czcionka' ); + } ); + + it( 'works for the listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.label ) ).to.deep.equal( [ + 'Domyślna', + 'Arial' + ] ); + } ); + + function localizedEditor( options ) { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ FontFamilyEditing, FontFamilyUI ], + toolbar: [ 'fontFamily' ], + language: 'pl', + fontFamily: { + options + } + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'fontFamily' ); + command = editor.commands.get( 'fontFamily' ); + + editorElement.remove(); + + return editor.destroy(); + } ); + } + } ); + } ); +} ); diff --git a/tests/fontfamily/utils.js b/tests/fontfamily/utils.js new file mode 100644 index 0000000..ffb4947 --- /dev/null +++ b/tests/fontfamily/utils.js @@ -0,0 +1,86 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { normalizeOptions } from '../../src/fontfamily/utils'; + +describe( 'FontFamily utils', () => { + describe( 'normalizeOptions()', () => { + it( 'should discard unsupported values', () => { + expect( normalizeOptions( [ () => {}, 0, true ] ) ).to.deep.equal( [] ); + } ); + + it( 'should pass through object definition', () => { + expect( normalizeOptions( [ + 'default', + { + title: 'Comic Sans', + model: 'comic', + view: { + name: 'span', + style: { + 'font-family': 'Comic Sans' + } + } + } + ] ) ).to.deep.equal( [ + { + model: undefined, + title: 'Default' + }, + { + title: 'Comic Sans', + model: 'comic', + view: { + name: 'span', + style: { + 'font-family': 'Comic Sans' + } + } + } + ] ); + } ); + + describe( 'shorthand presets', () => { + it( 'should return full preset from string presets', () => { + expect( normalizeOptions( ( [ + 'Arial', + '"Comic Sans MS", sans-serif', + 'Lucida Console, \'Courier New\', Courier, monospace' + ] ) ) ).to.deep.equal( [ + { + title: 'Arial', + model: 'Arial', + view: { + name: 'span', + style: { + 'font-family': 'Arial' + } + } + }, + { + title: 'Comic Sans MS', + model: 'Comic Sans MS', + view: { + name: 'span', + style: { + 'font-family': '\'Comic Sans MS\', sans-serif' + } + } + }, + { + title: 'Lucida Console', + model: 'Lucida Console', + view: { + name: 'span', + style: { + 'font-family': '\'Lucida Console\', \'Courier New\', Courier, monospace' + } + } + } + ] ); + } ); + } ); + } ); +} ); diff --git a/tests/fontsize.js b/tests/fontsize.js new file mode 100644 index 0000000..f7f42d1 --- /dev/null +++ b/tests/fontsize.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontSize from './../src/fontsize'; +import FontSizeEditing from './../src/fontsize/fontsizeediting'; +import FontSizeUI from './../src/fontsize/fontsizeui'; + +describe( 'FontSize', () => { + it( 'requires FontSizeEditing & FontSizeUI', () => { + expect( FontSize.requires ).to.deep.equal( [ FontSizeEditing, FontSizeUI ] ); + } ); + + it( 'defines plugin name', () => { + expect( FontSize.pluginName ).to.equal( 'FontSize' ); + } ); +} ); diff --git a/tests/fontsize/fontsizecommand.js b/tests/fontsize/fontsizecommand.js new file mode 100644 index 0000000..6fa67a4 --- /dev/null +++ b/tests/fontsize/fontsizecommand.js @@ -0,0 +1,35 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontSizeCommand from '../../src/fontsize/fontsizecommand'; +import FontCommand from '../../src/fontcommand'; + +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; + +describe( 'FontSizeCommand', () => { + let editor, command; + + beforeEach( () => { + return ModelTestEditor.create() + .then( newEditor => { + editor = newEditor; + + command = new FontSizeCommand( editor ); + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'is a FontCommand', () => { + expect( FontSizeCommand.prototype ).to.be.instanceOf( FontCommand ); + expect( command ).to.be.instanceOf( FontCommand ); + } ); + + it( 'operates on fontSize attribute', () => { + expect( command ).to.have.property( 'attributeKey', 'fontSize' ); + } ); +} ); diff --git a/tests/fontsize/fontsizeediting.js b/tests/fontsize/fontsizeediting.js new file mode 100644 index 0000000..8107966 --- /dev/null +++ b/tests/fontsize/fontsizeediting.js @@ -0,0 +1,220 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import FontSizeEditing from './../../src/fontsize/fontsizeediting'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'FontSizeEditing', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontSizeEditing, Paragraph ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'should set proper schema rules', () => { + expect( editor.model.schema.checkAttribute( [ '$block', '$text' ], 'fontSize' ) ).to.be.true; + expect( editor.model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'fontSize' ) ).to.be.true; + + expect( editor.model.schema.checkAttribute( [ '$block' ], 'fontSize' ) ).to.be.false; + } ); + + describe( 'config', () => { + describe( 'default value', () => { + it( 'should be set', () => { + expect( editor.config.get( 'fontSize.options' ) ).to.deep.equal( [ 'tiny', 'small', 'default', 'big', 'huge' ] ); + } ); + } ); + } ); + + describe( 'editing pipeline conversion', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontSizeEditing, Paragraph ], + fontSize: { + options: [ + 'tiny', + 'default', + 18, + { + title: 'My setting', + model: 'my', + view: { + name: 'mark', + style: { 'font-size': '30px' }, + class: 'my-style' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + it( 'should discard unknown fontSize attribute values', () => { + setModelData( doc, 'f<$text fontSize="foo-bar">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should convert fontSize attribute to predefined named preset', () => { + setModelData( doc, 'f<$text fontSize="tiny">oo' ); + + expect( editor.getData() ).to.equal( '

foo

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

foo

' ); + } ); + + it( 'should convert fontSize attribute from user defined settings', () => { + setModelData( doc, 'f<$text fontSize="my">oo' ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ FontSizeEditing, Paragraph ], + fontSize: { + options: [ + 'tiny', + 'default', + 18, + { + title: 'My setting', + model: 'my', + view: { + name: 'mark', + style: { 'font-size': '30px' }, + class: 'my-style' + } + }, + { + title: 'Big multiple classes', + model: 'big-multiple', + view: { + name: 'span', + class: [ 'foo', 'foo-big' ] + } + }, + { + title: 'Hybrid', + model: 'complex', + view: { + name: 'span', + class: [ 'text-complex' ] + }, + upcastAlso: [ + { name: 'span', style: { 'font-size': '77em' } }, + { name: 'span', attribute: { 'data-size': '77em' } } + ] + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.model; + } ); + } ); + + it( 'should convert from element with defined class', () => { + const data = '

foo

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

foo

' ); + } ); + + it( 'should convert from element with defined multiple classes', () => { + const data = '

foo

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

foo

' ); + } ); + + it( 'should convert from element with defined style', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="18">oo' ); + + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'should convert from element with defined style when with other styles', () => { + const data = '

foo

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

foo

' ); + } ); + + it( 'should convert from user defined element', () => { + const data = '

foo

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '[]f<$text fontSize="my">oo' ); + + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'should convert from complex definitions', () => { + editor.setData( + '

foo

' + + '

bar

' + + '

baz

' + ); + + expect( getModelData( doc ) ).to.equal( + '[]f<$text fontSize="complex">oo' + + 'b<$text fontSize="complex">ar' + + 'b<$text fontSize="complex">az' + ); + + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' + + '

baz

' + ); + } ); + } ); +} ); diff --git a/tests/fontsize/fontsizeui.js b/tests/fontsize/fontsizeui.js new file mode 100644 index 0000000..5787c04 --- /dev/null +++ b/tests/fontsize/fontsizeui.js @@ -0,0 +1,257 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import FontSizeEditing from '../../src/fontsize/fontsizeediting'; +import FontSizeUI from '../../src/fontsize/fontsizeui'; + +import fontSizeIcon from '../../theme/icons/font-size.svg'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { _clear as clearTranslations, add as addTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; +import { normalizeOptions } from '../../src/fontsize/utils'; + +testUtils.createSinonSandbox(); + +describe( 'FontSizeUI', () => { + let editor, command, element; + + before( () => { + addTranslations( 'en', { + 'Font Size': 'Font Size', + 'Default': 'Default', + 'Tiny': 'Tiny', + 'Small': 'Small', + 'Big': 'Big', + 'Huge': 'Huge' + } ); + + addTranslations( 'pl', { + 'Font Size': 'Rozmiar czcionki', + 'Default': 'Domyślny', + 'Tiny': 'Tyci', + 'Small': 'Mały', + 'Big': 'Duży', + 'Huge': 'Ogromny' + } ); + } ); + + after( () => { + clearTranslations(); + } ); + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ FontSizeEditing, FontSizeUI ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + element.remove(); + + return editor.destroy(); + } ); + + describe( 'fontSize Dropdown', () => { + let dropdown; + + beforeEach( () => { + command = editor.commands.get( 'fontSize' ); + dropdown = editor.ui.componentFactory.create( 'fontSize' ); + } ); + + it( 'button has the base properties', () => { + const button = dropdown.buttonView; + + expect( button ).to.have.property( 'label', 'Font Size' ); + expect( button ).to.have.property( 'tooltip', true ); + expect( button ).to.have.property( 'icon', fontSizeIcon ); + } ); + + it( 'should add custom CSS class to dropdown', () => { + dropdown.render(); + + expect( dropdown.element.classList.contains( 'ck-font-size-dropdown' ) ).to.be.true; + } ); + + it( 'should focus view after command execution', () => { + const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); + + dropdown.commandName = 'fontSize'; + dropdown.fire( 'execute' ); + + sinon.assert.calledOnce( focusSpy ); + } ); + + it( 'should activate current option in dropdown', () => { + const listView = dropdown.listView; + + command.value = undefined; + + // The third item is 'default' font size. + expect( listView.items.map( item => item.isActive ) ).to.deep.equal( [ false, false, true, false, false ] ); + + command.value = 'tiny'; + + // The first item is 'tiny' font size. + expect( listView.items.map( item => item.isActive ) ).to.deep.equal( [ true, false, false, false, false ] ); + } ); + + describe( 'model to command binding', () => { + it( 'isEnabled', () => { + command.isEnabled = false; + + expect( dropdown.buttonView.isEnabled ).to.be.false; + + command.isEnabled = true; + expect( dropdown.buttonView.isEnabled ).to.be.true; + } ); + } ); + + describe( 'config', () => { + describe( 'using presets', () => { + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ FontSizeEditing, FontSizeUI ], + fontSize: { + options: [ 'tiny', 'small', 'default', 'big', 'huge' ] + } + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'fontSize' ); + } ); + } ); + + it( 'adds css class to listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.class ) ).to.deep.equal( [ + 'ck-fontsize-option text-tiny', + 'ck-fontsize-option text-small', + 'ck-fontsize-option', + 'ck-fontsize-option text-big', + 'ck-fontsize-option text-huge' + ] ); + } ); + } ); + + describe( 'using numerical values', () => { + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ FontSizeEditing, FontSizeUI ], + fontSize: { + options: [ 10, 12, 'default', 16, 18 ] + } + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'fontSize' ); + } ); + } ); + + it( 'adds css class to listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.class ) ).to.deep.equal( [ + 'ck-fontsize-option', + 'ck-fontsize-option', + 'ck-fontsize-option', + 'ck-fontsize-option', + 'ck-fontsize-option' + ] ); + } ); + + it( 'adds font-size style to listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.style ) ).to.deep.equal( [ + 'font-size:10px', + 'font-size:12px', + undefined, + 'font-size:16px', + 'font-size:18px' + ] ); + } ); + } ); + } ); + + describe( 'localization', () => { + beforeEach( () => { + return localizedEditor( [ 'tiny', 'small', 'default', 'big', 'huge' ] ); + } ); + + it( 'does not alter normalizeOptions() internals', () => { + const options = normalizeOptions( [ 'tiny', 'small', 'default', 'big', 'huge' ] ); + expect( options ).to.deep.equal( [ + { title: 'Tiny', model: 'tiny', view: { name: 'span', class: 'text-tiny' } }, + { title: 'Small', model: 'small', view: { name: 'span', class: 'text-small' } }, + { title: 'Default', model: undefined }, + { title: 'Big', model: 'big', view: { name: 'span', class: 'text-big' } }, + { title: 'Huge', model: 'huge', view: { name: 'span', class: 'text-huge' } } + ] ); + } ); + + it( 'works for the #buttonView', () => { + const buttonView = dropdown.buttonView; + + expect( buttonView.label ).to.equal( 'Rozmiar czcionki' ); + } ); + + it( 'works for the listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.label ) ).to.deep.equal( [ + 'Tyci', + 'Mały', + 'Domyślny', + 'Duży', + 'Ogromny' + ] ); + } ); + + function localizedEditor( options ) { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ FontSizeEditing, FontSizeUI ], + toolbar: [ 'fontSize' ], + language: 'pl', + fontSize: { + options + } + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'fontSize' ); + command = editor.commands.get( 'fontSize' ); + + editorElement.remove(); + + return editor.destroy(); + } ); + } + } ); + } ); +} ); diff --git a/tests/fontsize/utils.js b/tests/fontsize/utils.js new file mode 100644 index 0000000..94069a5 --- /dev/null +++ b/tests/fontsize/utils.js @@ -0,0 +1,52 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { normalizeOptions } from '../../src/fontsize/utils'; + +describe( 'FontSizeEditing Utils', () => { + describe( 'normalizeOptions()', () => { + it( 'should discard unsupported values', () => { + expect( normalizeOptions( [ () => {}, 'default', 'unknown' ] ) ).to.deep.equal( [ { title: 'Default', model: undefined } ] ); + } ); + + it( 'should pass through object definition', () => { + expect( normalizeOptions( [ { + title: 'My Size', + model: 'my-size', + view: { name: 'span', style: 'font-size: 12em;' } + } ] ) ).to.deep.equal( [ + { + title: 'My Size', + model: 'my-size', + view: { name: 'span', style: 'font-size: 12em;' } + } + ] ); + } ); + + describe( 'named presets', () => { + it( 'should return defined presets', () => { + expect( normalizeOptions( [ 'tiny', 'small', 'default', 'big', 'huge' ] ) ).to.deep.equal( [ + { title: 'Tiny', model: 'tiny', view: { name: 'span', class: 'text-tiny' } }, + { title: 'Small', model: 'small', view: { name: 'span', class: 'text-small' } }, + { title: 'Default', model: undefined }, + { title: 'Big', model: 'big', view: { name: 'span', class: 'text-big' } }, + { title: 'Huge', model: 'huge', view: { name: 'span', class: 'text-huge' } } + ] ); + } ); + } ); + + 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', style: { 'font-size': '10px' } } }, + { title: '12', model: 12, view: { name: 'span', style: { 'font-size': '12px' } } }, + { title: 'Default', model: undefined }, + { title: '14.1', model: 14.1, view: { name: 'span', style: { 'font-size': '14.1px' } } }, + { title: '18.3', model: 18.3, view: { name: 'span', style: { 'font-size': '18.3px' } } } + ] ); + } ); + } ); + } ); +} ); diff --git a/tests/manual/font-family.html b/tests/manual/font-family.html new file mode 100644 index 0000000..e35a8fd --- /dev/null +++ b/tests/manual/font-family.html @@ -0,0 +1,45 @@ +
+

Font Family feature sample.

+ +

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. +

+ +

Courier New, Courier, monospace:

+

+ Jujubes sweet wafer. Pastry cotton candy sweet muffin dessert cookie. Chocolate cake candy canes dragée wafer donut bear claw. +

+ +

Georgia, serif:

+

+ Chocolate bar candy fruitcake fruitcake. Lollipop gingerbread pudding sweet roll biscuit halvah marzipan croissant soufflé. +

+ +

Lucida Sans Unicode, Lucida Grande, sans-serif:

+

+ Chocolate muffin apple pie toffee caramels chupa chups bear claw cotton candy. Lollipop dragée fruitcake fruitcake apple pie chocolate bar candy. +

+ +

Tahoma, Geneva, sans-serif:

+

+ Brownie biscuit donut gummies pie cheesecake. Dessert ice cream lemon drops candy soufflé cookie lemon drops. Chupa chups powder muffin sugar plum gummi bears fruitcake. +

+ +

Trebuchet MS, Helvetica, sans-serif:

+

+ Jujubes candy lollipop. Tootsie roll cookie chocolate gummies cupcake sweet fruitcake oat cake danish. +

+ +

Verdana, Geneva, sans-serif:

+

+ Danish brownie powder pie danish. Topping dragée oat cake caramels. Chocolate bar bonbon pastry apple pie icing chocolate danish carrot cake gummies. +

+ +
+ CKEditor logo +
+ Sample image with changed font caption. +
+
+
diff --git a/tests/manual/font-family.js b/tests/manual/font-family.js new file mode 100644 index 0000000..396dd02 --- /dev/null +++ b/tests/manual/font-family.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, 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' ), { + plugins: [ ArticlePluginSet, FontFamily ], + toolbar: [ + 'headings', '|', 'fontFamily', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/font-family.md b/tests/manual/font-family.md new file mode 100644 index 0000000..bdd7ecb --- /dev/null +++ b/tests/manual/font-family.md @@ -0,0 +1,12 @@ +### Loading + +The data should be loaded with paragraphs, each with different font. +Also the image caption should have "changed font" string with different font. + +### Testing + +Try to: +- Change font size by selecting many paragraphs. +- 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. diff --git a/tests/manual/font-size-numeric.html b/tests/manual/font-size-numeric.html new file mode 100644 index 0000000..6ef262a --- /dev/null +++ b/tests/manual/font-size-numeric.html @@ -0,0 +1,11 @@ +
+

Font Size feature sample.

+ +

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

+

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

+

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

+

Some text with the default size

+

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

+

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

+

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

+
diff --git a/tests/manual/font-size-numeric.js b/tests/manual/font-size-numeric.js new file mode 100644 index 0000000..b6a4c72 --- /dev/null +++ b/tests/manual/font-size-numeric.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, 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' ), { + plugins: [ ArticlePluginSet, FontSize ], + toolbar: [ + 'headings', '|', '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 ); + } ); diff --git a/tests/manual/font-size-numeric.md b/tests/manual/font-size-numeric.md new file mode 100644 index 0000000..bf60baf --- /dev/null +++ b/tests/manual/font-size-numeric.md @@ -0,0 +1,12 @@ +### Loading + +The data should be loaded with: +- 7 paragraphs with font sizes (10, 12, 14, default, 18, 20, 22), + +### Testing + +Try to: +- Change font by selecting many paragraphs. +- Change font by selecting some text. +- Change to default font by selecting many paragraphs. +- Change to default font by selecting some text. diff --git a/tests/manual/font-size-presets.html b/tests/manual/font-size-presets.html new file mode 100644 index 0000000..a757a47 --- /dev/null +++ b/tests/manual/font-size-presets.html @@ -0,0 +1,43 @@ + + +
+

Font Size feature sample.

+ +

+ This is a mixed text with different sizes of text: + tiny, + small, + big and + huge. +

+ +
    +
  • It's a list item with tiny text
  • +
  • It's a list item with small text
  • +
  • It's a list item with big text
  • +
  • It's a list item with huge text
  • +
+ +
+ CKEditor logo +
+ Sample image with crazy caption. +
+
+
diff --git a/tests/manual/font-size-presets.js b/tests/manual/font-size-presets.js new file mode 100644 index 0000000..8af7685 --- /dev/null +++ b/tests/manual/font-size-presets.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, 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' ), { + plugins: [ ArticlePluginSet, FontSize ], + toolbar: [ + 'headings', '|', 'fontSize', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/font-size-presets.md b/tests/manual/font-size-presets.md new file mode 100644 index 0000000..34c2488 --- /dev/null +++ b/tests/manual/font-size-presets.md @@ -0,0 +1,15 @@ +### Loading + +The data should be loaded with: +- heading with big fragment, +- paragraph with fragments with all font sizes (tiny, small, big, huge), +- list with 4 items - each with different font size fragment, +- image with caption with styled word. + +### Testing + +Try to: +- Change font by selecting many paragraphs. +- Change font by selecting some text. +- Change to default font by selecting many paragraphs. +- Change to default font by selecting some text. diff --git a/tests/manual/sample.jpg b/tests/manual/sample.jpg new file mode 100644 index 0000000..b77d07e Binary files /dev/null and b/tests/manual/sample.jpg differ diff --git a/theme/icons/font-family.svg b/theme/icons/font-family.svg new file mode 100644 index 0000000..330647f --- /dev/null +++ b/theme/icons/font-family.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/icons/font-size.svg b/theme/icons/font-size.svg new file mode 100644 index 0000000..bbae46a --- /dev/null +++ b/theme/icons/font-size.svg @@ -0,0 +1 @@ + \ No newline at end of file