1 | /**
|
2 | * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
3 | * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4 | */
|
5 |
|
6 | /**
|
7 | * @module image/imagestyle/imagestyleui
|
8 | */
|
9 |
|
10 | import { Plugin } from 'ckeditor5/src/core';
|
11 | import { ButtonView, createDropdown, addToolbarToDropdown, SplitButtonView } from 'ckeditor5/src/ui';
|
12 | import ImageStyleEditing from './imagestyleediting';
|
13 | import utils from './utils';
|
14 | import { isObject, identity } from 'lodash-es';
|
15 |
|
16 | import '../../theme/imagestyle.css';
|
17 |
|
18 | /**
|
19 | * The image style UI plugin.
|
20 | *
|
21 | * It registers buttons corresponding to the {@link module:image/image~ImageConfig#styles} configuration.
|
22 | * It also registers the {@link module:image/imagestyle/utils~DEFAULT_DROPDOWN_DEFINITIONS default drop-downs} and the
|
23 | * custom drop-downs defined by the developer in the {@link module:image/image~ImageConfig#toolbar} configuration.
|
24 | *
|
25 | * @extends module:core/plugin~Plugin
|
26 | */
|
27 | export default class ImageStyleUI extends Plugin {
|
28 | /**
|
29 | * @inheritDoc
|
30 | */
|
31 | static get requires() {
|
32 | return [ ImageStyleEditing ];
|
33 | }
|
34 |
|
35 | /**
|
36 | * @inheritDoc
|
37 | */
|
38 | static get pluginName() {
|
39 | return 'ImageStyleUI';
|
40 | }
|
41 |
|
42 | /**
|
43 | * Returns the default localized style titles provided by the plugin.
|
44 | *
|
45 | * The following localized titles corresponding with
|
46 | * {@link module:image/imagestyle/utils~DEFAULT_OPTIONS} are available:
|
47 | *
|
48 | * * `'Wrap text'`,
|
49 | * * `'Break text'`,
|
50 | * * `'In line'`,
|
51 | * * `'Full size image'`,
|
52 | * * `'Side image'`,
|
53 | * * `'Left aligned image'`,
|
54 | * * `'Centered image'`,
|
55 | * * `'Right aligned image'`
|
56 | *
|
57 | * @returns {Object.<String,String>}
|
58 | */
|
59 | get localizedDefaultStylesTitles() {
|
60 | const t = this.editor.t;
|
61 |
|
62 | return {
|
63 | 'Wrap text': t( 'Wrap text' ),
|
64 | 'Break text': t( 'Break text' ),
|
65 | 'In line': t( 'In line' ),
|
66 | 'Full size image': t( 'Full size image' ),
|
67 | 'Side image': t( 'Side image' ),
|
68 | 'Left aligned image': t( 'Left aligned image' ),
|
69 | 'Centered image': t( 'Centered image' ),
|
70 | 'Right aligned image': t( 'Right aligned image' )
|
71 | };
|
72 | }
|
73 |
|
74 | /**
|
75 | * @inheritDoc
|
76 | */
|
77 | init() {
|
78 | const plugins = this.editor.plugins;
|
79 | const toolbarConfig = this.editor.config.get( 'image.toolbar' ) || [];
|
80 |
|
81 | const definedStyles = translateStyles(
|
82 | plugins.get( 'ImageStyleEditing' ).normalizedStyles,
|
83 | this.localizedDefaultStylesTitles
|
84 | );
|
85 |
|
86 | for ( const styleConfig of definedStyles ) {
|
87 | this._createButton( styleConfig );
|
88 | }
|
89 |
|
90 | const definedDropdowns = translateStyles(
|
91 | [ ...toolbarConfig.filter( isObject ), ...utils.getDefaultDropdownDefinitions( plugins ) ],
|
92 | this.localizedDefaultStylesTitles
|
93 | );
|
94 |
|
95 | for ( const dropdownConfig of definedDropdowns ) {
|
96 | this._createDropdown( dropdownConfig, definedStyles );
|
97 | }
|
98 | }
|
99 |
|
100 | /**
|
101 | * Creates a dropdown and stores it in the editor {@link module:ui/componentfactory~ComponentFactory}.
|
102 | *
|
103 | * @private
|
104 | * @param {module:image/imagestyle/imagestyleui~ImageStyleDropdownDefinition} dropdownConfig
|
105 | * @param {Array.<module:image/imagestyle~ImageStyleOptionDefinition>} definedStyles
|
106 | */
|
107 | _createDropdown( dropdownConfig, definedStyles ) {
|
108 | const factory = this.editor.ui.componentFactory;
|
109 |
|
110 | factory.add( dropdownConfig.name, locale => {
|
111 | let defaultButton;
|
112 |
|
113 | const { defaultItem, items, title } = dropdownConfig;
|
114 | const buttonViews = items
|
115 | .filter( itemName => definedStyles.find( ( { name } ) => getUIComponentName( name ) === itemName ) )
|
116 | .map( buttonName => {
|
117 | const button = factory.create( buttonName );
|
118 |
|
119 | if ( buttonName === defaultItem ) {
|
120 | defaultButton = button;
|
121 | }
|
122 |
|
123 | return button;
|
124 | } );
|
125 |
|
126 | if ( items.length !== buttonViews.length ) {
|
127 | utils.warnInvalidStyle( { dropdown: dropdownConfig } );
|
128 | }
|
129 |
|
130 | const dropdownView = createDropdown( locale, SplitButtonView );
|
131 | const splitButtonView = dropdownView.buttonView;
|
132 |
|
133 | addToolbarToDropdown( dropdownView, buttonViews );
|
134 |
|
135 | splitButtonView.set( {
|
136 | label: getDropdownButtonTitle( title, defaultButton.label ),
|
137 | class: null,
|
138 | tooltip: true
|
139 | } );
|
140 |
|
141 | splitButtonView.bind( 'icon' ).toMany( buttonViews, 'isOn', ( ...areOn ) => {
|
142 | const index = areOn.findIndex( identity );
|
143 |
|
144 | return ( index < 0 ) ? defaultButton.icon : buttonViews[ index ].icon;
|
145 | } );
|
146 |
|
147 | splitButtonView.bind( 'label' ).toMany( buttonViews, 'isOn', ( ...areOn ) => {
|
148 | const index = areOn.findIndex( identity );
|
149 |
|
150 | return getDropdownButtonTitle( title, ( index < 0 ) ? defaultButton.label : buttonViews[ index ].label );
|
151 | } );
|
152 |
|
153 | splitButtonView.bind( 'isOn' ).toMany( buttonViews, 'isOn', ( ...areOn ) => areOn.some( identity ) );
|
154 |
|
155 | splitButtonView.bind( 'class' )
|
156 | .toMany( buttonViews, 'isOn', ( ...areOn ) => areOn.some( identity ) ? 'ck-splitbutton_flatten' : null );
|
157 |
|
158 | splitButtonView.on( 'execute', () => {
|
159 | if ( !buttonViews.some( ( { isOn } ) => isOn ) ) {
|
160 | defaultButton.fire( 'execute' );
|
161 | } else {
|
162 | dropdownView.isOpen = !dropdownView.isOpen;
|
163 | }
|
164 | } );
|
165 |
|
166 | dropdownView.bind( 'isEnabled' )
|
167 | .toMany( buttonViews, 'isEnabled', ( ...areEnabled ) => areEnabled.some( identity ) );
|
168 |
|
169 | return dropdownView;
|
170 | } );
|
171 | }
|
172 |
|
173 | /**
|
174 | * Creates a button and stores it in the editor {@link module:ui/componentfactory~ComponentFactory}.
|
175 | *
|
176 | * @private
|
177 | * @param {module:image/imagestyle~ImageStyleOptionDefinition} buttonConfig
|
178 | */
|
179 | _createButton( buttonConfig ) {
|
180 | const buttonName = buttonConfig.name;
|
181 |
|
182 | this.editor.ui.componentFactory.add( getUIComponentName( buttonName ), locale => {
|
183 | const command = this.editor.commands.get( 'imageStyle' );
|
184 | const view = new ButtonView( locale );
|
185 |
|
186 | view.set( {
|
187 | label: buttonConfig.title,
|
188 | icon: buttonConfig.icon,
|
189 | tooltip: true,
|
190 | isToggleable: true
|
191 | } );
|
192 |
|
193 | view.bind( 'isEnabled' ).to( command, 'isEnabled' );
|
194 | view.bind( 'isOn' ).to( command, 'value', value => value === buttonName );
|
195 | view.on( 'execute', this._executeCommand.bind( this, buttonName ) );
|
196 |
|
197 | return view;
|
198 | } );
|
199 | }
|
200 |
|
201 | _executeCommand( name ) {
|
202 | this.editor.execute( 'imageStyle', { value: name } );
|
203 | this.editor.editing.view.focus();
|
204 | }
|
205 | }
|
206 |
|
207 | // Returns the translated `title` from the passed styles array.
|
208 | //
|
209 | // @param {Array.<module:image/imagestyle~ImageStyleOptionDefinition|
|
210 | // module:image/imagestyle/imagestyleui~ImageStyleDropdownDefinition>} styles
|
211 | // @param {Object.<String,String>} titles
|
212 | //
|
213 | // @returns {Array.<module:image/imagestyle~ImageStyleOptionDefinition|module:image/imagestyle/imagestyleui~ImageStyleDropdownDefinition>}
|
214 | function translateStyles( styles, titles ) {
|
215 | for ( const style of styles ) {
|
216 | // Localize the titles of the styles, if a title corresponds with
|
217 | // a localized default provided by the plugin.
|
218 | if ( titles[ style.title ] ) {
|
219 | style.title = titles[ style.title ];
|
220 | }
|
221 | }
|
222 |
|
223 | return styles;
|
224 | }
|
225 |
|
226 | // Returns the image style component name with the "imageStyle:" prefix.
|
227 | //
|
228 | // @param {String} name
|
229 | // @returns {String}
|
230 | function getUIComponentName( name ) {
|
231 | return `imageStyle:${ name }`;
|
232 | }
|
233 |
|
234 | // Returns title for the splitbutton containing the dropdown title and default action item title.
|
235 | //
|
236 | // @param {String|undefined} dropdownTitle
|
237 | // @param {String} buttonTitle
|
238 | // @returns {String}
|
239 | function getDropdownButtonTitle( dropdownTitle, buttonTitle ) {
|
240 | return ( dropdownTitle ? dropdownTitle + ': ' : '' ) + buttonTitle;
|
241 | }
|
242 |
|
243 | /**
|
244 | * # **The image style custom drop-down definition descriptor**
|
245 | *
|
246 | * This definition can be implemented in the {@link module:image/image~ImageConfig#toolbar image toolbar configuration}
|
247 | * to define a completely custom drop-down in the image toolbar.
|
248 | *
|
249 | * ClassicEditor.create( editorElement, {
|
250 | * image: { toolbar: [
|
251 | * // One of the predefined drop-downs
|
252 | * 'imageStyle:wrapText',
|
253 | * // Custom drop-down
|
254 | * {
|
255 | * name: 'imageStyle:customDropdown',
|
256 | * title: Custom drop-down title,
|
257 | * items: [ 'imageStyle:alignLeft', 'imageStyle:alignRight' ],
|
258 | * defaultItem: 'imageStyle:alignLeft'
|
259 | * }
|
260 | * ] }
|
261 | * } );
|
262 | *
|
263 | * **Note:** At the moment it is possible to populate the custom drop-down with only the buttons registered by the `ImageStyle` plugin.
|
264 | *
|
265 | * The defined drop-down will be registered
|
266 | * as the {@link module:ui/dropdown/dropdownview~DropdownView}
|
267 | * with the {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} under the provided name in the
|
268 | * {@link module:ui/componentfactory~ComponentFactory}
|
269 | *
|
270 | * @property {String} name The unique name of the drop-down. It is recommended to precede it with the "imageStyle:" prefix
|
271 | * to avoid collision with the components' names registered by other plugins.
|
272 | *
|
273 | * @property {String} [title] The drop-down's title. It will be used as the split button label along with the title of the default item
|
274 | * in the following manner: "Custom drop-down title: Default item title".
|
275 | *
|
276 | * Setting `title` to one of
|
277 | * {@link module:image/imagestyle/imagestyleui~ImageStyleUI#localizedDefaultStylesTitles}
|
278 | * will automatically translate it to the language of the editor.
|
279 | *
|
280 | * @property {Array.<String>} items The list of the names of the buttons that will be placed in the drop-down's toolbar.
|
281 | * Each of the buttons has to be one of the {@link module:image/image~ImageConfig#styles default image style buttons}
|
282 | * or to be defined as the {@link module:image/imagestyle~ImageStyleOptionDefinition image styling option}.
|
283 | *
|
284 | * @property {String} defaultItem The name of one of the buttons from the items list,
|
285 | * which will be used as a default button for the drop-down's split button.
|
286 | *
|
287 | * @typedef {Object} module:image/imagestyle/imagestyleui~ImageStyleDropdownDefinition
|
288 | */
|