diff --git a/docs/_snippets/features/toolbar-nested-icon.html b/docs/_snippets/features/toolbar-nested-icon.html new file mode 100644 index 00000000000..d61f7b0c191 --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-icon.html @@ -0,0 +1,5 @@ +
+

+ The Red Planet is a target that has been aimed at for decades. Both Soviet (later Russian) and American designers looked at the sky and planned. Valeri Polyakov spent almost 438 days during a single long-term mission onboard the Mir space station to test the ability of humans to thrive in space for long periods of time. It was twenty years ago. +

+
diff --git a/docs/_snippets/features/toolbar-nested-icon.js b/docs/_snippets/features/toolbar-nested-icon.js new file mode 100644 index 00000000000..4f1198d7745 --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-icon.js @@ -0,0 +1,51 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals ClassicEditor, console, window, document */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#toolbar-nested-icon' ), { + toolbar: [ + { + label: 'Default icon', + items: [ 'bold', 'italic' ] + }, + { + label: 'Icon disabled', + icon: false, + items: [ 'bold', 'italic' ] + }, + { + label: 'Insert', + icon: 'plus', + items: [ 'uploadImage', 'insertTable' ] + }, + { + label: 'A drop-down with a custom icon', + // eslint-disable-next-line max-len + icon: '', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' + ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] + }, + cloudServices: CS_CONFIG, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/toolbar-nested-label.html b/docs/_snippets/features/toolbar-nested-label.html new file mode 100644 index 00000000000..a489246ba22 --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-label.html @@ -0,0 +1,5 @@ +
+

+ The Red Planet is a target that has been aimed at for decades. Both Soviet (later Russian) and American designers looked at the sky and planned. Valeri Polyakov spent almost 438 days during a single long-term mission onboard the Mir space station to test the ability of humans to thrive in space for long periods of time. It was twenty years ago. +

+
diff --git a/docs/_snippets/features/toolbar-nested-label.js b/docs/_snippets/features/toolbar-nested-label.js new file mode 100644 index 00000000000..a7699ca748b --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-label.js @@ -0,0 +1,33 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals ClassicEditor, console, window, document */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#toolbar-nested-label' ), { + toolbar: [ + { + label: 'Basic styles', + withText: true, + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' + ], + cloudServices: CS_CONFIG, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/toolbar-nested-simple.html b/docs/_snippets/features/toolbar-nested-simple.html new file mode 100644 index 00000000000..ab7dec4db5f --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-simple.html @@ -0,0 +1,5 @@ +
+

+ The Red Planet is a target that has been aimed at for decades. Both Soviet (later Russian) and American designers looked at the sky and planned. Valeri Polyakov spent almost 438 days during a single long-term mission onboard the Mir space station to test the ability of humans to thrive in space for long periods of time. It was twenty years ago. +

+
diff --git a/docs/_snippets/features/toolbar-nested-simple.js b/docs/_snippets/features/toolbar-nested-simple.js new file mode 100644 index 00000000000..1568a5b6633 --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-simple.js @@ -0,0 +1,33 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals ClassicEditor, console, window, document */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#toolbar-nested-simple' ), { + toolbar: [ + { + label: 'Basic styles', + icon: 'text', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' + ], + cloudServices: CS_CONFIG, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/toolbar-nested-tooltip.html b/docs/_snippets/features/toolbar-nested-tooltip.html new file mode 100644 index 00000000000..99601425450 --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-tooltip.html @@ -0,0 +1,5 @@ +
+

+ The Red Planet is a target that has been aimed at for decades. Both Soviet (later Russian) and American designers looked at the sky and planned. Valeri Polyakov spent almost 438 days during a single long-term mission onboard the Mir space station to test the ability of humans to thrive in space for long periods of time. It was twenty years ago. +

+
diff --git a/docs/_snippets/features/toolbar-nested-tooltip.js b/docs/_snippets/features/toolbar-nested-tooltip.js new file mode 100644 index 00000000000..cfc7d59436c --- /dev/null +++ b/docs/_snippets/features/toolbar-nested-tooltip.js @@ -0,0 +1,33 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals ClassicEditor, console, window, document */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +ClassicEditor + .create( document.querySelector( '#toolbar-nested-tooltip' ), { + toolbar: [ + { + label: 'Others', + tooltip: 'Additional editing features', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' + ], + cloudServices: CS_CONFIG, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/features/toolbar.md b/docs/features/toolbar.md index 3cb1ca112fe..84dc0987241 100644 --- a/docs/features/toolbar.md +++ b/docs/features/toolbar.md @@ -78,6 +78,118 @@ The demo below presents the "regular" toolbar look with `shouldNotGroupWhenFull` {@snippet features/toolbar-grouping} +## Grouping toolbar items in drop-downs (nested toolbars) + +To save space in your toolbar or arrange the features thematically, you can group several items into a dropdown. For instance, check out the following configuration: + +```js +toolbar: [ + { + label: 'Basic styles', + icon: 'text', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' +] +``` + +It will create a "Basic styles" dropdown with a "text" icon containing the "bold" and "italic" buttons. You can test it in the demo below. + +{@snippet features/toolbar-nested-simple} + +### Customization + +You can customize the look of the dropdown by configuring additional properties, such as the icon, label or tooltip text. + +#### Displaying the label + +You can control the way the UI element is displayed. For instance, to hide the icon and to display the label only, you can use the following configuration: + +```js +{ + label: 'Basic styles', + // Show the textual label of the dropdown. Note that the "icon" property is not configured. + withText: true, + items: [ 'bold', 'italic' ] +} +``` + +**Note**: The label will automatically show up if the `icon` is `false` ([learn more](#changing-the-icon)). + +{@snippet features/toolbar-nested-label} + +#### Changing the icon + +You can use one of the icons listed below for your dropdown: + +| Icon name | Preview | +|-------------------|-----------------------------------------------------------------------------| +| `'threeVerticalDots'` **(default)** | {@icon @ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg Three vertical dots} | +| `'alignLeft'` | {@icon @ckeditor/ckeditor5-core/theme/icons/align-left.svg Align left} | +| `'bold'` | {@icon @ckeditor/ckeditor5-core/theme/icons/bold.svg Bold} | +| `'importExport'` | {@icon @ckeditor/ckeditor5-core/theme/icons/importexport.svg Import export} | +| `'paragraph'` | {@icon @ckeditor/ckeditor5-core/theme/icons/paragraph.svg Paragraph} | +| `'text'` | {@icon @ckeditor/ckeditor5-core/theme/icons/text.svg Text} | + +* If no icon is specified, `'threeVerticalDots'` will be used as a default. +* If `icon: false` is configured, no icon will be displayed and the text label will show up instead. +* You can set a custom icon for the drop-down by passing an SVG string. + +Here is an example: + +```js +toolbar: [ + { + // Using a default icon because none was specified. + label: 'Default icon', + items: [ 'bold', 'italic' ] + }, + { + label: 'Icon disabled', + // This drop-down has the icon disabled and a text label instead. + icon: false, + items: [ 'bold', 'italic' ] + }, + { + label: 'Insert', + // A "plus" sign icon works best for content insertion tools. + icon: 'plus', + items: [ 'bold', 'italic' ] + }, + { + label: 'A dropdown with a custom icon', + // If you want your icon to change the color dynamically (e.g. when the dropdown opens), avoid fill="..." + // and stroke="..." styling attributes. Use solid shapes and avoid paths with strokes. + icon: '', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' +] +``` +And here is the effect: + +{@snippet features/toolbar-nested-icon} + +#### Customizing the tooltip + +By default, the tooltip of the button shares its text with the label. You can customize it to better describe your dropdown and make it more accessible by using the `tooltip` property ({@link module:ui/button/buttonview~ButtonView#tooltip learn more}): + +```js +toolbar: [ + { + label: 'Others', + tooltip: 'Additional editing features', + items: [ 'bold', 'italic' ] + }, + '|', + 'undo', 'redo' +] +``` + +{@snippet features/toolbar-nested-tooltip} + ## Multiline (wrapping) toolbar In the [extended toolbar configuration format](#extended-toolbar-configuration-format) it is also possible to arrange toolbar items into multiple lines. Here is how to achieve this: diff --git a/packages/ckeditor5-basic-styles/ckeditor5-metadata.json b/packages/ckeditor5-basic-styles/ckeditor5-metadata.json index 6ecb427a1bf..91308e78f17 100644 --- a/packages/ckeditor5-basic-styles/ckeditor5-metadata.json +++ b/packages/ckeditor5-basic-styles/ckeditor5-metadata.json @@ -10,7 +10,7 @@ { "type": "Button", "name": "bold", - "iconPath": "theme/icons/bold.svg" + "iconPath": "@ckeditor/ckeditor5-core/theme/icons/bold.svg" } ], "htmlOutput": [ diff --git a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md index f424ec5b98d..d4b4bdb1df5 100644 --- a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md +++ b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md @@ -12,7 +12,7 @@ The basic styles feature allows you to apply the most frequently used formatting Basic formatting options may be applied with the toolbar buttons (pictured below) or thanks to the {@link features/autoformat autoformatting feature} with Markdown code as you type. Use one these to format the text: -* Bold – Use the bold toolbar button {@icon @ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg Bold} or type `**text**` or `__text__` +* Bold – Use the bold toolbar button {@icon @ckeditor/ckeditor5-core/theme/icons/bold.svg Bold} or type `**text**` or `__text__` * Italic – Use the italic toolbar button {@icon @ckeditor/ckeditor5-basic-styles/theme/icons/italic.svg Italic} or type `*text*` or `_text_` * Code – Use the code toolbar button {@icon @ckeditor/ckeditor5-basic-styles/theme/icons/code.svg Code} or type ``` `text` ``` * Strikethrough – Use the strikethrough toolbar button {@icon @ckeditor/ckeditor5-basic-styles/theme/icons/strikethrough.svg Strikethrough} or type `~~text~~`. diff --git a/packages/ckeditor5-basic-styles/src/bold/boldui.js b/packages/ckeditor5-basic-styles/src/bold/boldui.js index 5b392b4de36..dfa4b7dcc1c 100644 --- a/packages/ckeditor5-basic-styles/src/bold/boldui.js +++ b/packages/ckeditor5-basic-styles/src/bold/boldui.js @@ -7,11 +7,9 @@ * @module basic-styles/bold/boldui */ -import { Plugin } from 'ckeditor5/src/core'; +import { Plugin, icons } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; -import boldIcon from '../../theme/icons/bold.svg'; - const BOLD = 'bold'; /** @@ -41,7 +39,7 @@ export default class BoldUI extends Plugin { view.set( { label: t( 'Bold' ), - icon: boldIcon, + icon: icons.bold, keystroke: 'CTRL+B', tooltip: true, isToggleable: true diff --git a/packages/ckeditor5-core/src/editor/editorconfig.jsdoc b/packages/ckeditor5-core/src/editor/editorconfig.jsdoc index f77415efc4a..22a99cb4eb3 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.jsdoc +++ b/packages/ckeditor5-core/src/editor/editorconfig.jsdoc @@ -110,7 +110,8 @@ * * **Note:** This configuration works only for simple plugins which utilize the * {@link module:core/plugin~PluginInterface plugin interface} and have no dependencies. To extend a - * build with complex features, create a {@glink installation/getting-started/quick-start-other#creating-custom-builds-with-online-builder custom build}. + * build with complex features, create a {@glink installation/getting-started/quick-start-other#creating-custom-builds-with-online-builder + * custom build}. * * **Note:** Make sure you include the new features in you toolbar configuration. Learn more * about the {@glink features/toolbar/toolbar toolbar setup}. @@ -119,7 +120,8 @@ */ /** - * The list of plugins which should not be loaded despite being available in an {@glink installation/getting-started/predefined-builds editor build}. + * The list of plugins which should not be loaded despite being available in an {@glink installation/getting-started/predefined-builds + * editor build}. * * const config = { * removePlugins: [ 'Bold', 'Italic' ] @@ -169,11 +171,90 @@ * * Line break will work only in the extended format when `shouldNotGroupWhenFull` option is set to `true`. * - * * **`toolbar.viewportTopOffset` (deprecated)** – The offset (in pixels) from the top of the viewport used when positioning a sticky toolbar. + * **Note**: To save space in your toolbar you can group several items into a dropdown: + * + * toolbar: [ + * { + * label: 'Basic styles', + * icon: 'text', + * items: [ 'bold', 'italic', ... ] + * }, + * '|', + * 'undo', 'redo' + * ] + * + * The code above will create a "Basic styles" dropdown with a "text" icon containing "bold" and "italic" buttons. + * You can customize the look of the dropdown by setting the `withText`, `icon`, and `tooltip` properties: + * + * * **Displaying a label** + * + * For instance, to hide the icon and to display the label only, you can use the following configuration: + * + * { + * label: 'Basic styles', + * // Show the textual label of the drop-down. Note that the "icon" property is not configured. + * withText: true, + * items: [ 'bold', 'italic', ... ] + * } + * + * * **Selecting an icon** + * + * You can use one of the common icons provided by the editor (`'bold'`, `'plus'`, `'text'`, `'importExport'`, `'alignLeft'`, + * `'paragraph'`, `'threeVerticalDots'`): + * + * { + * label: '...', + * // A "plus" sign icon works best for content insertion tools. + * icon: 'plus', + * items: [ ... ] + * } + * + * If no icon is specified, `'threeVerticalDots'` will be used as a default: + * + * // No icon specified, using a default one. + * { + * label: 'Default icon', + * items: [ ... ] + * } + * + * If `icon: false` is configured, no icon will be displayed at all and the text label will show up instead: + * + * // This drop-down has no icon. The text label will be displayed instead. + * { + * label: 'No icon', + * icon: false, + * items: [ ... ] + * } + * + * You can also set a custom icon for the drop-down by passing an SVG string: + * + * { + * label: '...', + * // If you want your icon to change the color dynamically (e.g. when the dropdown opens), avoid fill="..." + * // and stroke="..." styling attributes. Use solid shapes and avoid paths with strokes. + * icon: '...', + * items: [ ... ] + * } + * + * * **Customizing the tooltip** + * + * By default, the tooltip of the button shares its text with the label. You can customize it to better describe your dropdown + * using the `tooltip` property ({@link module:ui/button/buttonview~ButtonView#tooltip learn more}): + * + * { + * label: 'Drop-down label', + * tooltip: 'Custom tooltip of the drop-down', + * icon: '...', + * items: [ ... ] + * } + * + * * **`toolbar.viewportTopOffset` (deprecated)** – The offset (in pixels) from the top of the viewport used when positioning a + * sticky toolbar. * Useful when a page with which the editor is being integrated has some other sticky or fixed elements * (e.g. the top menu). Thanks to setting the toolbar offset the toolbar will not be positioned underneath or above the page's UI. * - * **This property has been deprecated and will be removed in the future versions of CKEditor. Please use `{@link module:core/editor/editorconfig~EditorConfig#ui EditorConfig#ui.viewportOffset}` instead.** + * **This property has been deprecated and will be removed in the future versions of CKEditor. Please use + * `{@link module:core/editor/editorconfig~EditorConfig#ui EditorConfig#ui.viewportOffset}` instead.** * * * **`toolbar.shouldNotGroupWhenFull`** – When set to `true`, the toolbar will stop grouping items * and let them wrap to the next line if there is not enough space to display them in a single row. @@ -315,27 +396,30 @@ * * Options which can be set using the UI config: * - * * **`ui.viewportOffset`** – The offset (in pixels) of the viewport from every direction used when positioning a sticky toolbar or other absolutely positioned UI elements. + * * **`ui.viewportOffset`** – The offset (in pixels) of the viewport from every direction used when positioning a sticky toolbar + * or other absolutely positioned UI elements. * Useful when a page with which the editor is being integrated has some other sticky or fixed elements - * (e.g. the top menu). Thanks to setting the UI viewport offset the toolbar and other contextual balloons will not be positioned underneath or above the page's UI. + * (e.g. the top menu). Thanks to setting the UI viewport offset the toolbar and other contextual balloons will not be positioned + * underneath or above the page's UI. * * ui: { * viewportOffset: { top: 10, right: 10, bottom: 10, left: 10 } * } * - * **Note:** If you want to modify the viewport offset in runtime (after editor was created), you can do that by overriding {@link module:core/editor/editorui~EditorUI#viewportOffset `editor.ui.viewportOffset`}. + * **Note:** If you want to modify the viewport offset in runtime (after editor was created), you can do that by overriding + * {@link module:core/editor/editorui~EditorUI#viewportOffset `editor.ui.viewportOffset`}. * * @member {Object} module:core/editor/editorconfig~EditorConfig#ui */ - /** +/** * Enables updating the source element after the editor destroy. * * Enabling this option might have some security implications, as the editor doesn't have control over all data * in the output. * - * Be careful, especially while using - * {@glink features/markdown Markdown}, {@glink features/general-html-support General HTML Support} or + * Be careful, especially while using + * {@glink features/markdown Markdown}, {@glink features/general-html-support General HTML Support} or * {@glink features/html-embed HTML embed} features. * * @member {Boolean} module:core/editor/editorconfig~EditorConfig#updateSourceElementOnDestroy diff --git a/packages/ckeditor5-core/src/index.js b/packages/ckeditor5-core/src/index.js index 28045c0f95f..f8d60ea5da2 100644 --- a/packages/ckeditor5-core/src/index.js +++ b/packages/ckeditor5-core/src/index.js @@ -58,14 +58,25 @@ import pilcrow from './../theme/icons/pilcrow.svg'; import quote from './../theme/icons/quote.svg'; import threeVerticalDots from './../theme/icons/three-vertical-dots.svg'; +import bold from './../theme/icons/bold.svg'; +import paragraph from './../theme/icons/paragraph.svg'; +import plus from './../theme/icons/plus.svg'; +import text from './../theme/icons/text.svg'; +import importExport from './../theme/icons/importexport.svg'; + export const icons = { + bold, cancel, caption, check, cog, eraser, - lowVision, image, + lowVision, + importExport, + paragraph, + plus, + text, alignBottom, alignMiddle, diff --git a/packages/ckeditor5-basic-styles/theme/icons/bold.svg b/packages/ckeditor5-core/theme/icons/bold.svg similarity index 100% rename from packages/ckeditor5-basic-styles/theme/icons/bold.svg rename to packages/ckeditor5-core/theme/icons/bold.svg diff --git a/packages/ckeditor5-core/theme/icons/importexport.svg b/packages/ckeditor5-core/theme/icons/importexport.svg new file mode 100644 index 00000000000..72c9e0e361a --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/importexport.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-paragraph/theme/icons/paragraph.svg b/packages/ckeditor5-core/theme/icons/paragraph.svg similarity index 100% rename from packages/ckeditor5-paragraph/theme/icons/paragraph.svg rename to packages/ckeditor5-core/theme/icons/paragraph.svg diff --git a/packages/ckeditor5-core/theme/icons/plus.svg b/packages/ckeditor5-core/theme/icons/plus.svg new file mode 100644 index 00000000000..384c0a100a5 --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/text.svg b/packages/ckeditor5-core/theme/icons/text.svg new file mode 100644 index 00000000000..73ba7776aa4 --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-paragraph/src/paragraphbuttonui.js b/packages/ckeditor5-paragraph/src/paragraphbuttonui.js index fe8b72b2e61..cb62aeda042 100644 --- a/packages/ckeditor5-paragraph/src/paragraphbuttonui.js +++ b/packages/ckeditor5-paragraph/src/paragraphbuttonui.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import icon from '../theme/icons/paragraph.svg'; +import icon from '@ckeditor/ckeditor5-core/theme/icons/paragraph.svg'; /** * This plugin defines the `'paragraph'` button. It can be used together with diff --git a/packages/ckeditor5-paragraph/tests/paragraphbuttonui.js b/packages/ckeditor5-paragraph/tests/paragraphbuttonui.js index 4bc9285e4fe..e9958c11617 100644 --- a/packages/ckeditor5-paragraph/tests/paragraphbuttonui.js +++ b/packages/ckeditor5-paragraph/tests/paragraphbuttonui.js @@ -11,7 +11,7 @@ import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import ParagraphButtonUI from '../src/paragraphbuttonui'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import icon from '../theme/icons/paragraph.svg'; +import icon from '@ckeditor/ckeditor5-core/theme/icons/paragraph.svg'; describe( 'HeadingButtonUI', () => { let editorElement, editor; diff --git a/packages/ckeditor5-theme-lark/package.json b/packages/ckeditor5-theme-lark/package.json index 0d1e1e0f687..a1e7179b037 100644 --- a/packages/ckeditor5-theme-lark/package.json +++ b/packages/ckeditor5-theme-lark/package.json @@ -28,7 +28,6 @@ "@ckeditor/ckeditor5-list": "^35.1.0", "@ckeditor/ckeditor5-media-embed": "^35.1.0", "@ckeditor/ckeditor5-page-break": "^35.1.0", - "@ckeditor/ckeditor5-paragraph": "^35.1.0", "@ckeditor/ckeditor5-remove-format": "^35.1.0", "@ckeditor/ckeditor5-restricted-editing": "^35.1.0", "@ckeditor/ckeditor5-select-all": "^35.1.0", diff --git a/packages/ckeditor5-theme-lark/tests/manual/iconset.js b/packages/ckeditor5-theme-lark/tests/manual/iconset.js index f8187c8280b..1f1f2b09e6a 100644 --- a/packages/ckeditor5-theme-lark/tests/manual/iconset.js +++ b/packages/ckeditor5-theme-lark/tests/manual/iconset.js @@ -9,7 +9,6 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; import Locale from '@ckeditor/ckeditor5-utils/src/locale'; -import bold from '@ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg'; import italic from '@ckeditor/ckeditor5-basic-styles/theme/icons/italic.svg'; import underline from '@ckeditor/ckeditor5-basic-styles/theme/icons/underline.svg'; import code from '@ckeditor/ckeditor5-basic-styles/theme/icons/code.svg'; @@ -54,8 +53,6 @@ import media from '@ckeditor/ckeditor5-media-embed/theme/icons/media.svg'; import pageBreak from '@ckeditor/ckeditor5-page-break/theme/icons/pagebreak.svg'; -import paragraph from '@ckeditor/ckeditor5-paragraph/theme/icons/paragraph.svg'; - import removeFormat from '@ckeditor/ckeditor5-remove-format/theme/icons/remove-format.svg'; import contentLock from '@ckeditor/ckeditor5-restricted-editing/theme/icons/contentlock.svg'; @@ -86,7 +83,7 @@ import '../../theme/ckeditor5-ui/components/editorui/editorui.css'; const icons = { // basic-styles - bold, italic, underline, code, strikethrough, subscript, superscript, + italic, underline, code, strikethrough, subscript, superscript, // ckfinder browseFiles, @@ -124,9 +121,6 @@ const icons = { // page-break pageBreak, - // paragraph - paragraph, - // remove-format removeFormat, diff --git a/packages/ckeditor5-theme-lark/tests/manual/theme.js b/packages/ckeditor5-theme-lark/tests/manual/theme.js index 92eed35dc05..f6ed719d459 100644 --- a/packages/ckeditor5-theme-lark/tests/manual/theme.js +++ b/packages/ckeditor5-theme-lark/tests/manual/theme.js @@ -24,7 +24,7 @@ import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarsepa import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils'; -import boldIcon from '@ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg'; +import boldIcon from '@ckeditor/ckeditor5-core/theme/icons/bold.svg'; import italicIcon from '@ckeditor/ckeditor5-basic-styles/theme/icons/italic.svg'; import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg'; import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg'; diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/toolbar/toolbar.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/toolbar/toolbar.css index d29db213f70..d208cb4c053 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/toolbar/toolbar.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/toolbar/toolbar.css @@ -93,6 +93,19 @@ } } + /* A drop-down containing the nested toolbar with configured items. */ + & .ck-toolbar__nested-toolbar-dropdown { + /* Prevent empty space in the panel when the dropdown label is visible and long but the toolbar has few items. */ + & > .ck-dropdown__panel { + min-width: auto; + } + + & > .ck-button > .ck-button__label { + max-width: 7em; + width: auto; + } + } + @nest .ck-toolbar-container & { border: 0; } diff --git a/packages/ckeditor5-ui/src/toolbar/toolbarview.js b/packages/ckeditor5-ui/src/toolbar/toolbarview.js index 8cf3ca3f284..b7542dcba34 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.js +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.js @@ -21,10 +21,24 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { createDropdown, addToolbarToDropdown } from '../dropdown/utils'; import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import normalizeToolbarConfig from './normalizetoolbarconfig'; +import { isObject } from 'lodash-es'; + import threeVerticalDots from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg'; import '../../theme/components/toolbar/toolbar.css'; +import { icons } from '@ckeditor/ckeditor5-core'; + +const NESTED_TOOLBAR_ICONS = { + alignLeft: icons.alignLeft, + bold: icons.bold, + importExport: icons.importExport, + paragraph: icons.paragraph, + plus: icons.plus, + text: icons.text, + threeVerticalDots: icons.threeVerticalDots +}; + /** * The toolbar view class. * @@ -330,7 +344,7 @@ export default class ToolbarView extends View { } // For the items that cannot be instantiated we are sending warning message. We also filter them out. - if ( !factory.has( name ) ) { + if ( !isObject( name ) && !factory.has( name ) ) { /** * There was a problem processing the configuration of the toolbar. The item with the given * name does not exist so it was omitted when rendering the toolbar. @@ -359,7 +373,10 @@ export default class ToolbarView extends View { const itemsToAdd = this._cleanSeparators( itemsToClean ) // Instantiate toolbar items. .map( name => { - if ( name === '|' ) { + if ( isObject( name ) ) { + return this._createNestedToolbarDropdown( name, factory ); + } + else if ( name === '|' ) { return new ToolbarSeparatorView(); } else if ( name === '-' ) { return new ToolbarLineBreakView(); @@ -405,6 +422,71 @@ export default class ToolbarView extends View { } ); } + /** + * Creates a user-defined dropdown containing a toolbar with items. + * + * @private + * @param {Object} definition A definition of the nested toolbar dropdown. + * @param {String} definition.label A label of the dropdown. + * @param {String|Boolean} [definition.icon] An icon of the drop-down. One of 'bold', 'plus', 'text', 'importExport', 'alignLeft', + * 'paragraph' or an SVG string. When `false` is passed, no icon will be used. + * @param {Boolean} [definition.withText=false] When set `true`, the label of the dropdown will be visible. See + * {@link module:ui/button/buttonview~ButtonView#withText} to learn more. + * @param {Boolean|String|Function} [definition.tooltip=true] A tooltip of the dropdown button. See + * {@link module:ui/button/buttonview~ButtonView#tooltip} to learn more. + * @param {module:ui/componentfactory~ComponentFactory} componentFactory Component factory used to create items + * of the nested toolbar. + * @returns {module:ui/dropdown/dropdownview~DropdownView} + */ + _createNestedToolbarDropdown( definition, componentFactory ) { + const { label, icon, items, tooltip = true, withText = false } = definition; + const locale = this.locale; + const dropdownView = createDropdown( locale ); + + if ( !label ) { + /** + * A dropdown definition in the toolbar configuration is missing a text label. + * + * Without a label, the dropdown becomes inaccessible to users relying on assistive technologies. + * Make sure the `label` property is set in your drop-down configuration: + * + * { + * label: 'A human-readable label', + * icon: '...', + * items: [ ... ] + * }, + * + * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}. + * + * @error toolbarview-nested-toolbar-dropdown-missing-label + */ + logWarning( 'toolbarview-nested-toolbar-dropdown-missing-label', definition ); + } + + dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown'; + dropdownView.buttonView.set( { + label, + tooltip, + withText: !!withText + } ); + + // Allow disabling icon by passing false. + if ( icon !== false ) { + // A pre-defined icon picked by name, SVG string, a fallback (default) icon. + dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon ] || icon || NESTED_TOOLBAR_ICONS.threeVerticalDots; + } + // If the icon is disabled, display the label automatically. + else { + dropdownView.buttonView.withText = true; + } + + addToolbarToDropdown( dropdownView, [] ); + + dropdownView.toolbarView.fillFromConfig( items, componentFactory ); + + return dropdownView; + } + /** * Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change * in the toolbar geometry. diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js index 71c1187768d..c5e1bd5887a 100644 --- a/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js +++ b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js @@ -9,7 +9,7 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor' import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Heading from '@ckeditor/ckeditor5-heading/src/heading'; -import boldIcon from '@ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg'; +import boldIcon from '@ckeditor/ckeditor5-core/theme/icons/bold.svg'; import ButtonView from '../../../../src/button/buttonview'; function customButtonView( editor ) { diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.html b/packages/ckeditor5-ui/tests/manual/toolbar/nested.html new file mode 100644 index 00000000000..9d67179b20c --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.html @@ -0,0 +1,16 @@ +
+

Basic features overview

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit.1

+

Basic styles

+

Ad alias, architecto culpa cumque dignissimos dolor eos incidunt ipsa itaque laboriosam magnam molestias nihil numquam odit quam, suscipit veritatis voluptate voluptatum.2

+ +

Blockquote

+
+

Quote

+ +

Quote

+
+
diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js new file mode 100644 index 00000000000..47aa1fec70e --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js @@ -0,0 +1,174 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor'; +import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; +import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily'; +import FontSize from '@ckeditor/ckeditor5-font/src/fontsize'; +import Image from '@ckeditor/ckeditor5-image/src/image'; +import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + ArticlePluginSet, + Code, + FontBackgroundColor, + FontColor, + FontFamily, + FontSize, + Image, + ImageInsert, + Strikethrough, + Subscript, + Superscript, + Underline + ], + toolbar: { + items: [ + 'heading', + '|', + 'bold', 'italic', 'link', + 'bulletedList', 'numberedList', + '|', + 'undo', 'redo', + '-', + { + label: 'A label and an icon', + icon: 'text', + withText: true, + items: [ + 'strikethrough', 'underline', 'code', 'subscript', 'superscript', + '|', + 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor' + ] + }, + { + label: 'Just label', + icon: false, + items: [ + 'strikethrough', 'underline', 'code', 'subscript', 'superscript', + '|', + 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor' + ] + }, + { + label: 'Just icon', + icon: 'plus', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + '-', + { + label: 'Icon: default', + withText: true, + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "bold"', + withText: true, + icon: 'bold', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "plus"', + withText: true, + icon: 'plus', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "text"', + withText: true, + icon: 'text', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "importExport"', + withText: true, + icon: 'importExport', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "alignLeft"', + withText: true, + icon: 'alignLeft', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "paragraph"', + withText: true, + icon: 'paragraph', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: "threeVerticalDots"', + withText: true, + icon: 'threeVerticalDots', + items: [ + 'insertImage', 'insertTable', 'mediaEmbed' + ] + }, + { + label: 'Icon: custom SVG', + withText: true, + // eslint-disable-next-line max-len + icon: '', + items: [ + 'bold', 'italic', 'link' + ] + }, + '-', + { + label: 'Custom tooltip (hover me)', + withText: true, + tooltip: 'Custom tooltip of the drop-down', + items: [ + 'bold', 'italic', 'link' + ] + } + ], + shouldNotGroupWhenFull: true + }, + image: { + toolbar: [ + 'imageTextAlternative' + ], + insert: { + integrations: [ + 'insertImageViaUrl' + ] + } + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.md b/packages/ckeditor5-ui/tests/manual/toolbar/nested.md new file mode 100644 index 00000000000..d8e6f0fdda1 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.md @@ -0,0 +1,9 @@ +# Configurable nested toolbars + +This manual test uses several nested (sub-)toolbars to group items and save space. + +1. The first row of the toolbar contains regular toolbar buttons (unrelated). +1. In the 2nd row of the toolbar, different customizations are showcased. Make sure they all work. +1. In the 3rd row of the toolbar, all pre-defined icons are showcased. Make sure they all display correctly. + * The last icon (CKEditor 5 logo) was passed as an SVG string. Make sure it works too. +1. In the 4th row of the toolbar, a drop-down with a custom tooltip is rendered. Hover the button to see the tooltip. diff --git a/packages/ckeditor5-ui/tests/toolbar/toolbarview.js b/packages/ckeditor5-ui/tests/toolbar/toolbarview.js index bc67955f8fc..b5bf45a9eac 100644 --- a/packages/ckeditor5-ui/tests/toolbar/toolbarview.js +++ b/packages/ckeditor5-ui/tests/toolbar/toolbarview.js @@ -21,6 +21,9 @@ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import Locale from '@ckeditor/ckeditor5-utils/src/locale'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; import ToolbarLineBreakView from '../../src/toolbar/toolbarlinebreakview'; +import DropdownView from '../../src/dropdown/dropdownview'; + +import { icons } from '@ckeditor/ckeditor5-core'; describe( 'ToolbarView', () => { let locale, view; @@ -650,6 +653,220 @@ describe( 'ToolbarView', () => { expect( items.get( 0 ).name ).to.equal( 'foo' ); expect( items.get( 1 ).name ).to.equal( 'bar' ); } ); + + describe( 'nested drop-downs with toolbar', () => { + let dropdownView, toolbarView; + + it( 'should create a drop-down with the default look and configured items', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ] + } + ], factory ); + + dropdownView = view.items.get( 1 ); + toolbarView = dropdownView.toolbarView; + + const items = view.items; + + expect( items ).to.have.length( 2 ); + expect( items.get( 0 ).name ).to.equal( 'foo' ); + expect( items.get( 1 ) ).to.be.instanceOf( DropdownView ); + + expect( dropdownView.buttonView.label, 'label' ).to.equal( 'Some label' ); + expect( dropdownView.buttonView.withText, 'withText' ).to.be.false; + expect( dropdownView.buttonView.icon, 'icon' ).to.equal( icons.threeVerticalDots ); + expect( dropdownView.buttonView.tooltip, 'tooltip' ).to.be.true; + + const nestedToolbarItems = toolbarView.items; + + expect( nestedToolbarItems.get( 0 ).name ).to.equal( 'bar' ); + expect( nestedToolbarItems.get( 1 ) ).to.be.instanceOf( ToolbarSeparatorView ); + expect( nestedToolbarItems.get( 2 ).name ).to.equal( 'foo' ); + } ); + + it( 'should set proper CSS class on the drop-down', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: 'plus' + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.class ).to.equal( 'ck-toolbar__nested-toolbar-dropdown' ); + } ); + + it( 'should allow configuring the drop-down\'s label', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: 'plus' + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.label ).to.equal( 'Some label' ); + } ); + + it( 'should allow configuring the drop-down\'s label visibility', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: 'plus', + withText: true + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.withText ).to.be.true; + } ); + + it( 'should allow configuring the drop-down\'s icon by SVG string', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: '' + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.icon ).to.equal( '' ); + } ); + + it( 'should allow disabling the drop-down\'s icon by passing false (text label shows up instead)', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + icon: false, + items: [ 'bar', '|', 'foo' ] + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.icon ).to.be.undefined; + expect( dropdownView.buttonView.withText ).to.be.true; + } ); + + describe( 'pre-configured icons', () => { + const iconNames = [ + 'alignLeft', + 'bold', + 'importExport', + 'paragraph', + 'plus', + 'text', + 'threeVerticalDots' + ]; + + for ( const name of iconNames ) { + it( `should provide the "${ name }" icon`, () => { + view.fillFromConfig( [ + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: name + } + ], factory ); + + dropdownView = view.items.get( 0 ); + + expect( dropdownView.buttonView.icon ).to.equal( icons[ name ] ); + } ); + } + } ); + + it( 'should fall back to a default icon when none was provided', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ] + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.icon ).to.equal( icons.threeVerticalDots ); + expect( dropdownView.buttonView.withText ).to.be.false; + } ); + + it( 'should allow configuring the drop-down\'s tooltip', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Some label', + items: [ 'bar', '|', 'foo' ], + icon: 'plus', + tooltip: 'Foo bar' + } + ], factory ); + + dropdownView = view.items.get( 1 ); + + expect( dropdownView.buttonView.tooltip ).to.equal( 'Foo bar' ); + } ); + + it( 'should allow deep nested structures', () => { + view.fillFromConfig( [ + 'foo', + { + label: 'Level 0', + items: [ + 'bar', + '|', + { + label: 'Level 1', + icon: 'bold', + items: [ 'bar' ] + } + ], + icon: 'plus', + tooltip: 'Foo bar' + } + ], factory ); + + const level0DropdownView = view.items.get( 1 ); + const level1DropdownView = level0DropdownView.toolbarView.items.get( 2 ); + + expect( level1DropdownView.toolbarView.items.length ).to.equal( 1 ); + expect( level1DropdownView.toolbarView.items.get( 0 ).name ).to.equal( 'bar' ); + } ); + + it( 'should warn when the drop-down has no label', () => { + const warnStub = testUtils.sinon.stub( console, 'warn' ); + const brokenDefinition = { + items: [ 'bar', '|', 'foo' ], + icon: 'plus', + tooltip: 'Foo bar' + }; + + view.fillFromConfig( [ 'foo', brokenDefinition ], factory ); + + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledWithExactly( warnStub, + sinon.match( /^toolbarview-nested-toolbar-dropdown-missing-label/ ), + brokenDefinition, + sinon.match.string // Link to the documentation + ); + } ); + } ); } ); describe( 'toolbar with static items', () => {