@ckeditor/ckeditor5-style 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
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 style/stylecommand
8
+ */
9
+
10
+ import { Command } from 'ckeditor5/src/core';
11
+ import { logWarning, first } from 'ckeditor5/src/utils';
12
+
13
+ /**
14
+ * Style command.
15
+ *
16
+ * Applies and removes styles from selection and elements.
17
+ *
18
+ * @extends module:core/command~Command
19
+ */
20
+ export default class StyleCommand extends Command {
21
+ constructor( editor, styles ) {
22
+ super( editor );
23
+
24
+ /**
25
+ * Set of currently applied styles on current selection.
26
+ *
27
+ * Names of styles correspond to the `name` property of
28
+ * {@link module:style/style~StyleDefinition configured definitions}.
29
+ *
30
+ * @observable
31
+ * @readonly
32
+ * @member {Boolean|String} #value
33
+ */
34
+
35
+ /**
36
+ * Styles object. Helps in getting styles definitions by
37
+ * class name, style name and model element name.
38
+ *
39
+ * @private
40
+ * @readonly
41
+ * @member {module:style/styleediting~Styles}
42
+ */
43
+ this.styles = styles;
44
+
45
+ /**
46
+ * Names of enabled styles (styles that can be applied to the current selection).
47
+ *
48
+ * Names of enabled styles correspond to the `name` property of
49
+ * {@link module:style/style~StyleDefinition configured definitions}.
50
+ *
51
+ * @readonly
52
+ * @observable
53
+ * @member {Array.<String>} #enabledStyles
54
+ */
55
+ this.set( 'enabledStyles', [] );
56
+
57
+ /**
58
+ * Refresh state.
59
+ */
60
+ this.refresh();
61
+ }
62
+
63
+ /**
64
+ * @inheritDoc
65
+ */
66
+ refresh() {
67
+ let value = [];
68
+ const editor = this.editor;
69
+ const selection = editor.model.document.selection;
70
+ const block = first( selection.getSelectedBlocks() );
71
+
72
+ this.enabledStyles = [];
73
+
74
+ if ( !block || !editor.model.schema.isObject( block ) ) {
75
+ value = this._prepareNewInlineElementValue( value, selection );
76
+ this.enabledStyles = this.styles.getInlineElementsNames();
77
+
78
+ if ( block ) {
79
+ value = this._prepareNewBlockElementValue( value, block );
80
+ }
81
+ }
82
+
83
+ this.isEnabled = this.enabledStyles.length > 0;
84
+ this.value = this.isEnabled ? value : [];
85
+ }
86
+
87
+ /**
88
+ * Executes the command &mdash; applies the style classes to the selection or removes it from the selection.
89
+ *
90
+ * If the command value already contains the requested style, it will remove the style classes. Otherwise, it will set it.
91
+ *
92
+ * The execution result differs, depending on the {@link module:engine/model/document~Document#selection} and the
93
+ * style type (inline or block):
94
+ *
95
+ * * When applying inline styles:
96
+ * * If the selection is on a range, the command applies the style classes to all nodes in that range.
97
+ * * If the selection is collapsed in a non-empty node, the command applies the style classes to the
98
+ * {@link module:engine/model/document~Document#selection} itself (note that typed characters copy style classes from the selection).
99
+ *
100
+ * * When applying block styles:
101
+ * * If the selection is on a range, the command applies the style classes to the nearest block parent element.
102
+ *
103
+ * * When selection is set on a widget object:
104
+ * * Do nothing. Widgets are not yet supported by the style command.
105
+ *
106
+ * @fires execute
107
+ * @param {String} styleName Style name matching the one defined in the config.
108
+ */
109
+ execute( styleName ) {
110
+ if ( !this.enabledStyles.includes( styleName ) ) {
111
+ /**
112
+ * Style command can be executed only on a correct style name.
113
+ * This warning may be caused by passing name that it not specified in any of the
114
+ * definitions in the styles config, when trying to apply style that is not allowed
115
+ * on given element or passing class name instead of the style name.
116
+ *
117
+ * @error style-command-executed-with-incorrect-style-name
118
+ */
119
+ logWarning( 'style-command-executed-with-incorrect-style-name' );
120
+ return;
121
+ }
122
+
123
+ const editor = this.editor;
124
+ const model = editor.model;
125
+ const doc = model.document;
126
+ const selection = doc.selection;
127
+
128
+ const selectedBlockElement = first( selection.getSelectedBlocks() );
129
+ const definition = this.styles.getDefinitionsByName( styleName );
130
+
131
+ if ( selectedBlockElement && definition.isBlock ) {
132
+ this._handleStyleUpdate( definition, selectedBlockElement );
133
+ } else {
134
+ this._handleStyleUpdate( definition, selection );
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Adds or removes classes to element, range or selection.
140
+ *
141
+ * @private
142
+ * @param {Object} definition Style definition object.
143
+ * @param {module:engine/model/selection~Selectable} selectable Selection, range or element to update the style on.
144
+ */
145
+ _handleStyleUpdate( definition, selectable ) {
146
+ const { name, element, classes } = definition;
147
+ const htmlSupport = this.editor.plugins.get( 'GeneralHtmlSupport' );
148
+
149
+ if ( this.value.includes( name ) ) {
150
+ htmlSupport.removeModelHtmlClass( element, classes, selectable );
151
+ } else {
152
+ htmlSupport.addModelHtmlClass( element, classes, selectable );
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Returns inline element value.
158
+ *
159
+ * @private
160
+ * @param {Array} value
161
+ * @param {module:engine/model/selection~Selection} selection
162
+ */
163
+ _prepareNewInlineElementValue( value, selection ) {
164
+ let newValue = [ ...value ];
165
+
166
+ const attributes = selection.getAttributes();
167
+
168
+ for ( const [ attribute ] of attributes ) {
169
+ newValue = [ ...newValue, ...this._getAttributeValue( attribute ) ];
170
+ }
171
+
172
+ return newValue;
173
+ }
174
+
175
+ /**
176
+ * Returns element value and sets enabled styles.
177
+ *
178
+ * @private
179
+ * @param {Array} value
180
+ * @param {Object|null} element
181
+ * @return {Array} Current block element styles value.
182
+ */
183
+ _prepareNewBlockElementValue( value, element ) {
184
+ const availableDefinitions = this.styles.getDefinitionsByElementName( element.name );
185
+
186
+ if ( availableDefinitions ) {
187
+ const blockStyleNames = availableDefinitions.map( ( { name } ) => name );
188
+ this.enabledStyles = [ ...this.enabledStyles, ...blockStyleNames ];
189
+ }
190
+
191
+ return [ ...value, ...this._getAttributeValue( 'htmlAttributes' ) ];
192
+ }
193
+
194
+ /**
195
+ * Get classes attribute value.
196
+ *
197
+ * @private
198
+ * @param {String} attribute
199
+ */
200
+ _getAttributeValue( attribute ) {
201
+ const value = [];
202
+ const classes = attribute === 'htmlAttributes' ?
203
+ this._getValueFromBlockElement() :
204
+ this._getValueFromFirstAllowedNode( attribute );
205
+
206
+ for ( const htmlClass of classes ) {
207
+ const { name } = this.styles.getDefinitionsByClassName( htmlClass ) || {};
208
+
209
+ value.push( name );
210
+ }
211
+
212
+ return value;
213
+ }
214
+
215
+ /**
216
+ * Gets classes from currently selected block element.
217
+ *
218
+ * @private
219
+ */
220
+ _getValueFromBlockElement() {
221
+ const selection = this.editor.model.document.selection;
222
+ const block = first( selection.getSelectedBlocks() );
223
+ const attributes = block.getAttribute( 'htmlAttributes' );
224
+
225
+ if ( attributes ) {
226
+ return attributes.classes;
227
+ }
228
+
229
+ return [];
230
+ }
231
+
232
+ /**
233
+ * Gets classes from currently selected text element.
234
+ *
235
+ * @private
236
+ * @param {String} attributeName Text attribute name.
237
+ */
238
+ _getValueFromFirstAllowedNode( attributeName ) {
239
+ const model = this.editor.model;
240
+ const schema = model.schema;
241
+ const selection = model.document.selection;
242
+
243
+ if ( selection.isCollapsed ) {
244
+ /* istanbul ignore next */
245
+ const { classes } = selection.getAttribute( attributeName ) || {};
246
+
247
+ /* istanbul ignore next */
248
+ return classes || [];
249
+ }
250
+
251
+ for ( const range of selection.getRanges() ) {
252
+ for ( const item of range.getItems() ) {
253
+ /* istanbul ignore else */
254
+ if ( schema.checkAttribute( item, attributeName ) ) {
255
+ /* istanbul ignore next */
256
+ const { classes } = item.getAttribute( attributeName ) || {};
257
+
258
+ /* istanbul ignore next */
259
+ return classes || [];
260
+ }
261
+ }
262
+ }
263
+
264
+ /* istanbul ignore next */
265
+ return [];
266
+ }
267
+ }
@@ -0,0 +1,161 @@
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 style/styleediting
8
+ */
9
+
10
+ import { Plugin } from 'ckeditor5/src/core';
11
+ import { normalizeConfig } from './utils';
12
+
13
+ import StyleCommand from './stylecommand';
14
+
15
+ /**
16
+ * The style engine feature.
17
+ *
18
+ * It configures the {@glink features/general-html-support General HTML Support feature} based on
19
+ * {@link module:style/style~StyleConfig#definitions configured style definitions} and introduces the
20
+ * {@link module:style/stylecommand~StyleCommand style command} that applies styles to the content of the document.
21
+ *
22
+ * @extends module:core/plugin~Plugin
23
+ */
24
+ export default class StyleEditing extends Plugin {
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ static get pluginName() {
29
+ return 'StyleEditing';
30
+ }
31
+
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ static get requires() {
36
+ return [ 'GeneralHtmlSupport' ];
37
+ }
38
+
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ init() {
43
+ const editor = this.editor;
44
+ const dataSchema = editor.plugins.get( 'DataSchema' );
45
+ const normalizedStyleDefinitions = normalizeConfig( dataSchema, editor.config.get( 'style.definitions' ) );
46
+ const styles = new Styles( normalizedStyleDefinitions );
47
+
48
+ editor.commands.add( 'style', new StyleCommand( editor, styles ) );
49
+
50
+ this._configureGHSDataFilter( normalizedStyleDefinitions );
51
+ }
52
+
53
+ /**
54
+ * This is where the styles feature configures the GHS feature. This method translates normalized
55
+ * {@link module:style/style~StyleDefinition style definitions} to {@link module:engine/view/matcher~MatcherPattern matcher patterns}
56
+ * and feeds them to the GHS {@link module:html-support/datafilter~DataFilter} plugin.
57
+ *
58
+ * @private
59
+ * @param {Object} normalizedStyleDefinitions
60
+ */
61
+ _configureGHSDataFilter( { block: blockDefinitions, inline: inlineDefinitions } ) {
62
+ const ghsDataFilter = this.editor.plugins.get( 'DataFilter' );
63
+
64
+ ghsDataFilter.loadAllowedConfig( blockDefinitions.map( normalizedStyleDefinitionToMatcherPattern ) );
65
+ ghsDataFilter.loadAllowedConfig( inlineDefinitions.map( normalizedStyleDefinitionToMatcherPattern ) );
66
+ }
67
+ }
68
+
69
+ /**
70
+ * The helper class storing various mappings based on
71
+ * {@link module:style/style~StyleConfig#definitions configured style definitions}. Used internally by
72
+ * {@link module:style/stylecommand~StyleCommand}.
73
+ *
74
+ * @private
75
+ */
76
+ class Styles {
77
+ /**
78
+ * @param {Object} An object with normalized style definitions grouped into `block` and `inline` categories (arrays).
79
+ */
80
+ constructor( styleDefinitions ) {
81
+ this.styleTypes = [ 'inline', 'block' ];
82
+ this.styleDefinitions = styleDefinitions;
83
+ this.elementToDefinition = new Map();
84
+ this.classToDefinition = new Map();
85
+ this.nameToDefinition = new Map();
86
+
87
+ this._prepareDefinitionsMapping();
88
+ }
89
+
90
+ /**
91
+ * Populates various maps to simplify getting config definitions
92
+ * by model name,class name and style name.
93
+ *
94
+ * @private
95
+ */
96
+ _prepareDefinitionsMapping() {
97
+ for ( const type of this.styleTypes ) {
98
+ for ( const { modelElements, name, element, classes, isBlock } of this.styleDefinitions[ type ] ) {
99
+ for ( const modelElement of modelElements ) {
100
+ const currentValue = this.elementToDefinition.get( modelElement ) || [];
101
+ const newValue = [ ...currentValue, { name, element, classes } ];
102
+ this.elementToDefinition.set( modelElement, newValue );
103
+ }
104
+
105
+ this.classToDefinition.set( classes.join( ' ' ), { name, element, classes } );
106
+ this.nameToDefinition.set( name, { name, element, classes, isBlock } );
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Returns all inline definitions elements names.
113
+ *
114
+ * @protected
115
+ * @return {Array.<String>} Inline elements names.
116
+ */
117
+ getInlineElementsNames() {
118
+ return this.styleDefinitions.inline.map( ( { name } ) => name );
119
+ }
120
+
121
+ /**
122
+ * Returns the style config definitions by the model element name.
123
+ *
124
+ * @protected
125
+ * @return {Object} Style config definition.
126
+ */
127
+ getDefinitionsByElementName( elementName ) {
128
+ return this.elementToDefinition.get( elementName );
129
+ }
130
+
131
+ /**
132
+ * Returns the style config definitions by the style name.
133
+ *
134
+ * @protected
135
+ * @return {Object} Style config definition.
136
+ */
137
+ getDefinitionsByName( name ) {
138
+ return this.nameToDefinition.get( name );
139
+ }
140
+
141
+ /**
142
+ * Returns the style config definitions by the style name.
143
+ *
144
+ * @protected
145
+ * @return {Object} Style config definition.
146
+ */
147
+ getDefinitionsByClassName( className ) {
148
+ return this.classToDefinition.get( className );
149
+ }
150
+ }
151
+
152
+ // Translates a normalized style definition to a view matcher pattern.
153
+ //
154
+ // @param {Object} definition A normalized style definition.
155
+ // @returns {module:engine/view/matcher~MatcherPattern}
156
+ function normalizedStyleDefinitionToMatcherPattern( { element, classes } ) {
157
+ return {
158
+ name: element,
159
+ classes
160
+ };
161
+ }
package/src/styleui.js ADDED
@@ -0,0 +1,99 @@
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 style/styleui
8
+ */
9
+
10
+ import { Plugin } from 'ckeditor5/src/core';
11
+ import { createDropdown } from 'ckeditor5/src/ui';
12
+
13
+ import StylePanelView from './ui/stylepanelview';
14
+ import { normalizeConfig } from './utils';
15
+
16
+ import '../theme/style.css';
17
+
18
+ /**
19
+ * The UI plugin of the style feature .
20
+ *
21
+ * It registers the `'style'` UI dropdown in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
22
+ * that displays a grid of styles and allows changing styles of the content.
23
+ *
24
+ * @extends module:core/plugin~Plugin
25
+ */
26
+ export default class StyleUI extends Plugin {
27
+ /**
28
+ * @inheritDoc
29
+ */
30
+ static get pluginName() {
31
+ return 'StyleUI';
32
+ }
33
+
34
+ /**
35
+ * @inheritDoc
36
+ */
37
+ init() {
38
+ const editor = this.editor;
39
+ const dataSchema = editor.plugins.get( 'DataSchema' );
40
+ const normalizedStyleDefinitions = normalizeConfig( dataSchema, editor.config.get( 'style.definitions' ) );
41
+
42
+ // Add the dropdown fo the component factory.
43
+ editor.ui.componentFactory.add( 'style', locale => {
44
+ const t = locale.t;
45
+ const dropdown = createDropdown( locale );
46
+ const panelView = new StylePanelView( locale, normalizedStyleDefinitions );
47
+ const styleCommand = editor.commands.get( 'style' );
48
+
49
+ // The entire dropdown will be disabled together with the command (e.g. when the editor goes read-only).
50
+ dropdown.bind( 'isEnabled' ).to( styleCommand );
51
+
52
+ // Put the styles panel is the dropdown.
53
+ dropdown.panelView.children.add( panelView );
54
+
55
+ // This dropdown has no icon. It displays text label depending on the selection.
56
+ dropdown.buttonView.withText = true;
57
+
58
+ // The label of the dropdown is dynamic and depends on how many styles are active at a time.
59
+ dropdown.buttonView.bind( 'label' ).to( styleCommand, 'value', value => {
60
+ if ( value.length > 1 ) {
61
+ return t( 'Multiple styles' );
62
+ } else if ( value.length === 1 ) {
63
+ return value[ 0 ];
64
+ } else {
65
+ return t( 'Styles' );
66
+ }
67
+ } );
68
+
69
+ // The dropdown has a static CSS class for easy customization. There's another CSS class
70
+ // that gets displayed when multiple styles are active at a time allowing visual customization of
71
+ // the label.
72
+ dropdown.bind( 'class' ).to( styleCommand, 'value', value => {
73
+ const classes = [
74
+ 'ck-style-dropdown'
75
+ ];
76
+
77
+ if ( value.length > 1 ) {
78
+ classes.push( 'ck-style-dropdown_multiple-active' );
79
+ }
80
+
81
+ return classes.join( ' ' );
82
+ } );
83
+
84
+ // Close the dropdown when a style is selected in the styles panel.
85
+ panelView.delegate( 'execute' ).to( dropdown );
86
+
87
+ // Execute the command when a style is selected in the styles panel.
88
+ panelView.on( 'execute', evt => {
89
+ editor.execute( 'style', evt.source.styleDefinition.name );
90
+ } );
91
+
92
+ // Bind the state of the styles panel to the command.
93
+ panelView.bind( 'activeStyles' ).to( styleCommand, 'value' );
94
+ panelView.bind( 'enabledStyles' ).to( styleCommand, 'enabledStyles' );
95
+
96
+ return dropdown;
97
+ } );
98
+ }
99
+ }
@@ -0,0 +1,118 @@
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 style/ui/stylegridbuttonview
8
+ */
9
+
10
+ import {
11
+ ButtonView,
12
+ View
13
+ } from 'ckeditor5/src/ui';
14
+
15
+ // These are intermediate element names that can't be rendered as style preview because they don't make sense standalone.
16
+ const NON_PREVIEWABLE_ELEMENT_NAMES = [
17
+ 'caption', 'colgroup', 'dd', 'dt', 'figcaption', 'legend', 'li', 'optgroup', 'option', 'rp',
18
+ 'rt', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'
19
+ ];
20
+
21
+ /**
22
+ * A class representing an individual button (style) in the grid. Renders a rich preview of the style.
23
+ *
24
+ * @protected
25
+ * @extends {module:ui/button/buttonview~ButtonView}
26
+ */
27
+ export default class StyleGridButtonView extends ButtonView {
28
+ /**
29
+ * Creates an instance of the {@link module:style/ui/stylegridbuttonview~StyleGridButtonView} class.
30
+ *
31
+ * @param {module:utils/locale~Locale} locale The localization services instance.
32
+ * @param {module:style/style~StyleDefinition} styleDefinition Definition of the style.
33
+ */
34
+ constructor( locale, styleDefinition ) {
35
+ super( locale );
36
+
37
+ /**
38
+ * Definition of the style the button will apply when executed.
39
+ *
40
+ * @readonly
41
+ * @member {module:style/style~StyleDefinition} #styleDefinition
42
+ */
43
+ this.styleDefinition = styleDefinition;
44
+
45
+ /**
46
+ * The view rendering the preview of the style.
47
+ *
48
+ * @protected
49
+ * @readonly
50
+ * @member {module:ui/view~View} #previewView
51
+ */
52
+ this.previewView = this._createPreview();
53
+
54
+ this.set( {
55
+ label: styleDefinition.name,
56
+ class: 'ck-style-grid__button',
57
+ withText: true
58
+ } );
59
+
60
+ this.extendTemplate( {
61
+ attributes: {
62
+ role: 'option'
63
+ }
64
+ } );
65
+
66
+ this.children.add( this.previewView, 0 );
67
+ }
68
+
69
+ /**
70
+ * Creates the view representing the preview of the style.
71
+ *
72
+ * @private
73
+ * @returns {module:ui/view~View}
74
+ */
75
+ _createPreview() {
76
+ const { element, classes } = this.styleDefinition;
77
+ const previewView = new View( this.locale );
78
+
79
+ previewView.setTemplate( {
80
+ tag: 'div',
81
+
82
+ attributes: {
83
+ class: [
84
+ 'ck',
85
+ 'ck-reset_all-excluded',
86
+ 'ck-style-grid__button__preview',
87
+ 'ck-content'
88
+ ]
89
+ },
90
+
91
+ children: [
92
+ {
93
+ tag: this._isPreviewable( element ) ? element : 'div',
94
+ attributes: {
95
+ class: classes
96
+ },
97
+ children: [
98
+ { text: 'AaBbCcDdEeFfGgHhIiJj' }
99
+ ]
100
+ }
101
+ ]
102
+ } );
103
+
104
+ return previewView;
105
+ }
106
+
107
+ /**
108
+ * Decides whether an element should be created in the preview or a substitute `<div>` should
109
+ * be used instead. This avoids previewing a standalone `<td>`, `<li>`, etc. without a parent.
110
+ *
111
+ * @private
112
+ * @param {module:style/style~StyleDefinition} styleDefinition
113
+ * @returns {Boolean} `true` when the element can be rendered. `false` otherwise.
114
+ */
115
+ _isPreviewable( elementName ) {
116
+ return !NON_PREVIEWABLE_ELEMENT_NAMES.includes( elementName );
117
+ }
118
+ }