UNPKG

9.57 kBJavaScriptView Raw
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
10import { Plugin } from 'ckeditor5/src/core';
11import { ButtonView, createDropdown, addToolbarToDropdown, SplitButtonView } from 'ckeditor5/src/ui';
12import ImageStyleEditing from './imagestyleediting';
13import utils from './utils';
14import { isObject, identity } from 'lodash-es';
15
16import '../../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 */
27export 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>}
214function 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}
230function 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}
239function 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 */