diff --git a/docs/features/image.md b/docs/features/image.md index cbd66991..0e4992bf 100644 --- a/docs/features/image.md +++ b/docs/features/image.md @@ -148,7 +148,7 @@ See the result below: ### Defining custom styles -Besides using the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles 5 predefined styles}: +Besides using the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing.defaultStyles 5 predefined styles}: * `'imageStyleFull'`, * `'imageStyleSide'`, diff --git a/src/image.js b/src/image.js index 4920cf2a..bbbff2bc 100644 --- a/src/image.js +++ b/src/image.js @@ -8,17 +8,16 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageEngine from './image/imageengine'; +import ImageEditing from '../src/image/imageediting'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ImageTextAlternative from './imagetextalternative'; -import { isImageWidgetSelected } from './image/utils'; import '../theme/image.css'; /** * The image plugin. * - * Uses the {@link module:image/image/imageengine~ImageEngine}. + * Uses the {@link module:image/image/imageediting~ImageEditing}. * * @extends module:core/plugin~Plugin */ @@ -27,7 +26,7 @@ export default class Image extends Plugin { * @inheritDoc */ static get requires() { - return [ ImageEngine, Widget, ImageTextAlternative ]; + return [ ImageEditing, Widget, ImageTextAlternative ]; } /** @@ -36,25 +35,6 @@ export default class Image extends Plugin { static get pluginName() { return 'Image'; } - - /** - * @inheritDoc - */ - init() { - const editor = this.editor; - const balloonToolbar = editor.plugins.get( 'BalloonToolbar' ); - - // If `BalloonToolbar` plugin is loaded, it should be disabled for images - // which have their own toolbar to avoid duplication. - // https://github.com/ckeditor/ckeditor5-image/issues/110 - if ( balloonToolbar ) { - this.listenTo( balloonToolbar, 'show', evt => { - if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) { - evt.stop(); - } - }, { priority: 'high' } ); - } - } } /** diff --git a/src/image/imageengine.js b/src/image/imageediting.js similarity index 88% rename from src/image/imageengine.js rename to src/image/imageediting.js index aba9453d..1cba7a30 100644 --- a/src/image/imageengine.js +++ b/src/image/imageediting.js @@ -4,7 +4,7 @@ */ /** - * @module image/image/imageengine + * @module image/image/imageediting */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; @@ -29,7 +29,7 @@ import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position'; * * @extends module:core/plugin~Plugin */ -export default class ImageEngine extends Plugin { +export default class ImageEditing extends Plugin { /** * @inheritDoc */ @@ -47,19 +47,14 @@ export default class ImageEngine extends Plugin { allowAttributes: [ 'alt', 'src', 'srcset' ] } ); - const dataElementCreator = ( modelElement, viewWriter ) => createImageViewElement( viewWriter ); conversion.for( 'dataDowncast' ).add( downcastElementToElement( { model: 'image', - view: dataElementCreator + view: ( modelElement, viewWriter ) => createImageViewElement( viewWriter ) } ) ); - const editingElementCreator = ( modelElement, viewWriter ) => { - return toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ); - }; - conversion.for( 'editingDowncast' ).add( downcastElementToElement( { model: 'image', - view: editingElementCreator + view: ( modelElement, viewWriter ) => toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ) } ) ); conversion.for( 'downcast' ) diff --git a/src/imagecaption.js b/src/imagecaption.js index 45cd5c09..f83111a8 100644 --- a/src/imagecaption.js +++ b/src/imagecaption.js @@ -8,7 +8,7 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageCaptionEngine from './imagecaption/imagecaptionengine'; +import ImageCaptionEditing from './imagecaption/imagecaptionediting'; import '../theme/imagecaption.css'; @@ -22,7 +22,7 @@ export default class ImageCaption extends Plugin { * @inheritDoc */ static get requires() { - return [ ImageCaptionEngine ]; + return [ ImageCaptionEditing ]; } /** diff --git a/src/imagecaption/imagecaptionengine.js b/src/imagecaption/imagecaptionediting.js similarity index 98% rename from src/imagecaption/imagecaptionengine.js rename to src/imagecaption/imagecaptionediting.js index 1cd3bd09..8145411a 100644 --- a/src/imagecaption/imagecaptionengine.js +++ b/src/imagecaption/imagecaptionediting.js @@ -4,7 +4,7 @@ */ /** - * @module image/imagecaption/imagecaptionengine + * @module image/imagecaption/imagecaptionediting */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; @@ -25,7 +25,7 @@ import { * * @extends module:core/plugin~Plugin */ -export default class ImageCaptionEngine extends Plugin { +export default class ImageCaptionEditing extends Plugin { /** * @inheritDoc */ diff --git a/src/imagestyle.js b/src/imagestyle.js index 96dc7d37..9875ac34 100644 --- a/src/imagestyle.js +++ b/src/imagestyle.js @@ -8,15 +8,14 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageStyleEngine from './imagestyle/imagestyleengine'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; - -import '../theme/imagestyle.css'; +import ImageStyleEditing from './imagestyle/imagestyleediting'; +import ImageStyleUI from './imagestyle/imagestyleui'; /** * The image style plugin. * - * Uses the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine}. + * It loads the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing} + * and {@link module:image/imagestyle/imagestyleui~ImageStyleUI} plugins. * * @extends module:core/plugin~Plugin */ @@ -25,7 +24,7 @@ export default class ImageStyle extends Plugin { * @inheritDoc */ static get requires() { - return [ ImageStyleEngine ]; + return [ ImageStyleEditing, ImageStyleUI ]; } /** @@ -34,55 +33,15 @@ export default class ImageStyle extends Plugin { static get pluginName() { return 'ImageStyle'; } - - /** - * @inheritDoc - */ - init() { - const editor = this.editor; - const styles = editor.plugins.get( ImageStyleEngine ).imageStyles; - - for ( const style of styles ) { - this._createButton( style ); - } - } - - /** - * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}. - * - * @private - * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style - */ - _createButton( style ) { - const editor = this.editor; - const command = editor.commands.get( style.name ); - - editor.ui.componentFactory.add( style.name, locale => { - const view = new ButtonView( locale ); - - view.set( { - label: style.title, - icon: style.icon, - tooltip: true - } ); - - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); - view.bind( 'isOn' ).to( command, 'value' ); - - this.listenTo( view, 'execute', () => editor.execute( style.name ) ); - - return view; - } ); - } } /** * Available image styles. - * The option is used by the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine} feature. + * The option is used by the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing} feature. * * The default value is: * - * const imageConfig = { + * const imageConfig = { * styles: [ 'imageStyleFull', 'imageStyleSide' ] * }; * @@ -91,12 +50,12 @@ export default class ImageStyle extends Plugin { * * the "full" style which doesn't apply any class, e.g. for images styled to span 100% width of the content, * * the "side" style with the `.image-style-side` CSS class. * - * See {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} to learn more about default + * See {@link module:image/imagestyle/utils~defaultStyles} to learn more about default * styles provided by the image feature. * - * The {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles default styles} can be customized, + * The {@link module:image/imagestyle/utils~defaultStyles default styles} can be customized, * e.g. to change the icon, title or CSS class of the style. The feature also provides several - * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons default icons} to chose from. + * {@link module:image/imagestyle/utils~defaultIcons default icons} to chose from. * * import customIcon from 'custom-icon.svg'; * @@ -129,7 +88,7 @@ export default class ImageStyle extends Plugin { * ] * }; * - * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine#localizedDefaultStylesTitles} + * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleui~ImageStyleUI#localizedDefaultStylesTitles} * will automatically translate it to the language of the editor. * * Read more about styling images in the {@glink features/image#Image-styles Image styles guide}. @@ -147,5 +106,5 @@ export default class ImageStyle extends Plugin { * toolbar: [ 'imageStyleFull', 'imageStyleSide' ] * }; * - * @member {Array.} module:image/image~ImageConfig#styles + * @member {Array.} module:image/image~ImageConfig#styles */ diff --git a/src/imagestyle/converters.js b/src/imagestyle/converters.js index b07b096b..b2e154a0 100644 --- a/src/imagestyle/converters.js +++ b/src/imagestyle/converters.js @@ -12,7 +12,7 @@ import first from '@ckeditor/ckeditor5-utils/src/first'; /** * Returns a converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute. * - * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat} + * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat} * for more details. * @returns {Function} A model-to-view attribute converter. */ @@ -42,7 +42,7 @@ export function modelToViewStyleAttribute( styles ) { /** * Returns a view-to-model converter converting image CSS classes to a proper value in the model. * - * @param {Array.} styles Styles for which the converter is created. + * @param {Array.} styles Styles for which the converter is created. * @returns {Function} A view-to-model converter. */ export function viewToModelStyleAttribute( styles ) { @@ -76,8 +76,8 @@ export function viewToModelStyleAttribute( styles ) { // Returns style with given `name` from array of styles. // // @param {String} name -// @param {Array. } styles -// @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined} +// @param {Array. } styles +// @return {module:image/imagestyle/imagestyleediting~ImageStyleFormat|undefined} function getStyleByName( name, styles ) { for ( const style of styles ) { if ( style.name === name ) { diff --git a/src/imagestyle/imagestylecommand.js b/src/imagestyle/imagestylecommand.js index 1a934fe3..f24ef05c 100644 --- a/src/imagestyle/imagestylecommand.js +++ b/src/imagestyle/imagestylecommand.js @@ -20,7 +20,7 @@ export default class ImageStyleCommand extends Command { * Creates an instance of the image style command. Each command instance is handling one style. * * @param {module:core/editor/editor~Editor} editor The editor instance. - * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style A style to be applied by this command. + * @param {module:image/imagestyle/imagestyleediting~ImageStyleFormat} style A style to be applied by this command. */ constructor( editor, style ) { super( editor ); @@ -38,7 +38,7 @@ export default class ImageStyleCommand extends Command { * A style handled by this command. * * @readonly - * @member {module:image/imagestyle/imagestyleengine~ImageStyleFormat} #style + * @member {module:image/imagestyle/imagestyleediting~ImageStyleFormat} #style */ this.style = style; } diff --git a/src/imagestyle/imagestyleediting.js b/src/imagestyle/imagestyleediting.js new file mode 100644 index 00000000..a12a7af9 --- /dev/null +++ b/src/imagestyle/imagestyleediting.js @@ -0,0 +1,95 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module image/imagestyle/imagestyleediting + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import ImageStyleCommand from './imagestylecommand'; +import ImageEditing from '../image/imageediting'; +import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters'; +import { normalizeImageStyles } from './utils'; + +/** + * The image style engine plugin. It sets the default configuration, creates converters and registers + * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}. + * + * @extends {module:core/plugin~Plugin} + */ +export default class ImageStyleEditing extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ ImageEditing ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'ImageStyleEditing'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const schema = editor.model.schema; + const data = editor.data; + const editing = editor.editing; + + // Define default configuration. + editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] ); + + // Get configuration. + const styles = normalizeImageStyles( editor.config.get( 'image.styles' ) ); + + // Allow imageStyle attribute in image. + // We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559. + schema.extend( 'image', { allowAttributes: 'imageStyle' } ); + + // Converters for imageStyle attribute from model to view. + const modelToViewConverter = modelToViewStyleAttribute( styles ); + editing.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter ); + data.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter ); + + // Converter for figure element from view to model. + data.upcastDispatcher.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } ); + + // Register separate command for each style. + for ( const style of styles ) { + editor.commands.add( style.name, new ImageStyleCommand( editor, style ) ); + } + } +} + +/** + * Image style format descriptor. + * + * import fullWidthIcon from 'path/to/icon.svg`; + * + * const imageStyleFormat = { + * name: 'fullSizeImage', + * icon: fullWidthIcon, + * title: 'Full size image', + * className: 'image-full-size' + * } + * + * @typedef {Object} module:image/imagestyle/imagestyleediting~ImageStyleFormat + * @property {String} name The unique name of the style. It will be used to: + * * register the {@link module:core/command~Command command} which will apply this style, + * * store the style's button in the editor {@link module:ui/componentfactory~ComponentFactory}, + * * store the style in the `imageStyle` model attribute. + * @property {Boolean} [isDefault] When set, the style will be used as the default one. + * A default style does not apply any CSS class to the view element. + * @property {String} icon One of the following to be used when creating the style's button: + * * An SVG icon source (as an XML string), + * * One of {@link module:image/imagestyle/utils~defaultIcons} to use a default icon provided by the plugin. + * @property {String} title The style's title. + * @property {String} className The CSS class used to represent the style in view. + */ diff --git a/src/imagestyle/imagestyleengine.js b/src/imagestyle/imagestyleengine.js deleted file mode 100644 index e3d7f1d9..00000000 --- a/src/imagestyle/imagestyleengine.js +++ /dev/null @@ -1,293 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module image/imagestyle/imagestyleengine - */ - -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageStyleCommand from './imagestylecommand'; -import ImageEngine from '../image/imageengine'; -import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters'; -import log from '@ckeditor/ckeditor5-utils/src/log'; - -import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg'; -import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg'; -import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg'; -import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg'; - -/** - * The image style engine plugin. It sets the default configuration, creates converters and registers - * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}. - * - * @extends {module:core/plugin~Plugin} - */ -export default class ImageStyleEngine extends Plugin { - /** - * @inheritDoc - */ - static get requires() { - return [ ImageEngine ]; - } - - /** - * @inheritDoc - */ - static get pluginName() { - return 'ImageStyleEngine'; - } - - /** - * @inheritDoc - */ - init() { - const editor = this.editor; - const schema = editor.model.schema; - const data = editor.data; - const editing = editor.editing; - - // Define default configuration. - editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] ); - - // Get configuration. - const styles = this.imageStyles; - - // Allow imageStyle attribute in image. - // We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559. - schema.extend( 'image', { allowAttributes: 'imageStyle' } ); - - // Converters for imageStyle attribute from model to view. - const modelToViewConverter = modelToViewStyleAttribute( styles ); - editing.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter ); - data.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter ); - - // Converter for figure element from view to model. - data.upcastDispatcher.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } ); - - // Register separate command for each style. - for ( const style of styles ) { - editor.commands.add( style.name, new ImageStyleCommand( editor, style ) ); - } - } - - /** - * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the - * {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat} format, translated - * `title` and a complete `icon` markup for each style. - * - * @readonly - * @type {Array.} - */ - get imageStyles() { - // Return cached value if there is one to improve the performance. - if ( this._cachedImageStyles ) { - return this._cachedImageStyles; - } - - const styles = []; - const editor = this.editor; - const titles = this.localizedDefaultStylesTitles; - const configuredStyles = editor.config.get( 'image.styles' ); - - for ( let style of configuredStyles ) { - style = normalizeStyle( style ); - - // Localize the titles of the styles, if a title corresponds with - // a localized default provided by the plugin. - if ( titles[ style.title ] ) { - style.title = titles[ style.title ]; - } - - // Don't override the user-defined styles array, clone it instead. - styles.push( style ); - } - - return ( this._cachedImageStyles = styles ); - } - - /** - * Returns the default localized style titles provided by the plugin e.g. ready to - * use in the {@link #imageStyles}. - * - * The following localized titles corresponding with - * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} are available: - * - * * `'Full size image'`, - * * `'Side image'`, - * * `'Left aligned image'`, - * * `'Centered image'`, - * * `'Right aligned image'` - * - * @readonly - * @type {Object.} - */ - get localizedDefaultStylesTitles() { - const t = this.editor.t; - - return { - 'Full size image': t( 'Full size image' ), - 'Side image': t( 'Side image' ), - 'Left aligned image': t( 'Left aligned image' ), - 'Centered image': t( 'Centered image' ), - 'Right aligned image': t( 'Right aligned image' ), - }; - } -} - -/** - * Default image styles provided by the plugin, which can be referred in the - * {@link module:image/image~ImageConfig#styles} config. - * - * Among them, 2 default semantic content styles are available: - * - * * `imageStyleFull` is a full–width image without any CSS class, - * * `imageStyleSide` is a side image styled with the `image-style-side` CSS class - * - * There are also 3 styles focused on formatting: - * - * * `imageStyleAlignLeft` aligns the image to the left using the `image-style-align-left` class, - * * `imageStyleAlignCenter` centers the image to the left using the `image-style-align-center` class, - * * `imageStyleAlignRight` aligns the image to the right using the `image-style-align-right` class, - * - * @member {Object.} - */ -ImageStyleEngine.defaultStyles = { - // This option is equal to situation when no style is applied. - imageStyleFull: { - name: 'imageStyleFull', - title: 'Full size image', - icon: fullWidthIcon, - isDefault: true - }, - - // This represents side image. - imageStyleSide: { - name: 'imageStyleSide', - title: 'Side image', - icon: rightIcon, - className: 'image-style-side' - }, - - // This style represents an imaged aligned to the left. - imageStyleAlignLeft: { - name: 'imageStyleAlignLeft', - title: 'Left aligned image', - icon: leftIcon, - className: 'image-style-align-left' - }, - - // This style represents a centered imaged. - imageStyleAlignCenter: { - name: 'imageStyleAlignCenter', - title: 'Centered image', - icon: centerIcon, - className: 'image-style-align-center' - }, - - // This style represents an imaged aligned to the right. - imageStyleAlignRight: { - name: 'imageStyleAlignRight', - title: 'Right aligned image', - icon: rightIcon, - className: 'image-style-align-right' - } -}; - -/** - * Default image style icons provided by the plugin, which can be referred in the - * {@link module:image/image~ImageConfig#styles} config. - * - * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`. - * - * @member {Object.} - */ -ImageStyleEngine.defaultIcons = { - full: fullWidthIcon, - left: leftIcon, - right: rightIcon, - center: centerIcon, -}; - -// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles} -// and returns it in a {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}. -// -// @private -// @param {Object} style -// @returns {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat} -function normalizeStyle( style ) { - const defaultStyles = ImageStyleEngine.defaultStyles; - const defaultIcons = ImageStyleEngine.defaultIcons; - - // Just the name of the style has been passed. - if ( typeof style == 'string' ) { - // If it's one of the defaults, just use it. - // Clone the style to avoid overriding defaults. - if ( defaultStyles[ style ] ) { - style = Object.assign( {}, defaultStyles[ style ] ); - } - // If it's just a name but none of the defaults, warn because probably it's a mistake. - else { - log.warn( - 'image-style-not-found: There is no such image style of given name.', - { name: style } - ); - - // Normalize the style anyway to prevent errors. - style = { - name: style - }; - } - } - - // If an object style has been passed and if the name matches one of the defaults, - // extend it with defaults – the user wants to customize a default style. - // Note: Don't override the user–defined style object, clone it instead. - else if ( defaultStyles[ style.name ] ) { - const defaultStyle = defaultStyles[ style.name ]; - const extendedStyle = Object.assign( {}, style ); - - for ( const prop in defaultStyle ) { - if ( !style.hasOwnProperty( prop ) ) { - extendedStyle[ prop ] = defaultStyle[ prop ]; - } - } - - style = extendedStyle; - } - - // If an icon is defined as a string and correspond with a name - // in default icons, use the default icon provided by the plugin. - if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) { - style.icon = defaultIcons[ style.icon ]; - } - - return style; -} - -/** - * Image style format descriptor. - * - * import fullWidthIcon from 'path/to/icon.svg`; - * - * const imageStyleFormat = { - * name: 'fullSizeImage', - * icon: fullWidthIcon, - * title: 'Full size image', - * className: 'image-full-size' - * } - * - * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat - * @property {String} name The unique name of the style. It will be used to: - * * register the {@link module:core/command~Command command} which will apply this style, - * * store the style's button in the editor {@link module:ui/componentfactory~ComponentFactory}, - * * store the style in the `imageStyle` model attribute. - * @property {Boolean} [isDefault] When set, the style will be used as the default one. - * A default style does not apply any CSS class to the view element. - * @property {String} icon One of the following to be used when creating the style's button: - * * An SVG icon source (as an XML string), - * * One of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons} to use a default icon provided by the plugin. - * @property {String} title The style's title. - * @property {String} className The CSS class used to represent the style in view. - */ diff --git a/src/imagestyle/imagestyleui.js b/src/imagestyle/imagestyleui.js new file mode 100644 index 00000000..f4ec6a94 --- /dev/null +++ b/src/imagestyle/imagestyleui.js @@ -0,0 +1,109 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module image/imagestyle/imagestyleui + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import { normalizeImageStyles } from './utils'; + +import '../../theme/imagestyle.css'; + +/** + * The image style UI plugin. + * + * @extends module:core/plugin~Plugin + */ +export default class ImageStyleUI extends Plugin { + /** + * Returns the default localized style titles provided by the plugin. + * + * The following localized titles corresponding with + * {@link module:image/imagestyle/utils~defaultStyles} are available: + * + * * `'Full size image'`, + * * `'Side image'`, + * * `'Left aligned image'`, + * * `'Centered image'`, + * * `'Right aligned image'` + * + * @returns {Object.} + */ + get localizedDefaultStylesTitles() { + const t = this.editor.t; + + return { + 'Full size image': t( 'Full size image' ), + 'Side image': t( 'Side image' ), + 'Left aligned image': t( 'Left aligned image' ), + 'Centered image': t( 'Centered image' ), + 'Right aligned image': t( 'Right aligned image' ) + }; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const configuredStyles = editor.config.get( 'image.styles' ); + + const translatedStyles = translateStyles( normalizeImageStyles( configuredStyles ), this.localizedDefaultStylesTitles ); + + for ( const style of translatedStyles ) { + this._createButton( style ); + } + } + + /** + * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}. + * + * @private + * @param {module:image/imagestyle/imagestyleediting~ImageStyleFormat} style + */ + _createButton( style ) { + const editor = this.editor; + + editor.ui.componentFactory.add( style.name, locale => { + const command = editor.commands.get( style.name ); + const view = new ButtonView( locale ); + + view.set( { + label: style.title, + icon: style.icon, + tooltip: true + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + view.bind( 'isOn' ).to( command, 'value' ); + + this.listenTo( view, 'execute', () => editor.execute( style.name ) ); + + return view; + } ); + } +} + +/** + * Returns translated `title` from the passed styles array. + * + * @param {Array.} styles + * @param titles + * @returns {Array.} + */ +function translateStyles( styles, titles ) { + for ( const style of styles ) { + // Localize the titles of the styles, if a title corresponds with + // a localized default provided by the plugin. + if ( titles[ style.title ] ) { + style.title = titles[ style.title ]; + } + } + + return styles; +} diff --git a/src/imagestyle/utils.js b/src/imagestyle/utils.js new file mode 100644 index 00000000..c3f33e9b --- /dev/null +++ b/src/imagestyle/utils.js @@ -0,0 +1,153 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module image/imagestyle/utils + */ + +import log from '@ckeditor/ckeditor5-utils/src/log'; + +import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg'; +import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg'; +import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg'; +import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg'; + +/** + * Default image styles provided by the plugin, which can be referred in the + * {@link module:image/image~ImageConfig#styles} config. + * + * Among them, 2 default semantic content styles are available: + * + * * `imageStyleFull` is a full–width image without any CSS class, + * * `imageStyleSide` is a side image styled with the `image-style-side` CSS class + * + * There are also 3 styles focused on formatting: + * + * * `imageStyleAlignLeft` aligns the image to the left using the `image-style-align-left` class, + * * `imageStyleAlignCenter` centers the image to the left using the `image-style-align-center` class, + * * `imageStyleAlignRight` aligns the image to the right using the `image-style-align-right` class, + * + * @member {Object.} + */ +const defaultStyles = { + // This option is equal to situation when no style is applied. + imageStyleFull: { + name: 'imageStyleFull', + title: 'Full size image', + icon: fullWidthIcon, + isDefault: true + }, + + // This represents side image. + imageStyleSide: { + name: 'imageStyleSide', + title: 'Side image', + icon: rightIcon, + className: 'image-style-side' + }, + + // This style represents an imaged aligned to the left. + imageStyleAlignLeft: { + name: 'imageStyleAlignLeft', + title: 'Left aligned image', + icon: leftIcon, + className: 'image-style-align-left' + }, + + // This style represents a centered imaged. + imageStyleAlignCenter: { + name: 'imageStyleAlignCenter', + title: 'Centered image', + icon: centerIcon, + className: 'image-style-align-center' + }, + + // This style represents an imaged aligned to the right. + imageStyleAlignRight: { + name: 'imageStyleAlignRight', + title: 'Right aligned image', + icon: rightIcon, + className: 'image-style-align-right' + } +}; + +/** + * Default image style icons provided by the plugin, which can be referred in the + * {@link module:image/image~ImageConfig#styles} config. + * + * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`. + * + * @member {Object.} + */ +const defaultIcons = { + full: fullWidthIcon, + left: leftIcon, + right: rightIcon, + center: centerIcon +}; + +/** + * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the + * {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat} format and a complete `icon` markup for each style. + * + * @returns {Array.} + */ +export function normalizeImageStyles( configuredStyles = [] ) { + return configuredStyles + .map( _normalizeStyle ) + .map( style => Object.assign( {}, style ) ); +} + +// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles} +// and returns it in a {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}. +// +// @param {Object} style +// @returns {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat} +function _normalizeStyle( style ) { + // Just the name of the style has been passed. + if ( typeof style == 'string' ) { + // If it's one of the defaults, just use it. + // Clone the style to avoid overriding defaults. + if ( defaultStyles[ style ] ) { + style = Object.assign( {}, defaultStyles[ style ] ); + } + // If it's just a name but none of the defaults, warn because probably it's a mistake. + else { + log.warn( + 'image-style-not-found: There is no such image style of given name.', + { name: style } + ); + + // Normalize the style anyway to prevent errors. + style = { + name: style + }; + } + } + + // If an object style has been passed and if the name matches one of the defaults, + // extend it with defaults – the user wants to customize a default style. + // Note: Don't override the user–defined style object, clone it instead. + else if ( defaultStyles[ style.name ] ) { + const defaultStyle = defaultStyles[ style.name ]; + const extendedStyle = Object.assign( {}, style ); + + for ( const prop in defaultStyle ) { + if ( !style.hasOwnProperty( prop ) ) { + extendedStyle[ prop ] = defaultStyle[ prop ]; + } + } + + style = extendedStyle; + } + + // If an icon is defined as a string and correspond with a name + // in default icons, use the default icon provided by the plugin. + if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) { + style.icon = defaultIcons[ style.icon ]; + } + + return style; +} diff --git a/src/imagetextalternative.js b/src/imagetextalternative.js index b280ef93..b6d7bdc4 100644 --- a/src/imagetextalternative.js +++ b/src/imagetextalternative.js @@ -8,19 +8,14 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine'; -import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler'; -import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview'; -import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; -import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; -import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils'; -import { isImageWidgetSelected } from './image/utils'; +import ImageTextAlternativeEditing from './imagetextalternative/imagetextalternativeediting'; +import ImageTextAlternativeUI from './imagetextalternative/imagetextalternativeui'; /** * The image text alternative plugin. * - * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}. + * It loads the {@link module:image/imagetextalternative/imagetextalternativeediting~ImageTextAlternativeEditing} + * and {@link module:image/imagetextalternative/imagetextalternativeui~ImageTextAlternativeUI} plugins. * * @extends module:core/plugin~Plugin */ @@ -29,7 +24,7 @@ export default class ImageTextAlternative extends Plugin { * @inheritDoc */ static get requires() { - return [ ImageTextAlternativeEngine, ContextualBalloon ]; + return [ ImageTextAlternativeEditing, ImageTextAlternativeUI ]; } /** @@ -38,165 +33,4 @@ export default class ImageTextAlternative extends Plugin { static get pluginName() { return 'ImageTextAlternative'; } - - /** - * @inheritDoc - */ - init() { - this._createButton(); - this._createForm(); - } - - /** - * Creates a button showing the balloon panel for changing the image text alternative and - * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}. - * - * @private - */ - _createButton() { - const editor = this.editor; - const command = editor.commands.get( 'imageTextAlternative' ); - const t = editor.t; - - editor.ui.componentFactory.add( 'imageTextAlternative', locale => { - const view = new ButtonView( locale ); - - view.set( { - label: t( 'Change image text alternative' ), - icon: textAlternativeIcon, - tooltip: true - } ); - - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); - - this.listenTo( view, 'execute', () => this._showForm() ); - - return view; - } ); - } - - /** - * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} - * form. - * - * @private - */ - _createForm() { - const editor = this.editor; - const view = editor.editing.view; - const viewDocument = view.document; - - /** - * The contextual balloon plugin instance. - * - * @private - * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon} - */ - this._balloon = this.editor.plugins.get( 'ContextualBalloon' ); - - /** - * A form containing a textarea and buttons, used to change the `alt` text value. - * - * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form - */ - this._form = new TextAlternativeFormView( editor.locale ); - - // Render the form so its #element is available for clickOutsideHandler. - this._form.render(); - - this.listenTo( this._form, 'submit', () => { - editor.execute( 'imageTextAlternative', { - newValue: this._form.labeledInput.inputView.element.value - } ); - - this._hideForm( true ); - } ); - - this.listenTo( this._form, 'cancel', () => { - this._hideForm( true ); - } ); - - // Close the form on Esc key press. - this._form.keystrokes.set( 'Esc', ( data, cancel ) => { - this._hideForm( true ); - cancel(); - } ); - - // Reposition the balloon or hide the form if an image widget is no longer selected. - this.listenTo( view, 'render', () => { - if ( !isImageWidgetSelected( viewDocument.selection ) ) { - this._hideForm( true ); - } else if ( this._isVisible ) { - repositionContextualBalloon( editor ); - } - } ); - - // Close on click outside of balloon panel element. - clickOutsideHandler( { - emitter: this._form, - activator: () => this._isVisible, - contextElements: [ this._form.element ], - callback: () => this._hideForm() - } ); - } - - /** - * Shows the {@link #_form} in the {@link #_balloon}. - * - * @private - */ - _showForm() { - if ( this._isVisible ) { - return; - } - - const editor = this.editor; - const command = editor.commands.get( 'imageTextAlternative' ); - const labeledInput = this._form.labeledInput; - - if ( !this._balloon.hasView( this._form ) ) { - this._balloon.add( { - view: this._form, - position: getBalloonPositionData( editor ) - } ); - } - - // Make sure that each time the panel shows up, the field remains in sync with the value of - // the command. If the user typed in the input, then canceled the balloon (`labeledInput#value` - // stays unaltered) and re-opened it without changing the value of the command, they would see the - // old value instead of the actual value of the command. - // https://github.com/ckeditor/ckeditor5-image/issues/114 - labeledInput.value = labeledInput.inputView.element.value = command.value || ''; - - this._form.labeledInput.select(); - } - - /** - * Removes the {@link #_form} from the {@link #_balloon}. - * - * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards. - * @private - */ - _hideForm( focusEditable ) { - if ( !this._isVisible ) { - return; - } - - this._balloon.remove( this._form ); - - if ( focusEditable ) { - this.editor.editing.view.focus(); - } - } - - /** - * Returns `true` when the {@link #_form} is the visible view - * in the {@link #_balloon}. - * - * @private - * @type {Boolean} - */ - get _isVisible() { - return this._balloon.visibleView == this._form; - } } diff --git a/src/imagetextalternative/imagetextalternativeengine.js b/src/imagetextalternative/imagetextalternativeediting.js similarity index 80% rename from src/imagetextalternative/imagetextalternativeengine.js rename to src/imagetextalternative/imagetextalternativeediting.js index 1375d04b..b1531fd0 100644 --- a/src/imagetextalternative/imagetextalternativeengine.js +++ b/src/imagetextalternative/imagetextalternativeediting.js @@ -4,7 +4,7 @@ */ /** - * @module image/imagetextalternative/imagetextalternativeengine + * @module image/imagetextalternative/imagetextalternativeediting */ import ImageTextAlternativeCommand from './imagetextalternativecommand'; @@ -16,7 +16,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; * * @extends module:core/plugin~Plugin */ -export default class ImageTextAlternativeEngine extends Plugin { +export default class ImageTextAlternativeEditing extends Plugin { /** * @inheritDoc */ diff --git a/src/imagetextalternative/imagetextalternativeui.js b/src/imagetextalternative/imagetextalternativeui.js new file mode 100644 index 00000000..9aba8f43 --- /dev/null +++ b/src/imagetextalternative/imagetextalternativeui.js @@ -0,0 +1,194 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module image/imagetextalternative/imagetextalternativeui + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler'; +import TextAlternativeFormView from './ui/textalternativeformview'; +import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; +import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; +import { repositionContextualBalloon, getBalloonPositionData } from '../image/ui/utils'; +import { isImageWidgetSelected } from '../image/utils'; + +/** + * The image text alternative UI plugin. + * + * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}. + * + * @extends module:core/plugin~Plugin + */ +export default class ImageTextAlternativeUI extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ ContextualBalloon ]; + } + + /** + * @inheritDoc + */ + init() { + this._createButton(); + this._createForm(); + } + + /** + * Creates a button showing the balloon panel for changing the image text alternative and + * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}. + * + * @private + */ + _createButton() { + const editor = this.editor; + const t = editor.t; + + editor.ui.componentFactory.add( 'imageTextAlternative', locale => { + const command = editor.commands.get( 'imageTextAlternative' ); + const view = new ButtonView( locale ); + + view.set( { + label: t( 'Change image text alternative' ), + icon: textAlternativeIcon, + tooltip: true + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + this.listenTo( view, 'execute', () => this._showForm() ); + + return view; + } ); + } + + /** + * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} + * form. + * + * @private + */ + _createForm() { + const editor = this.editor; + const view = editor.editing.view; + const viewDocument = view.document; + + /** + * The contextual balloon plugin instance. + * + * @private + * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon} + */ + this._balloon = this.editor.plugins.get( 'ContextualBalloon' ); + + /** + * A form containing a textarea and buttons, used to change the `alt` text value. + * + * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form + */ + this._form = new TextAlternativeFormView( editor.locale ); + + // Render the form so its #element is available for clickOutsideHandler. + this._form.render(); + + this.listenTo( this._form, 'submit', () => { + editor.execute( 'imageTextAlternative', { + newValue: this._form.labeledInput.inputView.element.value + } ); + + this._hideForm( true ); + } ); + + this.listenTo( this._form, 'cancel', () => { + this._hideForm( true ); + } ); + + // Close the form on Esc key press. + this._form.keystrokes.set( 'Esc', ( data, cancel ) => { + this._hideForm( true ); + cancel(); + } ); + + // Reposition the balloon or hide the form if an image widget is no longer selected. + this.listenTo( view, 'render', () => { + if ( !isImageWidgetSelected( viewDocument.selection ) ) { + this._hideForm( true ); + } else if ( this._isVisible ) { + repositionContextualBalloon( editor ); + } + } ); + + // Close on click outside of balloon panel element. + clickOutsideHandler( { + emitter: this._form, + activator: () => this._isVisible, + contextElements: [ this._form.element ], + callback: () => this._hideForm() + } ); + } + + /** + * Shows the {@link #_form} in the {@link #_balloon}. + * + * @private + */ + _showForm() { + if ( this._isVisible ) { + return; + } + + const editor = this.editor; + const command = editor.commands.get( 'imageTextAlternative' ); + const labeledInput = this._form.labeledInput; + + if ( !this._balloon.hasView( this._form ) ) { + this._balloon.add( { + view: this._form, + position: getBalloonPositionData( editor ) + } ); + } + + // Make sure that each time the panel shows up, the field remains in sync with the value of + // the command. If the user typed in the input, then canceled the balloon (`labeledInput#value` + // stays unaltered) and re-opened it without changing the value of the command, they would see the + // old value instead of the actual value of the command. + // https://github.com/ckeditor/ckeditor5-image/issues/114 + labeledInput.value = labeledInput.inputView.element.value = command.value || ''; + + this._form.labeledInput.select(); + } + + /** + * Removes the {@link #_form} from the {@link #_balloon}. + * + * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards. + * @private + */ + _hideForm( focusEditable ) { + if ( !this._isVisible ) { + return; + } + + this._balloon.remove( this._form ); + + if ( focusEditable ) { + this.editor.editing.view.focus(); + } + } + + /** + * Returns `true` when the {@link #_form} is the visible view + * in the {@link #_balloon}. + * + * @private + * @type {Boolean} + */ + get _isVisible() { + return this._balloon.visibleView == this._form; + } +} diff --git a/src/imagetoolbar.js b/src/imagetoolbar.js index 2f4c8379..1c128f6d 100644 --- a/src/imagetoolbar.js +++ b/src/imagetoolbar.js @@ -40,6 +40,25 @@ export default class ImageToolbar extends Plugin { return 'ImageToolbar'; } + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const balloonToolbar = editor.plugins.get( 'BalloonToolbar' ); + + // If `BalloonToolbar` plugin is loaded, it should be disabled for images + // which have their own toolbar to avoid duplication. + // https://github.com/ckeditor/ckeditor5-image/issues/110 + if ( balloonToolbar ) { + this.listenTo( balloonToolbar, 'show', evt => { + if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) { + evt.stop(); + } + }, { priority: 'high' } ); + } + } + /** * @inheritDoc */ diff --git a/tests/image.js b/tests/image.js index ee727716..c9209cf9 100644 --- a/tests/image.js +++ b/tests/image.js @@ -5,7 +5,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Image from '../src/image'; -import ImageEngine from '../src/image/imageengine'; +import ImageEditing from '../src/image/imageediting'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ImageTextAlternative from '../src/imagetextalternative'; import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -43,8 +43,8 @@ describe( 'Image', () => { expect( editor.plugins.get( Image ) ).to.instanceOf( Image ); } ); - it( 'should load ImageEngine plugin', () => { - expect( editor.plugins.get( ImageEngine ) ).to.instanceOf( ImageEngine ); + it( 'should load ImageEditing plugin', () => { + expect( editor.plugins.get( ImageEditing ) ).to.instanceOf( ImageEditing ); } ); it( 'should load Widget plugin', () => { diff --git a/tests/image/converters.js b/tests/image/converters.js index 7a9d1979..f49bd016 100644 --- a/tests/image/converters.js +++ b/tests/image/converters.js @@ -8,7 +8,7 @@ import { modelToViewAttributeConverter } from '../../src/image/converters'; import { toImageWidget } from '../../src/image/utils'; -import { createImageViewElement } from '../../src/image/imageengine'; +import { createImageViewElement } from '../../src/image/imageediting'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters'; diff --git a/tests/image/imageengine.js b/tests/image/imageediting.js similarity index 98% rename from tests/image/imageengine.js rename to tests/image/imageediting.js index 1618b094..c715f7df 100644 --- a/tests/image/imageengine.js +++ b/tests/image/imageediting.js @@ -4,19 +4,19 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import ImageEngine from '../../src/image/imageengine'; +import ImageEditing from '../../src/image/imageediting'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { isImageWidget } from '../../src/image/utils'; import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'; -describe( 'ImageEngine', () => { +describe( 'ImageEditing', () => { let editor, model, document, view, viewDocument; beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ ImageEngine ] + plugins: [ ImageEditing ] } ) .then( newEditor => { editor = newEditor; @@ -28,7 +28,7 @@ describe( 'ImageEngine', () => { } ); it( 'should be loaded', () => { - expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine ); + expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing ); } ); it( 'should set proper schema rules', () => { diff --git a/tests/imagecaption.js b/tests/imagecaption.js index d8f1ae7d..c548e707 100644 --- a/tests/imagecaption.js +++ b/tests/imagecaption.js @@ -7,7 +7,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import ImageCaption from '../src/imagecaption'; -import ImageCaptionEngine from '../src/imagecaption/imagecaptionengine'; +import ImageCaptionEditing from '../src/imagecaption/imagecaptionediting'; describe( 'ImageCaption', () => { let editor; @@ -29,7 +29,7 @@ describe( 'ImageCaption', () => { expect( editor.plugins.get( ImageCaption ) ).to.instanceOf( ImageCaption ); } ); - it( 'should load ImageCaptionEngine plugin', () => { - expect( editor.plugins.get( ImageCaptionEngine ) ).to.instanceOf( ImageCaptionEngine ); + it( 'should load ImageCaptionEditing plugin', () => { + expect( editor.plugins.get( ImageCaptionEditing ) ).to.instanceOf( ImageCaptionEditing ); } ); } ); diff --git a/tests/imagecaption/imagecaptionengine.js b/tests/imagecaption/imagecaptionediting.js similarity index 97% rename from tests/imagecaption/imagecaptionengine.js rename to tests/imagecaption/imagecaptionediting.js index c59dea0b..fb988e53 100644 --- a/tests/imagecaption/imagecaptionengine.js +++ b/tests/imagecaption/imagecaptionediting.js @@ -5,9 +5,9 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import ImageCaptionEngine from '../../src/imagecaption/imagecaptionengine'; -import ImageEngine from '../../src/image/imageengine'; -import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine'; +import ImageCaptionEditing from '../../src/imagecaption/imagecaptionediting'; +import ImageEditing from '../../src/image/imageediting'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ViewAttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement'; @@ -18,13 +18,13 @@ import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -describe( 'ImageCaptionEngine', () => { +describe( 'ImageCaptionEditing', () => { let editor, model, doc, view; beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ ImageCaptionEngine, ImageEngine, UndoEngine, Paragraph ] + plugins: [ ImageCaptionEditing, ImageEditing, UndoEditing, Paragraph ] } ) .then( newEditor => { editor = newEditor; @@ -44,7 +44,7 @@ describe( 'ImageCaptionEngine', () => { } ); it( 'should be loaded', () => { - expect( editor.plugins.get( ImageCaptionEngine ) ).to.be.instanceOf( ImageCaptionEngine ); + expect( editor.plugins.get( ImageCaptionEditing ) ).to.be.instanceOf( ImageCaptionEditing ); } ); it( 'should set proper schema rules', () => { diff --git a/tests/imagestyle.js b/tests/imagestyle.js index 15da3735..461f2e70 100644 --- a/tests/imagestyle.js +++ b/tests/imagestyle.js @@ -5,17 +5,12 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import ImageStyle from '../src/imagestyle'; -import ImageStyleEngine from '../src/imagestyle/imagestyleengine'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import ImageStyleEditing from '../src/imagestyle/imagestyleediting'; +import ImageStyleUI from '../src/imagestyle/imagestyleui'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; describe( 'ImageStyle', () => { let editor; - const styles = [ - { name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true }, - { name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' }, - { name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' } - ]; beforeEach( () => { const editorElement = global.document.createElement( 'div' ); @@ -23,10 +18,7 @@ describe( 'ImageStyle', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ ImageStyle ], - image: { - styles - } + plugins: [ ImageStyle ] } ) .then( newEditor => { editor = newEditor; @@ -41,48 +33,11 @@ describe( 'ImageStyle', () => { expect( editor.plugins.get( ImageStyle ) ).to.be.instanceOf( ImageStyle ); } ); - it( 'should load ImageStyleEngine plugin', () => { - expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine ); + it( 'should load ImageStyleEditing plugin', () => { + expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing ); } ); - it( 'should register buttons for each style', () => { - const spy = sinon.spy( editor, 'execute' ); - - for ( const style of styles ) { - const command = editor.commands.get( style.name ); - const buttonView = editor.ui.componentFactory.create( style.name ); - - expect( buttonView ).to.be.instanceOf( ButtonView ); - expect( buttonView.label ).to.equal( style.title ); - expect( buttonView.icon ).to.equal( style.icon ); - - command.isEnabled = true; - expect( buttonView.isEnabled ).to.be.true; - command.isEnabled = false; - expect( buttonView.isEnabled ).to.be.false; - - buttonView.fire( 'execute' ); - sinon.assert.calledWithExactly( editor.execute, style.name ); - - spy.reset(); - } - } ); - - it( 'should not add buttons to image toolbar if configuration is present', () => { - const editorElement = global.document.createElement( 'div' ); - global.document.body.appendChild( editorElement ); - - return ClassicTestEditor - .create( editorElement, { - plugins: [ ImageStyle ], - image: { - styles, - toolbar: [ 'foo', 'bar' ] - } - } ) - .then( newEditor => { - expect( newEditor.config.get( 'image.toolbar' ) ).to.eql( [ 'foo', 'bar' ] ); - newEditor.destroy(); - } ); + it( 'should load ImageStyleUI plugin', () => { + expect( editor.plugins.get( ImageStyleUI ) ).to.be.instanceOf( ImageStyleUI ); } ); } ); diff --git a/tests/imagestyle/imagestyleengine.js b/tests/imagestyle/imagestyleediting.js similarity index 63% rename from tests/imagestyle/imagestyleengine.js rename to tests/imagestyle/imagestyleediting.js index 4383e8c8..d05ce51e 100644 --- a/tests/imagestyle/imagestyleengine.js +++ b/tests/imagestyle/imagestyleediting.js @@ -4,25 +4,19 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import ImageStyleEngine from '../../src/imagestyle/imagestyleengine'; -import ImageEngine from '../../src/image/imageengine'; +import ImageStyleEditing from '../../src/imagestyle/imagestyleediting'; +import ImageEditing from '../../src/image/imageediting'; import ImageStyleCommand from '../../src/imagestyle/imagestylecommand'; -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import log from '@ckeditor/ckeditor5-utils/src/log'; + import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg'; -import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg'; -import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg'; -import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg'; - import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; testUtils.createSinonSandbox(); -describe( 'ImageStyleEngine', () => { - let editor, plugin, model, document, viewDocument; +describe( 'ImageStyleEditing', () => { + let editor, model, document, viewDocument; afterEach( () => { editor.destroy(); @@ -32,7 +26,7 @@ describe( 'ImageStyleEngine', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ], + plugins: [ ImageStyleEditing ], } ) .then( newEditor => { editor = newEditor; @@ -40,11 +34,11 @@ describe( 'ImageStyleEngine', () => { } ); it( 'should be loaded', () => { - expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine ); + expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing ); } ); - it( 'should load image engine', () => { - expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine ); + it( 'should load image editing', () => { + expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing ); } ); } ); @@ -52,7 +46,7 @@ describe( 'ImageStyleEngine', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ], + plugins: [ ImageStyleEditing ], image: { styles: [ { name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true }, @@ -72,7 +66,7 @@ describe( 'ImageStyleEngine', () => { it( 'should define image.styles config', () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ] + plugins: [ ImageStyleEditing ] } ) .then( newEditor => { editor = newEditor; @@ -263,11 +257,11 @@ describe( 'ImageStyleEngine', () => { } ); } ); - describe( 'imageStyles()', () => { + describe( 'config', () => { it( 'should fall back to defaults when no image.styles', () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ] + plugins: [ ImageStyleEditing ] } ) .then( newEditor => { editor = newEditor; @@ -279,7 +273,7 @@ describe( 'ImageStyleEngine', () => { it( 'should not alter the image.styles config', () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ], + plugins: [ ImageStyleEditing ], image: { styles: [ 'imageStyleSide' @@ -296,7 +290,7 @@ describe( 'ImageStyleEngine', () => { it( 'should not alter object definitions in the image.styles config', () => { return VirtualTestEditor .create( { - plugins: [ ImageStyleEngine ], + plugins: [ ImageStyleEditing ], image: { styles: [ { name: 'imageStyleSide' } @@ -309,187 +303,5 @@ describe( 'ImageStyleEngine', () => { expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'imageStyleSide' } ] ); } ); } ); - - it( 'should cache the styles', () => { - return VirtualTestEditor - .create( { - plugins: [ ImageStyleEngine ] - } ) - .then( newEditor => { - editor = newEditor; - plugin = editor.plugins.get( ImageStyleEngine ); - - expect( plugin.imageStyles ).to.equal( plugin.imageStyles ); - } ); - } ); - - describe( 'object format', () => { - beforeEach( () => { - class TranslationMock extends Plugin { - init() { - sinon.stub( this.editor, 't' ).returns( 'Default translation' ); - } - } - - return VirtualTestEditor - .create( { - plugins: [ TranslationMock, ImageStyleEngine ], - image: { - styles: [ - // Custom user styles. - { name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' }, - { name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' }, - { name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' }, - - // Customized default styles. - { name: 'imageStyleFull', icon: 'left', title: 'Custom title' } - ] - } - } ) - .then( newEditor => { - editor = newEditor; - plugin = editor.plugins.get( ImageStyleEngine ); - } ); - } ); - - it( 'should pass through if #name not found in default styles', () => { - expect( plugin.imageStyles[ 0 ] ).to.deep.equal( { - name: 'foo', - title: 'foo', - icon: 'custom', - isDefault: true, - className: 'foo-class' - } ); - } ); - - it( 'should use one of default icons if #icon matches', () => { - expect( plugin.imageStyles[ 1 ].icon ).to.equal( ImageStyleEngine.defaultIcons.right ); - } ); - - it( 'should use one of default translations if #title matches', () => { - expect( plugin.imageStyles[ 2 ].title ).to.deep.equal( 'Default translation' ); - } ); - - it( 'should extend one of default styles if #name matches', () => { - expect( plugin.imageStyles[ 3 ] ).to.deep.equal( { - name: 'imageStyleFull', - title: 'Custom title', - icon: ImageStyleEngine.defaultIcons.left, - isDefault: true - } ); - } ); - } ); - - describe( 'string format', () => { - it( 'should use one of default styles if #name matches', () => { - return VirtualTestEditor - .create( { - plugins: [ ImageStyleEngine ], - image: { - styles: [ 'imageStyleFull' ] - } - } ) - .then( newEditor => { - editor = newEditor; - plugin = editor.plugins.get( ImageStyleEngine ); - expect( plugin.imageStyles[ 0 ] ).to.deep.equal( ImageStyleEngine.defaultStyles.imageStyleFull ); - } ); - } ); - - it( 'should warn if a #name not found in default styles', () => { - testUtils.sinon.stub( log, 'warn' ); - - return VirtualTestEditor - .create( { - plugins: [ ImageStyleEngine ], - image: { - styles: [ 'foo' ] - } - } ) - .then( newEditor => { - editor = newEditor; - plugin = editor.plugins.get( ImageStyleEngine ); - - expect( plugin.imageStyles[ 0 ] ).to.deep.equal( { - name: 'foo' - } ); - - sinon.assert.calledOnce( log.warn ); - sinon.assert.calledWithExactly( log.warn, - sinon.match( /^image-style-not-found/ ), - { name: 'foo' } - ); - } ); - } ); - } ); - } ); - - describe( 'localizedDefaultStylesTitles()', () => { - it( 'should return localized titles of default styles', () => { - return VirtualTestEditor - .create( { - plugins: [ ImageStyleEngine ] - } ) - .then( newEditor => { - editor = newEditor; - plugin = editor.plugins.get( ImageStyleEngine ); - - expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( { - 'Full size image': 'Full size image', - 'Side image': 'Side image', - 'Left aligned image': 'Left aligned image', - 'Centered image': 'Centered image', - 'Right aligned image': 'Right aligned image' - } ); - } ); - } ); - } ); - - describe( 'defaultStyles', () => { - it( 'should be defined', () => { - expect( ImageStyleEngine.defaultStyles ).to.deep.equal( { - imageStyleFull: { - name: 'imageStyleFull', - title: 'Full size image', - icon: fullWidthIcon, - isDefault: true - }, - imageStyleSide: { - name: 'imageStyleSide', - title: 'Side image', - icon: rightIcon, - className: 'image-style-side' - }, - imageStyleAlignLeft: { - name: 'imageStyleAlignLeft', - title: 'Left aligned image', - icon: leftIcon, - className: 'image-style-align-left' - }, - imageStyleAlignCenter: { - name: 'imageStyleAlignCenter', - title: 'Centered image', - icon: centerIcon, - className: 'image-style-align-center' - }, - imageStyleAlignRight: { - name: 'imageStyleAlignRight', - title: 'Right aligned image', - icon: rightIcon, - className: 'image-style-align-right' - } - } ); - } ); - } ); - - describe( 'defaultIcons', () => { - it( 'should be defined', () => { - expect( ImageStyleEngine.defaultIcons ).to.deep.equal( { - full: fullWidthIcon, - left: leftIcon, - right: rightIcon, - center: centerIcon, - } ); - } ); } ); } ); diff --git a/tests/imagestyle/imagestyleui.js b/tests/imagestyle/imagestyleui.js new file mode 100644 index 00000000..b7e35c9a --- /dev/null +++ b/tests/imagestyle/imagestyleui.js @@ -0,0 +1,134 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import ImageStyleEditing from '../../src/imagestyle/imagestyleediting'; +import ImageStyleUI from '../../src/imagestyle/imagestyleui'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; + +describe( 'ImageStyleUI', () => { + let editor; + + const styles = [ + { name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true }, + { name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' }, + { name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' } + ]; + + beforeEach( () => { + const editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ImageStyleEditing, ImageStyleUI ], + image: { + styles + } + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should register buttons for each style', () => { + const spy = sinon.spy( editor, 'execute' ); + + for ( const style of styles ) { + const command = editor.commands.get( style.name ); + const buttonView = editor.ui.componentFactory.create( style.name ); + + expect( buttonView ).to.be.instanceOf( ButtonView ); + expect( buttonView.label ).to.equal( style.title ); + expect( buttonView.icon ).to.equal( style.icon ); + + command.isEnabled = true; + expect( buttonView.isEnabled ).to.be.true; + command.isEnabled = false; + expect( buttonView.isEnabled ).to.be.false; + + buttonView.fire( 'execute' ); + sinon.assert.calledWithExactly( editor.execute, style.name ); + + spy.reset(); + } + } ); + + it( 'should translate buttons if taken from default styles', () => { + const editorElement = global.document.createElement( 'div' ); + + global.document.body.appendChild( editorElement ); + + class TranslationMock extends Plugin { + init() { + sinon.stub( this.editor, 't' ).returns( 'Default title' ); + } + } + + return ClassicTestEditor + .create( editorElement, { + plugins: [ TranslationMock, ImageStyleEditing, ImageStyleUI ], + image: { + styles: [ + { name: 'style 1', title: 'Side image', icon: 'style1-icon', isDefault: true } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + + const buttonView = editor.ui.componentFactory.create( 'style 1' ); + + expect( buttonView.label ).to.equal( 'Default title' ); + } ); + } ); + + it( 'should not add buttons to image toolbar if configuration is present', () => { + const editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ImageStyleEditing, ImageStyleUI ], + image: { + styles, + toolbar: [ 'foo', 'bar' ] + } + } ) + .then( newEditor => { + expect( newEditor.config.get( 'image.toolbar' ) ).to.deep.equal( [ 'foo', 'bar' ] ); + newEditor.destroy(); + } ); + } ); + + describe( 'localizedDefaultStylesTitles()', () => { + it( 'should return localized titles of default styles', () => { + return VirtualTestEditor + .create( { + plugins: [ ImageStyleUI ] + } ) + .then( newEditor => { + editor = newEditor; + + const plugin = editor.plugins.get( ImageStyleUI ); + + expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( { + 'Full size image': 'Full size image', + 'Side image': 'Side image', + 'Left aligned image': 'Left aligned image', + 'Centered image': 'Centered image', + 'Right aligned image': 'Right aligned image' + } ); + } ); + } ); + } ); +} ); diff --git a/tests/imagestyle/utils.js b/tests/imagestyle/utils.js new file mode 100644 index 00000000..1acd8386 --- /dev/null +++ b/tests/imagestyle/utils.js @@ -0,0 +1,111 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import log from '@ckeditor/ckeditor5-utils/src/log'; + +import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg'; +import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg'; +import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg'; +import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { normalizeImageStyles } from '../../src/imagestyle/utils'; + +testUtils.createSinonSandbox(); + +describe( 'ImageStyle utils', () => { + let imageStyles; + + describe( 'imageStyles()', () => { + describe( 'object format', () => { + beforeEach( () => { + imageStyles = normalizeImageStyles( [ + { name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' }, + { name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' }, + { name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' }, + + // Customized default styles. + { name: 'imageStyleFull', icon: 'left', title: 'Custom title' } + ] ); + } ); + + it( 'should pass through if #name not found in default styles', () => { + expect( imageStyles[ 0 ] ).to.deep.equal( { + name: 'foo', + title: 'foo', + icon: 'custom', + isDefault: true, + className: 'foo-class' + } ); + } ); + + it( 'should use one of default icons if #icon matches', () => { + expect( imageStyles[ 1 ].icon ).to.equal( rightIcon ); + } ); + + it( 'should extend one of default styles if #name matches', () => { + expect( imageStyles[ 3 ] ).to.deep.equal( { + name: 'imageStyleFull', + title: 'Custom title', + icon: leftIcon, + isDefault: true + } ); + } ); + } ); + + describe( 'string format', () => { + it( 'should use one of default styles if #name matches', () => { + expect( normalizeImageStyles( [ 'imageStyleFull' ] ) ).to.deep.equal( [ { + name: 'imageStyleFull', + title: 'Full size image', + icon: fullWidthIcon, + isDefault: true + } ] ); + + expect( normalizeImageStyles( [ 'imageStyleSide' ] ) ).to.deep.equal( [ { + name: 'imageStyleSide', + title: 'Side image', + icon: rightIcon, + className: 'image-style-side' + } ] ); + + expect( normalizeImageStyles( [ 'imageStyleAlignLeft' ] ) ).to.deep.equal( [ { + name: 'imageStyleAlignLeft', + title: 'Left aligned image', + icon: leftIcon, + className: 'image-style-align-left' + } ] ); + + expect( normalizeImageStyles( [ 'imageStyleAlignCenter' ] ) ).to.deep.equal( [ { + name: 'imageStyleAlignCenter', + title: 'Centered image', + icon: centerIcon, + className: 'image-style-align-center' + } ] ); + + expect( normalizeImageStyles( [ 'imageStyleAlignRight' ] ) ).to.deep.equal( [ { + name: 'imageStyleAlignRight', + title: 'Right aligned image', + icon: rightIcon, + className: 'image-style-align-right' + } ] ); + } ); + + it( 'should warn if a #name not found in default styles', () => { + testUtils.sinon.stub( log, 'warn' ); + + expect( normalizeImageStyles( [ 'foo' ] ) ).to.deep.equal( [ { + name: 'foo' + } ] ); + + sinon.assert.calledOnce( log.warn ); + sinon.assert.calledWithExactly( log.warn, + sinon.match( /^image-style-not-found/ ), + { name: 'foo' } + ); + } ); + } ); + } ); +} ); diff --git a/tests/imagetextalternative.js b/tests/imagetextalternative.js index 35b497c4..b8f7fd70 100644 --- a/tests/imagetextalternative.js +++ b/tests/imagetextalternative.js @@ -3,20 +3,15 @@ * For licensing, see LICENSE.md. */ -/* global Event, document */ - import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Image from '../src/image'; import ImageTextAlternative from '../src/imagetextalternative'; -import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import View from '@ckeditor/ckeditor5-ui/src/view'; +import ImageTextAlternativeEditing from '../src/imagetextalternative/imagetextalternativeediting'; +import ImageTextAlternativeUI from '../src/imagetextalternative/imagetextalternativeui'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; -import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; describe( 'ImageTextAlternative', () => { - let editor, model, view, doc, plugin, command, form, balloon, editorElement, button; + let editor, plugin, editorElement; beforeEach( () => { editorElement = global.document.createElement( 'div' ); @@ -28,15 +23,8 @@ describe( 'ImageTextAlternative', () => { } ) .then( newEditor => { editor = newEditor; - model = editor.model; - view = editor.editing.view; - doc = model.document; newEditor.editing.view.attachDomRoot( editorElement ); plugin = editor.plugins.get( ImageTextAlternative ); - command = editor.commands.get( 'imageTextAlternative' ); - form = plugin._form; - balloon = editor.plugins.get( 'ContextualBalloon' ); - button = editor.ui.componentFactory.create( 'imageTextAlternative' ); } ); } ); @@ -50,188 +38,11 @@ describe( 'ImageTextAlternative', () => { expect( plugin ).to.be.instanceOf( ImageTextAlternative ); } ); - it( 'should load ImageTextAlternativeEngine plugin', () => { - expect( editor.plugins.get( ImageTextAlternativeEngine ) ).to.be.instanceOf( ImageTextAlternativeEngine ); - } ); - - describe( 'toolbar button', () => { - it( 'should be registered in component factory', () => { - expect( button ).to.be.instanceOf( ButtonView ); - } ); - - it( 'should have isEnabled property bind to command\'s isEnabled property', () => { - command.isEnabled = true; - expect( button.isEnabled ).to.be.true; - - command.isEnabled = false; - expect( button.isEnabled ).to.be.false; - } ); - - it( 'should show balloon panel on execute', () => { - expect( balloon.visibleView ).to.be.null; - - setData( model, '[foo bar]' ); - - button.fire( 'execute' ); - expect( balloon.visibleView ).to.equal( form ); - - // Make sure successive execute does not throw, e.g. attempting - // to display the form twice. - button.fire( 'execute' ); - expect( balloon.visibleView ).to.equal( form ); - } ); - - it( 'should set alt attribute value to textarea and select it', () => { - const spy = sinon.spy( form.labeledInput, 'select' ); - - setData( model, '[foo bar]' ); - - button.fire( 'execute' ); - sinon.assert.calledOnce( spy ); - expect( form.labeledInput.value ).equals( 'foo bar' ); - } ); - - it( 'should set empty text to textarea and select it when there is no alt attribute', () => { - const spy = sinon.spy( form.labeledInput, 'select' ); - - setData( model, '[]' ); - - button.fire( 'execute' ); - sinon.assert.calledOnce( spy ); - expect( form.labeledInput.value ).equals( '' ); - } ); + it( 'should load ImageTextAlternativeEditing plugin', () => { + expect( editor.plugins.get( ImageTextAlternativeEditing ) ).to.be.instanceOf( ImageTextAlternativeEditing ); } ); - describe( 'balloon panel form', () => { - // https://github.com/ckeditor/ckeditor5-image/issues/114 - it( 'should make sure the input always stays in sync with the value of the command', () => { - const button = editor.ui.componentFactory.create( 'imageTextAlternative' ); - - // Mock the value of the input after some past editing. - form.labeledInput.value = 'foo'; - - // Mock the user using the form, changing the value but clicking "Cancel". - // so the command's value is not updated. - form.labeledInput.inputView.element.value = 'This value was canceled.'; - - // Mock the user editing the same image once again. - setData( model, '[foo]' ); - - button.fire( 'execute' ); - expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' ); - } ); - - it( 'should execute command on submit', () => { - const spy = sinon.spy( editor, 'execute' ); - form.fire( 'submit' ); - - sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'imageTextAlternative', { - newValue: form.labeledInput.inputView.element.value - } ); - } ); - - it( 'should hide the panel on cancel and focus the editing view', () => { - const spy = sinon.spy( editor.editing.view, 'focus' ); - - setData( model, '[foo bar]' ); - - editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' ); - expect( balloon.visibleView ).to.equal( form ); - - form.fire( 'cancel' ); - expect( balloon.visibleView ).to.be.null; - sinon.assert.calledOnce( spy ); - } ); - - it( 'should not engage when the form is in the balloon yet invisible', () => { - setData( model, '[]' ); - button.fire( 'execute' ); - expect( balloon.visibleView ).to.equal( form ); - - const lastView = new View(); - lastView.element = document.createElement( 'div' ); - - balloon.add( { view: lastView } ); - expect( balloon.visibleView ).to.equal( lastView ); - - button.fire( 'execute' ); - expect( balloon.visibleView ).to.equal( lastView ); - } ); - - describe( 'integration with the editor selection (#render event)', () => { - it( 'should re-position the form', () => { - setData( model, '[]' ); - button.fire( 'execute' ); - - const spy = sinon.spy( balloon, 'updatePosition' ); - - view.fire( 'render' ); - sinon.assert.calledOnce( spy ); - } ); - - it( 'should hide the form and focus editable when image widget has been removed by external change', () => { - setData( model, '[]' ); - button.fire( 'execute' ); - - const removeSpy = sinon.spy( balloon, 'remove' ); - const focusSpy = sinon.spy( editor.editing.view, 'focus' ); - - // EnqueueChange automatically fires #render event. - model.enqueueChange( 'transparent', writer => { - writer.remove( doc.selection.getFirstRange() ); - } ); - - sinon.assert.calledWithExactly( removeSpy, form ); - sinon.assert.calledOnce( focusSpy ); - } ); - } ); - - describe( 'close listeners', () => { - describe( 'keyboard', () => { - it( 'should close upon Esc key press and focus the editing view', () => { - const hideSpy = sinon.spy( plugin, '_hideForm' ); - const focusSpy = sinon.spy( editor.editing.view, 'focus' ); - - setData( model, '[]' ); - button.fire( 'execute' ); - - const keyEvtData = { - keyCode: keyCodes.esc, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - - form.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( hideSpy ); - sinon.assert.calledOnce( keyEvtData.preventDefault ); - sinon.assert.calledOnce( focusSpy ); - } ); - } ); - - describe( 'mouse', () => { - it( 'should close and not focus editable on click outside the panel', () => { - const hideSpy = sinon.spy( plugin, '_hideForm' ); - const focusSpy = sinon.spy( editor.editing.view, 'focus' ); - - setData( model, '[]' ); - button.fire( 'execute' ); - - global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); - sinon.assert.called( hideSpy ); - sinon.assert.notCalled( focusSpy ); - } ); - - it( 'should not close on click inside the panel', () => { - const spy = sinon.spy( plugin, '_hideForm' ); - - setData( model, '[]' ); - button.fire( 'execute' ); - - form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); - sinon.assert.notCalled( spy ); - } ); - } ); - } ); + it( 'should load ImageTextAlternativeUI plugin', () => { + expect( editor.plugins.get( ImageTextAlternativeUI ) ).to.be.instanceOf( ImageTextAlternativeUI ); } ); } ); diff --git a/tests/imagetextalternative/imagetextalternativeengine.js b/tests/imagetextalternative/imagetextalternativeediting.js similarity index 69% rename from tests/imagetextalternative/imagetextalternativeengine.js rename to tests/imagetextalternative/imagetextalternativeediting.js index aafb8b99..86471b80 100644 --- a/tests/imagetextalternative/imagetextalternativeengine.js +++ b/tests/imagetextalternative/imagetextalternativeediting.js @@ -4,22 +4,23 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import ImageTextAlternativeEngine from '../../src/imagetextalternative/imagetextalternativeengine'; +import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting'; import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand'; -describe( 'ImageTextAlternativeEngine', () => { +describe( 'ImageTextAlternativeEditing', () => { let editor; + beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ ImageTextAlternativeEngine ] + plugins: [ ImageTextAlternativeEditing ] } ) .then( newEditor => { editor = newEditor; } ); } ); - it( 'should register ImageAlteranteTextCommand', () => { + it( 'should register ImageAlternativeTextCommand', () => { expect( editor.commands.get( 'imageTextAlternative' ) ).to.be.instanceOf( ImageTextAlternativeCommand ); } ); } ); diff --git a/tests/imagetextalternative/imagetextalternativeui.js b/tests/imagetextalternative/imagetextalternativeui.js new file mode 100644 index 00000000..b01f5c18 --- /dev/null +++ b/tests/imagetextalternative/imagetextalternativeui.js @@ -0,0 +1,228 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global Event, document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Image from '../../src/image'; +import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting'; +import ImageTextAlternativeUI from '../../src/imagetextalternative/imagetextalternativeui'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import View from '@ckeditor/ckeditor5-ui/src/view'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; + +describe( 'ImageTextAlternative', () => { + let editor, model, doc, plugin, command, form, balloon, editorElement, button; + + beforeEach( () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ ImageTextAlternativeEditing, ImageTextAlternativeUI, Image ] + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + newEditor.editing.view.attachDomRoot( editorElement ); + plugin = editor.plugins.get( ImageTextAlternativeUI ); + command = editor.commands.get( 'imageTextAlternative' ); + form = plugin._form; + balloon = editor.plugins.get( 'ContextualBalloon' ); + button = editor.ui.componentFactory.create( 'imageTextAlternative' ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + describe( 'toolbar button', () => { + it( 'should be registered in component factory', () => { + expect( button ).to.be.instanceOf( ButtonView ); + } ); + + it( 'should have isEnabled property bind to command\'s isEnabled property', () => { + command.isEnabled = true; + expect( button.isEnabled ).to.be.true; + + command.isEnabled = false; + expect( button.isEnabled ).to.be.false; + } ); + + it( 'should show balloon panel on execute', () => { + expect( balloon.visibleView ).to.be.null; + + setData( model, '[foo bar]' ); + + button.fire( 'execute' ); + expect( balloon.visibleView ).to.equal( form ); + + // Make sure successive execute does not throw, e.g. attempting + // to display the form twice. + button.fire( 'execute' ); + expect( balloon.visibleView ).to.equal( form ); + } ); + + it( 'should set alt attribute value to textarea and select it', () => { + const spy = sinon.spy( form.labeledInput, 'select' ); + + setData( model, '[foo bar]' ); + + button.fire( 'execute' ); + sinon.assert.calledOnce( spy ); + expect( form.labeledInput.value ).equals( 'foo bar' ); + } ); + + it( 'should set empty text to textarea and select it when there is no alt attribute', () => { + const spy = sinon.spy( form.labeledInput, 'select' ); + + setData( model, '[]' ); + + button.fire( 'execute' ); + sinon.assert.calledOnce( spy ); + expect( form.labeledInput.value ).equals( '' ); + } ); + } ); + + describe( 'balloon panel form', () => { + // https://github.com/ckeditor/ckeditor5-image/issues/114 + it( 'should make sure the input always stays in sync with the value of the command', () => { + const button = editor.ui.componentFactory.create( 'imageTextAlternative' ); + + // Mock the value of the input after some past editing. + form.labeledInput.value = 'foo'; + + // Mock the user using the form, changing the value but clicking "Cancel". + // so the command's value is not updated. + form.labeledInput.inputView.element.value = 'This value was canceled.'; + + // Mock the user editing the same image once again. + setData( model, '[foo]' ); + + button.fire( 'execute' ); + expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' ); + } ); + + it( 'should execute command on submit', () => { + const spy = sinon.spy( editor, 'execute' ); + form.fire( 'submit' ); + + sinon.assert.calledOnce( spy ); + sinon.assert.calledWithExactly( spy, 'imageTextAlternative', { + newValue: form.labeledInput.inputView.element.value + } ); + } ); + + it( 'should hide the panel on cancel and focus the editing view', () => { + const spy = sinon.spy( editor.editing.view, 'focus' ); + + setData( model, '[foo bar]' ); + + editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' ); + expect( balloon.visibleView ).to.equal( form ); + + form.fire( 'cancel' ); + expect( balloon.visibleView ).to.be.null; + sinon.assert.calledOnce( spy ); + } ); + + it( 'should not engage when the form is in the balloon yet invisible', () => { + setData( model, '[]' ); + button.fire( 'execute' ); + expect( balloon.visibleView ).to.equal( form ); + + const lastView = new View(); + lastView.element = document.createElement( 'div' ); + + balloon.add( { view: lastView } ); + expect( balloon.visibleView ).to.equal( lastView ); + + button.fire( 'execute' ); + expect( balloon.visibleView ).to.equal( lastView ); + } ); + + describe( 'integration with the editor selection (#render event)', () => { + it( 'should re-position the form', () => { + setData( model, '[]' ); + button.fire( 'execute' ); + + const spy = sinon.spy( balloon, 'updatePosition' ); + + editor.editing.view.fire( 'render' ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'should hide the form and focus editable when image widget has been removed by external change', () => { + setData( model, '[]' ); + button.fire( 'execute' ); + + const removeSpy = sinon.spy( balloon, 'remove' ); + const focusSpy = sinon.spy( editor.editing.view, 'focus' ); + + // EnqueueChange automatically fires #render event. + model.enqueueChange( 'transparent', writer => { + writer.remove( doc.selection.getFirstRange() ); + } ); + + sinon.assert.calledWithExactly( removeSpy, form ); + sinon.assert.calledOnce( focusSpy ); + } ); + } ); + + describe( 'close listeners', () => { + describe( 'keyboard', () => { + it( 'should close upon Esc key press and focus the editing view', () => { + const hideSpy = sinon.spy( plugin, '_hideForm' ); + const focusSpy = sinon.spy( editor.editing.view, 'focus' ); + + setData( model, '[]' ); + button.fire( 'execute' ); + + const keyEvtData = { + keyCode: keyCodes.esc, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + form.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( hideSpy ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( focusSpy ); + } ); + } ); + + describe( 'mouse', () => { + it( 'should close and not focus editable on click outside the panel', () => { + const hideSpy = sinon.spy( plugin, '_hideForm' ); + const focusSpy = sinon.spy( editor.editing.view, 'focus' ); + + setData( model, '[]' ); + button.fire( 'execute' ); + + global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); + sinon.assert.called( hideSpy ); + sinon.assert.notCalled( focusSpy ); + } ); + + it( 'should not close on click inside the panel', () => { + const spy = sinon.spy( plugin, '_hideForm' ); + + setData( model, '[]' ); + button.fire( 'execute' ); + + form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); + sinon.assert.notCalled( spy ); + } ); + } ); + } ); + } ); +} ); diff --git a/tests/imageupload/imageuploadcommand.js b/tests/imageupload/imageuploadcommand.js index 86e6105c..97b5dd85 100644 --- a/tests/imageupload/imageuploadcommand.js +++ b/tests/imageupload/imageuploadcommand.js @@ -12,7 +12,7 @@ import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository'; import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks'; import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import Image from '../../src/image/imageengine'; +import Image from '../../src/image/imageediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters'; import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position'; diff --git a/tests/imageupload/imageuploadediting.js b/tests/imageupload/imageuploadediting.js index 30a19848..9243446a 100644 --- a/tests/imageupload/imageuploadediting.js +++ b/tests/imageupload/imageuploadediting.js @@ -8,11 +8,11 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageEngine from '../../src/image/imageengine'; +import ImageEditing from '../../src/image/imageediting'; import ImageUploadEditing from '../../src/imageupload/imageuploadediting'; import ImageUploadCommand from '../../src/imageupload/imageuploadcommand'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer'; import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository'; @@ -54,7 +54,7 @@ describe( 'ImageUploadEditing', () => { return VirtualTestEditor .create( { - plugins: [ ImageEngine, ImageUploadEditing, Paragraph, UndoEngine, UploadAdapterPluginMock ] + plugins: [ ImageEditing, ImageUploadEditing, Paragraph, UndoEditing, UploadAdapterPluginMock ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/imageupload/imageuploadprogress.js b/tests/imageupload/imageuploadprogress.js index e785a467..6bf361b2 100644 --- a/tests/imageupload/imageuploadprogress.js +++ b/tests/imageupload/imageuploadprogress.js @@ -8,7 +8,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import ImageEngine from '../../src/image/imageengine'; +import ImageEditing from '../../src/image/imageediting'; import ImageUploadEditing from '../../src/imageupload/imageuploadediting'; import ImageUploadProgress from '../../src/imageupload/imageuploadprogress'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -50,7 +50,7 @@ describe( 'ImageUploadProgress', () => { return VirtualTestEditor .create( { - plugins: [ ImageEngine, Paragraph, ImageUploadEditing, ImageUploadProgress, UploadAdapterPluginMock ] + plugins: [ ImageEditing, Paragraph, ImageUploadEditing, ImageUploadProgress, UploadAdapterPluginMock ] } ) .then( newEditor => { editor = newEditor; diff --git a/tests/integration.js b/tests/integration.js index 0a35e014..dce07457 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -9,9 +9,10 @@ import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloonto import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import Image from '../src/image'; +import ImageToolbar from '../src/imagetoolbar'; import View from '@ckeditor/ckeditor5-ui/src/view'; -describe( 'Image integration', () => { +describe( 'ImageToolbar integration', () => { describe( 'with the BalloonToolbar', () => { let balloon, balloonToolbar, newEditor, editorElement; @@ -21,7 +22,7 @@ describe( 'Image integration', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ Image, BalloonToolbar, Paragraph ] + plugins: [ Image, ImageToolbar, BalloonToolbar, Paragraph ] } ) .then( editor => { newEditor = editor; diff --git a/tests/manual/imageplaceholder.js b/tests/manual/imageplaceholder.js index 35d10895..d717ff01 100644 --- a/tests/manual/imageplaceholder.js +++ b/tests/manual/imageplaceholder.js @@ -6,11 +6,11 @@ /* global document */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import ImageEngine from '../../src/image/imageengine'; +import ImageEditing from '../../src/image/imageediting'; import ImageUploadEditing from '../../src/imageupload/imageuploadediting'; import ImageUploadProgress from '../../src/imageupload/imageuploadprogress'; -VirtualTestEditor.create( { plugins: [ ImageEngine, ImageUploadEditing, ImageUploadProgress ] } ) +VirtualTestEditor.create( { plugins: [ ImageEditing, ImageUploadEditing, ImageUploadProgress ] } ) .then( editor => { const imageUploadProgress = editor.plugins.get( ImageUploadProgress ); const img = document.createElement( 'img' );