From 55fc88ecd18a8de43a044bccb2549a0046470ec5 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 26 Oct 2023 20:25:26 +0200 Subject: [PATCH 01/53] PoC of declarative nested toolbar dropdown with split button. --- .../ckeditor5-core/src/editor/editorconfig.ts | 2 + .../src/dropdown/button/splitbuttonview.ts | 43 +++++++------ .../ckeditor5-ui/src/toolbar/toolbarview.ts | 62 ++++++++++++++----- .../tests/manual/toolbar/nested.js | 8 +++ 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index cfdf8f6d200..3e3b3554ea1 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -590,6 +590,8 @@ export type ToolbarConfigItem = string | { icon?: string | false; withText?: boolean; tooltip?: boolean | string | ( ( label: string, keystroke: string | undefined ) => string ); + actionItem: string; + isVertical?: boolean; }; /** diff --git a/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts b/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts index 7fa91274ebc..66a29d88c03 100644 --- a/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts +++ b/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts @@ -12,6 +12,7 @@ import ButtonView from '../../button/buttonview'; import type ViewCollection from '../../viewcollection'; import type Button from '../../button/button'; import type DropdownButton from './dropdownbutton'; +import type { FocusableView } from '../../focuscycler'; import { KeystrokeHandler, @@ -170,7 +171,7 @@ export default class SplitButtonView extends View implements Dro /** * @inheritDoc */ - constructor( locale?: Locale ) { + constructor( locale?: Locale, ActionButtonClass?: new ( locale?: Locale ) => ButtonView & FocusableView ) { super( locale ); const bind = this.bindTemplate; @@ -193,7 +194,7 @@ export default class SplitButtonView extends View implements Dro this.set( 'withText', false ); this.children = this.createCollection(); - this.actionView = this._createActionView(); + this.actionView = this._createActionView( ActionButtonClass ); this.arrowView = this._createArrowView(); this.keystrokes = new KeystrokeHandler(); this.focusTracker = new FocusTracker(); @@ -269,22 +270,28 @@ export default class SplitButtonView extends View implements Dro * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #actionView} and binds it with main split button * attributes. */ - private _createActionView() { - const actionView = new ButtonView(); - - actionView.bind( - 'icon', - 'isEnabled', - 'isOn', - 'isToggleable', - 'keystroke', - 'label', - 'tabindex', - 'tooltip', - 'tooltipPosition', - 'type', - 'withText' - ).to( this ); + private _createActionView( ActionButtonClass?: new ( locale?: Locale ) => ButtonView & FocusableView ) { + let actionView: ButtonView & FocusableView | undefined; + + if ( ActionButtonClass ) { + actionView = new ActionButtonClass(); + } else { + actionView = new ButtonView(); + + actionView.bind( + 'icon', + 'isEnabled', + 'isOn', + 'isToggleable', + 'keystroke', + 'label', + 'tabindex', + 'tooltip', + 'tooltipPosition', + 'type', + 'withText' + ).to( this ); + } actionView.extendTemplate( { attributes: { diff --git a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts index 1be26234d75..d61871b2d19 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts @@ -19,6 +19,8 @@ import type ComponentFactory from '../componentfactory'; import type ViewCollection from '../viewcollection'; import type DropdownView from '../dropdown/dropdownview'; import type DropdownPanelFocusable from '../dropdown/dropdownpanelfocusable'; +import ButtonView from '../button/buttonview'; +import SplitButtonView from '../dropdown/button/splitbuttonview'; import { FocusTracker, @@ -507,7 +509,28 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } const locale = this.locale; - const dropdownView = createDropdown( locale ); + let DropdownButtonClass = undefined; + + if ( 'actionItem' in definition ) { + const ActionButtonClass = class extends ButtonView { + constructor( locale?: Locale ) { + super( locale ); + + // TODO verify if item is a ButtonView + return componentFactory.create( definition.actionItem ) as ButtonView; + } + }; + + DropdownButtonClass = class extends SplitButtonView { + constructor( locale?: Locale ) { + super( locale ); + + return new SplitButtonView( locale, ActionButtonClass ); + } + }; + } + + const dropdownView = createDropdown( locale, DropdownButtonClass ); if ( !label ) { /** @@ -532,25 +555,34 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown'; - dropdownView.buttonView.set( { - label, - tooltip, - withText: !!withText - } ); - // Allow disabling icon by passing false. - if ( icon !== false ) { - // A pre-defined icon picked by name, SVG string, a fallback (default) icon. - dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon! ] || icon || threeVerticalDots; - } - // If the icon is disabled, display the label automatically. - else { - dropdownView.buttonView.withText = true; + if ( 'actionItem' in definition ) { + dropdownView.buttonView.set( { + label, + tooltip + } ); + // TODO withText and icon are not used + } else { + dropdownView.buttonView.set( { + label, + tooltip, + withText: !!withText + } ); + + // Allow disabling icon by passing false. + if ( icon !== false ) { + // A pre-defined icon picked by name, SVG string, a fallback (default) icon. + dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon! ] || icon || threeVerticalDots; + } + // If the icon is disabled, display the label automatically. + else { + dropdownView.buttonView.withText = true; + } } addToolbarToDropdown( dropdownView, () => ( dropdownView.toolbarView!._buildItemsFromConfig( items, componentFactory, removeItems ) - ) ); + ), { isVertical: definition.isVertical } ); return dropdownView; } diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js index fbf4c6a692a..dff82131d57 100644 --- a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js @@ -70,6 +70,14 @@ ClassicEditor 'insertImage', 'insertTable', 'mediaEmbed' ] }, + { + actionItem: 'bold', + label: 'Some custom split button', + isVertical: true, + items: [ + 'strikethrough', 'underline', 'code', 'subscript', 'superscript' + ] + }, '-', { label: 'Icon: default', From 8a2b334615b74f58c03631a077d0898efec93d48 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 27 Oct 2023 17:42:17 +0200 Subject: [PATCH 02/53] Refactored the way of passing custom dropdown action button to the dropdown. --- .../src/dropdown/button/splitbuttonview.ts | 14 ++++------ packages/ckeditor5-ui/src/dropdown/utils.ts | 7 +++-- .../ckeditor5-ui/src/toolbar/toolbarview.ts | 28 ++++++------------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts b/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts index 66a29d88c03..ecbe0a248b2 100644 --- a/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts +++ b/packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.ts @@ -171,7 +171,7 @@ export default class SplitButtonView extends View implements Dro /** * @inheritDoc */ - constructor( locale?: Locale, ActionButtonClass?: new ( locale?: Locale ) => ButtonView & FocusableView ) { + constructor( locale?: Locale, actionButton?: ButtonView & FocusableView ) { super( locale ); const bind = this.bindTemplate; @@ -194,7 +194,7 @@ export default class SplitButtonView extends View implements Dro this.set( 'withText', false ); this.children = this.createCollection(); - this.actionView = this._createActionView( ActionButtonClass ); + this.actionView = this._createActionView( actionButton ); this.arrowView = this._createArrowView(); this.keystrokes = new KeystrokeHandler(); this.focusTracker = new FocusTracker(); @@ -270,14 +270,10 @@ export default class SplitButtonView extends View implements Dro * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #actionView} and binds it with main split button * attributes. */ - private _createActionView( ActionButtonClass?: new ( locale?: Locale ) => ButtonView & FocusableView ) { - let actionView: ButtonView & FocusableView | undefined; - - if ( ActionButtonClass ) { - actionView = new ActionButtonClass(); - } else { - actionView = new ButtonView(); + private _createActionView( actionButton?: ButtonView & FocusableView ) { + const actionView = actionButton || new ButtonView(); + if ( !actionButton ) { actionView.bind( 'icon', 'isEnabled', diff --git a/packages/ckeditor5-ui/src/dropdown/utils.ts b/packages/ckeditor5-ui/src/dropdown/utils.ts index 4942b95f78f..efa9a303872 100644 --- a/packages/ckeditor5-ui/src/dropdown/utils.ts +++ b/packages/ckeditor5-ui/src/dropdown/utils.ts @@ -105,15 +105,16 @@ import ListItemGroupView from '../list/listitemgroupview'; * {@link module:ui/dropdown/utils~addToolbarToDropdown} utils. * * @param locale The locale instance. - * @param ButtonClass The dropdown button view class. Needs to implement the + * @param ButtonClassOrInstance The dropdown button view class. Needs to implement the * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface. * @returns The dropdown view instance. */ export function createDropdown( locale: Locale | undefined, - ButtonClass: new ( locale?: Locale ) => DropdownButton & FocusableView = DropdownButtonView + ButtonClassOrInstance: + ( new ( locale?: Locale ) => DropdownButton & FocusableView ) | DropdownButton & FocusableView = DropdownButtonView ): DropdownView { - const buttonView = new ButtonClass( locale ); + const buttonView = typeof ButtonClassOrInstance == 'function' ? new ButtonClassOrInstance( locale ) : ButtonClassOrInstance; const panelView = new DropdownPanelView( locale ); const dropdownView = new DropdownView( locale, buttonView, panelView ); diff --git a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts index d61871b2d19..3846d4ce8e7 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts @@ -8,7 +8,7 @@ */ import View from '../view'; -import FocusCycler from '../focuscycler'; +import FocusCycler, { type FocusableView } from '../focuscycler'; import ToolbarSeparatorView from './toolbarseparatorview'; import ToolbarLineBreakView from './toolbarlinebreakview'; import preventDefault from '../bindings/preventdefault'; @@ -19,7 +19,8 @@ import type ComponentFactory from '../componentfactory'; import type ViewCollection from '../viewcollection'; import type DropdownView from '../dropdown/dropdownview'; import type DropdownPanelFocusable from '../dropdown/dropdownpanelfocusable'; -import ButtonView from '../button/buttonview'; +import type ButtonView from '../button/buttonview'; +import type DropdownButton from '../dropdown/button/dropdownbutton'; import SplitButtonView from '../dropdown/button/splitbuttonview'; import { @@ -509,28 +510,17 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } const locale = this.locale; - let DropdownButtonClass = undefined; + let dropdownButton: + ( new ( locale?: Locale ) => DropdownButton & FocusableView ) | DropdownButton & FocusableView = SplitButtonView; if ( 'actionItem' in definition ) { - const ActionButtonClass = class extends ButtonView { - constructor( locale?: Locale ) { - super( locale ); + const actionButton = componentFactory.create( definition.actionItem ) as ButtonView; + // TODO verify if item is a ButtonView - // TODO verify if item is a ButtonView - return componentFactory.create( definition.actionItem ) as ButtonView; - } - }; - - DropdownButtonClass = class extends SplitButtonView { - constructor( locale?: Locale ) { - super( locale ); - - return new SplitButtonView( locale, ActionButtonClass ); - } - }; + dropdownButton = new SplitButtonView( locale, actionButton ); } - const dropdownView = createDropdown( locale, DropdownButtonClass ); + const dropdownView = createDropdown( locale, dropdownButton ); if ( !label ) { /** From f331afc5d551f34b48d196598ad66708d94ec52f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 28 Oct 2023 16:14:14 +0200 Subject: [PATCH 03/53] Customizable toolbar buttons. The CollapsibleView extracted to UI library. --- .../ckeditor5-core/src/editor/editorconfig.ts | 12 ++++- packages/ckeditor5-core/src/index.ts | 2 + .../src/imageinsert/imageinsertui.ts | 15 ++++++- packages/ckeditor5-list/package.json | 4 +- .../listproperties/ui/listpropertiesview.ts | 3 +- .../components/collapsible}/collapsible.css | 0 .../src/collapsible}/collapsibleview.ts | 20 ++++++--- packages/ckeditor5-ui/src/index.ts | 2 + .../ckeditor5-ui/src/toolbar/toolbarview.ts | 44 ++++++++++++++++--- .../tests/manual/toolbar/nested.js | 22 +++++++++- .../components/collapsible}/collapsible.css | 0 11 files changed, 106 insertions(+), 18 deletions(-) rename packages/ckeditor5-theme-lark/theme/{ckeditor5-list => ckeditor5-ui/components/collapsible}/collapsible.css (100%) rename packages/{ckeditor5-list/src/listproperties/ui => ckeditor5-ui/src/collapsible}/collapsibleview.ts (88%) rename packages/{ckeditor5-list/theme => ckeditor5-ui/theme/components/collapsible}/collapsible.css (100%) diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index 3e3b3554ea1..1db1e1fbe6f 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -584,7 +584,17 @@ export type ToolbarConfig = Array | { icon?: string; }; -export type ToolbarConfigItem = string | { +export type ToolbarConfigItem = string | ToolbarConfigCustomItem | ToolbarConfigDropdownItem; + +export type ToolbarConfigCustomItem = { + actionItem: string; + label?: string; + icon?: string | false; + withText?: boolean; + tooltip?: boolean | string | ( ( label: string, keystroke: string | undefined ) => string ); +}; + +export type ToolbarConfigDropdownItem = { items: Array; label: string; icon?: string | false; diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index 8bd7878460a..ada6d7b29bd 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -23,6 +23,8 @@ export type { LanguageConfig, ToolbarConfig, ToolbarConfigItem, + ToolbarConfigCustomItem, + ToolbarConfigDropdownItem, UiConfig } from './editor/editorconfig'; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index eba93beb57b..de0f83324dd 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -9,7 +9,7 @@ import { Plugin, icons, type Command } from 'ckeditor5/src/core'; import type { Locale } from 'ckeditor5/src/utils'; -import { SplitButtonView, createDropdown, type DropdownView, type LabeledFieldView } from 'ckeditor5/src/ui'; +import { SplitButtonView, createDropdown, type DropdownView, type LabeledFieldView, CollapsibleView } from 'ckeditor5/src/ui'; import ImageInsertPanelView from './ui/imageinsertpanelview'; import { prepareIntegrations } from './utils'; @@ -52,6 +52,19 @@ export default class ImageInsertUI extends Plugin { // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); + + editor.ui.componentFactory.add( 'insertImageView', locale => { + const imageInsertView = new ImageInsertPanelView( locale, prepareIntegrations( editor ) ); + const collapsibleView = new CollapsibleView( locale, [ imageInsertView ] ); + const t = locale.t; + + collapsibleView.set( { + label: t( 'Insert with link' ), + isCollapsed: true + } ); + + return collapsibleView; + } ); } /** diff --git a/packages/ckeditor5-list/package.json b/packages/ckeditor5-list/package.json index 89109c9213f..3cb785b818b 100644 --- a/packages/ckeditor5-list/package.json +++ b/packages/ckeditor5-list/package.json @@ -12,8 +12,7 @@ ], "main": "src/index.ts", "dependencies": { - "ckeditor5": "40.0.0", - "@ckeditor/ckeditor5-ui": "40.0.0" + "ckeditor5": "40.0.0" }, "devDependencies": { "@ckeditor/ckeditor5-alignment": "40.0.0", @@ -47,6 +46,7 @@ "@ckeditor/ckeditor5-table": "40.0.0", "@ckeditor/ckeditor5-theme-lark": "40.0.0", "@ckeditor/ckeditor5-typing": "40.0.0", + "@ckeditor/ckeditor5-ui": "40.0.0", "@ckeditor/ckeditor5-undo": "40.0.0", "@ckeditor/ckeditor5-utils": "40.0.0", "@ckeditor/ckeditor5-widget": "40.0.0", diff --git a/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts b/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts index 0c2aa1c51b3..98da0004418 100644 --- a/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts +++ b/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts @@ -15,6 +15,7 @@ import { LabeledFieldView, createLabeledInputNumber, addKeyboardHandlingForGrid, + CollapsibleView, type ButtonView, type InputNumberView } from 'ckeditor5/src/ui'; @@ -26,8 +27,6 @@ import { type Locale } from 'ckeditor5/src/utils'; -import CollapsibleView from './collapsibleview'; - import type { ListPropertiesConfig } from '../../listconfig'; import '../../../theme/listproperties.css'; diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-list/collapsible.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css similarity index 100% rename from packages/ckeditor5-theme-lark/theme/ckeditor5-list/collapsible.css rename to packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css diff --git a/packages/ckeditor5-list/src/listproperties/ui/collapsibleview.ts b/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts similarity index 88% rename from packages/ckeditor5-list/src/listproperties/ui/collapsibleview.ts rename to packages/ckeditor5-ui/src/collapsible/collapsibleview.ts index 4bab348e0c8..4562b430bd9 100644 --- a/packages/ckeditor5-list/src/listproperties/ui/collapsibleview.ts +++ b/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts @@ -4,17 +4,18 @@ */ /** - * @module list/listproperties/ui/collapsibleview + * @module ui/collapsible/collapsibleview */ -import type { Locale } from 'ckeditor5/src/utils'; +import type { Locale } from '@ckeditor/ckeditor5-utils'; -import { View, ButtonView, type ViewCollection } from 'ckeditor5/src/ui'; +import View from '../view'; +import ButtonView from '../button/buttonview'; +import type ViewCollection from '../viewcollection'; -// eslint-disable-next-line ckeditor5-rules/ckeditor-imports -import dropdownArrowIcon from '@ckeditor/ckeditor5-ui/theme/icons/dropdown-arrow.svg'; +import dropdownArrowIcon from '../../theme/icons/dropdown-arrow.svg'; -import '../../../theme/collapsible.css'; +import '../../theme/components/collapsible/collapsible.css'; /** * A collapsible UI component. Consists of a labeled button and a container which can be collapsed @@ -118,6 +119,13 @@ export default class CollapsibleView extends View { this._collapsibleAriaLabelUid = this.buttonView.labelView.element!.id; } + /** + * TODO + */ + public focus(): void { + this.buttonView.focus(); + } + /** * Creates the main {@link #buttonView} of the collapsible. */ diff --git a/packages/ckeditor5-ui/src/index.ts b/packages/ckeditor5-ui/src/index.ts index a269d0a8bf1..d4b2cb4034e 100644 --- a/packages/ckeditor5-ui/src/index.ts +++ b/packages/ckeditor5-ui/src/index.ts @@ -86,6 +86,8 @@ export { default as SearchInfoView } from './search/searchinfoview'; export { default as FilteredView, type FilteredViewExecuteEvent } from './search/filteredview'; export { default as HighlightedTextView } from './highlightedtext/highlightedtextview'; +export { default as CollapsibleView } from './collapsible/collapsibleview'; + export { default as TooltipManager } from './tooltipmanager'; export { default as Template, type TemplateDefinition } from './template'; diff --git a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts index 3846d4ce8e7..2ade26aff87 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts @@ -41,7 +41,9 @@ import { import { icons, type ToolbarConfig, - type ToolbarConfigItem + type ToolbarConfigItem, + type ToolbarConfigCustomItem, + type ToolbarConfigDropdownItem } from '@ckeditor/ckeditor5-core'; import { isObject } from 'lodash-es'; @@ -341,8 +343,10 @@ export default class ToolbarView extends View implements DropdownPanelFocusable const normalizedRemoveItems = removeItems || config.removeItems; const itemsToAdd = this._cleanItemsConfiguration( config.items, factory, normalizedRemoveItems ) .map( item => { - if ( isObject( item ) ) { + if ( isObject( item ) && 'items' in item ) { return this._createNestedToolbarDropdown( item, factory, normalizedRemoveItems ); + } else if ( isObject( item ) ) { + return this._createCustomizedItem( item, factory ); } else if ( item === '|' ) { return new ToolbarSeparatorView(); } else if ( item === '-' ) { @@ -437,6 +441,8 @@ export default class ToolbarView extends View implements DropdownPanelFocusable return false; } + // TODO add validation for ToolbarConfigCustomItem + return true; } ); @@ -496,7 +502,7 @@ export default class ToolbarView extends View implements DropdownPanelFocusable * of the nested toolbar. */ private _createNestedToolbarDropdown( - definition: Exclude, + definition: ToolbarConfigDropdownItem, componentFactory: ComponentFactory, removeItems: Array ) { @@ -510,8 +516,8 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } const locale = this.locale; - let dropdownButton: - ( new ( locale?: Locale ) => DropdownButton & FocusableView ) | DropdownButton & FocusableView = SplitButtonView; + + let dropdownButton: ( new ( locale?: Locale ) => DropdownButton & FocusableView ) | DropdownButton & FocusableView | undefined; if ( 'actionItem' in definition ) { const actionButton = componentFactory.create( definition.actionItem ) as ButtonView; @@ -576,6 +582,34 @@ export default class ToolbarView extends View implements DropdownPanelFocusable return dropdownView; } + + /** + * TODO + */ + private _createCustomizedItem( + definition: ToolbarConfigCustomItem, + componentFactory: ComponentFactory + ) { + const itemView = componentFactory.create( definition.actionItem ); + + if ( definition.withText && 'withText' in itemView ) { + itemView.withText = true; + } + + if ( definition.icon && 'icon' in itemView ) { + itemView.icon = definition.icon; + } + + if ( definition.label && 'label' in itemView ) { + itemView.label = definition.label; + } + + if ( definition.tooltip && 'tooltip' in itemView ) { + itemView.tooltip = definition.tooltip; + } + + return itemView; + } } /** diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js index dff82131d57..a074861ecc8 100644 --- a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js @@ -18,6 +18,7 @@ import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import { icons } from '@ckeditor/ckeditor5-core'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -75,7 +76,26 @@ ClassicEditor label: 'Some custom split button', isVertical: true, items: [ - 'strikethrough', 'underline', 'code', 'subscript', 'superscript' + { + actionItem: 'strikethrough', + withText: true, + icon: icons.cog + }, { + actionItem: 'underline', + withText: true, + label: 'foooo', + tooltip: 'Just testing' + }, { + actionItem: 'code', + withText: true + }, { + actionItem: 'subscript', + withText: true + }, { + actionItem: 'superscript', + withText: true + }, + 'insertImageView' ] }, '-', diff --git a/packages/ckeditor5-list/theme/collapsible.css b/packages/ckeditor5-ui/theme/components/collapsible/collapsible.css similarity index 100% rename from packages/ckeditor5-list/theme/collapsible.css rename to packages/ckeditor5-ui/theme/components/collapsible/collapsible.css From db2e844936600337a176e20bb52df08ec8f874ea Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 31 Oct 2023 21:09:50 +0100 Subject: [PATCH 04/53] Cleaning ImageInsertUI. --- .../src/imageinsert/imageinsertui.ts | 204 +++++++------ .../imageinsert/ui/imageinsertformrowview.ts | 102 ------- .../imageinsert/ui/imageinsertpanelview.ts | 168 +++-------- .../src/imageinsert/ui/imageinserturlview.ts | 269 ++++++++++++++++++ .../ckeditor5-image/src/imageinsert/utils.ts | 77 ----- .../tests/manual/imageinsert.js | 4 +- .../ckeditor5-image/theme/imageinsert.css | 32 +++ .../theme/imageinsertformrowview.css | 36 --- 8 files changed, 457 insertions(+), 435 deletions(-) delete mode 100644 packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.ts create mode 100644 packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts delete mode 100644 packages/ckeditor5-image/src/imageinsert/utils.ts delete mode 100644 packages/ckeditor5-image/theme/imageinsertformrowview.css diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index de0f83324dd..5ee5dfbf277 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -7,16 +7,22 @@ * @module image/imageinsert/imageinsertui */ -import { Plugin, icons, type Command } from 'ckeditor5/src/core'; +import { Plugin, icons } from 'ckeditor5/src/core'; import type { Locale } from 'ckeditor5/src/utils'; -import { SplitButtonView, createDropdown, type DropdownView, type LabeledFieldView, CollapsibleView } from 'ckeditor5/src/ui'; +import { + SplitButtonView, + createDropdown, + CollapsibleView, + type DropdownView, + type View, + type ButtonView +} from 'ckeditor5/src/ui'; import ImageInsertPanelView from './ui/imageinsertpanelview'; -import { prepareIntegrations } from './utils'; -import type ImageUtils from '../imageutils'; import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; import type UploadImageCommand from '../imageupload/uploadimagecommand'; import type InsertImageCommand from '../image/insertimagecommand'; +import ImageInsertUrlView from './ui/imageinserturlview'; /** * The image insert dropdown plugin. @@ -54,7 +60,7 @@ export default class ImageInsertUI extends Plugin { editor.ui.componentFactory.add( 'imageInsert', componentCreator ); editor.ui.componentFactory.add( 'insertImageView', locale => { - const imageInsertView = new ImageInsertPanelView( locale, prepareIntegrations( editor ) ); + const imageInsertView = new ImageInsertPanelView( locale, this._prepareIntegrations() ); const collapsibleView = new CollapsibleView( locale, [ imageInsertView ] ); const t = locale.t; @@ -79,108 +85,55 @@ export default class ImageInsertUI extends Plugin { const uploadImageCommand: UploadImageCommand | undefined = editor.commands.get( 'uploadImage' ); const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; - this.dropdownView = createDropdown( locale, uploadImageCommand ? SplitButtonView : undefined ); - - const buttonView = this.dropdownView.buttonView; - const panelView = this.dropdownView.panelView; - - buttonView.set( { - label: t( 'Insert image' ), - icon: icons.image, - tooltip: true - } ); - - panelView.extendTemplate( { - attributes: { - class: 'ck-image-insert__panel' - } - } ); + let dropdownButton; if ( uploadImageCommand ) { - const splitButtonView = this.dropdownView.buttonView as SplitButtonView; - - // We are injecting custom button replacement to readonly field. - ( splitButtonView as any ).actionView = editor.ui.componentFactory.create( 'uploadImage' ); - // After we replaced action button with `uploadImage` component, - // we have lost a proper styling and some minor visual quirks have appeared. - // Brining back original split button classes helps fix the button styling - // See https://github.com/ckeditor/ckeditor5/issues/7986. - splitButtonView.actionView.extendTemplate( { + const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as ButtonView; + + uploadImageButton.extendTemplate( { attributes: { - class: 'ck ck-button ck-splitbutton__action' + class: 'ck ck-button' } } ); + + dropdownButton = new SplitButtonView( locale, uploadImageButton ); } - return this._setUpDropdown( uploadImageCommand || insertImageCommand ); - } + const dropdownView = createDropdown( locale, dropdownButton ); - /** - * Sets up the dropdown view. - * - * @param command An uploadImage or insertImage command. - */ - private _setUpDropdown( command: Command ): DropdownView { - const editor = this.editor; - const t = editor.t; - const dropdownView = this.dropdownView!; - const panelView = dropdownView.panelView; - const imageUtils: ImageUtils = this.editor.plugins.get( 'ImageUtils' ); - const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!; + if ( !uploadImageCommand ) { + dropdownView.buttonView.set( { + label: t( 'Insert image' ), + icon: icons.image, + tooltip: true + } ); + } - let imageInsertView: ImageInsertPanelView; + dropdownView.panelView.extendTemplate( { + attributes: { + class: 'ck-image-insert__panel' + } + } ); + + this.dropdownView = dropdownView; - dropdownView.bind( 'isEnabled' ).to( command ); + dropdownView.bind( 'isEnabled' ).to( uploadImageCommand || insertImageCommand ); dropdownView.once( 'change:isOpen', () => { - imageInsertView = new ImageInsertPanelView( editor.locale, prepareIntegrations( editor ) ); + const imageInsertPanelView = new ImageInsertPanelView( editor.locale, this._prepareIntegrations() ); - imageInsertView.delegate( 'submit', 'cancel' ).to( dropdownView ); - panelView.children.add( imageInsertView ); + imageInsertPanelView.delegate( 'submit', 'cancel' ).to( dropdownView ); + dropdownView.panelView.children.add( imageInsertPanelView ); } ); - dropdownView.on( 'change:isOpen', () => { - const selectedElement = editor.model.document.selection.getSelectedElement()!; - const insertButtonView = imageInsertView.insertButtonView; - const insertImageViaUrlForm = imageInsertView.getIntegration( 'insertImageViaUrl' ) as LabeledFieldView; - - if ( dropdownView.isOpen ) { - if ( imageUtils.isImage( selectedElement ) ) { - imageInsertView.imageURLInputValue = replaceImageSourceCommand.value!; - insertButtonView.label = t( 'Update' ); - insertImageViaUrlForm.label = t( 'Update image URL' ); - } else { - imageInsertView.imageURLInputValue = ''; - insertButtonView.label = t( 'Insert' ); - insertImageViaUrlForm.label = t( 'Insert image via URL' ); - } - } - // Note: Use the low priority to make sure the following listener starts working after the - // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the - // invisible form/input cannot be focused/selected. - }, { priority: 'low' } ); - - this.delegate( 'cancel' ).to( dropdownView ); - dropdownView.on( 'submit', () => { closePanel(); - onSubmit(); } ); dropdownView.on( 'cancel', () => { closePanel(); } ); - function onSubmit() { - const selectedElement = editor.model.document.selection.getSelectedElement()!; - - if ( imageUtils.isImage( selectedElement ) ) { - editor.execute( 'replaceImageSource', { source: imageInsertView.imageURLInputValue } ); - } else { - editor.execute( 'insertImage', { source: imageInsertView.imageURLInputValue } ); - } - } - function closePanel() { editor.editing.view.focus(); dropdownView.isOpen = false; @@ -188,4 +141,87 @@ export default class ImageInsertUI extends Plugin { return dropdownView; } + + /** + * TODO + */ + private _prepareIntegrations(): Array { + const editor = this.editor; + const items = editor.config.get( 'image.insert.integrations' ) || [ 'insertImageViaUrl' ]; + + return items.map( item => { + if ( item == 'insertImageViaUrl' ) { + return this._createInsertUrlView(); + } + else if ( item == 'openCKFinder' && editor.ui.componentFactory.has( 'ckfinder' ) ) { + return this._createCKFinderView(); + } + else { + return this._createGenericIntegration( item ); + } + } ); + } + + /** + * TODO + */ + private _createInsertUrlView() { + const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; + const imageInsertUrlView = new ImageInsertUrlView( this.editor.locale ); + + imageInsertUrlView.delegate( 'submit', 'cancel' ).to( this.dropdownView! ); + imageInsertUrlView.bind( 'isImageSelected' ).to( replaceImageSourceCommand, 'isEnabled' ); + + // Set initial value because integrations are created on first dropdown open. + imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; + + this.dropdownView!.on( 'change:isOpen', () => { + if ( this.dropdownView!.isOpen ) { + // Make sure that each time the panel shows up, the URL field remains in sync with the value of + // the command. If the user typed in the input, then canceled and re-opened it without changing + // the value of the media command (e.g. because they didn't change the selection), they would see + // the old value instead of the actual value of the command. + imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; + } + + // Note: Use the low priority to make sure the following listener starts working after the + // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the + // invisible form/input cannot be focused/selected. + }, { priority: 'low' } ); + + this.dropdownView!.on( 'submit', () => { + if ( replaceImageSourceCommand.isEnabled ) { + this.editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); + } else { + this.editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); + } + } ); + + return imageInsertUrlView; + } + + /** + * TODO + */ + private _createCKFinderView() { + const ckFinderButton = this._createGenericIntegration( 'ckfinder' ); + + ckFinderButton.set( 'class', 'ck-image-insert__ck-finder-button' ); + + return ckFinderButton; + } + + /** + * TODO + */ + private _createGenericIntegration( name: string ) { + const button = this.editor.ui.componentFactory.create( name ) as ButtonView; + + button.set( 'withText', true ); + + // We want to close the dropdown panel view when user clicks the ckFinderButton. + button.delegate( 'execute' ).to( this.dropdownView!, 'cancel' ); + + return button; + } } diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.ts deleted file mode 100644 index ef153326e5c..00000000000 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformrowview.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module image/imageinsert/ui/imageinsertformrowview - */ - -import type { Locale } from 'ckeditor5/src/utils'; -import { View, type ViewCollection, type LabelView } from 'ckeditor5/src/ui'; - -import '../../../theme/imageinsertformrowview.css'; - -/** - * The class representing a single row in a complex form, - * used by {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. - * - * **Note**: For now this class is private. When more use cases appear (beyond `ckeditor5-table` and `ckeditor5-image`), - * it will become a component in `ckeditor5-ui`. - * - * @private - */ -export default class ImageUploadFormRowView extends View { - /** - * An additional CSS class added to the {@link #element}. - * - * @observable - */ - declare public class: string | null; - - /** - * A collection of row items (buttons, dropdowns, etc.). - */ - public readonly children: ViewCollection; - - /** - * The role property reflected by the `role` DOM attribute of the {@link #element}. - * - * **Note**: Used only when a `labelView` is passed to constructor `options`. - * - * @observable - * @private - */ - declare public _role: string | null; - - /** - * The ARIA property reflected by the `aria-labelledby` DOM attribute of the {@link #element}. - * - * **Note**: Used only when a `labelView` is passed to constructor `options`. - * - * @observable - * @private - */ - declare public _ariaLabelledBy: string | null; - - /** - * Creates an instance of the form row class. - * - * @param locale The locale instance. - * @param options.labelView When passed, the row gets the `group` and `aria-labelledby` - * DOM attributes and gets described by the label. - */ - constructor( locale: Locale, options: { children?: Array; class?: string; labelView?: LabelView } = {} ) { - super( locale ); - - const bind = this.bindTemplate; - - this.set( 'class', options.class || null ); - - this.children = this.createCollection(); - - if ( options.children ) { - options.children.forEach( child => this.children.add( child ) ); - } - - this.set( '_role', null ); - - this.set( '_ariaLabelledBy', null ); - - if ( options.labelView ) { - this.set( { - _role: 'group', - _ariaLabelledBy: options.labelView.id - } ); - } - - this.setTemplate( { - tag: 'div', - attributes: { - class: [ - 'ck', - 'ck-form__row', - bind.to( 'class' ) - ], - role: bind.to( '_role' ), - 'aria-labelledby': bind.to( '_ariaLabelledBy' ) - }, - children: this.children - } ); - } -} diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts index b782c4479f2..dbe79b8a200 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts @@ -7,39 +7,24 @@ * @module image/imageinsert/ui/imageinsertpanelview */ -import { icons } from 'ckeditor5/src/core'; -import { ButtonView, View, ViewCollection, submitHandler, FocusCycler, type InputTextView, type LabeledFieldView } from 'ckeditor5/src/ui'; -import { Collection, FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils'; - -import ImageInsertFormRowView from './imageinsertformrowview'; +import { + View, + ViewCollection, + submitHandler, + FocusCycler, + FocusCyclerForwardCycleEvent, + FocusCyclerBackwardCycleEvent +} from 'ckeditor5/src/ui'; +import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils'; import '../../../theme/imageinsert.css'; -export type ViewWithName = View & { name: string }; - /** * The insert an image via URL view controller class. * * See {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. */ export default class ImageInsertPanelView extends View { - /** - * The "insert/update" button view. - */ - public insertButtonView: ButtonView; - - /** - * The "cancel" button view. - */ - public cancelButtonView: ButtonView; - - /** - * The value of the URL input. - * - * @observable - */ - declare public imageURLInputValue: string; - /** * Tracks information about DOM focus in the form. */ @@ -62,10 +47,8 @@ export default class ImageInsertPanelView extends View { /** * A collection of the defined integrations for inserting the images. - * - * @private */ - declare public _integrations: Collection; + private readonly children: ViewCollection; /** * Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}. @@ -73,22 +56,13 @@ export default class ImageInsertPanelView extends View { * @param locale The localization services instance. * @param integrations An integrations object that contains components (or tokens for components) to be shown in the panel view. */ - constructor( locale: Locale, integrations: Record = {} ) { + constructor( locale: Locale, integrations: Array = [] ) { super( locale ); - const { insertButtonView, cancelButtonView } = this._createActionButtons( locale ); - - this.insertButtonView = insertButtonView; - - this.cancelButtonView = cancelButtonView; - - this.set( 'imageURLInputValue', '' ); - this.focusTracker = new FocusTracker(); - this.keystrokes = new KeystrokeHandler(); - this._focusables = new ViewCollection(); + this.children = this.createCollection(); this._focusCycler = new FocusCycler( { focusables: this._focusables, @@ -103,22 +77,21 @@ export default class ImageInsertPanelView extends View { } } ); - this.set( '_integrations', new Collection() ); - - for ( const [ integration, integrationView ] of Object.entries( integrations ) ) { - if ( integration === 'insertImageViaUrl' ) { - ( integrationView as LabeledFieldView ).fieldView - .bind( 'value' ).to( this, 'imageURLInputValue', ( value: string ) => value || '' ); - - ( integrationView as LabeledFieldView ).fieldView.on( 'input', () => { - this.imageURLInputValue = ( integrationView as LabeledFieldView ).fieldView.element!.value.trim(); - } ); - } - - ( integrationView as ViewWithName ).name = integration; - - this._integrations.add( integrationView as ViewWithName ); - } + this.children.addMany( integrations ); + + // for ( const view of this.children ) { + // if ( 'focusCycler' in view ) { + // view.focusCycler.on( 'forwardCycle', evt => { + // this._focusCycler.focusNext(); + // evt.stop(); + // } ); + // + // view.focusCycler.on( 'backwardCycle', evt => { + // this._focusCycler.focusPrevious(); + // evt.stop(); + // } ); + // } + // } this.setTemplate( { tag: 'form', @@ -127,21 +100,10 @@ export default class ImageInsertPanelView extends View { class: [ 'ck', 'ck-image-insert-form' - ], - - tabindex: '-1' + ] }, - children: [ - ...this._integrations, - new ImageInsertFormRowView( locale, { - children: [ - this.insertButtonView, - this.cancelButtonView - ], - class: 'ck-image-insert-form__action-row' - } ) - ] + children: this.children } ); } @@ -155,32 +117,16 @@ export default class ImageInsertPanelView extends View { view: this } ); - const childViews = [ - ...this._integrations, - this.insertButtonView, - this.cancelButtonView - ]; - - childViews.forEach( v => { + this.children.forEach( view => { // Register the view as focusable. - this._focusables.add( v ); + this._focusables.add( view ); // Register the view in the focus tracker. - this.focusTracker.add( v.element! ); + this.focusTracker.add( view.element! ); } ); // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element! ); - - const stopPropagation = ( data: KeyboardEvent ) => data.stopPropagation(); - - // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's - // keystroke handler would take over the key management in the URL input. We need to prevent - // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible. - this.keystrokes.set( 'arrowright', stopPropagation ); - this.keystrokes.set( 'arrowleft', stopPropagation ); - this.keystrokes.set( 'arrowup', stopPropagation ); - this.keystrokes.set( 'arrowdown', stopPropagation ); } /** @@ -193,51 +139,6 @@ export default class ImageInsertPanelView extends View { this.keystrokes.destroy(); } - /** - * Returns a view of the integration. - * - * @param name The name of the integration. - */ - public getIntegration( name: string ): View { - return this._integrations.find( integration => integration.name === name )!; - } - - /** - * Creates the following form controls: - * - * * {@link #insertButtonView}, - * * {@link #cancelButtonView}. - * - * @param locale The localization services instance. - */ - private _createActionButtons( locale: Locale ): { insertButtonView: ButtonView; cancelButtonView: ButtonView } { - const t = locale.t; - const insertButtonView = new ButtonView( locale ); - const cancelButtonView = new ButtonView( locale ); - - insertButtonView.set( { - label: t( 'Insert' ), - icon: icons.check, - class: 'ck-button-save', - type: 'submit', - withText: true, - isEnabled: this.imageURLInputValue - } ); - - cancelButtonView.set( { - label: t( 'Cancel' ), - icon: icons.cancel, - class: 'ck-button-cancel', - withText: true - } ); - - insertButtonView.bind( 'isEnabled' ).to( this, 'imageURLInputValue', value => !!value ); - insertButtonView.delegate( 'execute' ).to( this, 'submit' ); - cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); - - return { insertButtonView, cancelButtonView }; - } - /** * Focuses the first {@link #_focusables focusable} in the form. */ @@ -247,8 +148,7 @@ export default class ImageInsertPanelView extends View { } /** - * Fired when the form view is submitted (when one of the children triggered the submit event), - * e.g. by a click on {@link ~ImageInsertPanelView#insertButtonView}. + * TODO * * @eventName ~ImageInsertPanelView#submit */ @@ -258,7 +158,7 @@ export type SubmitEvent = { }; /** - * Fired when the form view is canceled, e.g. by a click on {@link ~ImageInsertPanelView#cancelButtonView}. + * TODO * * @eventName ~ImageInsertPanelView#cancel */ diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts new file mode 100644 index 00000000000..ab7a21b933c --- /dev/null +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -0,0 +1,269 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module image/imageinsert/ui/imageinserturlview + */ + +import { icons } from 'ckeditor5/src/core'; +import { + ButtonView, + View, + ViewCollection, + FocusCycler, + LabeledFieldView, + createLabeledInputText, + type InputTextView +} from 'ckeditor5/src/ui'; +import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils'; + +/** + * The insert an image via URL view controller class. + * + * See {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. + */ +export default class ImageInsertUrlView extends View { + /** + * TODO + */ + public urlInputView: LabeledFieldView; + + /** + * The "insert/update" button view. + */ + public insertButtonView: ButtonView; + + /** + * The "cancel" button view. + */ + public cancelButtonView: ButtonView; + + /** + * The value of the URL input. + * + * @observable + */ + declare public imageURLInputValue: string; + + /** + * TODO + * + * @observable + */ + declare public isImageSelected: boolean; + + /** + * Tracks information about DOM focus in the form. + */ + public readonly focusTracker: FocusTracker; + + /** + * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. + */ + public readonly keystrokes: KeystrokeHandler; + + /** + * Helps cycling over {@link #_focusables} in the form. + */ + public readonly focusCycler: FocusCycler; + + /** + * A collection of views that can be focused in the form. + */ + private readonly _focusables: ViewCollection; + + /** + * Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}. + * + * @param locale The localization services instance. + */ + constructor( locale: Locale ) { + super( locale ); + + this.set( 'imageURLInputValue', '' ); + this.set( 'isImageSelected', false ); + + this.focusTracker = new FocusTracker(); + this.keystrokes = new KeystrokeHandler(); + this._focusables = new ViewCollection(); + + this.focusCycler = new FocusCycler( { + focusables: this._focusables, + focusTracker: this.focusTracker, + keystrokeHandler: this.keystrokes, + actions: { + // Navigate form fields backwards using the Shift + Tab keystroke. + focusPrevious: 'shift + tab', + + // Navigate form fields forwards using the Tab key. + focusNext: 'tab' + } + } ); + + this.urlInputView = this._createUrlInputView(); + this.insertButtonView = this._createInsertButton(); + this.cancelButtonView = this._createCancelButton(); + + this.setTemplate( { + tag: 'div', + + children: [ + this.urlInputView, + { + tag: 'div', + + attributes: { + class: [ + 'ck', + 'ck-form__row', + 'ck-image-insert-form__action-row' + ] + }, + + children: [ + this.insertButtonView, + this.cancelButtonView + ] + } + ] + } ); + } + + /** + * @inheritDoc + */ + public override render(): void { + super.render(); + + const childViews = [ + this.urlInputView, + this.insertButtonView, + this.cancelButtonView + ]; + + childViews.forEach( view => { + // Register the view as focusable. + this._focusables.add( view ); + + // Register the view in the focus tracker. + this.focusTracker.add( view.element! ); + } ); + + // Start listening for the keystrokes coming from #element. + this.keystrokes.listenTo( this.element! ); + + const stopPropagation = ( data: KeyboardEvent ) => data.stopPropagation(); + + // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's + // keystroke handler would take over the key management in the URL input. We need to prevent + // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible. + this.keystrokes.set( 'arrowright', stopPropagation ); + this.keystrokes.set( 'arrowleft', stopPropagation ); + this.keystrokes.set( 'arrowup', stopPropagation ); + this.keystrokes.set( 'arrowdown', stopPropagation ); + } + + /** + * @inheritDoc + */ + public override destroy(): void { + super.destroy(); + + this.focusTracker.destroy(); + this.keystrokes.destroy(); + } + + /** + * Creates the {@link #urlInputView}. + */ + private _createUrlInputView() { + const locale = this.locale!; + const t = locale.t; + const urlInputView = new LabeledFieldView( locale, createLabeledInputText ); + + urlInputView.bind( 'label' ).to( this, 'isImageSelected', + value => value ? t( 'Update image URL' ) : t( 'Insert image via URL' ) + ); + + urlInputView.fieldView.placeholder = 'https://example.com/image.png'; + + urlInputView.fieldView.bind( 'value' ).to( this, 'imageURLInputValue', ( value: string ) => value || '' ); + urlInputView.fieldView.on( 'input', () => { + this.imageURLInputValue = urlInputView.fieldView.element!.value.trim(); + } ); + + return urlInputView; + } + + /** + * Creates the {@link #insertButtonView}. + */ + private _createInsertButton(): ButtonView { + const locale = this.locale!; + const t = locale.t; + const insertButtonView = new ButtonView( locale ); + + insertButtonView.set( { + icon: icons.check, + class: 'ck-button-save', + type: 'submit', + withText: true + } ); + + insertButtonView.bind( 'label' ).to( this, 'isImageSelected', value => value ? t( 'Update' ) : t( 'Insert' ) ); + insertButtonView.bind( 'isEnabled' ).to( this, 'imageURLInputValue', value => !!value ); + + insertButtonView.delegate( 'execute' ).to( this, 'submit' ); + + return insertButtonView; + } + + /** + * Creates the {@link #cancelButtonView}. + */ + private _createCancelButton(): ButtonView { + const locale = this.locale!; + const t = locale.t; + const cancelButtonView = new ButtonView( locale ); + + cancelButtonView.set( { + label: t( 'Cancel' ), + icon: icons.cancel, + class: 'ck-button-cancel', + withText: true + } ); + + cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); + + return cancelButtonView; + } + + /** + * Focuses the first {@link #_focusables focusable} in the form. + */ + public focus(): void { + this.focusCycler.focusFirst(); + } +} + +/** + * TODO + * + * @eventName ~ImageInsertUrlView#submit + */ +export type SubmitEvent = { + name: 'submit'; + args: []; +}; + +/** + * TODO + * + * @eventName ~ImageInsertUrlView#cancel + */ +export type CancelEvent = { + name: 'cancel'; + args: []; +}; diff --git a/packages/ckeditor5-image/src/imageinsert/utils.ts b/packages/ckeditor5-image/src/imageinsert/utils.ts deleted file mode 100644 index eb9eb390dec..00000000000 --- a/packages/ckeditor5-image/src/imageinsert/utils.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module image/imageinsert/utils - */ - -import type { Locale } from 'ckeditor5/src/utils'; -import type { Editor } from 'ckeditor5/src/core'; -import { LabeledFieldView, createLabeledInputText, type View, type ButtonView } from 'ckeditor5/src/ui'; - -import type ImageInsertUI from './imageinsertui'; - -/** - * Creates integrations object that will be passed to the - * {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. - * - * @param editor Editor instance. - * - * @returns Integrations object. - */ -export function prepareIntegrations( editor: Editor ): Record { - const panelItems = editor.config.get( 'image.insert.integrations' ); - const imageInsertUIPlugin: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - - const PREDEFINED_INTEGRATIONS: Record = { - 'insertImageViaUrl': createLabeledInputView( editor.locale ) - }; - - if ( !panelItems ) { - return PREDEFINED_INTEGRATIONS; - } - - // Prepares ckfinder component for the `openCKFinder` integration token. - if ( panelItems.find( item => item === 'openCKFinder' ) && editor.ui.componentFactory.has( 'ckfinder' ) ) { - const ckFinderButton = editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; - ckFinderButton.set( { - withText: true, - class: 'ck-image-insert__ck-finder-button' - } ); - - // We want to close the dropdown panel view when user clicks the ckFinderButton. - ckFinderButton.delegate( 'execute' ).to( imageInsertUIPlugin, 'cancel' ); - - PREDEFINED_INTEGRATIONS.openCKFinder = ckFinderButton; - } - - // Creates integrations object of valid views to pass it to the ImageInsertPanelView. - return panelItems.reduce( ( object: Record, key ) => { - if ( PREDEFINED_INTEGRATIONS[ key ] ) { - object[ key ] = PREDEFINED_INTEGRATIONS[ key ]; - } else if ( editor.ui.componentFactory.has( key ) ) { - object[ key ] = editor.ui.componentFactory.create( key ); - } - - return object; - }, {} ); -} - -/** - * Creates labeled field view. - * - * @param locale The localization services instance. - */ -export function createLabeledInputView( locale: Locale ): LabeledFieldView { - const t = locale.t; - const labeledInputView = new LabeledFieldView( locale, createLabeledInputText ); - - labeledInputView.set( { - label: t( 'Insert image via URL' ) - } ); - labeledInputView.fieldView.placeholder = 'https://example.com/image.png'; - - return labeledInputView; -} diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index fff9865912d..a9b193c394a 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -35,8 +35,8 @@ ClassicEditor toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], insert: { integrations: [ - 'insertImageViaUrl', - 'openCKFinder' + 'openCKFinder', + 'insertImageViaUrl' ] } }, diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index db5f6b4fa3d..ab3595c27d6 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -21,3 +21,35 @@ margin: 0; border: none; } + +.ck.ck-image-insert-form { + &:focus { + /* See: https://github.com/ckeditor/ckeditor5/issues/4773 */ + outline: none; + } +} + +.ck.ck-form__row { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + + /* Ignore labels that work as fieldset legends */ + & > *:not(.ck-label) { + flex-grow: 1; + } + + &.ck-image-insert-form__action-row { + margin-top: var(--ck-spacing-standard); + + & .ck-button-save, + & .ck-button-cancel { + justify-content: center; + } + + & .ck-button .ck-button__label { + color: var(--ck-color-text); + } + } +} diff --git a/packages/ckeditor5-image/theme/imageinsertformrowview.css b/packages/ckeditor5-image/theme/imageinsertformrowview.css deleted file mode 100644 index 28716183f64..00000000000 --- a/packages/ckeditor5-image/theme/imageinsertformrowview.css +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -.ck.ck-image-insert-form { - &:focus { - /* See: https://github.com/ckeditor/ckeditor5/issues/4773 */ - outline: none; - } -} - -.ck.ck-form__row { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - - /* Ignore labels that work as fieldset legends */ - & > *:not(.ck-label) { - flex-grow: 1; - } - - &.ck-image-insert-form__action-row { - margin-top: var(--ck-spacing-standard); - - & .ck-button-save, - & .ck-button-cancel { - justify-content: center; - } - - & .ck-button .ck-button__label { - color: var(--ck-color-text); - } - } -} From b7b04935af6a92e9149565b148bccbfe7024dc23 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 3 Nov 2023 10:48:40 +0100 Subject: [PATCH 05/53] Reverted unnecessary changes. --- .../ckeditor5-core/src/editor/editorconfig.ts | 14 +-- packages/ckeditor5-core/src/index.ts | 2 - .../src/imageinsert/imageinsertui.ts | 14 --- .../ckeditor5-ui/src/toolbar/toolbarview.ts | 94 ++++--------------- 4 files changed, 20 insertions(+), 104 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index 1db1e1fbe6f..cfdf8f6d200 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -584,24 +584,12 @@ export type ToolbarConfig = Array | { icon?: string; }; -export type ToolbarConfigItem = string | ToolbarConfigCustomItem | ToolbarConfigDropdownItem; - -export type ToolbarConfigCustomItem = { - actionItem: string; - label?: string; - icon?: string | false; - withText?: boolean; - tooltip?: boolean | string | ( ( label: string, keystroke: string | undefined ) => string ); -}; - -export type ToolbarConfigDropdownItem = { +export type ToolbarConfigItem = string | { items: Array; label: string; icon?: string | false; withText?: boolean; tooltip?: boolean | string | ( ( label: string, keystroke: string | undefined ) => string ); - actionItem: string; - isVertical?: boolean; }; /** diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index ada6d7b29bd..8bd7878460a 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -23,8 +23,6 @@ export type { LanguageConfig, ToolbarConfig, ToolbarConfigItem, - ToolbarConfigCustomItem, - ToolbarConfigDropdownItem, UiConfig } from './editor/editorconfig'; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 5ee5dfbf277..d1cec582015 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -12,7 +12,6 @@ import type { Locale } from 'ckeditor5/src/utils'; import { SplitButtonView, createDropdown, - CollapsibleView, type DropdownView, type View, type ButtonView @@ -58,19 +57,6 @@ export default class ImageInsertUI extends Plugin { // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); - - editor.ui.componentFactory.add( 'insertImageView', locale => { - const imageInsertView = new ImageInsertPanelView( locale, this._prepareIntegrations() ); - const collapsibleView = new CollapsibleView( locale, [ imageInsertView ] ); - const t = locale.t; - - collapsibleView.set( { - label: t( 'Insert with link' ), - isCollapsed: true - } ); - - return collapsibleView; - } ); } /** diff --git a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts index 2ade26aff87..1be26234d75 100644 --- a/packages/ckeditor5-ui/src/toolbar/toolbarview.ts +++ b/packages/ckeditor5-ui/src/toolbar/toolbarview.ts @@ -8,7 +8,7 @@ */ import View from '../view'; -import FocusCycler, { type FocusableView } from '../focuscycler'; +import FocusCycler from '../focuscycler'; import ToolbarSeparatorView from './toolbarseparatorview'; import ToolbarLineBreakView from './toolbarlinebreakview'; import preventDefault from '../bindings/preventdefault'; @@ -19,9 +19,6 @@ import type ComponentFactory from '../componentfactory'; import type ViewCollection from '../viewcollection'; import type DropdownView from '../dropdown/dropdownview'; import type DropdownPanelFocusable from '../dropdown/dropdownpanelfocusable'; -import type ButtonView from '../button/buttonview'; -import type DropdownButton from '../dropdown/button/dropdownbutton'; -import SplitButtonView from '../dropdown/button/splitbuttonview'; import { FocusTracker, @@ -41,9 +38,7 @@ import { import { icons, type ToolbarConfig, - type ToolbarConfigItem, - type ToolbarConfigCustomItem, - type ToolbarConfigDropdownItem + type ToolbarConfigItem } from '@ckeditor/ckeditor5-core'; import { isObject } from 'lodash-es'; @@ -343,10 +338,8 @@ export default class ToolbarView extends View implements DropdownPanelFocusable const normalizedRemoveItems = removeItems || config.removeItems; const itemsToAdd = this._cleanItemsConfiguration( config.items, factory, normalizedRemoveItems ) .map( item => { - if ( isObject( item ) && 'items' in item ) { + if ( isObject( item ) ) { return this._createNestedToolbarDropdown( item, factory, normalizedRemoveItems ); - } else if ( isObject( item ) ) { - return this._createCustomizedItem( item, factory ); } else if ( item === '|' ) { return new ToolbarSeparatorView(); } else if ( item === '-' ) { @@ -441,8 +434,6 @@ export default class ToolbarView extends View implements DropdownPanelFocusable return false; } - // TODO add validation for ToolbarConfigCustomItem - return true; } ); @@ -502,7 +493,7 @@ export default class ToolbarView extends View implements DropdownPanelFocusable * of the nested toolbar. */ private _createNestedToolbarDropdown( - definition: ToolbarConfigDropdownItem, + definition: Exclude, componentFactory: ComponentFactory, removeItems: Array ) { @@ -516,17 +507,7 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } const locale = this.locale; - - let dropdownButton: ( new ( locale?: Locale ) => DropdownButton & FocusableView ) | DropdownButton & FocusableView | undefined; - - if ( 'actionItem' in definition ) { - const actionButton = componentFactory.create( definition.actionItem ) as ButtonView; - // TODO verify if item is a ButtonView - - dropdownButton = new SplitButtonView( locale, actionButton ); - } - - const dropdownView = createDropdown( locale, dropdownButton ); + const dropdownView = createDropdown( locale ); if ( !label ) { /** @@ -551,65 +532,28 @@ export default class ToolbarView extends View implements DropdownPanelFocusable } dropdownView.class = 'ck-toolbar__nested-toolbar-dropdown'; + dropdownView.buttonView.set( { + label, + tooltip, + withText: !!withText + } ); - if ( 'actionItem' in definition ) { - dropdownView.buttonView.set( { - label, - tooltip - } ); - // TODO withText and icon are not used - } else { - dropdownView.buttonView.set( { - label, - tooltip, - withText: !!withText - } ); - - // Allow disabling icon by passing false. - if ( icon !== false ) { - // A pre-defined icon picked by name, SVG string, a fallback (default) icon. - dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon! ] || icon || threeVerticalDots; - } - // If the icon is disabled, display the label automatically. - else { - dropdownView.buttonView.withText = true; - } + // Allow disabling icon by passing false. + if ( icon !== false ) { + // A pre-defined icon picked by name, SVG string, a fallback (default) icon. + dropdownView.buttonView.icon = NESTED_TOOLBAR_ICONS[ icon! ] || icon || threeVerticalDots; + } + // If the icon is disabled, display the label automatically. + else { + dropdownView.buttonView.withText = true; } addToolbarToDropdown( dropdownView, () => ( dropdownView.toolbarView!._buildItemsFromConfig( items, componentFactory, removeItems ) - ), { isVertical: definition.isVertical } ); + ) ); return dropdownView; } - - /** - * TODO - */ - private _createCustomizedItem( - definition: ToolbarConfigCustomItem, - componentFactory: ComponentFactory - ) { - const itemView = componentFactory.create( definition.actionItem ); - - if ( definition.withText && 'withText' in itemView ) { - itemView.withText = true; - } - - if ( definition.icon && 'icon' in itemView ) { - itemView.icon = definition.icon; - } - - if ( definition.label && 'label' in itemView ) { - itemView.label = definition.label; - } - - if ( definition.tooltip && 'tooltip' in itemView ) { - itemView.tooltip = definition.tooltip; - } - - return itemView; - } } /** From 1b246d0a67e5a0944e0cd26be903ed6722852fe9 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 3 Nov 2023 11:56:56 +0100 Subject: [PATCH 06/53] In-review code refactoring. --- .../src/imageinsert/imageinsertui.ts | 55 ++++++++--------- ...ertpanelview.ts => imageinsertformview.ts} | 60 +++++++------------ .../src/imageinsert/ui/imageinserturlview.ts | 27 ++++++--- .../imagetextalternativeui.ts | 10 ++-- .../ui/textalternativeformview.ts | 20 +++++++ .../tests/imageinsert/imageinsertui.js | 20 +++---- .../imageinsert/ui/imageinsertpanelview.js | 20 +++---- .../ckeditor5-image/theme/imageinsert.css | 37 +++++++----- 8 files changed, 133 insertions(+), 116 deletions(-) rename packages/ckeditor5-image/src/imageinsert/ui/{imageinsertpanelview.ts => imageinsertformview.ts} (74%) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index d1cec582015..5ac753e792a 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -17,11 +17,14 @@ import { type ButtonView } from 'ckeditor5/src/ui'; -import ImageInsertPanelView from './ui/imageinsertpanelview'; +import ImageInsertFormView from './ui/imageinsertformview'; import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; import type UploadImageCommand from '../imageupload/uploadimagecommand'; import type InsertImageCommand from '../image/insertimagecommand'; -import ImageInsertUrlView from './ui/imageinserturlview'; +import ImageInsertUrlView, { + type ImageInsertUrlViewCancelEvent, + type ImageInsertUrlViewSubmitEvent +} from './ui/imageinserturlview'; /** * The image insert dropdown plugin. @@ -95,36 +98,15 @@ export default class ImageInsertUI extends Plugin { } ); } - dropdownView.panelView.extendTemplate( { - attributes: { - class: 'ck-image-insert__panel' - } - } ); - this.dropdownView = dropdownView; dropdownView.bind( 'isEnabled' ).to( uploadImageCommand || insertImageCommand ); dropdownView.once( 'change:isOpen', () => { - const imageInsertPanelView = new ImageInsertPanelView( editor.locale, this._prepareIntegrations() ); - - imageInsertPanelView.delegate( 'submit', 'cancel' ).to( dropdownView ); - dropdownView.panelView.children.add( imageInsertPanelView ); + const imageInsertFormView = new ImageInsertFormView( editor.locale, this._prepareIntegrations() ); + dropdownView.panelView.children.add( imageInsertFormView ); } ); - dropdownView.on( 'submit', () => { - closePanel(); - } ); - - dropdownView.on( 'cancel', () => { - closePanel(); - } ); - - function closePanel() { - editor.editing.view.focus(); - dropdownView.isOpen = false; - } - return dropdownView; } @@ -155,7 +137,6 @@ export default class ImageInsertUI extends Plugin { const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; const imageInsertUrlView = new ImageInsertUrlView( this.editor.locale ); - imageInsertUrlView.delegate( 'submit', 'cancel' ).to( this.dropdownView! ); imageInsertUrlView.bind( 'isImageSelected' ).to( replaceImageSourceCommand, 'isEnabled' ); // Set initial value because integrations are created on first dropdown open. @@ -175,14 +156,18 @@ export default class ImageInsertUI extends Plugin { // invisible form/input cannot be focused/selected. }, { priority: 'low' } ); - this.dropdownView!.on( 'submit', () => { + imageInsertUrlView.on( 'submit', () => { if ( replaceImageSourceCommand.isEnabled ) { this.editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); } else { this.editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); } + + this._closePanel(); } ); + imageInsertUrlView.on( 'cancel', () => this._closePanel() ); + return imageInsertUrlView; } @@ -192,7 +177,7 @@ export default class ImageInsertUI extends Plugin { private _createCKFinderView() { const ckFinderButton = this._createGenericIntegration( 'ckfinder' ); - ckFinderButton.set( 'class', 'ck-image-insert__ck-finder-button' ); + ckFinderButton.class = 'ck-image-insert__ck-finder-button'; return ckFinderButton; } @@ -203,11 +188,21 @@ export default class ImageInsertUI extends Plugin { private _createGenericIntegration( name: string ) { const button = this.editor.ui.componentFactory.create( name ) as ButtonView; - button.set( 'withText', true ); + button.withText = true; // We want to close the dropdown panel view when user clicks the ckFinderButton. - button.delegate( 'execute' ).to( this.dropdownView!, 'cancel' ); + button.on( 'execute', () => { + this._closePanel(); + } ); return button; } + + /** + * TODO + */ + private _closePanel(): void { + this.editor.editing.view.focus(); + this.dropdownView!.isOpen = false; + } } diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts similarity index 74% rename from packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts rename to packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index dbe79b8a200..6c9674be79a 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertpanelview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -4,7 +4,7 @@ */ /** - * @module image/imageinsert/ui/imageinsertpanelview + * @module image/imageinsert/ui/imageinsertformview */ import { @@ -12,19 +12,19 @@ import { ViewCollection, submitHandler, FocusCycler, - FocusCyclerForwardCycleEvent, - FocusCyclerBackwardCycleEvent + type FocusCyclerForwardCycleEvent, + type FocusCyclerBackwardCycleEvent } from 'ckeditor5/src/ui'; import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils'; import '../../../theme/imageinsert.css'; /** - * The insert an image via URL view controller class. + * TODO * - * See {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. + * See {@link module:image/imageinsert/ui/imageinsertformview~ImageInsertFormView}. */ -export default class ImageInsertPanelView extends View { +export default class ImageInsertFormView extends View { /** * Tracks information about DOM focus in the form. */ @@ -79,19 +79,19 @@ export default class ImageInsertPanelView extends View { this.children.addMany( integrations ); - // for ( const view of this.children ) { - // if ( 'focusCycler' in view ) { - // view.focusCycler.on( 'forwardCycle', evt => { - // this._focusCycler.focusNext(); - // evt.stop(); - // } ); - // - // view.focusCycler.on( 'backwardCycle', evt => { - // this._focusCycler.focusPrevious(); - // evt.stop(); - // } ); - // } - // } + for ( const view of this.children ) { + if ( isViewWithFocusCycler( view ) ) { + view.focusCycler.on( 'forwardCycle', evt => { + this._focusCycler.focusNext(); + evt.stop(); + } ); + + view.focusCycler.on( 'backwardCycle', evt => { + this._focusCycler.focusPrevious(); + evt.stop(); + } ); + } + } this.setTemplate( { tag: 'form', @@ -147,22 +147,6 @@ export default class ImageInsertPanelView extends View { } } -/** - * TODO - * - * @eventName ~ImageInsertPanelView#submit - */ -export type SubmitEvent = { - name: 'submit'; - args: []; -}; - -/** - * TODO - * - * @eventName ~ImageInsertPanelView#cancel - */ -export type CancelEvent = { - name: 'cancel'; - args: []; -}; +function isViewWithFocusCycler( view: View ): view is View & { focusCycler: FocusCycler } { + return 'focusCycler' in view; +} diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts index ab7a21b933c..bddfa5d53b4 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -22,7 +22,7 @@ import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils /** * The insert an image via URL view controller class. * - * See {@link module:image/imageinsert/ui/imageinsertpanelview~ImageInsertPanelView}. + * See {@link module:image/imageinsert/ui/imageinsertformview~ImageInsertFormView}. */ export default class ImageInsertUrlView extends View { /** @@ -109,16 +109,21 @@ export default class ImageInsertUrlView extends View { this.setTemplate( { tag: 'div', + attributes: { + class: [ + 'ck', + 'ck-image-insert-url' + ] + }, + children: [ this.urlInputView, { tag: 'div', - attributes: { class: [ 'ck', - 'ck-form__row', - 'ck-image-insert-form__action-row' + 'ck-image-insert-url__action-row' ] }, @@ -241,10 +246,14 @@ export default class ImageInsertUrlView extends View { } /** - * Focuses the first {@link #_focusables focusable} in the form. + * Focuses the view. */ - public focus(): void { - this.focusCycler.focusFirst(); + public focus( direction: 1 | -1 ): void { + if ( direction === -1 ) { + this.focusCycler.focusLast(); + } else { + this.focusCycler.focusFirst(); + } } } @@ -253,7 +262,7 @@ export default class ImageInsertUrlView extends View { * * @eventName ~ImageInsertUrlView#submit */ -export type SubmitEvent = { +export type ImageInsertUrlViewSubmitEvent = { name: 'submit'; args: []; }; @@ -263,7 +272,7 @@ export type SubmitEvent = { * * @eventName ~ImageInsertUrlView#cancel */ -export type CancelEvent = { +export type ImageInsertUrlViewCancelEvent = { name: 'cancel'; args: []; }; diff --git a/packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.ts b/packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.ts index 5653d4ac9be..4845341947d 100644 --- a/packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.ts +++ b/packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.ts @@ -16,9 +16,11 @@ import { type ViewWithCssTransitionDisabler } from 'ckeditor5/src/ui'; -import TextAlternativeFormView from './ui/textalternativeformview'; +import TextAlternativeFormView, { + type TextAlternativeFormViewCancelEvent, + type TextAlternativeFormViewSubmitEvent +} from './ui/textalternativeformview'; import { repositionContextualBalloon, getBalloonPositionData } from '../image/ui/utils'; -import type { CancelEvent, SubmitEvent } from '../imageinsert/ui/imageinsertpanelview'; import type ImageTextAlternativeCommand from './imagetextalternativecommand'; import type ImageUtils from '../imageutils'; @@ -117,7 +119,7 @@ export default class ImageTextAlternativeUI extends Plugin { // Render the form so its #element is available for clickOutsideHandler. this._form.render(); - this.listenTo( this._form, 'submit', () => { + this.listenTo( this._form, 'submit', () => { editor.execute( 'imageTextAlternative', { newValue: this._form!.labeledInput.fieldView.element!.value } ); @@ -125,7 +127,7 @@ export default class ImageTextAlternativeUI extends Plugin { this._hideForm( true ); } ); - this.listenTo( this._form, 'cancel', () => { + this.listenTo( this._form, 'cancel', () => { this._hideForm( true ); } ); diff --git a/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts b/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts index 36a9496a04c..5d48b710078 100644 --- a/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts +++ b/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts @@ -195,3 +195,23 @@ export default class TextAlternativeFormView extends View { return labeledInput; } } + +/** + * TODO + * + * @eventName ~TextAlternativeFormView#submit + */ +export type TextAlternativeFormViewSubmitEvent = { + name: 'submit'; + args: []; +}; + +/** + * TODO + * + * @eventName ~TextAlternativeFormView#cancel + */ +export type TextAlternativeFormViewCancelEvent = { + name: 'cancel'; + args: []; +}; diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js index 281633876b2..06a4ca7d5f6 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js @@ -15,7 +15,7 @@ import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository'; import ImageInsert from '../../src/imageinsert'; import ImageInsertViaUrl from '../../src/imageinsertviaurl'; import ImageInsertUI from '../../src/imageinsert/imageinsertui'; -import ImageInsertPanelView from '../../src/imageinsert/ui/imageinsertpanelview'; +import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; @@ -116,14 +116,14 @@ describe( 'ImageInsertUI', () => { dropdown.isOpen = true; expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView ); + expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); dropdown.isOpen = false; dropdown.isOpen = true; // Make sure it happens only once. expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView ); + expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); } ); describe( 'dropdown action button', () => { @@ -325,11 +325,11 @@ describe( 'ImageInsertUI', () => { it( 'should focus on "insert image via URL" input after opening', () => { let spy; - // The ImageInsertPanelView is added on first open. + // The ImageInsertFormView is added on first open. // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 dropdown.on( 'change:isOpen', () => { - const imageInsertPanelView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertPanelView, 'focus' ); + const imageInsertFormView = dropdown.panelView.children.first; + spy = sinon.spy( imageInsertFormView, 'focus' ); } ); dropdown.buttonView.fire( 'open' ); @@ -427,7 +427,7 @@ describe( 'ImageInsertUI', () => { dropdown.buttonView.fire( 'open' ); expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView ); + expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); } ); describe( 'dropdown button', () => { @@ -623,11 +623,11 @@ describe( 'ImageInsertUI', () => { it( 'should focus on "insert image via URL" input after opening', () => { let spy; - // The ImageInsertPanelView is added on first open. + // The ImageInsertFormView is added on first open. // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 dropdown.on( 'change:isOpen', () => { - const imageInsertPanelView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertPanelView, 'focus' ); + const imageInsertFormView = dropdown.panelView.children.first; + spy = sinon.spy( imageInsertFormView, 'focus' ); } ); dropdown.buttonView.fire( 'open' ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js index a6cd2122e17..02daced955f 100644 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js @@ -7,7 +7,7 @@ import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; -import ImageInsertPanelView from '../../../src/imageinsert/ui/imageinsertpanelview'; +import ImageInsertFormView from '../../../src/imageinsert/ui/imageinsertformview'; import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; @@ -23,11 +23,11 @@ import { createLabeledInputView } from '../../../src/imageinsert/utils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -describe( 'ImageInsertPanelView', () => { +describe( 'ImageInsertFormView', () => { let view; beforeEach( () => { - view = new ImageInsertPanelView( { t: val => val }, { + view = new ImageInsertFormView( { t: val => val }, { 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) } ); view.render(); @@ -60,8 +60,8 @@ describe( 'ImageInsertPanelView', () => { } ); describe( 'integrations', () => { - it( 'should contain 2 integrations when they were passed to the ImageInsertPanelView as integrations object', () => { - const view = new ImageInsertPanelView( { t: val => val }, { + it( 'should contain 2 integrations when they were passed to the ImageInsertFormView as integrations object', () => { + const view = new ImageInsertFormView( { t: val => val }, { 'integration1': new View(), 'integration2': new ButtonView() } ); @@ -71,7 +71,7 @@ describe( 'ImageInsertPanelView', () => { } ); it( 'should contain insertImageViaUrl view when it is passed via integrations object', () => { - const view = new ImageInsertPanelView( { t: val => val }, { + const view = new ImageInsertFormView( { t: val => val }, { 'insertImageViaUrl': createLabeledInputView( { t: val => val } ), 'integration1': new View(), 'integration2': new ButtonView() @@ -83,7 +83,7 @@ describe( 'ImageInsertPanelView', () => { } ); it( 'should contain no integrations when they were not provided', () => { - const view = new ImageInsertPanelView( { t: val => val } ); + const view = new ImageInsertFormView( { t: val => val } ); expect( view._integrations ).to.be.instanceOf( Collection ); expect( view._integrations.length ).to.equal( 0 ); @@ -153,7 +153,7 @@ describe( 'ImageInsertPanelView', () => { } ); it( 'should register child views\' #element in #focusTracker with no integrations', () => { - const view = new ImageInsertPanelView( { t: () => {} } ); + const view = new ImageInsertFormView( { t: () => {} } ); const spy = testUtils.sinon.spy( view.focusTracker, 'add' ); view.render(); @@ -165,7 +165,7 @@ describe( 'ImageInsertPanelView', () => { } ); it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => { - const view = new ImageInsertPanelView( { t: () => {} }, { + const view = new ImageInsertFormView( { t: () => {} }, { 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) } ); @@ -181,7 +181,7 @@ describe( 'ImageInsertPanelView', () => { } ); it( 'starts listening for #keystrokes coming from #element', () => { - const view = new ImageInsertPanelView( { t: () => {} } ); + const view = new ImageInsertFormView( { t: () => {} } ); const spy = sinon.spy( view.keystrokes, 'listenTo' ); diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index ab3595c27d6..e42c5d9c9da 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -.ck.ck-image-insert__panel { - padding: var(--ck-spacing-large); -} +@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; .ck.ck-image-insert__ck-finder-button { display: block; @@ -23,29 +21,38 @@ } .ck.ck-image-insert-form { + padding: var(--ck-spacing-large); + &:focus { /* See: https://github.com/ckeditor/ckeditor5/issues/4773 */ outline: none; } } -.ck.ck-form__row { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - - /* Ignore labels that work as fieldset legends */ - & > *:not(.ck-label) { - flex-grow: 1; - } +.ck.ck-image-insert-url { + & .ck-image-insert-url__action-row { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; - &.ck-image-insert-form__action-row { - margin-top: var(--ck-spacing-standard); + margin-top: var(--ck-spacing-large); & .ck-button-save, & .ck-button-cancel { justify-content: center; + flex-grow: 1; + + /* TODO: CSS grid */ + & + * { + @mixin ck-dir ltr { + margin-left: var(--ck-spacing-large); + } + + @mixin ck-dir rtl { + margin-right: var(--ck-spacing-large); + } + } } & .ck-button .ck-button__label { From 5fcf6e55ef13dd8caa664dd3bce3b0c7963a9fa2 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 3 Nov 2023 14:49:51 +0100 Subject: [PATCH 07/53] Removed unused code. --- .../tests/manual/toolbar/nested.js | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js index a074861ecc8..fbf4c6a692a 100644 --- a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js @@ -18,7 +18,6 @@ import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; -import { icons } from '@ckeditor/ckeditor5-core'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -71,33 +70,6 @@ ClassicEditor 'insertImage', 'insertTable', 'mediaEmbed' ] }, - { - actionItem: 'bold', - label: 'Some custom split button', - isVertical: true, - items: [ - { - actionItem: 'strikethrough', - withText: true, - icon: icons.cog - }, { - actionItem: 'underline', - withText: true, - label: 'foooo', - tooltip: 'Just testing' - }, { - actionItem: 'code', - withText: true - }, { - actionItem: 'subscript', - withText: true - }, { - actionItem: 'superscript', - withText: true - }, - 'insertImageView' - ] - }, '-', { label: 'Icon: default', From e542d0c7dfe42abe824f9786dce2092e04b6e45f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 6 Nov 2023 17:52:15 +0100 Subject: [PATCH 08/53] InsertImageUI should allow registered integrations. --- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 18 +++ .../src/imageinsert/imageinsertui.ts | 150 ++++++++++-------- .../ckeditor5-image/src/imageinsertviaurl.ts | 15 +- .../src/imageupload/imageuploadui.ts | 29 ++++ .../tests/manual/imageinsert.js | 9 +- 5 files changed, 148 insertions(+), 73 deletions(-) diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 24f3c695c34..d0db57f5fd7 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -9,6 +9,7 @@ import { Plugin } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; +import type { ImageInsertUI } from '@ckeditor/ckeditor5-image'; import type CKFinderCommand from './ckfindercommand'; @@ -53,5 +54,22 @@ export default class CKFinderUI extends Plugin { return button; } ); + + if ( editor.plugins.has( 'ImageInsertUI' ) ) { + const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); + + imageInsertUI.registerIntegration( 'assetManager', type => { + const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; + + if ( type == 'formView' ) { + button.class = 'ck-image-insert__ck-finder-button'; + button.withText = true; + button.label = t( 'Insert with a File manager' ); // TODO add to context (note that it's shared with CKBox) + // TODO this should change to 'Replace with a File manager' if image is selected + } + + return button; + } ); + } } } diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 5ac753e792a..1cf8ae2dd1c 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -8,19 +8,19 @@ */ import { Plugin, icons } from 'ckeditor5/src/core'; -import type { Locale } from 'ckeditor5/src/utils'; +import { logWarning, type Locale } from 'ckeditor5/src/utils'; import { + ButtonView, SplitButtonView, + DropdownButtonView, createDropdown, type DropdownView, type View, - type ButtonView + type FocusableView } from 'ckeditor5/src/ui'; import ImageInsertFormView from './ui/imageinsertformview'; import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; -import type UploadImageCommand from '../imageupload/uploadimagecommand'; -import type InsertImageCommand from '../image/insertimagecommand'; import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent @@ -48,6 +48,11 @@ export default class ImageInsertUI extends Plugin { */ public dropdownView?: DropdownView; + /** + * TODO + */ + private _integrations = new Map(); + /** * @inheritDoc */ @@ -60,6 +65,37 @@ export default class ImageInsertUI extends Plugin { // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); + + this.registerIntegration( 'url', type => { + if ( type == 'formView' ) { + return this._createInsertUrlView(); + } else { + const button = new ButtonView( editor.locale ); + const t = editor.locale.t; + + button.set( { + label: t( 'Insert image' ), // TODO or Update image + icon: icons.image, + tooltip: true + } ); + + return button; + } + } ); + } + + /** + * TODO + */ + public registerIntegration( name: string, callback: IntegrationCallback ): void { + if ( this._integrations.has( name ) ) { + /** + * TODO + */ + logWarning( 'image-insert-zzzzz', { name } ); + } + + this._integrations.set( name, callback ); } /** @@ -69,41 +105,35 @@ export default class ImageInsertUI extends Plugin { */ private _createDropdownView( locale: Locale ): DropdownView { const editor = this.editor; - const t = locale.t; - const uploadImageCommand: UploadImageCommand | undefined = editor.commands.get( 'uploadImage' ); - const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; + const integrations = this._prepareIntegrations(); + let dropdownButton: SplitButtonView | DropdownButtonView | undefined; - let dropdownButton; + if ( integrations.length > 1 ) { + // TODO remove cast as ButtonView & FocusableView + dropdownButton = new SplitButtonView( locale, integrations[ 0 ]( 'toolbarButton' ) as ButtonView & FocusableView ); + } else if ( integrations.length == 1 ) { + dropdownButton = new DropdownButtonView( locale ); - if ( uploadImageCommand ) { - const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as ButtonView; + // TODO remove cast as ButtonView + // TODO how to make it without reference button + const referenceButton = integrations[ 0 ]( 'toolbarButton' ) as ButtonView; - uploadImageButton.extendTemplate( { - attributes: { - class: 'ck ck-button' - } + dropdownButton.set( { + label: referenceButton.label, + icon: referenceButton.icon, + tooltip: referenceButton.tooltip } ); - - dropdownButton = new SplitButtonView( locale, uploadImageButton ); } - const dropdownView = createDropdown( locale, dropdownButton ); + const dropdownView = this.dropdownView = createDropdown( locale, dropdownButton ); - if ( !uploadImageCommand ) { - dropdownView.buttonView.set( { - label: t( 'Insert image' ), - icon: icons.image, - tooltip: true - } ); - } - - this.dropdownView = dropdownView; - - dropdownView.bind( 'isEnabled' ).to( uploadImageCommand || insertImageCommand ); + // TODO + // dropdownView.bind( 'isEnabled' ).to( uploadImageCommand || insertImageCommand ); dropdownView.once( 'change:isOpen', () => { - const imageInsertFormView = new ImageInsertFormView( editor.locale, this._prepareIntegrations() ); + const integrationsView = integrations.map( callback => callback( 'formView' ) ); + const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationsView ); dropdownView.panelView.children.add( imageInsertFormView ); } ); @@ -113,21 +143,27 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - private _prepareIntegrations(): Array { + private _prepareIntegrations(): Array { const editor = this.editor; - const items = editor.config.get( 'image.insert.integrations' ) || [ 'insertImageViaUrl' ]; + const items = editor.config.get( 'image.insert.integrations' )!; + const result: Array = []; + + for ( const item of items ) { + if ( !this._integrations.has( item ) ) { + if ( ![ 'upload', 'assetManager', 'url' ].includes( item ) ) { + /** + * TODO + */ + logWarning( 'image-insert-zzzzzz', { item } ); + } - return items.map( item => { - if ( item == 'insertImageViaUrl' ) { - return this._createInsertUrlView(); + continue; } - else if ( item == 'openCKFinder' && editor.ui.componentFactory.has( 'ckfinder' ) ) { - return this._createCKFinderView(); - } - else { - return this._createGenericIntegration( item ); - } - } ); + + result.push( this._integrations.get( item )! ); + } + + return result; } /** @@ -171,33 +207,6 @@ export default class ImageInsertUI extends Plugin { return imageInsertUrlView; } - /** - * TODO - */ - private _createCKFinderView() { - const ckFinderButton = this._createGenericIntegration( 'ckfinder' ); - - ckFinderButton.class = 'ck-image-insert__ck-finder-button'; - - return ckFinderButton; - } - - /** - * TODO - */ - private _createGenericIntegration( name: string ) { - const button = this.editor.ui.componentFactory.create( name ) as ButtonView; - - button.withText = true; - - // We want to close the dropdown panel view when user clicks the ckFinderButton. - button.on( 'execute', () => { - this._closePanel(); - } ); - - return button; - } - /** * TODO */ @@ -206,3 +215,8 @@ export default class ImageInsertUI extends Plugin { this.dropdownView!.isOpen = false; } } + +/** + * TODO + */ +export type IntegrationCallback = ( type: 'toolbarButton' | 'formView' ) => View; diff --git a/packages/ckeditor5-image/src/imageinsertviaurl.ts b/packages/ckeditor5-image/src/imageinsertviaurl.ts index cb0a343647a..0b979e455fb 100644 --- a/packages/ckeditor5-image/src/imageinsertviaurl.ts +++ b/packages/ckeditor5-image/src/imageinsertviaurl.ts @@ -7,7 +7,7 @@ * @module image/imageinsertviaurl */ -import { Plugin } from 'ckeditor5/src/core'; +import { Plugin, type Editor } from 'ckeditor5/src/core'; import ImageInsertUI from './imageinsert/imageinsertui'; /** @@ -35,4 +35,17 @@ export default class ImageInsertViaUrl extends Plugin { public static get requires() { return [ ImageInsertUI ] as const; } + + /** + * @inheritDoc + */ + constructor( editor: Editor ) { + super( editor ); + + editor.config.define( 'image.insert.integrations', [ + 'assetManager', + 'upload', + 'url' + ] ); + } } diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 9a8151bb90c..9aecb37b49b 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -12,6 +12,7 @@ import { Plugin, icons } from 'ckeditor5/src/core'; import { FileDialogButtonView } from 'ckeditor5/src/upload'; import { createImageTypeRegExp } from './utils'; import type UploadImageCommand from './uploadimagecommand'; +import type ImageInsertUI from '../imageinsert/imageinsertui'; /** * The image upload button plugin. @@ -70,5 +71,33 @@ export default class ImageUploadUI extends Plugin { // Setup `uploadImage` button and add `imageUpload` button as an alias for backward compatibility. editor.ui.componentFactory.add( 'uploadImage', componentCreator ); editor.ui.componentFactory.add( 'imageUpload', componentCreator ); + + if ( editor.plugins.has( 'ImageInsertUI' ) ) { + const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); + + imageInsertUI.registerIntegration( 'upload', type => { + const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; + + if ( type == 'formView' ) { + uploadImageButton.extendTemplate( { + attributes: { + class: [ 'ck', 'ck-button', 'ck-image-insert__ck-finder-button' ] + } + } ); + + uploadImageButton.buttonView.withText = true; + uploadImageButton.buttonView.label = t( 'Upload from computer' ); // TODO add to context + // TODO this should change to 'Replace from computer' if image is selected + } else { + uploadImageButton.extendTemplate( { + attributes: { + class: 'ck ck-button' + } + } ); + } + + return uploadImageButton; + } ); + } } } diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index a9b193c394a..21176308908 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -34,10 +34,11 @@ ClassicEditor image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], insert: { - integrations: [ - 'openCKFinder', - 'insertImageViaUrl' - ] + // integrations: [ + // 'upload', + // 'fileManager', + // 'url' + // ] } }, ckfinder: { From 83437c6820882c9530f166f931e3d445a81c062a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 7 Nov 2023 18:55:37 +0100 Subject: [PATCH 09/53] Refactored FileDialogButtonView. --- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 9 ++++-- .../src/imageinsert/imageinsertui.ts | 23 ++++++++++++-- .../src/imageupload/imageuploadui.ts | 22 +++++++------- .../tests/manual/imageinsert.js | 10 +++---- .../ckeditor5-image/theme/imageinsert.css | 7 ----- .../src/ui/filedialogbuttonview.ts | 30 +++++++++---------- 6 files changed, 56 insertions(+), 45 deletions(-) diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index d0db57f5fd7..8825363dff9 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -64,8 +64,13 @@ export default class CKFinderUI extends Plugin { if ( type == 'formView' ) { button.class = 'ck-image-insert__ck-finder-button'; button.withText = true; - button.label = t( 'Insert with a File manager' ); // TODO add to context (note that it's shared with CKBox) - // TODO this should change to 'Replace with a File manager' if image is selected + // button.label = t( 'Insert with a File manager' ); + + // TODO add to context (note that it's shared with CKBox) + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with a File manager' ) : + t( 'Insert with a File manager' ) + ); } return button; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 1cf8ae2dd1c..d13a9daaf7b 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -15,7 +15,6 @@ import { DropdownButtonView, createDropdown, type DropdownView, - type View, type FocusableView } from 'ckeditor5/src/ui'; @@ -25,6 +24,7 @@ import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent } from './ui/imageinserturlview'; +import type ImageUtils from '../imageutils'; /** * The image insert dropdown plugin. @@ -48,6 +48,13 @@ export default class ImageInsertUI extends Plugin { */ public dropdownView?: DropdownView; + /** + * TODO + * + * @observable + */ + declare public isImageSelected: boolean; + /** * TODO */ @@ -57,6 +64,8 @@ export default class ImageInsertUI extends Plugin { * @inheritDoc */ public init(): void { + this.set( 'isImageSelected', false ); + const editor = this.editor; const componentCreator = ( locale: Locale ) => { return this._createDropdownView( locale ); @@ -82,6 +91,13 @@ export default class ImageInsertUI extends Plugin { return button; } } ); + + this.listenTo( editor.model.document, 'change', () => { + const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); + const element = this.editor.model.document.selection.getSelectedElement(); + + this.isImageSelected = imageUtils.isImage( element ); + } ); } /** @@ -134,6 +150,7 @@ export default class ImageInsertUI extends Plugin { dropdownView.once( 'change:isOpen', () => { const integrationsView = integrations.map( callback => callback( 'formView' ) ); const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationsView ); + dropdownView.panelView.children.add( imageInsertFormView ); } ); @@ -173,7 +190,7 @@ export default class ImageInsertUI extends Plugin { const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; const imageInsertUrlView = new ImageInsertUrlView( this.editor.locale ); - imageInsertUrlView.bind( 'isImageSelected' ).to( replaceImageSourceCommand, 'isEnabled' ); + imageInsertUrlView.bind( 'isImageSelected' ).to( this ); // Set initial value because integrations are created on first dropdown open. imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; @@ -219,4 +236,4 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ -export type IntegrationCallback = ( type: 'toolbarButton' | 'formView' ) => View; +export type IntegrationCallback = ( type: 'toolbarButton' | 'formView' ) => FocusableView; diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 9aecb37b49b..af42099c431 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -47,13 +47,13 @@ export default class ImageUploadUI extends Plugin { allowMultipleFiles: true } ); - view.buttonView.set( { + view.set( { label: t( 'Insert image' ), icon: icons.image, tooltip: true } ); - view.buttonView.bind( 'isEnabled' ).to( command ); + view.bind( 'isEnabled' ).to( command ); view.on( 'done', ( evt, files: FileList ) => { const imagesToUpload = Array.from( files ).filter( file => imageTypesRegExp.test( file.type ) ); @@ -81,19 +81,17 @@ export default class ImageUploadUI extends Plugin { if ( type == 'formView' ) { uploadImageButton.extendTemplate( { attributes: { - class: [ 'ck', 'ck-button', 'ck-image-insert__ck-finder-button' ] + class: 'ck-image-insert__ck-finder-button' } } ); - uploadImageButton.buttonView.withText = true; - uploadImageButton.buttonView.label = t( 'Upload from computer' ); // TODO add to context - // TODO this should change to 'Replace from computer' if image is selected - } else { - uploadImageButton.extendTemplate( { - attributes: { - class: 'ck ck-button' - } - } ); + uploadImageButton.withText = true; + + // TODO add to context (note that it's shared with CKBox) + uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace from computer' ) : + t( 'Upload from computer' ) + ); } return uploadImageButton; diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index 21176308908..1fe24910880 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -34,11 +34,11 @@ ClassicEditor image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], insert: { - // integrations: [ - // 'upload', - // 'fileManager', - // 'url' - // ] + integrations: [ + 'upload', + 'assetManager', + 'url' + ] } }, ckfinder: { diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index e42c5d9c9da..0c20ed039c6 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -13,13 +13,6 @@ border-radius: var(--ck-border-radius); } -/* https://github.com/ckeditor/ckeditor5/issues/7986 */ -.ck.ck-splitbutton > .ck-file-dialog-button.ck-button { - padding: 0; - margin: 0; - border: none; -} - .ck.ck-image-insert-form { padding: var(--ck-spacing-large); diff --git a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts index 8342c74ee69..92dd6c7c061 100644 --- a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts +++ b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts @@ -38,7 +38,7 @@ import type { Locale } from '@ckeditor/ckeditor5-utils'; * } ); * ``` */ -export default class FileDialogButtonView extends View { +export default class FileDialogButtonView extends ButtonView { /** * The button view of the component. */ @@ -72,7 +72,8 @@ export default class FileDialogButtonView extends View { constructor( locale?: Locale ) { super( locale ); - this.buttonView = new ButtonView( locale ); + // TODO should we leave this for backward compatibility? + this.buttonView = this; this._fileInputView = new FileInputView( locale ); this._fileInputView.bind( 'acceptedType' ).to( this ); @@ -80,27 +81,24 @@ export default class FileDialogButtonView extends View { this._fileInputView.delegate( 'done' ).to( this ); - this.setTemplate( { - tag: 'span', - attributes: { - class: 'ck-file-dialog-button' - }, - children: [ - this.buttonView, - this._fileInputView - ] + this.on( 'execute', () => { + this._fileInputView.open(); } ); - this.buttonView.on( 'execute', () => { - this._fileInputView.open(); + this.extendTemplate( { + attributes: { + class: 'ck-file-dialog-button' + } } ); } /** - * Focuses the {@link #buttonView}. + * @inheritDoc */ - public focus(): void { - this.buttonView.focus(); + public override render(): void { + super.render(); + + this.children.add( this._fileInputView ); } } From 76a2af6cd99c29b1f1b63f4c3cffa6bf8771d805 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 8 Nov 2023 18:21:52 +0100 Subject: [PATCH 10/53] New icons in insert image dropdown. Handling isEnabled state. CKBox integration. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 27 +++++- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 12 ++- packages/ckeditor5-core/src/index.ts | 4 + .../theme/icons/image-folder.svg | 1 + .../theme/icons/image-upload.svg | 1 + .../src/imageinsert/imageinsertui.ts | 96 +++++++++++-------- .../src/imageinsert/ui/imageinserturlview.ts | 16 +++- .../src/imageupload/imageuploadui.ts | 6 +- 8 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 packages/ckeditor5-core/theme/icons/image-folder.svg create mode 100644 packages/ckeditor5-core/theme/icons/image-upload.svg diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index 4be1e84590a..e71120ef47e 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -7,9 +7,11 @@ * @module ckbox/ckboxui */ -import { Plugin } from 'ckeditor5/src/core'; +import { icons, Plugin } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; +import type { ImageInsertUI } from '@ckeditor/ckeditor5-image'; + import browseFilesIcon from '../theme/icons/browse-files.svg'; import type CKBoxCommand from './ckboxcommand'; @@ -57,5 +59,28 @@ export default class CKBoxUI extends Plugin { return button; } ); + + if ( editor.plugins.has( 'ImageInsertUI' ) ) { + const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); + + imageInsertUI.registerIntegration( 'assetManager', command, type => { + const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; + + button.icon = icons.imageFolder; + + if ( type == 'formView' ) { + button.class = 'ck-image-insert__ck-finder-button'; + button.withText = true; + + // TODO add to context (note that it's shared with CKFinder) + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with File Manager' ) : + t( 'Insert with File Manager' ) + ); + } + + return button; + } ); + } } } diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 8825363dff9..e6d1c580ac2 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -7,7 +7,7 @@ * @module ckfinder/ckfinderui */ -import { Plugin } from 'ckeditor5/src/core'; +import { icons, Plugin } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; import type { ImageInsertUI } from '@ckeditor/ckeditor5-image'; @@ -57,19 +57,21 @@ export default class CKFinderUI extends Plugin { if ( editor.plugins.has( 'ImageInsertUI' ) ) { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); + const command: CKFinderCommand = editor.commands.get( 'ckfinder' )!; - imageInsertUI.registerIntegration( 'assetManager', type => { + imageInsertUI.registerIntegration( 'assetManager', command, type => { const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; + button.icon = icons.imageFolder; + if ( type == 'formView' ) { button.class = 'ck-image-insert__ck-finder-button'; button.withText = true; - // button.label = t( 'Insert with a File manager' ); // TODO add to context (note that it's shared with CKBox) button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with a File manager' ) : - t( 'Insert with a File manager' ) + t( 'Replace with File Manager' ) : + t( 'Insert with File Manager' ) ); } diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index 8bd7878460a..690796b36c0 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -42,6 +42,8 @@ import history from './../theme/icons/history.svg'; import lowVision from './../theme/icons/low-vision.svg'; import loupe from './../theme/icons/loupe.svg'; import image from './../theme/icons/image.svg'; +import imageUpload from './../theme/icons/image-upload.svg'; +import imageFolder from './../theme/icons/image-folder.svg'; import alignBottom from './../theme/icons/align-bottom.svg'; import alignMiddle from './../theme/icons/align-middle.svg'; @@ -85,6 +87,8 @@ export const icons = { eraser, history, image, + imageUpload, + imageFolder, lowVision, loupe, importExport, diff --git a/packages/ckeditor5-core/theme/icons/image-folder.svg b/packages/ckeditor5-core/theme/icons/image-folder.svg new file mode 100644 index 00000000000..2148287a229 --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/image-folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-upload.svg b/packages/ckeditor5-core/theme/icons/image-upload.svg new file mode 100644 index 00000000000..056c560c31d --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/image-upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index d13a9daaf7b..feccc6db18f 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -8,7 +8,7 @@ */ import { Plugin, icons } from 'ckeditor5/src/core'; -import { logWarning, type Locale } from 'ckeditor5/src/utils'; +import { logWarning, type Locale, type Observable } from 'ckeditor5/src/utils'; import { ButtonView, SplitButtonView, @@ -20,11 +20,12 @@ import { import ImageInsertFormView from './ui/imageinsertformview'; import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; +import type ImageUtils from '../imageutils'; +import type InsertImageCommand from '../image/insertimagecommand'; import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent } from './ui/imageinserturlview'; -import type ImageUtils from '../imageutils'; /** * The image insert dropdown plugin. @@ -58,7 +59,7 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - private _integrations = new Map(); + private _integrations = new Map(); /** * @inheritDoc @@ -75,22 +76,11 @@ export default class ImageInsertUI extends Plugin { editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); - this.registerIntegration( 'url', type => { - if ( type == 'formView' ) { - return this._createInsertUrlView(); - } else { - const button = new ButtonView( editor.locale ); - const t = editor.locale.t; + const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; - button.set( { - label: t( 'Insert image' ), // TODO or Update image - icon: icons.image, - tooltip: true - } ); - - return button; - } - } ); + this.registerIntegration( 'url', insertImageCommand, + type => type == 'formView' ? this._createInsertUrlView() : this._createInsertButton() + ); this.listenTo( editor.model.document, 'change', () => { const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); @@ -103,7 +93,7 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - public registerIntegration( name: string, callback: IntegrationCallback ): void { + public registerIntegration( name: string, observable: IntegrationData[ 'observable' ], callback: IntegrationCallback ): void { if ( this._integrations.has( name ) ) { /** * TODO @@ -111,7 +101,7 @@ export default class ImageInsertUI extends Plugin { logWarning( 'image-insert-zzzzz', { name } ); } - this._integrations.set( name, callback ); + this._integrations.set( name, { observable, callback } ); } /** @@ -126,29 +116,20 @@ export default class ImageInsertUI extends Plugin { let dropdownButton: SplitButtonView | DropdownButtonView | undefined; if ( integrations.length > 1 ) { - // TODO remove cast as ButtonView & FocusableView - dropdownButton = new SplitButtonView( locale, integrations[ 0 ]( 'toolbarButton' ) as ButtonView & FocusableView ); + dropdownButton = new SplitButtonView( locale, integrations[ 0 ].callback( 'toolbarButton' ) as ButtonView & FocusableView ); } else if ( integrations.length == 1 ) { - dropdownButton = new DropdownButtonView( locale ); - - // TODO remove cast as ButtonView - // TODO how to make it without reference button - const referenceButton = integrations[ 0 ]( 'toolbarButton' ) as ButtonView; - - dropdownButton.set( { - label: referenceButton.label, - icon: referenceButton.icon, - tooltip: referenceButton.tooltip - } ); + dropdownButton = this._createInsertButton( DropdownButtonView ); } const dropdownView = this.dropdownView = createDropdown( locale, dropdownButton ); + const observables = integrations.map( ( { observable } ) => observable ); - // TODO - // dropdownView.bind( 'isEnabled' ).to( uploadImageCommand || insertImageCommand ); + dropdownView.bind( 'isEnabled' ).toMany( observables, 'isEnabled', ( ...isEnabled ) => ( + isEnabled.some( isEnabled => isEnabled ) + ) ); dropdownView.once( 'change:isOpen', () => { - const integrationsView = integrations.map( callback => callback( 'formView' ) ); + const integrationsView = integrations.map( ( { callback } ) => callback( 'formView' ) ); const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationsView ); dropdownView.panelView.children.add( imageInsertFormView ); @@ -160,10 +141,10 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - private _prepareIntegrations(): Array { + private _prepareIntegrations(): Array { const editor = this.editor; const items = editor.config.get( 'image.insert.integrations' )!; - const result: Array = []; + const result: Array = []; for ( const item of items ) { if ( !this._integrations.has( item ) ) { @@ -186,11 +167,15 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - private _createInsertUrlView() { + private _createInsertUrlView(): FocusableView { const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; + const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; const imageInsertUrlView = new ImageInsertUrlView( this.editor.locale ); imageInsertUrlView.bind( 'isImageSelected' ).to( this ); + imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( + isEnabled.some( isCommandEnabled => isCommandEnabled ) + ) ); // Set initial value because integrations are created on first dropdown open. imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; @@ -224,6 +209,34 @@ export default class ImageInsertUI extends Plugin { return imageInsertUrlView; } + private _createInsertButton( + ButtonClass: new ( locale?: Locale ) => T + ): T; + private _createInsertButton(): ButtonView; + + /** + * TODO + */ + private _createInsertButton( + ButtonClass: new ( locale?: Locale ) => ButtonView = ButtonView + ): ButtonView { + const editor = this.editor; + const button = new ButtonClass( editor.locale ); + const t = editor.locale.t; + + button.set( { + icon: icons.image, + tooltip: true + } ); + + // TODO add 'Replace image' to context + button.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image' ) : t( 'Insert image' ) + ); + + return button; + } + /** * TODO */ @@ -237,3 +250,8 @@ export default class ImageInsertUI extends Plugin { * TODO */ export type IntegrationCallback = ( type: 'toolbarButton' | 'formView' ) => FocusableView; + +type IntegrationData = { + observable: Observable & { isEnabled: boolean }; + callback: IntegrationCallback; +}; diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts index bddfa5d53b4..daa066f9a6e 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -54,6 +54,13 @@ export default class ImageInsertUrlView extends View { */ declare public isImageSelected: boolean; + /** + * TODO + * + * @observable + */ + declare public isEnabled: boolean; + /** * Tracks information about DOM focus in the form. */ @@ -84,6 +91,7 @@ export default class ImageInsertUrlView extends View { this.set( 'imageURLInputValue', '' ); this.set( 'isImageSelected', false ); + this.set( 'isEnabled', true ); this.focusTracker = new FocusTracker(); this.keystrokes = new KeystrokeHandler(); @@ -192,6 +200,8 @@ export default class ImageInsertUrlView extends View { value => value ? t( 'Update image URL' ) : t( 'Insert image via URL' ) ); + urlInputView.bind( 'isEnabled' ).to( this ); + urlInputView.fieldView.placeholder = 'https://example.com/image.png'; urlInputView.fieldView.bind( 'value' ).to( this, 'imageURLInputValue', ( value: string ) => value || '' ); @@ -218,7 +228,9 @@ export default class ImageInsertUrlView extends View { } ); insertButtonView.bind( 'label' ).to( this, 'isImageSelected', value => value ? t( 'Update' ) : t( 'Insert' ) ); - insertButtonView.bind( 'isEnabled' ).to( this, 'imageURLInputValue', value => !!value ); + insertButtonView.bind( 'isEnabled' ).to( this, 'imageURLInputValue', this, 'isEnabled', + ( ...values ) => values.every( value => value ) + ); insertButtonView.delegate( 'execute' ).to( this, 'submit' ); @@ -240,6 +252,8 @@ export default class ImageInsertUrlView extends View { withText: true } ); + cancelButtonView.bind( 'isEnabled' ).to( this ); + cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); return cancelButtonView; diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index af42099c431..237ac37afd4 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -36,6 +36,7 @@ export default class ImageUploadUI extends Plugin { public init(): void { const editor = this.editor; const t = editor.t; + const componentCreator = ( locale: Locale ) => { const view = new FileDialogButtonView( locale ); const command: UploadImageCommand = editor.commands.get( 'uploadImage' )!; @@ -74,10 +75,13 @@ export default class ImageUploadUI extends Plugin { if ( editor.plugins.has( 'ImageInsertUI' ) ) { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); + const command: UploadImageCommand = editor.commands.get( 'uploadImage' )!; - imageInsertUI.registerIntegration( 'upload', type => { + imageInsertUI.registerIntegration( 'upload', command, type => { const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; + uploadImageButton.icon = icons.imageUpload; + if ( type == 'formView' ) { uploadImageButton.extendTemplate( { attributes: { From 9b6a5726ec1ccabd45ded695293977fa57ecb963 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 9 Nov 2023 17:40:02 +0100 Subject: [PATCH 11/53] Collapsible insert image via URL. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 5 ++- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 5 ++- .../theme/icons/image-folder.svg | 2 +- .../theme/icons/image-upload.svg | 2 +- .../src/imageinsert/imageinsertui.ts | 31 +++++++++++++++---- .../src/imageinsert/ui/imageinsertformview.ts | 20 +++++++----- .../src/imageupload/imageuploadui.ts | 6 ---- .../ckeditor5-image/theme/imageinsert.css | 26 +++++++++------- 8 files changed, 57 insertions(+), 40 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index e71120ef47e..6ab94b6ee75 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -69,13 +69,12 @@ export default class CKBoxUI extends Plugin { button.icon = icons.imageFolder; if ( type == 'formView' ) { - button.class = 'ck-image-insert__ck-finder-button'; button.withText = true; // TODO add to context (note that it's shared with CKFinder) button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with File Manager' ) : - t( 'Insert with File Manager' ) + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) ); } diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index e6d1c580ac2..1a94d393904 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -65,13 +65,12 @@ export default class CKFinderUI extends Plugin { button.icon = icons.imageFolder; if ( type == 'formView' ) { - button.class = 'ck-image-insert__ck-finder-button'; button.withText = true; // TODO add to context (note that it's shared with CKBox) button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with File Manager' ) : - t( 'Insert with File Manager' ) + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) ); } diff --git a/packages/ckeditor5-core/theme/icons/image-folder.svg b/packages/ckeditor5-core/theme/icons/image-folder.svg index 2148287a229..ef51931f10f 100644 --- a/packages/ckeditor5-core/theme/icons/image-folder.svg +++ b/packages/ckeditor5-core/theme/icons/image-folder.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-upload.svg b/packages/ckeditor5-core/theme/icons/image-upload.svg index 056c560c31d..9642195035e 100644 --- a/packages/ckeditor5-core/theme/icons/image-upload.svg +++ b/packages/ckeditor5-core/theme/icons/image-upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index feccc6db18f..92d59a71dac 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -13,6 +13,7 @@ import { ButtonView, SplitButtonView, DropdownButtonView, + CollapsibleView, createDropdown, type DropdownView, type FocusableView @@ -168,9 +169,15 @@ export default class ImageInsertUI extends Plugin { * TODO */ private _createInsertUrlView(): FocusableView { - const replaceImageSourceCommand: ReplaceImageSourceCommand = this.editor.commands.get( 'replaceImageSource' )!; - const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; - const imageInsertUrlView = new ImageInsertUrlView( this.editor.locale ); + const editor = this.editor; + const locale = editor.locale; + const t = locale.t; + + const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!; + const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; + + const imageInsertUrlView = new ImageInsertUrlView( locale ); + const collapsibleView = new CollapsibleView( locale, [ imageInsertUrlView ] ); imageInsertUrlView.bind( 'isImageSelected' ).to( this ); imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( @@ -187,6 +194,9 @@ export default class ImageInsertUI extends Plugin { // the value of the media command (e.g. because they didn't change the selection), they would see // the old value instead of the actual value of the command. imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; + + // TODO should we reset it to the collapsed state? List properties does not collapse. + collapsibleView.isCollapsed = true; } // Note: Use the low priority to make sure the following listener starts working after the @@ -196,9 +206,9 @@ export default class ImageInsertUI extends Plugin { imageInsertUrlView.on( 'submit', () => { if ( replaceImageSourceCommand.isEnabled ) { - this.editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); + editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); } else { - this.editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); + editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); } this._closePanel(); @@ -206,7 +216,16 @@ export default class ImageInsertUI extends Plugin { imageInsertUrlView.on( 'cancel', () => this._closePanel() ); - return imageInsertUrlView; + collapsibleView.set( { + isCollapsed: true + } ); + + collapsibleView.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with link' ) : // TODO context + t( 'Insert with link' ) // TODO context + ); + + return collapsibleView; } private _createInsertButton( diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index 6c9674be79a..ed998cd79eb 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -12,6 +12,7 @@ import { ViewCollection, submitHandler, FocusCycler, + CollapsibleView, type FocusCyclerForwardCycleEvent, type FocusCyclerBackwardCycleEvent } from 'ckeditor5/src/ui'; @@ -77,9 +78,16 @@ export default class ImageInsertFormView extends View { } } ); - this.children.addMany( integrations ); + for ( const view of integrations ) { + this.children.add( view ); + this._focusables.add( view ); + + if ( view instanceof CollapsibleView ) { + this._focusables.addMany( view.children ); + } + } - for ( const view of this.children ) { + for ( const view of this._focusables ) { if ( isViewWithFocusCycler( view ) ) { view.focusCycler.on( 'forwardCycle', evt => { this._focusCycler.focusNext(); @@ -117,13 +125,9 @@ export default class ImageInsertFormView extends View { view: this } ); - this.children.forEach( view => { - // Register the view as focusable. - this._focusables.add( view ); - - // Register the view in the focus tracker. + for ( const view of this._focusables ) { this.focusTracker.add( view.element! ); - } ); + } // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element! ); diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 237ac37afd4..2c56780d677 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -83,12 +83,6 @@ export default class ImageUploadUI extends Plugin { uploadImageButton.icon = icons.imageUpload; if ( type == 'formView' ) { - uploadImageButton.extendTemplate( { - attributes: { - class: 'ck-image-insert__ck-finder-button' - } - } ); - uploadImageButton.withText = true; // TODO add to context (note that it's shared with CKBox) diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index 0c20ed039c6..8d8908a992b 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -5,20 +5,22 @@ @import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; -.ck.ck-image-insert__ck-finder-button { - display: block; - width: 100%; - margin: var(--ck-spacing-standard) auto; - border: 1px solid hsl(0, 0%, 80%); - border-radius: var(--ck-border-radius); -} - .ck.ck-image-insert-form { - padding: var(--ck-spacing-large); + & > .ck.ck-button { + display: block; + width: 100%; + + @mixin ck-dir ltr { + text-align: left; + } + + @mixin ck-dir rtl { + text-align: right; + } + } - &:focus { - /* See: https://github.com/ckeditor/ckeditor5/issues/4773 */ - outline: none; + & > .ck.ck-collapsible { + border-top: 1px solid var(--ck-color-base-border); } } From 63bc9e648621cf8b137ae634ed8004bfce32724e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 9 Nov 2023 18:04:33 +0100 Subject: [PATCH 12/53] Insert image via URL should not be collapsible if it is the only integration. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 4 ++ packages/ckeditor5-ckfinder/src/ckfinderui.ts | 4 ++ .../src/imageinsert/imageinsertui.ts | 38 +++++++++++-------- .../src/imageupload/imageuploadui.ts | 4 ++ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index 6ab94b6ee75..f217595a410 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -76,6 +76,10 @@ export default class CKBoxUI extends Plugin { t( 'Replace with file manager' ) : t( 'Insert with file manager' ) ); + + button.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); } return button; diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 1a94d393904..15be359b85e 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -72,6 +72,10 @@ export default class CKFinderUI extends Plugin { t( 'Replace with file manager' ) : t( 'Insert with file manager' ) ); + + button.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); } return button; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 92d59a71dac..8241fef7cf0 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -80,7 +80,7 @@ export default class ImageInsertUI extends Plugin { const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; this.registerIntegration( 'url', insertImageCommand, - type => type == 'formView' ? this._createInsertUrlView() : this._createInsertButton() + ( type, isOnlyOne ) => type == 'formView' ? this._createInsertUrlView( isOnlyOne ) : this._createInsertButton() ); this.listenTo( editor.model.document, 'change', () => { @@ -117,7 +117,9 @@ export default class ImageInsertUI extends Plugin { let dropdownButton: SplitButtonView | DropdownButtonView | undefined; if ( integrations.length > 1 ) { - dropdownButton = new SplitButtonView( locale, integrations[ 0 ].callback( 'toolbarButton' ) as ButtonView & FocusableView ); + const actionButton = integrations[ 0 ].callback( 'toolbarButton', false ) as ButtonView & FocusableView; + + dropdownButton = new SplitButtonView( locale, actionButton ); } else if ( integrations.length == 1 ) { dropdownButton = this._createInsertButton( DropdownButtonView ); } @@ -130,7 +132,7 @@ export default class ImageInsertUI extends Plugin { ) ); dropdownView.once( 'change:isOpen', () => { - const integrationsView = integrations.map( ( { callback } ) => callback( 'formView' ) ); + const integrationsView = integrations.map( ( { callback } ) => callback( 'formView', integrations.length == 1 ) ); const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationsView ); dropdownView.panelView.children.add( imageInsertFormView ); @@ -168,7 +170,7 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - private _createInsertUrlView(): FocusableView { + private _createInsertUrlView( isOnlyOne: boolean ): FocusableView { const editor = this.editor; const locale = editor.locale; const t = locale.t; @@ -177,7 +179,7 @@ export default class ImageInsertUI extends Plugin { const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; const imageInsertUrlView = new ImageInsertUrlView( locale ); - const collapsibleView = new CollapsibleView( locale, [ imageInsertUrlView ] ); + const collapsibleView = isOnlyOne ? null : new CollapsibleView( locale, [ imageInsertUrlView ] ); imageInsertUrlView.bind( 'isImageSelected' ).to( this ); imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( @@ -196,7 +198,9 @@ export default class ImageInsertUI extends Plugin { imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; // TODO should we reset it to the collapsed state? List properties does not collapse. - collapsibleView.isCollapsed = true; + if ( collapsibleView ) { + collapsibleView.isCollapsed = true; + } } // Note: Use the low priority to make sure the following listener starts working after the @@ -216,16 +220,20 @@ export default class ImageInsertUI extends Plugin { imageInsertUrlView.on( 'cancel', () => this._closePanel() ); - collapsibleView.set( { - isCollapsed: true - } ); + if ( collapsibleView ) { + collapsibleView.set( { + isCollapsed: true + } ); - collapsibleView.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with link' ) : // TODO context - t( 'Insert with link' ) // TODO context - ); + collapsibleView.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with link' ) : // TODO context + t( 'Insert with link' ) // TODO context + ); + + return collapsibleView; + } - return collapsibleView; + return imageInsertUrlView; } private _createInsertButton( @@ -268,7 +276,7 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ -export type IntegrationCallback = ( type: 'toolbarButton' | 'formView' ) => FocusableView; +export type IntegrationCallback = ( type: 'toolbarButton' | 'formView', isOnlyOne: boolean ) => FocusableView; type IntegrationData = { observable: Observable & { isEnabled: boolean }; diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 2c56780d677..e58c0be12f2 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -90,6 +90,10 @@ export default class ImageUploadUI extends Plugin { t( 'Replace from computer' ) : t( 'Upload from computer' ) ); + + uploadImageButton.on( 'execute', () => { + imageInsertUI.dropdownView!.isOpen = false; + } ); } return uploadImageButton; From 293bbf300fb8f821c7da502ef56f0a590c514b31 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 9 Nov 2023 18:12:50 +0100 Subject: [PATCH 13/53] Fixed focus cycling if only insert via URL integration is enabled. --- .../src/imageinsert/ui/imageinsertformview.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index ed998cd79eb..f70f4c15e08 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -87,17 +87,19 @@ export default class ImageInsertFormView extends View { } } - for ( const view of this._focusables ) { - if ( isViewWithFocusCycler( view ) ) { - view.focusCycler.on( 'forwardCycle', evt => { - this._focusCycler.focusNext(); - evt.stop(); - } ); - - view.focusCycler.on( 'backwardCycle', evt => { - this._focusCycler.focusPrevious(); - evt.stop(); - } ); + if ( this._focusables.length > 1 ) { + for ( const view of this._focusables ) { + if ( isViewWithFocusCycler( view ) ) { + view.focusCycler.on( 'forwardCycle', evt => { + this._focusCycler.focusNext(); + evt.stop(); + } ); + + view.focusCycler.on( 'backwardCycle', evt => { + this._focusCycler.focusPrevious(); + evt.stop(); + } ); + } } } From 32e7d928acdf8c8b77f25a8a0e62cdd5f22dce2b Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 10 Nov 2023 11:54:06 +0100 Subject: [PATCH 14/53] Re-used the same padding for list buttons and collapsible button. --- .../components/collapsible/collapsible.css | 4 ++-- .../theme/ckeditor5-ui/components/list/list.css | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css index 52e53ad85d6..768b6bd6190 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/collapsible/collapsible.css @@ -11,7 +11,7 @@ & > .ck.ck-button { width: 100%; font-weight: bold; - padding: var(--ck-spacing-medium) var(--ck-spacing-large); + padding: var(--ck-list-button-padding); border-radius: 0; color: inherit; @@ -32,7 +32,7 @@ } & > .ck-collapsible__children { - padding: 0 var(--ck-spacing-large) var(--ck-spacing-large); + padding: var(--ck-spacing-medium) var(--ck-spacing-large) var(--ck-spacing-large); } &.ck-collapsible_collapsed { diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css index f0108787875..d42164f952f 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css @@ -8,6 +8,12 @@ @import "../../../mixins/_shadow.css"; @import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; +:root { + --ck-list-button-padding: + calc(.2 * var(--ck-line-height-base) * var(--ck-font-size-base)) + calc(.4 * var(--ck-line-height-base) * var(--ck-font-size-base)); +} + .ck.ck-list { @mixin ck-rounded-corners; @@ -35,9 +41,7 @@ /* List items should have the same height. Use absolute units to make sure it is so because e.g. different heading styles may have different height https://github.com/ckeditor/ckeditor5-heading/issues/63 */ - padding: - calc(.2 * var(--ck-line-height-base) * var(--ck-font-size-base)) - calc(.4 * var(--ck-line-height-base) * var(--ck-font-size-base)); + padding: var(--ck-list-button-padding); & .ck-button__label { /* https://github.com/ckeditor/ckeditor5-heading/issues/63 */ From 854825b5c504ca2fe92171589280301471b389f6 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 10 Nov 2023 11:54:53 +0100 Subject: [PATCH 15/53] Styled the insert image form. --- .../ckeditor5-image/theme/imageinsert.css | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index 8d8908a992b..d996053306c 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -5,10 +5,36 @@ @import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; +:root { + --ck-image-insert-insert-by-url-width: 250px; +} + +.ck.ck-image-insert-url { + --ck-input-width: 100%; + + & .ck-image-insert-url__action-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-column-gap: var(--ck-spacing-large); + margin-top: var(--ck-spacing-large); + + & .ck-button-save, + & .ck-button-cancel { + justify-content: center; + min-width: auto; + } + + & .ck-button .ck-button__label { + color: var(--ck-color-text); + } + } +} + .ck.ck-image-insert-form { & > .ck.ck-button { display: block; width: 100%; + padding: var(--ck-list-button-padding); @mixin ck-dir ltr { text-align: left; @@ -21,37 +47,12 @@ & > .ck.ck-collapsible { border-top: 1px solid var(--ck-color-base-border); + min-width: var(--ck-image-insert-insert-by-url-width); } -} - -.ck.ck-image-insert-url { - & .ck-image-insert-url__action-row { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - - margin-top: var(--ck-spacing-large); - & .ck-button-save, - & .ck-button-cancel { - justify-content: center; - flex-grow: 1; - - /* TODO: CSS grid */ - & + * { - @mixin ck-dir ltr { - margin-left: var(--ck-spacing-large); - } - - @mixin ck-dir rtl { - margin-right: var(--ck-spacing-large); - } - } - } - - & .ck-button .ck-button__label { - color: var(--ck-color-text); - } + /* This is the case when there are no other integrations configured than insert by URL */ + & > .ck.ck-image-insert-url { + min-width: var(--ck-image-insert-insert-by-url-width); + padding: var(--ck-spacing-large); } } From 96efa087f5a3e8aa3c60c80649fd292e9a04a7e3 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 10 Nov 2023 11:55:50 +0100 Subject: [PATCH 16/53] Tests: Extended the image insert manual test with additional configurations. --- .../tests/manual/imageinsert.html | 11 ++- .../tests/manual/imageinsert.js | 92 +++++++++++-------- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.html b/packages/ckeditor5-image/tests/manual/imageinsert.html index e76e370667e..aa4134de0cd 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.html +++ b/packages/ckeditor5-image/tests/manual/imageinsert.html @@ -10,7 +10,16 @@ } -
+

All insert image integrations

+ +
+

Image upload via URL with CKFinder integration

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+ +

Insert by URL image integration only

+ +

Image upload via URL with CKFinder integration

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index 1fe24910880..79114a8723a 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -12,43 +12,59 @@ import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage'; import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder'; import ImageInsert from '../../src/imageinsert'; import AutoImage from '../../src/autoimage'; +import { ListProperties } from '@ckeditor/ckeditor5-list'; -ClassicEditor - .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, ImageInsert, AutoImage, LinkImage, CKFinderUploadAdapter, CKFinder ], - toolbar: [ - 'heading', - '|', - 'bold', - 'italic', - 'link', - 'bulletedList', - 'numberedList', - 'blockQuote', - 'insertImage', - 'insertTable', - 'mediaEmbed', - 'undo', - 'redo' - ], - image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], - insert: { - integrations: [ - 'upload', - 'assetManager', - 'url' - ] +createEditor( '#editor-all', [ + 'upload', + 'assetManager', + 'url' +] ); + +createEditor( '#editor-just-url', [ + 'url' +] ); + +function createEditor( elementId, integrations ) { + ClassicEditor + .create( document.querySelector( elementId ), { + plugins: [ ArticlePluginSet, ImageInsert, AutoImage, LinkImage, CKFinderUploadAdapter, CKFinder, ListProperties ], + toolbar: [ + 'heading', + '|', + 'bold', + 'italic', + 'link', + 'bulletedList', + 'numberedList', + 'blockQuote', + 'insertImage', + 'insertTable', + 'mediaEmbed', + 'undo', + 'redo' + ], + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], + insert: { + integrations + } + }, + ckfinder: { + // eslint-disable-next-line max-len + uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' } - }, - ckfinder: { - // eslint-disable-next-line max-len - uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' - } - } ) - .then( editor => { - window.editor = editor; - } ) - .catch( err => { - console.error( err.stack ); - } ); + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); +} From 04558d623ce98802deb746b40abdfc75b4f97029 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 10 Nov 2023 12:00:42 +0100 Subject: [PATCH 17/53] Split the image insert CSS into base and theme files. --- .../ckeditor5-image/theme/imageinsert.css | 47 ---------------- .../theme/ckeditor5-image/imageinsert.css | 56 +++++++++++++++++++ 2 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css diff --git a/packages/ckeditor5-image/theme/imageinsert.css b/packages/ckeditor5-image/theme/imageinsert.css index d996053306c..ae013ba39ac 100644 --- a/packages/ckeditor5-image/theme/imageinsert.css +++ b/packages/ckeditor5-image/theme/imageinsert.css @@ -3,56 +3,9 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; - -:root { - --ck-image-insert-insert-by-url-width: 250px; -} - .ck.ck-image-insert-url { - --ck-input-width: 100%; - & .ck-image-insert-url__action-row { display: grid; grid-template-columns: repeat(2, 1fr); - grid-column-gap: var(--ck-spacing-large); - margin-top: var(--ck-spacing-large); - - & .ck-button-save, - & .ck-button-cancel { - justify-content: center; - min-width: auto; - } - - & .ck-button .ck-button__label { - color: var(--ck-color-text); - } - } -} - -.ck.ck-image-insert-form { - & > .ck.ck-button { - display: block; - width: 100%; - padding: var(--ck-list-button-padding); - - @mixin ck-dir ltr { - text-align: left; - } - - @mixin ck-dir rtl { - text-align: right; - } - } - - & > .ck.ck-collapsible { - border-top: 1px solid var(--ck-color-base-border); - min-width: var(--ck-image-insert-insert-by-url-width); - } - - /* This is the case when there are no other integrations configured than insert by URL */ - & > .ck.ck-image-insert-url { - min-width: var(--ck-image-insert-insert-by-url-width); - padding: var(--ck-spacing-large); } } diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css new file mode 100644 index 00000000000..04b4f4da22b --- /dev/null +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css"; + +:root { + --ck-image-insert-insert-by-url-width: 250px; +} + +.ck.ck-image-insert-url { + --ck-input-width: 100%; + + & .ck-image-insert-url__action-row { + grid-column-gap: var(--ck-spacing-large); + margin-top: var(--ck-spacing-large); + + & .ck-button-save, + & .ck-button-cancel { + justify-content: center; + min-width: auto; + } + + & .ck-button .ck-button__label { + color: var(--ck-color-text); + } + } +} + +.ck.ck-image-insert-form { + & > .ck.ck-button { + display: block; + width: 100%; + padding: var(--ck-list-button-padding); + + @mixin ck-dir ltr { + text-align: left; + } + + @mixin ck-dir rtl { + text-align: right; + } + } + + & > .ck.ck-collapsible { + border-top: 1px solid var(--ck-color-base-border); + min-width: var(--ck-image-insert-insert-by-url-width); + } + + /* This is the case when there are no other integrations configured than insert by URL */ + & > .ck.ck-image-insert-url { + min-width: var(--ck-image-insert-insert-by-url-width); + padding: var(--ck-spacing-large); + } +} From 656ef179db27b671b9c7192c09b1bd50f9863c47 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 10 Nov 2023 12:26:39 +0100 Subject: [PATCH 18/53] Code refactoring and minor CSS fix. --- .../ckeditor5-image/src/imageinsert/imageinsertui.ts | 4 ++-- .../theme/ckeditor5-image/imageinsert.css | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 8241fef7cf0..523b6cc4214 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -132,8 +132,8 @@ export default class ImageInsertUI extends Plugin { ) ); dropdownView.once( 'change:isOpen', () => { - const integrationsView = integrations.map( ( { callback } ) => callback( 'formView', integrations.length == 1 ) ); - const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationsView ); + const integrationViews = integrations.map( ( { callback } ) => callback( 'formView', integrations.length == 1 ) ); + const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationViews ); dropdownView.panelView.children.add( imageInsertFormView ); } ); diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css index 04b4f4da22b..be447b995a2 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css @@ -44,7 +44,14 @@ } & > .ck.ck-collapsible { - border-top: 1px solid var(--ck-color-base-border); + &:not(:first-child) { + border-top: 1px solid var(--ck-color-base-border); + } + + &:not(:last-child) { + border-bottom: 1px solid var(--ck-color-base-border); + } + min-width: var(--ck-image-insert-insert-by-url-width); } From 49f6fea9fda2841af18445cceaabb8a48b429e9a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 10 Nov 2023 13:54:51 +0100 Subject: [PATCH 19/53] Disabled empty LabeledFieldView should not display label over the placeholder. --- .../ckeditor5-ui/components/labeledfield/labeledfieldview.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css index 8ebd48858c9..63d144d03df 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css @@ -87,7 +87,7 @@ /* Fields that are disabled or not focused and without a placeholder should have full-sized labels. */ /* stylelint-disable-next-line no-descending-specificity */ - &.ck-disabled.ck-labeled-field-view_empty > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label, + &.ck-disabled.ck-labeled-field-view_empty:not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label, &.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label { @mixin ck-dir ltr { transform: translate(var(--ck-labeled-field-label-default-position-x), var(--ck-labeled-field-label-default-position-y)) scale(1); From 3443be5cfe67229490995c3fad20a28497d85c2b Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 10 Nov 2023 14:13:57 +0100 Subject: [PATCH 20/53] Fixed arrow keys handling in insert image form. --- .../src/imageinsert/ui/imageinsertformview.ts | 10 +++++++ .../src/imageinsert/ui/imageinserturlview.ts | 30 +++++-------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index f70f4c15e08..80e414cc80c 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -133,6 +133,16 @@ export default class ImageInsertFormView extends View { // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element! ); + + const stopPropagation = ( data: KeyboardEvent ) => data.stopPropagation(); + + // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's + // keystroke handler would take over the key management in the URL input. We need to prevent + // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible. + this.keystrokes.set( 'arrowright', stopPropagation ); + this.keystrokes.set( 'arrowleft', stopPropagation ); + this.keystrokes.set( 'arrowup', stopPropagation ); + this.keystrokes.set( 'arrowdown', stopPropagation ); } /** diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts index daa066f9a6e..b1847ae6f06 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -114,6 +114,12 @@ export default class ImageInsertUrlView extends View { this.insertButtonView = this._createInsertButton(); this.cancelButtonView = this._createCancelButton(); + this._focusables.addMany( [ + this.urlInputView, + this.insertButtonView, + this.cancelButtonView + ] ); + this.setTemplate( { tag: 'div', @@ -150,32 +156,12 @@ export default class ImageInsertUrlView extends View { public override render(): void { super.render(); - const childViews = [ - this.urlInputView, - this.insertButtonView, - this.cancelButtonView - ]; - - childViews.forEach( view => { - // Register the view as focusable. - this._focusables.add( view ); - - // Register the view in the focus tracker. + for ( const view of this._focusables ) { this.focusTracker.add( view.element! ); - } ); + } // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element! ); - - const stopPropagation = ( data: KeyboardEvent ) => data.stopPropagation(); - - // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's - // keystroke handler would take over the key management in the URL input. We need to prevent - // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible. - this.keystrokes.set( 'arrowright', stopPropagation ); - this.keystrokes.set( 'arrowleft', stopPropagation ); - this.keystrokes.set( 'arrowup', stopPropagation ); - this.keystrokes.set( 'arrowdown', stopPropagation ); } /** From 6d1df897920a9cb81b75e9673a28a506fee924b9 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 10 Nov 2023 14:35:09 +0100 Subject: [PATCH 21/53] Fixed focus cycling while the dropdown is focused itself. --- packages/ckeditor5-image/src/imageinsert/imageinsertui.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 523b6cc4214..2e81ff80712 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -136,6 +136,10 @@ export default class ImageInsertUI extends Plugin { const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationViews ); dropdownView.panelView.children.add( imageInsertFormView ); + + // Add panelView to keystrokes handling, so it can handle Tab and Shift+Tab while the panel itself is focused. + imageInsertFormView.keystrokes.listenTo( dropdownView.panelView.element! ); + // TODO should this be handled this way or by adding `tabindex="-1"` to the ImageInsertFormView? } ); return dropdownView; From c33acbd7a5b0f4c1632a344fc3daf4e398356bf4 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 10 Nov 2023 17:36:33 +0100 Subject: [PATCH 22/53] Updated panel focus handling to a consistent method. --- packages/ckeditor5-image/src/imageinsert/imageinsertui.ts | 4 ---- .../ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts | 3 ++- .../theme/ckeditor5-image/imageinsert.css | 4 ++++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 2e81ff80712..523b6cc4214 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -136,10 +136,6 @@ export default class ImageInsertUI extends Plugin { const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationViews ); dropdownView.panelView.children.add( imageInsertFormView ); - - // Add panelView to keystrokes handling, so it can handle Tab and Shift+Tab while the panel itself is focused. - imageInsertFormView.keystrokes.listenTo( dropdownView.panelView.element! ); - // TODO should this be handled this way or by adding `tabindex="-1"` to the ImageInsertFormView? } ); return dropdownView; diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index 80e414cc80c..5536a180818 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -110,7 +110,8 @@ export default class ImageInsertFormView extends View { class: [ 'ck', 'ck-image-insert-form' - ] + ], + tabindex: -1 }, children: this.children diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css index be447b995a2..32896f67690 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-image/imageinsert.css @@ -60,4 +60,8 @@ min-width: var(--ck-image-insert-insert-by-url-width); padding: var(--ck-spacing-large); } + + &:focus { + outline: none; + } } From 5f6b37a485405affee9ef1f7ed612d341c9bb87f Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 22 Nov 2023 21:39:49 +0100 Subject: [PATCH 23/53] The insert image component should not create dropdown if the integration does not require form view, and it is the only enabled integration, --- packages/ckeditor5-ckbox/src/ckboxui.ts | 14 +-- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 14 +-- packages/ckeditor5-core/src/index.ts | 6 +- .../theme/icons/image-asset-manager.svg | 1 + .../theme/icons/image-folder.svg | 1 - .../theme/icons/image-upload.svg | 2 +- .../ckeditor5-core/theme/icons/image-url.svg | 1 + packages/ckeditor5-image/src/imageconfig.ts | 21 ++-- .../src/imageinsert/imageinsertui.ts | 70 +++++++---- .../ckeditor5-image/src/imageinsertviaurl.ts | 2 +- .../src/imageupload/imageuploadui.ts | 12 +- .../tests/manual/imageinsert.html | 7 ++ .../tests/manual/imageinsert.js | 118 +++++++++++------- 13 files changed, 166 insertions(+), 103 deletions(-) create mode 100644 packages/ckeditor5-core/theme/icons/image-asset-manager.svg delete mode 100644 packages/ckeditor5-core/theme/icons/image-folder.svg create mode 100644 packages/ckeditor5-core/theme/icons/image-url.svg diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index f217595a410..891b2a1098b 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -66,17 +66,17 @@ export default class CKBoxUI extends Plugin { imageInsertUI.registerIntegration( 'assetManager', command, type => { const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; - button.icon = icons.imageFolder; + button.icon = icons.imageAssetManager; + + // TODO add to context (note that it's shared with CKFinder) + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) + ); if ( type == 'formView' ) { button.withText = true; - // TODO add to context (note that it's shared with CKFinder) - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 15be359b85e..9e2c49f2576 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -62,17 +62,17 @@ export default class CKFinderUI extends Plugin { imageInsertUI.registerIntegration( 'assetManager', command, type => { const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; - button.icon = icons.imageFolder; + button.icon = icons.imageAssetManager; + + // TODO add to context (note that it's shared with CKBox) + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) + ); if ( type == 'formView' ) { button.withText = true; - // TODO add to context (note that it's shared with CKBox) - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index 4cdb52ac498..c40f41b5f97 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -44,7 +44,8 @@ import textAlternative from './../theme/icons/text-alternative.svg'; import loupe from './../theme/icons/loupe.svg'; import image from './../theme/icons/image.svg'; import imageUpload from './../theme/icons/image-upload.svg'; -import imageFolder from './../theme/icons/image-folder.svg'; +import imageAssetManager from './../theme/icons/image-asset-manager.svg'; +import imageUrl from './../theme/icons/image-url.svg'; import alignBottom from './../theme/icons/align-bottom.svg'; import alignMiddle from './../theme/icons/align-middle.svg'; @@ -89,7 +90,8 @@ export const icons = { history, image, imageUpload, - imageFolder, + imageAssetManager, + imageUrl, lowVision, textAlternative, loupe, diff --git a/packages/ckeditor5-core/theme/icons/image-asset-manager.svg b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg new file mode 100644 index 00000000000..e9fc719f718 --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-folder.svg b/packages/ckeditor5-core/theme/icons/image-folder.svg deleted file mode 100644 index ef51931f10f..00000000000 --- a/packages/ckeditor5-core/theme/icons/image-folder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-upload.svg b/packages/ckeditor5-core/theme/icons/image-upload.svg index 9642195035e..6a88559923b 100644 --- a/packages/ckeditor5-core/theme/icons/image-upload.svg +++ b/packages/ckeditor5-core/theme/icons/image-upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-url.svg b/packages/ckeditor5-core/theme/icons/image-url.svg new file mode 100644 index 00000000000..94ae0dd68b5 --- /dev/null +++ b/packages/ckeditor5-core/theme/icons/image-url.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-image/src/imageconfig.ts b/packages/ckeditor5-image/src/imageconfig.ts index 88e027d234b..aeedee6a6a9 100644 --- a/packages/ckeditor5-image/src/imageconfig.ts +++ b/packages/ckeditor5-image/src/imageconfig.ts @@ -458,30 +458,27 @@ export interface ImageInsertConfig { * The image insert panel view configuration contains a list of {@link module:image/imageinsert~ImageInsert} integrations. * * The option accepts string tokens. - * * for predefined integrations, we have two special strings: `insertImageViaUrl` and `openCKFinder`. - * The former adds the **Insert image via URL** feature, while the latter adds the built-in **CKFinder** integration. - * * for custom integrations, each string should be a name of the component registered in the - * {@link module:ui/componentfactory~ComponentFactory component factory}. - * If you have a plugin `PluginX` that registers `pluginXButton` component, then the integration token - * in that case should be `pluginXButton`. + * * for predefined integrations, we have 3 special strings: `upload`, `url`, and `assetManager`. + * * for custom integrations, each string should be a name of the integration registered by the + * {@link module:image/imageinsert/imageinsertui~ImageInsertUI#registerIntegration `ImageInsertUI#registerIntegration()`}. * * ```ts - * // Add `insertImageViaUrl`, `openCKFinder` and custom `pluginXButton` integrations. + * // Add `upload`, `assetManager` and `url` integrations. * const imageInsertConfig = { * insert: { * integrations: [ - * 'insertImageViaUrl', - * 'openCKFinder', - * 'pluginXButton' + * 'upload', + * 'assetManager', + * 'url' * ] * } * }; * ``` * * @internal - * @default [ 'insertImageViaUrl' ] + * @default [ 'upload', 'assetManager', 'url' ] */ - integrations: Array; + integrations?: Array; /** * This option allows to override the image type used by the {@link module:image/image/insertimagecommand~InsertImageCommand} diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 523b6cc4214..f8bf459d26d 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -69,9 +69,7 @@ export default class ImageInsertUI extends Plugin { this.set( 'isImageSelected', false ); const editor = this.editor; - const componentCreator = ( locale: Locale ) => { - return this._createDropdownView( locale ); - }; + const componentCreator = ( locale: Locale ) => this._createToolbarComponent( locale ); // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); @@ -79,8 +77,11 @@ export default class ImageInsertUI extends Plugin { const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; - this.registerIntegration( 'url', insertImageCommand, - ( type, isOnlyOne ) => type == 'formView' ? this._createInsertUrlView( isOnlyOne ) : this._createInsertButton() + this.registerIntegration( + 'url', + insertImageCommand, + ( type, isOnlyOne ) => type == 'formView' ? this._createInsertUrlView( isOnlyOne ) : this._createInsertUrlButton(), + { requiresForm: true } ); this.listenTo( editor.model.document, 'change', () => { @@ -94,7 +95,12 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - public registerIntegration( name: string, observable: IntegrationData[ 'observable' ], callback: IntegrationCallback ): void { + public registerIntegration( + name: string, + observable: Observable & { isEnabled: boolean }, + callback: IntegrationCallback, + options: { requiresForm?: boolean } = {} + ): void { if ( this._integrations.has( name ) ) { /** * TODO @@ -102,26 +108,37 @@ export default class ImageInsertUI extends Plugin { logWarning( 'image-insert-zzzzz', { name } ); } - this._integrations.set( name, { observable, callback } ); + this._integrations.set( name, { ...options, observable, callback } ); } /** - * Creates the dropdown view. - * - * @param locale The localization services instance. + * Creates the toolbar component. */ - private _createDropdownView( locale: Locale ): DropdownView { + private _createToolbarComponent( locale: Locale ): DropdownView | FocusableView { const editor = this.editor; + const t = locale.t; const integrations = this._prepareIntegrations(); + let dropdownButton: SplitButtonView | DropdownButtonView | undefined; + const firstIntegration = integrations[ 0 ]; - if ( integrations.length > 1 ) { - const actionButton = integrations[ 0 ].callback( 'toolbarButton', false ) as ButtonView & FocusableView; + if ( integrations.length == 1 ) { + // Do not use dropdown for a single integration button (integration that does not require form view). + if ( !firstIntegration.requiresForm ) { + return firstIntegration.callback( 'toolbarButton', true ); + } + + dropdownButton = this._createInsertUrlButton( DropdownButtonView ); + } else { + const actionButton = firstIntegration.callback( 'toolbarButton', false ) as ButtonView & FocusableView; dropdownButton = new SplitButtonView( locale, actionButton ); - } else if ( integrations.length == 1 ) { - dropdownButton = this._createInsertButton( DropdownButtonView ); + dropdownButton.tooltip = true; + dropdownButton.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image' ) : // TODO context + t( 'Insert image' ) // TODO context + ); } const dropdownView = this.dropdownView = createDropdown( locale, dropdownButton ); @@ -164,6 +181,15 @@ export default class ImageInsertUI extends Plugin { result.push( this._integrations.get( item )! ); } + if ( !result.length ) { + result.push( this._integrations.get( 'url' )! ); + + /** + * TODO + */ + logWarning( 'image-insert-aaaa' ); + } + return result; } @@ -197,7 +223,6 @@ export default class ImageInsertUI extends Plugin { // the old value instead of the actual value of the command. imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; - // TODO should we reset it to the collapsed state? List properties does not collapse. if ( collapsibleView ) { collapsibleView.isCollapsed = true; } @@ -236,15 +261,15 @@ export default class ImageInsertUI extends Plugin { return imageInsertUrlView; } - private _createInsertButton( + private _createInsertUrlButton( ButtonClass: new ( locale?: Locale ) => T ): T; - private _createInsertButton(): ButtonView; + private _createInsertUrlButton(): ButtonView; /** * TODO */ - private _createInsertButton( + private _createInsertUrlButton( ButtonClass: new ( locale?: Locale ) => ButtonView = ButtonView ): ButtonView { const editor = this.editor; @@ -252,13 +277,13 @@ export default class ImageInsertUI extends Plugin { const t = editor.locale.t; button.set( { - icon: icons.image, + icon: icons.imageUrl, tooltip: true } ); - // TODO add 'Replace image' to context + // TODO add 'Update image URL' and 'Insert image via URL' to context button.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image' ) : t( 'Insert image' ) + t( 'Update image URL' ) : t( 'Insert image via URL' ) ); return button; @@ -281,4 +306,5 @@ export type IntegrationCallback = ( type: 'toolbarButton' | 'formView', isOnlyOn type IntegrationData = { observable: Observable & { isEnabled: boolean }; callback: IntegrationCallback; + requiresForm?: boolean; }; diff --git a/packages/ckeditor5-image/src/imageinsertviaurl.ts b/packages/ckeditor5-image/src/imageinsertviaurl.ts index 0b979e455fb..51d68c31e15 100644 --- a/packages/ckeditor5-image/src/imageinsertviaurl.ts +++ b/packages/ckeditor5-image/src/imageinsertviaurl.ts @@ -43,8 +43,8 @@ export default class ImageInsertViaUrl extends Plugin { super( editor ); editor.config.define( 'image.insert.integrations', [ - 'assetManager', 'upload', + 'assetManager', 'url' ] ); } diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index e58c0be12f2..c7a3d5f322f 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -82,15 +82,15 @@ export default class ImageUploadUI extends Plugin { uploadImageButton.icon = icons.imageUpload; + // TODO add to context (note that it's shared with CKBox) + uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace from computer' ) : + t( 'Upload from computer' ) + ); + if ( type == 'formView' ) { uploadImageButton.withText = true; - // TODO add to context (note that it's shared with CKBox) - uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace from computer' ) : - t( 'Upload from computer' ) - ); - uploadImageButton.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.html b/packages/ckeditor5-image/tests/manual/imageinsert.html index de7cc867e63..f95b98c8ef4 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.html +++ b/packages/ckeditor5-image/tests/manual/imageinsert.html @@ -10,6 +10,13 @@ } +

+ Insert image integrations: + + + +

+

Insert configured as "auto"

Image upload via URL with CKFinder integration

diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.js b/packages/ckeditor5-image/tests/manual/imageinsert.js index 948d39ae120..6127f8cad75 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.js +++ b/packages/ckeditor5-image/tests/manual/imageinsert.js @@ -13,50 +13,80 @@ import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder'; import ImageInsert from '../../src/imageinsert'; import AutoImage from '../../src/autoimage'; -createEditor( 'editor1', 'auto' ); -createEditor( 'editor2', 'block' ); -createEditor( 'editor3', 'inline' ); - -function createEditor( elementId, imageType ) { - ClassicEditor - .create( document.querySelector( '#' + elementId ), { - plugins: [ ArticlePluginSet, ImageInsert, AutoImage, LinkImage, CKFinderUploadAdapter, CKFinder ], - toolbar: [ - 'heading', - '|', - 'bold', - 'italic', - 'link', - 'bulletedList', - 'numberedList', - 'blockQuote', - 'insertImage', - 'insertTable', - 'mediaEmbed', - 'undo', - 'redo' - ], - image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ], - type: imageType - } - }, - ckfinder: { - // eslint-disable-next-line max-len - uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' +async function createEditor( elementId, imageType ) { + const editor = await ClassicEditor.create( document.querySelector( '#' + elementId ), { + plugins: [ ArticlePluginSet, ImageInsert, AutoImage, LinkImage, CKFinderUploadAdapter, CKFinder ], + toolbar: [ + 'heading', + '|', + 'bold', + 'italic', + 'link', + 'bulletedList', + 'numberedList', + 'blockQuote', + 'insertImage', + 'insertTable', + 'mediaEmbed', + 'undo', + 'redo' + ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], + insert: { + integrations: getSelectedIntegrations(), + type: imageType } - } ) - .then( editor => { - window[ elementId ] = editor; - - CKEditorInspector.attach( { [ imageType ]: editor } ); - } ) - .catch( err => { - console.error( err ); + }, + ckfinder: { + // eslint-disable-next-line max-len + uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' + }, + updateSourceElementOnDestroy: true + } ); + + window[ elementId ] = editor; + + CKEditorInspector.attach( { [ imageType ]: editor } ); +} + +setupEditors( { + editor1: 'auto', + editor2: 'block', + editor3: 'inline' +} ).catch( err => { + console.error( err ); +} ); + +async function setupEditors( opt ) { + await startEditors(); + + for ( const element of document.querySelectorAll( 'input[name=imageInsertIntegration]' ) ) { + element.addEventListener( 'change', () => { + restartEditors().catch( err => console.error( err ) ); } ); + } + + async function restartEditors() { + await stopEditors(); + await startEditors(); + } + + async function startEditors() { + for ( const [ elementId, imageType ] of Object.entries( opt ) ) { + await createEditor( elementId, imageType ); + } + } + + async function stopEditors( ) { + for ( const editorId of Object.keys( opt ) ) { + await window[ editorId ].destroy(); + } + } +} + +function getSelectedIntegrations() { + return Array.from( document.querySelectorAll( 'input[name=imageInsertIntegration]' ) ) + .filter( element => element.checked ) + .map( element => element.value ); } From c20fb1357227cd5ce356a51519b283e00c9273c7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 23 Nov 2023 14:25:45 +0100 Subject: [PATCH 24/53] Updated image icons. --- packages/ckeditor5-core/theme/icons/image-asset-manager.svg | 2 +- packages/ckeditor5-core/theme/icons/image-upload.svg | 2 +- packages/ckeditor5-core/theme/icons/image-url.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/theme/icons/image-asset-manager.svg b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg index e9fc719f718..3aee54400b2 100644 --- a/packages/ckeditor5-core/theme/icons/image-asset-manager.svg +++ b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-upload.svg b/packages/ckeditor5-core/theme/icons/image-upload.svg index 6a88559923b..2a789667920 100644 --- a/packages/ckeditor5-core/theme/icons/image-upload.svg +++ b/packages/ckeditor5-core/theme/icons/image-upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-url.svg b/packages/ckeditor5-core/theme/icons/image-url.svg index 94ae0dd68b5..6dc5a0f73ed 100644 --- a/packages/ckeditor5-core/theme/icons/image-url.svg +++ b/packages/ckeditor5-core/theme/icons/image-url.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From b57f83d07fe940bd83f2938ede996026287fcfba Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 23 Nov 2023 16:57:25 +0100 Subject: [PATCH 25/53] Updated icons and context file. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 1 - packages/ckeditor5-ckfinder/src/ckfinderui.ts | 1 - packages/ckeditor5-image/lang/contexts.json | 5 +++++ .../src/imageinsert/imageinsertui.ts | 13 +++++++------ .../src/imageupload/imageuploadui.ts | 1 - 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index 891b2a1098b..acf201ac116 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -68,7 +68,6 @@ export default class CKBoxUI extends Plugin { button.icon = icons.imageAssetManager; - // TODO add to context (note that it's shared with CKFinder) button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 9e2c49f2576..ec601cd98b5 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -64,7 +64,6 @@ export default class CKFinderUI extends Plugin { button.icon = icons.imageAssetManager; - // TODO add to context (note that it's shared with CKBox) button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) diff --git a/packages/ckeditor5-image/lang/contexts.json b/packages/ckeditor5-image/lang/contexts.json index 95139a0ea44..cc591f80f8b 100644 --- a/packages/ckeditor5-image/lang/contexts.json +++ b/packages/ckeditor5-image/lang/contexts.json @@ -12,6 +12,9 @@ "Text alternative": "The label for the image text alternative input.", "Enter image caption": "The placeholder text for the image caption displayed when the caption is empty.", "Insert image": "The label for the insert image toolbar button.", + "Replace image": "The label for the replace image toolbar button.", + "Upload from computer": "The label for the upload image from computer toolbar button.", + "Replace from computer": "The label for the replace image by upload from computer toolbar button.", "Upload failed": "The title of the notification displayed when upload fails.", "Image toolbar": "The label used by assistive technologies describing an image toolbar attached to an image widget.", "Resize image": "The label used for the dropdown in the image toolbar containing defined resize options.", @@ -23,6 +26,8 @@ "Update": "The label of the form submit button if the image source URL input has a value.", "Insert image via URL": "The input label for the Insert image via URL form.", "Update image URL": "The input label for the Insert image via URL form for a pre-existing image.", + "Insert with file manager": "The label for the insert image with the file manager toolbar button.", + "Replace with file manager": "The label for the replace image with the file manager toolbar button.", "Caption for the image": "Text used by screen readers do describe an image when the image has no text alternative.", "Caption for image: %0": "Text used by screen readers do describe an image when there is a text alternative available, e.g. 'Caption for image: this is a description of the image.'" } diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index f8bf459d26d..5b7751f04c1 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -135,9 +135,10 @@ export default class ImageInsertUI extends Plugin { dropdownButton = new SplitButtonView( locale, actionButton ); dropdownButton.tooltip = true; + dropdownButton.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image' ) : // TODO context - t( 'Insert image' ) // TODO context + t( 'Replace image' ) : + t( 'Insert image' ) ); } @@ -251,8 +252,8 @@ export default class ImageInsertUI extends Plugin { } ); collapsibleView.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with link' ) : // TODO context - t( 'Insert with link' ) // TODO context + t( 'Update image URL' ) : + t( 'Insert image via URL' ) ); return collapsibleView; @@ -281,9 +282,9 @@ export default class ImageInsertUI extends Plugin { tooltip: true } ); - // TODO add 'Update image URL' and 'Insert image via URL' to context button.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Update image URL' ) : t( 'Insert image via URL' ) + t( 'Update image URL' ) : + t( 'Insert image via URL' ) ); return button; diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index c7a3d5f322f..7a408df368d 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -82,7 +82,6 @@ export default class ImageUploadUI extends Plugin { uploadImageButton.icon = icons.imageUpload; - // TODO add to context (note that it's shared with CKBox) uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace from computer' ) : t( 'Upload from computer' ) From 7c0a4982e2b6826fb499724826ea1e5378d582f7 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 23 Nov 2023 17:05:42 +0100 Subject: [PATCH 26/53] Shared translation context entries moved to the core package. --- packages/ckeditor5-core/lang/contexts.json | 4 +++- packages/ckeditor5-image/lang/contexts.json | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/lang/contexts.json b/packages/ckeditor5-core/lang/contexts.json index 9406575f765..d96e6b04153 100644 --- a/packages/ckeditor5-core/lang/contexts.json +++ b/packages/ckeditor5-core/lang/contexts.json @@ -7,5 +7,7 @@ "Show more items": "Label of a toolbar button which reveals more toolbar items.", "%0 of %1": "Label for an ‘X of Y’ status of a typical next/previous navigation. For instance, ‘Page 5 of 20’ or 'Search result 5 of 20'.", "Cannot upload file:": "A generic error message displayed on upload failure. The file name is concatenated to this text.", - "Rich Text Editor. Editing area: %0": "Accessible label of the specific editing area of the editor acting as a root of the entire application." + "Rich Text Editor. Editing area: %0": "Accessible label of the specific editing area of the editor acting as a root of the entire application.", + "Insert with file manager": "The label for the insert image with the file manager toolbar button.", + "Replace with file manager": "The label for the replace image with the file manager toolbar button." } diff --git a/packages/ckeditor5-image/lang/contexts.json b/packages/ckeditor5-image/lang/contexts.json index cc591f80f8b..b7c04c17a19 100644 --- a/packages/ckeditor5-image/lang/contexts.json +++ b/packages/ckeditor5-image/lang/contexts.json @@ -26,8 +26,6 @@ "Update": "The label of the form submit button if the image source URL input has a value.", "Insert image via URL": "The input label for the Insert image via URL form.", "Update image URL": "The input label for the Insert image via URL form for a pre-existing image.", - "Insert with file manager": "The label for the insert image with the file manager toolbar button.", - "Replace with file manager": "The label for the replace image with the file manager toolbar button.", "Caption for the image": "Text used by screen readers do describe an image when the image has no text alternative.", "Caption for image: %0": "Text used by screen readers do describe an image when there is a text alternative available, e.g. 'Caption for image: this is a description of the image.'" } From 58fa2b38ea4cbd72409e1f3fad8fa0fa891757eb Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 23 Nov 2023 17:40:47 +0100 Subject: [PATCH 27/53] Updated image icon. --- packages/ckeditor5-core/theme/icons/image.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/theme/icons/image.svg b/packages/ckeditor5-core/theme/icons/image.svg index 1449860d776..d4c065a4399 100644 --- a/packages/ckeditor5-core/theme/icons/image.svg +++ b/packages/ckeditor5-core/theme/icons/image.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 93479c3867912b3d3a23c0fb4ae1e888659965a5 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 24 Nov 2023 12:03:51 +0100 Subject: [PATCH 28/53] Updated icons and labels. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 15 ++++++++++----- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 15 ++++++++++----- packages/ckeditor5-core/lang/contexts.json | 6 ++++-- packages/ckeditor5-image/lang/contexts.json | 6 ++++-- .../src/imageupload/imageuploadui.ts | 19 ++++++++++++------- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index acf201ac116..8d84d59d61d 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -68,17 +68,22 @@ export default class CKBoxUI extends Plugin { button.icon = icons.imageAssetManager; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - if ( type == 'formView' ) { button.withText = true; + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) + ); + button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); + } else { + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image with file manager' ) : + t( 'Insert image with file manager' ) + ); } return button; diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index ec601cd98b5..7a1af104db7 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -64,17 +64,22 @@ export default class CKFinderUI extends Plugin { button.icon = icons.imageAssetManager; - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace with file manager' ) : - t( 'Insert with file manager' ) - ); - if ( type == 'formView' ) { button.withText = true; + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace with file manager' ) : + t( 'Insert with file manager' ) + ); + button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); + } else { + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image with file manager' ) : + t( 'Insert image with file manager' ) + ); } return button; diff --git a/packages/ckeditor5-core/lang/contexts.json b/packages/ckeditor5-core/lang/contexts.json index d96e6b04153..f69cab2a449 100644 --- a/packages/ckeditor5-core/lang/contexts.json +++ b/packages/ckeditor5-core/lang/contexts.json @@ -8,6 +8,8 @@ "%0 of %1": "Label for an ‘X of Y’ status of a typical next/previous navigation. For instance, ‘Page 5 of 20’ or 'Search result 5 of 20'.", "Cannot upload file:": "A generic error message displayed on upload failure. The file name is concatenated to this text.", "Rich Text Editor. Editing area: %0": "Accessible label of the specific editing area of the editor acting as a root of the entire application.", - "Insert with file manager": "The label for the insert image with the file manager toolbar button.", - "Replace with file manager": "The label for the replace image with the file manager toolbar button." + "Insert with file manager": "The label for the insert image with the file manager toolbar button with visible label in insert image dropdown.", + "Replace with file manager": "The label for the replace image with the file manager toolbar button with visible label in insert image dropdown.", + "Insert image with file manager": "The label for the insert image with the file manager toolbar button.", + "Replace image with file manager": "The label for the replace image with the file manager toolbar button." } diff --git a/packages/ckeditor5-image/lang/contexts.json b/packages/ckeditor5-image/lang/contexts.json index b7c04c17a19..b0c3a1c1d3a 100644 --- a/packages/ckeditor5-image/lang/contexts.json +++ b/packages/ckeditor5-image/lang/contexts.json @@ -13,8 +13,10 @@ "Enter image caption": "The placeholder text for the image caption displayed when the caption is empty.", "Insert image": "The label for the insert image toolbar button.", "Replace image": "The label for the replace image toolbar button.", - "Upload from computer": "The label for the upload image from computer toolbar button.", - "Replace from computer": "The label for the replace image by upload from computer toolbar button.", + "Upload from computer": "The label for the upload image from computer toolbar button with visible label in insert image dropdown.", + "Replace from computer": "The label for the replace image by upload from computer toolbar button with visible label in insert image dropdown.", + "Upload image from computer": "The label for the upload image from computer toolbar button.", + "Replace image from computer": "The label for the replace image by upload from computer toolbar button.", "Upload failed": "The title of the notification displayed when upload fails.", "Image toolbar": "The label used by assistive technologies describing an image toolbar attached to an image widget.", "Resize image": "The label used for the dropdown in the image toolbar containing defined resize options.", diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 7a408df368d..3af2be9885d 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -49,8 +49,8 @@ export default class ImageUploadUI extends Plugin { } ); view.set( { - label: t( 'Insert image' ), - icon: icons.image, + label: t( 'Upload image from computer' ), + icon: icons.imageUpload, tooltip: true } ); @@ -82,17 +82,22 @@ export default class ImageUploadUI extends Plugin { uploadImageButton.icon = icons.imageUpload; - uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace from computer' ) : - t( 'Upload from computer' ) - ); - if ( type == 'formView' ) { uploadImageButton.withText = true; + uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace from computer' ) : + t( 'Upload from computer' ) + ); + uploadImageButton.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); + } else { + uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image from computer' ) : + t( 'Upload image from computer' ) + ); } return uploadImageButton; From 9f80741c7eb4d85a9e9db4ca951294ef84a3bbac Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 27 Nov 2023 10:55:51 +0100 Subject: [PATCH 29/53] Cleaned up icons. --- .../src/ckboximageedit/ckboximageeditcommand.ts | 1 + packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg | 2 +- packages/ckeditor5-core/theme/icons/text-alternative.svg | 2 +- scripts/clean-up-svg-icons.js | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts index 0ea42a46927..50a3731cfad 100644 --- a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts @@ -180,6 +180,7 @@ export default class CKBoxImageEditCommand extends Command { this._wrapper.remove(); this._wrapper = null; + this.refresh(); this.editor.editing.view.focus(); } diff --git a/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg b/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg index ece03d6ca14..8c702cd4cf4 100644 --- a/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg +++ b/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/text-alternative.svg b/packages/ckeditor5-core/theme/icons/text-alternative.svg index 03c2fa36852..f1a83fb48b5 100644 --- a/packages/ckeditor5-core/theme/icons/text-alternative.svg +++ b/packages/ckeditor5-core/theme/icons/text-alternative.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/scripts/clean-up-svg-icons.js b/scripts/clean-up-svg-icons.js index 7c6f9390cfd..9b6f6dd7abf 100644 --- a/scripts/clean-up-svg-icons.js +++ b/scripts/clean-up-svg-icons.js @@ -49,8 +49,7 @@ const { execSync } = require( 'child_process' ); // because, for instance, CSS animations may depend on it. const EXCLUDED_ICONS = [ 'return-arrow.svg', - 'project-logo.svg', - 'text-alternative.svg' + 'project-logo.svg' ]; // A pattern to match all the icons. From cad7b7752822104c0408bcb46dc20a981777d490 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 27 Nov 2023 16:58:01 +0100 Subject: [PATCH 30/53] Insert image via URL extracted as a separate integration. --- packages/ckeditor5-ckbox/src/ckboxui.ts | 32 +-- packages/ckeditor5-ckfinder/src/ckfinderui.ts | 32 +-- .../src/imageinsert/imageinsertui.ts | 187 +++++------------- .../src/imageinsert/imageinsertviaurlui.ts | 152 ++++++++++++++ .../ckeditor5-image/src/imageinsertviaurl.ts | 18 +- .../src/imageupload/imageuploadui.ts | 32 +-- packages/ckeditor5-ui/src/index.ts | 2 +- 7 files changed, 262 insertions(+), 193 deletions(-) create mode 100644 packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index 8d84d59d61d..2154c89a980 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -63,14 +63,27 @@ export default class CKBoxUI extends Plugin { if ( editor.plugins.has( 'ImageInsertUI' ) ) { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - imageInsertUI.registerIntegration( 'assetManager', command, type => { - const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; + imageInsertUI.registerIntegration( { + name: 'assetManager', + observable: command, - button.icon = icons.imageAssetManager; + buttonViewCreator: () => { + const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; - if ( type == 'formView' ) { - button.withText = true; + button.icon = icons.imageAssetManager; + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image with file manager' ) : + t( 'Insert image with file manager' ) + ); + + return button; + }, + + formViewCreator: () => { + const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; + button.icon = icons.imageAssetManager; + button.withText = true; button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) @@ -79,14 +92,9 @@ export default class CKBoxUI extends Plugin { button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); - } else { - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image with file manager' ) : - t( 'Insert image with file manager' ) - ); - } - return button; + return button; + } } ); } } diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 7a1af104db7..3862d35a2b3 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -59,14 +59,27 @@ export default class CKFinderUI extends Plugin { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); const command: CKFinderCommand = editor.commands.get( 'ckfinder' )!; - imageInsertUI.registerIntegration( 'assetManager', command, type => { - const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; + imageInsertUI.registerIntegration( { + name: 'assetManager', + observable: command, - button.icon = icons.imageAssetManager; + buttonViewCreator: () => { + const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; - if ( type == 'formView' ) { - button.withText = true; + button.icon = icons.imageAssetManager; + button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image with file manager' ) : + t( 'Insert image with file manager' ) + ); + + return button; + }, + + formViewCreator: () => { + const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; + button.icon = icons.imageAssetManager; + button.withText = true; button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace with file manager' ) : t( 'Insert with file manager' ) @@ -75,14 +88,9 @@ export default class CKFinderUI extends Plugin { button.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); - } else { - button.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image with file manager' ) : - t( 'Insert image with file manager' ) - ); - } - return button; + return button; + } } ); } } diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 5b7751f04c1..5711f8e904a 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -7,26 +7,19 @@ * @module image/imageinsert/imageinsertui */ -import { Plugin, icons } from 'ckeditor5/src/core'; -import { logWarning, type Locale, type Observable } from 'ckeditor5/src/utils'; +import { Plugin, type Editor } from 'ckeditor5/src/core'; +import { logWarning, CKEditorError, type Locale, type Observable } from 'ckeditor5/src/utils'; import { - ButtonView, - SplitButtonView, - DropdownButtonView, - CollapsibleView, createDropdown, + SplitButtonView, + type ButtonView, + type DropdownButtonView, type DropdownView, type FocusableView } from 'ckeditor5/src/ui'; import ImageInsertFormView from './ui/imageinsertformview'; -import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; import type ImageUtils from '../imageutils'; -import type InsertImageCommand from '../image/insertimagecommand'; -import ImageInsertUrlView, { - type ImageInsertUrlViewCancelEvent, - type ImageInsertUrlViewSubmitEvent -} from './ui/imageinserturlview'; /** * The image insert dropdown plugin. @@ -62,6 +55,19 @@ export default class ImageInsertUI extends Plugin { */ private _integrations = new Map(); + /** + * @inheritDoc + */ + constructor( editor: Editor ) { + super( editor ); + + editor.config.define( 'image.insert.integrations', [ + 'upload', + 'assetManager', + 'url' + ] ); + } + /** * @inheritDoc */ @@ -75,15 +81,6 @@ export default class ImageInsertUI extends Plugin { editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); - const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; - - this.registerIntegration( - 'url', - insertImageCommand, - ( type, isOnlyOne ) => type == 'formView' ? this._createInsertUrlView( isOnlyOne ) : this._createInsertUrlButton(), - { requiresForm: true } - ); - this.listenTo( editor.model.document, 'change', () => { const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); const element = this.editor.model.document.selection.getSelectedElement(); @@ -95,12 +92,19 @@ export default class ImageInsertUI extends Plugin { /** * TODO */ - public registerIntegration( - name: string, - observable: Observable & { isEnabled: boolean }, - callback: IntegrationCallback, - options: { requiresForm?: boolean } = {} - ): void { + public registerIntegration( { + name, + observable, + buttonViewCreator, + formViewCreator, + requiresForm + }: { + name: string; + observable: Observable & { isEnabled: boolean }; + buttonViewCreator: IntegrationCallback; + formViewCreator: IntegrationCallback; + requiresForm?: boolean; +} ): void { if ( this._integrations.has( name ) ) { /** * TODO @@ -108,7 +112,12 @@ export default class ImageInsertUI extends Plugin { logWarning( 'image-insert-zzzzz', { name } ); } - this._integrations.set( name, { ...options, observable, callback } ); + this._integrations.set( name, { + observable, + buttonViewCreator, + formViewCreator, + requiresForm + } ); } /** @@ -126,12 +135,12 @@ export default class ImageInsertUI extends Plugin { if ( integrations.length == 1 ) { // Do not use dropdown for a single integration button (integration that does not require form view). if ( !firstIntegration.requiresForm ) { - return firstIntegration.callback( 'toolbarButton', true ); + return firstIntegration.buttonViewCreator( true ); } - dropdownButton = this._createInsertUrlButton( DropdownButtonView ); + dropdownButton = firstIntegration.buttonViewCreator( true ) as DropdownButtonView; } else { - const actionButton = firstIntegration.callback( 'toolbarButton', false ) as ButtonView & FocusableView; + const actionButton = firstIntegration.buttonViewCreator( false ) as ButtonView & FocusableView; dropdownButton = new SplitButtonView( locale, actionButton ); dropdownButton.tooltip = true; @@ -150,7 +159,7 @@ export default class ImageInsertUI extends Plugin { ) ); dropdownView.once( 'change:isOpen', () => { - const integrationViews = integrations.map( ( { callback } ) => callback( 'formView', integrations.length == 1 ) ); + const integrationViews = integrations.map( ( { formViewCreator } ) => formViewCreator( integrations.length == 1 ) ); const imageInsertFormView = new ImageInsertFormView( editor.locale, integrationViews ); dropdownView.panelView.children.add( imageInsertFormView ); @@ -183,129 +192,25 @@ export default class ImageInsertUI extends Plugin { } if ( !result.length ) { - result.push( this._integrations.get( 'url' )! ); - /** * TODO + * @error image-insert-aaaa */ - logWarning( 'image-insert-aaaa' ); + throw new CKEditorError( 'image-insert-aaaa' ); } return result; } - - /** - * TODO - */ - private _createInsertUrlView( isOnlyOne: boolean ): FocusableView { - const editor = this.editor; - const locale = editor.locale; - const t = locale.t; - - const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!; - const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; - - const imageInsertUrlView = new ImageInsertUrlView( locale ); - const collapsibleView = isOnlyOne ? null : new CollapsibleView( locale, [ imageInsertUrlView ] ); - - imageInsertUrlView.bind( 'isImageSelected' ).to( this ); - imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( - isEnabled.some( isCommandEnabled => isCommandEnabled ) - ) ); - - // Set initial value because integrations are created on first dropdown open. - imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; - - this.dropdownView!.on( 'change:isOpen', () => { - if ( this.dropdownView!.isOpen ) { - // Make sure that each time the panel shows up, the URL field remains in sync with the value of - // the command. If the user typed in the input, then canceled and re-opened it without changing - // the value of the media command (e.g. because they didn't change the selection), they would see - // the old value instead of the actual value of the command. - imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; - - if ( collapsibleView ) { - collapsibleView.isCollapsed = true; - } - } - - // Note: Use the low priority to make sure the following listener starts working after the - // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the - // invisible form/input cannot be focused/selected. - }, { priority: 'low' } ); - - imageInsertUrlView.on( 'submit', () => { - if ( replaceImageSourceCommand.isEnabled ) { - editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); - } else { - editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); - } - - this._closePanel(); - } ); - - imageInsertUrlView.on( 'cancel', () => this._closePanel() ); - - if ( collapsibleView ) { - collapsibleView.set( { - isCollapsed: true - } ); - - collapsibleView.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Update image URL' ) : - t( 'Insert image via URL' ) - ); - - return collapsibleView; - } - - return imageInsertUrlView; - } - - private _createInsertUrlButton( - ButtonClass: new ( locale?: Locale ) => T - ): T; - private _createInsertUrlButton(): ButtonView; - - /** - * TODO - */ - private _createInsertUrlButton( - ButtonClass: new ( locale?: Locale ) => ButtonView = ButtonView - ): ButtonView { - const editor = this.editor; - const button = new ButtonClass( editor.locale ); - const t = editor.locale.t; - - button.set( { - icon: icons.imageUrl, - tooltip: true - } ); - - button.bind( 'label' ).to( this, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Update image URL' ) : - t( 'Insert image via URL' ) - ); - - return button; - } - - /** - * TODO - */ - private _closePanel(): void { - this.editor.editing.view.focus(); - this.dropdownView!.isOpen = false; - } } /** * TODO */ -export type IntegrationCallback = ( type: 'toolbarButton' | 'formView', isOnlyOne: boolean ) => FocusableView; +export type IntegrationCallback = ( isOnlyOne: boolean ) => FocusableView; type IntegrationData = { observable: Observable & { isEnabled: boolean }; - callback: IntegrationCallback; + buttonViewCreator: IntegrationCallback; + formViewCreator: IntegrationCallback; requiresForm?: boolean; }; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts new file mode 100644 index 00000000000..2fdf5fa84d6 --- /dev/null +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts @@ -0,0 +1,152 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module image/imageinsert/imageinsertviaurlui + */ + +import { icons, Plugin } from 'ckeditor5/src/core'; +import { ButtonView, CollapsibleView, DropdownButtonView, type FocusableView } from 'ckeditor5/src/ui'; + +import ImageInsertUI from './imageinsertui'; +import type InsertImageCommand from '../image/insertimagecommand'; +import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; +import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent } from './ui/imageinserturlview'; + +/** + * TODO + */ +export default class ImageInsertViaUrlUI extends Plugin { + private _imageInsertUI!: ImageInsertUI; + + /** + * @inheritDoc + */ + public static get pluginName() { + return 'ImageInsertViaUrlUI' as const; + } + + /** + * @inheritDoc + */ + public static get requires() { + return [ ImageInsertUI ] as const; + } + + /** + * @inheritDoc + */ + public init(): void { + this._imageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); + const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; + + this._imageInsertUI.registerIntegration( { + name: 'url', + observable: insertImageCommand, + requiresForm: true, + buttonViewCreator: isOnlyOne => this._createInsertUrlButton( isOnlyOne ), + formViewCreator: isOnlyOne => this._createInsertUrlView( isOnlyOne ) + } ); + } + + /** + * TODO + */ + private _createInsertUrlView( isOnlyOne: boolean ): FocusableView { + const editor = this.editor; + const locale = editor.locale; + const t = locale.t; + + const replaceImageSourceCommand: ReplaceImageSourceCommand = editor.commands.get( 'replaceImageSource' )!; + const insertImageCommand: InsertImageCommand = editor.commands.get( 'insertImage' )!; + + const imageInsertUrlView = new ImageInsertUrlView( locale ); + const collapsibleView = isOnlyOne ? null : new CollapsibleView( locale, [ imageInsertUrlView ] ); + + imageInsertUrlView.bind( 'isImageSelected' ).to( this._imageInsertUI ); + imageInsertUrlView.bind( 'isEnabled' ).toMany( [ insertImageCommand, replaceImageSourceCommand ], 'isEnabled', ( ...isEnabled ) => ( + isEnabled.some( isCommandEnabled => isCommandEnabled ) + ) ); + + // Set initial value because integrations are created on first dropdown open. + imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; + + this._imageInsertUI.dropdownView!.on( 'change:isOpen', () => { + if ( this._imageInsertUI.dropdownView!.isOpen ) { + // Make sure that each time the panel shows up, the URL field remains in sync with the value of + // the command. If the user typed in the input, then canceled and re-opened it without changing + // the value of the media command (e.g. because they didn't change the selection), they would see + // the old value instead of the actual value of the command. + imageInsertUrlView.imageURLInputValue = replaceImageSourceCommand.value || ''; + + if ( collapsibleView ) { + collapsibleView.isCollapsed = true; + } + } + + // Note: Use the low priority to make sure the following listener starts working after the + // default action of the drop-down is executed (i.e. the panel showed up). Otherwise, the + // invisible form/input cannot be focused/selected. + }, { priority: 'low' } ); + + imageInsertUrlView.on( 'submit', () => { + if ( replaceImageSourceCommand.isEnabled ) { + editor.execute( 'replaceImageSource', { source: imageInsertUrlView.imageURLInputValue } ); + } else { + editor.execute( 'insertImage', { source: imageInsertUrlView.imageURLInputValue } ); + } + + this._closePanel(); + } ); + + imageInsertUrlView.on( 'cancel', () => this._closePanel() ); + + if ( collapsibleView ) { + collapsibleView.set( { + isCollapsed: true + } ); + + collapsibleView.bind( 'label' ).to( this._imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Update image URL' ) : + t( 'Insert image via URL' ) + ); + + return collapsibleView; + } + + return imageInsertUrlView; + } + + /** + * TODO + */ + private _createInsertUrlButton( isOnlyOne: boolean ): ButtonView { + const ButtonClass = isOnlyOne ? DropdownButtonView : ButtonView; + + const editor = this.editor; + const button = new ButtonClass( editor.locale ); + const t = editor.locale.t; + + button.set( { + icon: icons.imageUrl, + tooltip: true + } ); + + button.bind( 'label' ).to( this._imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Update image URL' ) : + t( 'Insert image via URL' ) + ); + + return button; + } + + /** + * TODO + */ + private _closePanel(): void { + this.editor.editing.view.focus(); + this._imageInsertUI.dropdownView!.isOpen = false; + } +} diff --git a/packages/ckeditor5-image/src/imageinsertviaurl.ts b/packages/ckeditor5-image/src/imageinsertviaurl.ts index 51d68c31e15..98d96567a84 100644 --- a/packages/ckeditor5-image/src/imageinsertviaurl.ts +++ b/packages/ckeditor5-image/src/imageinsertviaurl.ts @@ -7,8 +7,9 @@ * @module image/imageinsertviaurl */ -import { Plugin, type Editor } from 'ckeditor5/src/core'; +import { Plugin } from 'ckeditor5/src/core'; import ImageInsertUI from './imageinsert/imageinsertui'; +import ImageInsertViaUrlUI from './imageinsert/imageinsertviaurlui'; /** * The image insert via URL plugin. @@ -33,19 +34,6 @@ export default class ImageInsertViaUrl extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageInsertUI ] as const; - } - - /** - * @inheritDoc - */ - constructor( editor: Editor ) { - super( editor ); - - editor.config.define( 'image.insert.integrations', [ - 'upload', - 'assetManager', - 'url' - ] ); + return [ ImageInsertViaUrlUI, ImageInsertUI ] as const; } } diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index 3af2be9885d..f24706559bb 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -77,14 +77,27 @@ export default class ImageUploadUI extends Plugin { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); const command: UploadImageCommand = editor.commands.get( 'uploadImage' )!; - imageInsertUI.registerIntegration( 'upload', command, type => { - const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; + imageInsertUI.registerIntegration( { + name: 'upload', + observable: command, - uploadImageButton.icon = icons.imageUpload; + buttonViewCreator: () => { + const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; - if ( type == 'formView' ) { - uploadImageButton.withText = true; + uploadImageButton.icon = icons.imageUpload; + uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? + t( 'Replace image from computer' ) : + t( 'Upload image from computer' ) + ); + + return uploadImageButton; + }, + + formViewCreator: () => { + const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; + uploadImageButton.icon = icons.imageUpload; + uploadImageButton.withText = true; uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace from computer' ) : t( 'Upload from computer' ) @@ -93,14 +106,9 @@ export default class ImageUploadUI extends Plugin { uploadImageButton.on( 'execute', () => { imageInsertUI.dropdownView!.isOpen = false; } ); - } else { - uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? - t( 'Replace image from computer' ) : - t( 'Upload image from computer' ) - ); - } - return uploadImageButton; + return uploadImageButton; + } } ); } } diff --git a/packages/ckeditor5-ui/src/index.ts b/packages/ckeditor5-ui/src/index.ts index d4b2cb4034e..3f7e5f6a05d 100644 --- a/packages/ckeditor5-ui/src/index.ts +++ b/packages/ckeditor5-ui/src/index.ts @@ -16,7 +16,7 @@ export { default as addKeyboardHandlingForGrid } from './bindings/addkeyboardhan export { default as BodyCollection } from './editorui/bodycollection'; export { type ButtonExecuteEvent } from './button/button'; -export { type default as ButtonLabel } from './button/buttonlabel'; +export type { default as ButtonLabel } from './button/buttonlabel'; export { default as ButtonView } from './button/buttonview'; export { default as ButtonLabelView } from './button/buttonlabelview'; export { default as SwitchButtonView } from './button/switchbuttonview'; From 454efb490c3a8e3410667c4c7a441fd3f268690b Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 27 Nov 2023 18:43:56 +0100 Subject: [PATCH 31/53] Code cleaning. --- .../ckboximageedit/ckboximageeditcommand.js | 27 ++++++++ packages/ckeditor5-image/src/imageconfig.ts | 1 - .../src/imageinsert/imageinsertui.ts | 67 +++++++++++++------ .../src/imageinsert/imageinsertviaurlui.ts | 13 ++-- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js b/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js index f60b795ab9d..c52a3120e85 100644 --- a/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js +++ b/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js @@ -284,6 +284,33 @@ describe( 'CKBoxImageEditCommand', () => { sinon.assert.calledOnce( focusSpy ); } ); + + it( 'should refresh the command after closing the CKBox Image Editor dialog', () => { + const ckboxImageId = 'example-id'; + + setModelData( model, + `[]` + ); + + const imageElement = editor.model.document.selection.getSelectedElement(); + + const onClose = command._prepareOptions( { + element: imageElement, + ckboxImageId, + controller: new AbortController() + } ).onClose; + + const refreshSpy = testUtils.sinon.spy( command, 'refresh' ); + + expect( command.value ).to.be.false; + + command.execute(); + expect( command.value ).to.be.true; + + onClose(); + expect( command.value ).to.be.false; + sinon.assert.calledOnce( refreshSpy ); + } ); } ); describe( 'saving edited asset', () => { diff --git a/packages/ckeditor5-image/src/imageconfig.ts b/packages/ckeditor5-image/src/imageconfig.ts index aeedee6a6a9..f492b274a5a 100644 --- a/packages/ckeditor5-image/src/imageconfig.ts +++ b/packages/ckeditor5-image/src/imageconfig.ts @@ -475,7 +475,6 @@ export interface ImageInsertConfig { * }; * ``` * - * @internal * @default [ 'upload', 'assetManager', 'url' ] */ integrations?: Array; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 5711f8e904a..321c7fdd8c5 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -7,8 +7,16 @@ * @module image/imageinsert/imageinsertui */ -import { Plugin, type Editor } from 'ckeditor5/src/core'; -import { logWarning, CKEditorError, type Locale, type Observable } from 'ckeditor5/src/utils'; +import { + Plugin, + type Editor +} from 'ckeditor5/src/core'; +import { + CKEditorError, + logWarning, + type Locale, + type Observable +} from 'ckeditor5/src/utils'; import { createDropdown, SplitButtonView, @@ -44,14 +52,14 @@ export default class ImageInsertUI extends Plugin { public dropdownView?: DropdownView; /** - * TODO + * Observable property used to alter labels while some image is selected and when it is not. * * @observable */ declare public isImageSelected: boolean; /** - * TODO + * Registered integrations map. */ private _integrations = new Map(); @@ -90,7 +98,7 @@ export default class ImageInsertUI extends Plugin { } /** - * TODO + * Registers the insert image dropdown integration. */ public registerIntegration( { name, @@ -101,15 +109,19 @@ export default class ImageInsertUI extends Plugin { }: { name: string; observable: Observable & { isEnabled: boolean }; - buttonViewCreator: IntegrationCallback; - formViewCreator: IntegrationCallback; + buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; + formViewCreator: ( isOnlyOne: boolean ) => FocusableView; requiresForm?: boolean; } ): void { if ( this._integrations.has( name ) ) { /** - * TODO + * There are two insert image integrations registered with the same name. + * + * // TODO add more details. + * + * @error image-insert-integration-exists */ - logWarning( 'image-insert-zzzzz', { name } ); + logWarning( 'image-insert-integration-exists', { name } ); } this._integrations.set( name, { @@ -169,20 +181,33 @@ export default class ImageInsertUI extends Plugin { } /** - * TODO + * Validates the integrations list. */ private _prepareIntegrations(): Array { const editor = this.editor; const items = editor.config.get( 'image.insert.integrations' )!; const result: Array = []; + if ( !items.length ) { + /** + * The insert image feature requires a list of integrations to be provided in the editor configuration. + * + * // TODO add details. + * + * @error image-insert-not-specified-integrations + */ + throw new CKEditorError( 'image-insert-not-specified-integrations' ); + } + for ( const item of items ) { if ( !this._integrations.has( item ) ) { if ( ![ 'upload', 'assetManager', 'url' ].includes( item ) ) { /** - * TODO + * The specified insert image integration name is unknown or the providing plugin is not loaded in the editor. + * + * @error image-insert-unknown-integration */ - logWarning( 'image-insert-zzzzzz', { item } ); + logWarning( 'image-insert-unknown-integration', { item } ); } continue; @@ -193,24 +218,22 @@ export default class ImageInsertUI extends Plugin { if ( !result.length ) { /** - * TODO - * @error image-insert-aaaa + * The image insert feature requires integrations to be registered by separate features. + * + * // TODO add details. + * + * @error image-insert-not-registered-integrations */ - throw new CKEditorError( 'image-insert-aaaa' ); + throw new CKEditorError( 'image-insert-not-registered-integrations' ); } return result; } } -/** - * TODO - */ -export type IntegrationCallback = ( isOnlyOne: boolean ) => FocusableView; - type IntegrationData = { observable: Observable & { isEnabled: boolean }; - buttonViewCreator: IntegrationCallback; - formViewCreator: IntegrationCallback; + buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; + formViewCreator: ( isOnlyOne: boolean ) => FocusableView; requiresForm?: boolean; }; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts index 2fdf5fa84d6..272c6fe02c9 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts @@ -16,7 +16,12 @@ import type ReplaceImageSourceCommand from '../image/replaceimagesourcecommand'; import ImageInsertUrlView, { type ImageInsertUrlViewCancelEvent, type ImageInsertUrlViewSubmitEvent } from './ui/imageinserturlview'; /** - * TODO + * The image insert via URL plugin (UI part). + * + * For a detailed overview, check the {@glink features/images/images-inserting + * Insert images via source URL} documentation. + * + * This plugin registers the {@link module:image/imageinsert/imageinsertui~ImageInsertUI} integration for `url`. */ export default class ImageInsertViaUrlUI extends Plugin { private _imageInsertUI!: ImageInsertUI; @@ -52,7 +57,7 @@ export default class ImageInsertViaUrlUI extends Plugin { } /** - * TODO + * Creates the view displayed in the dropdown. */ private _createInsertUrlView( isOnlyOne: boolean ): FocusableView { const editor = this.editor; @@ -120,7 +125,7 @@ export default class ImageInsertViaUrlUI extends Plugin { } /** - * TODO + * Creates the toolbar button. */ private _createInsertUrlButton( isOnlyOne: boolean ): ButtonView { const ButtonClass = isOnlyOne ? DropdownButtonView : ButtonView; @@ -143,7 +148,7 @@ export default class ImageInsertViaUrlUI extends Plugin { } /** - * TODO + * Closes the dropdown. */ private _closePanel(): void { this.editor.editing.view.focus(); From c9ca9956aa161a4416b3716d611a0ce67f561e32 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 29 Nov 2023 09:10:34 +0100 Subject: [PATCH 32/53] The ImageInsertUI enabled by default. --- packages/ckeditor5-image/src/imageblock.ts | 3 ++- packages/ckeditor5-image/src/imageinline.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-image/src/imageblock.ts b/packages/ckeditor5-image/src/imageblock.ts index 2a1cc3a777a..70a9304371e 100644 --- a/packages/ckeditor5-image/src/imageblock.ts +++ b/packages/ckeditor5-image/src/imageblock.ts @@ -12,6 +12,7 @@ import { Widget } from 'ckeditor5/src/widget'; import ImageTextAlternative from './imagetextalternative'; import ImageBlockEditing from './image/imageblockediting'; +import ImageInsertUI from './imageinsert/imageinsertui'; import '../theme/image.css'; @@ -31,7 +32,7 @@ export default class ImageBlock extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageBlockEditing, Widget, ImageTextAlternative ] as const; + return [ ImageBlockEditing, Widget, ImageTextAlternative, ImageInsertUI ] as const; } /** diff --git a/packages/ckeditor5-image/src/imageinline.ts b/packages/ckeditor5-image/src/imageinline.ts index 0b74c64f2b3..9d6f2cc1010 100644 --- a/packages/ckeditor5-image/src/imageinline.ts +++ b/packages/ckeditor5-image/src/imageinline.ts @@ -12,6 +12,7 @@ import { Widget } from 'ckeditor5/src/widget'; import ImageTextAlternative from './imagetextalternative'; import ImageInlineEditing from './image/imageinlineediting'; +import ImageInsertUI from './imageinsert/imageinsertui'; import '../theme/image.css'; @@ -31,7 +32,7 @@ export default class ImageInline extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageInlineEditing, Widget, ImageTextAlternative ] as const; + return [ ImageInlineEditing, Widget, ImageTextAlternative, ImageInsertUI ] as const; } /** From 31e00a30c30cf4b795530c5d415bd71c03f709e5 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 30 Nov 2023 16:28:20 +0100 Subject: [PATCH 33/53] Tuning warnings. --- .../src/imageinsert/imageinsertui.ts | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 321c7fdd8c5..911ba38d311 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -12,7 +12,6 @@ import { type Editor } from 'ckeditor5/src/core'; import { - CKEditorError, logWarning, type Locale, type Observable @@ -115,9 +114,9 @@ export default class ImageInsertUI extends Plugin { } ): void { if ( this._integrations.has( name ) ) { /** - * There are two insert image integrations registered with the same name. + * There are two insert-image integrations registered with the same name. * - * // TODO add more details. + * Make sure that you do not load multiple asset manager plugins. * * @error image-insert-integration-exists */ @@ -141,6 +140,10 @@ export default class ImageInsertUI extends Plugin { const integrations = this._prepareIntegrations(); + if ( !integrations.length ) { + return null as any; + } + let dropdownButton: SplitButtonView | DropdownButtonView | undefined; const firstIntegration = integrations[ 0 ]; @@ -192,11 +195,15 @@ export default class ImageInsertUI extends Plugin { /** * The insert image feature requires a list of integrations to be provided in the editor configuration. * - * // TODO add details. + * The default list of integrations is `upload`, `assetManager`, `url`. Those integrations are included + * in the insert image dropdown if the given feature plugin is loaded. You should omit the `integrations` + * configuration key to use the default set or provide a selected list of integrations that should be used. * - * @error image-insert-not-specified-integrations + * @error image-insert-integrations-not-specified */ - throw new CKEditorError( 'image-insert-not-specified-integrations' ); + logWarning( 'image-insert-integrations-not-specified' ); + + return result; } for ( const item of items ) { @@ -220,11 +227,15 @@ export default class ImageInsertUI extends Plugin { /** * The image insert feature requires integrations to be registered by separate features. * - * // TODO add details. + * The `insertImage` toolbar button requires integrations to be registered by other features. + * For example {@link module:image/imageupload~ImageUpload ImageUpload}, + * {@link module:image/imageinsert~ImageInsert ImageInsert}, + * {@link module:image/imageinsertviaurl~ImageInsertViaUrl ImageInsertViaUrl}, + * {@link module:ckbox/ckbox~CKBox CKBox} * - * @error image-insert-not-registered-integrations + * @error image-insert-integrations-not-registered */ - throw new CKEditorError( 'image-insert-not-registered-integrations' ); + logWarning( 'image-insert-integrations-not-registered' ); } return result; From dd27ecd8c9c23e0b505ee34adba761b7432cd251 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 30 Nov 2023 16:51:17 +0100 Subject: [PATCH 34/53] Added docs. --- .../src/imageinsert/ui/imageinsertformview.ts | 4 ++-- .../src/imageinsert/ui/imageinserturlview.ts | 14 +++++++------- .../ui/textalternativeformview.ts | 4 ++-- .../src/collapsible/collapsibleview.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts index 5536a180818..c8aeee49ab7 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinsertformview.ts @@ -21,9 +21,9 @@ import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils import '../../../theme/imageinsert.css'; /** - * TODO + * The view displayed in the insert image dropdown. * - * See {@link module:image/imageinsert/ui/imageinsertformview~ImageInsertFormView}. + * See {@link module:image/imageinsert/imageinsertui~ImageInsertUI}. */ export default class ImageInsertFormView extends View { /** diff --git a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts index b1847ae6f06..a755de9e085 100644 --- a/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts +++ b/packages/ckeditor5-image/src/imageinsert/ui/imageinserturlview.ts @@ -20,13 +20,13 @@ import { import { FocusTracker, KeystrokeHandler, type Locale } from 'ckeditor5/src/utils'; /** - * The insert an image via URL view controller class. + * The insert an image via URL view. * - * See {@link module:image/imageinsert/ui/imageinsertformview~ImageInsertFormView}. + * See {@link module:image/imageinsert/imageinsertviaurlui~ImageInsertViaUrlUI}. */ export default class ImageInsertUrlView extends View { /** - * TODO + * The URL input field view. */ public urlInputView: LabeledFieldView; @@ -48,14 +48,14 @@ export default class ImageInsertUrlView extends View { declare public imageURLInputValue: string; /** - * TODO + * Observable property used to alter labels while some image is selected and when it is not. * * @observable */ declare public isImageSelected: boolean; /** - * TODO + * Observable property indicating whether the form interactive elements should be enabled. * * @observable */ @@ -258,7 +258,7 @@ export default class ImageInsertUrlView extends View { } /** - * TODO + * Fired when the form view is submitted. * * @eventName ~ImageInsertUrlView#submit */ @@ -268,7 +268,7 @@ export type ImageInsertUrlViewSubmitEvent = { }; /** - * TODO + * Fired when the form view is canceled. * * @eventName ~ImageInsertUrlView#cancel */ diff --git a/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts b/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts index 5d48b710078..d2f17a24399 100644 --- a/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts +++ b/packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.ts @@ -197,7 +197,7 @@ export default class TextAlternativeFormView extends View { } /** - * TODO + * Fired when the form view is submitted. * * @eventName ~TextAlternativeFormView#submit */ @@ -207,7 +207,7 @@ export type TextAlternativeFormViewSubmitEvent = { }; /** - * TODO + * Fired when the form view is canceled. * * @eventName ~TextAlternativeFormView#cancel */ diff --git a/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts b/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts index 4562b430bd9..96177ae6a19 100644 --- a/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts +++ b/packages/ckeditor5-ui/src/collapsible/collapsibleview.ts @@ -120,7 +120,7 @@ export default class CollapsibleView extends View { } /** - * TODO + * Focuses the first focusable. */ public focus(): void { this.buttonView.focus(); From 33d131dfc83cf2201a130921c4812573ff199020 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 30 Nov 2023 17:50:55 +0100 Subject: [PATCH 35/53] Updating tests. --- .../src/imageinsert/imageinsertui.ts | 9 +- .../src/imageupload/imageuploadui.ts | 5 +- .../imageinsert/ui/imageinsertformrowview.js | 100 ------------- .../tests/imageinsert/utils.js | 140 ------------------ .../listproperties/ui/listpropertiesview.js | 2 +- .../tests/collapsible}/collapsibleview.js | 17 ++- .../src/ui/filedialogbuttonview.ts | 11 -- .../tests/ui/filedialogbuttonview.js | 17 +-- 8 files changed, 27 insertions(+), 274 deletions(-) delete mode 100644 packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js delete mode 100644 packages/ckeditor5-image/tests/imageinsert/utils.js rename packages/{ckeditor5-list/tests/listproperties/ui => ckeditor5-ui/tests/collapsible}/collapsibleview.js (91%) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 911ba38d311..b26d49f7d36 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -26,7 +26,7 @@ import { } from 'ckeditor5/src/ui'; import ImageInsertFormView from './ui/imageinsertformview'; -import type ImageUtils from '../imageutils'; +import ImageUtils from '../imageutils'; /** * The image insert dropdown plugin. @@ -45,6 +45,13 @@ export default class ImageInsertUI extends Plugin { return 'ImageInsertUI' as const; } + /** + * @inheritDoc + */ + public static get requires() { + return [ ImageUtils ] as const; + } + /** * The dropdown view responsible for displaying the image insert UI. */ diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index f24706559bb..c0a41216085 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -45,10 +45,7 @@ export default class ImageUploadUI extends Plugin { view.set( { acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ), - allowMultipleFiles: true - } ); - - view.set( { + allowMultipleFiles: true, label: t( 'Upload image from computer' ), icon: icons.imageUpload, tooltip: true diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js deleted file mode 100644 index 2d52d391bda..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import View from '@ckeditor/ckeditor5-ui/src/view'; -import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview'; -import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; - -describe( 'ImageUploadFormRowView', () => { - let view, locale; - - beforeEach( () => { - locale = { t: val => val }; - view = new ImageUploadFormRowView( locale ); - view.render(); - } ); - - afterEach( () => { - view.element.remove(); - } ); - - describe( 'constructor()', () => { - it( 'should set view#locale', () => { - expect( view.locale ).to.equal( locale ); - } ); - - it( 'should create view#children collection', () => { - expect( view.children ).to.be.instanceOf( ViewCollection ); - expect( view.children ).to.have.length( 0 ); - } ); - - it( 'should set view#class', () => { - expect( view.class ).to.be.null; - } ); - - it( 'should set the template', () => { - expect( view.element.classList.contains( 'ck' ) ).to.be.true; - expect( view.element.classList.contains( 'ck-form__row' ) ).to.be.true; - } ); - - describe( 'options', () => { - it( 'should set view#class when class was passed', () => { - const view = new ImageUploadFormRowView( locale, { - class: 'foo' - } ); - - expect( view.class ).to.equal( 'foo' ); - - view.destroy(); - } ); - - it( 'should fill view#children when children were passed', () => { - const view = new ImageUploadFormRowView( locale, { - children: [ - new View() - ] - } ); - - expect( view.children ).to.have.length( 1 ); - - view.destroy(); - } ); - - it( 'should use a label view when passed', () => { - const labelView = new View(); - labelView.id = '123'; - - const view = new ImageUploadFormRowView( locale, { - labelView - } ); - - view.render(); - - expect( view.element.getAttribute( 'role' ) ).to.equal( 'group' ); - expect( view.element.getAttribute( 'aria-labelledby' ) ).to.equal( '123' ); - - view.destroy(); - } ); - } ); - - describe( 'template bindings', () => { - it( 'should bind #class to the template', () => { - view.class = 'foo'; - expect( view.element.classList.contains( 'foo' ) ).to.be.true; - } ); - - it( 'should bind #children to the template', () => { - const child = new View(); - child.setTemplate( { tag: 'div' } ); - - view.children.add( child ); - - expect( view.element.firstChild ).to.equal( child.element ); - - view.destroy(); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/utils.js b/packages/ckeditor5-image/tests/imageinsert/utils.js deleted file mode 100644 index 4bd56c46995..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/utils.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals document */ - -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; - -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import Image from '../../src/image'; -import ImageUploadUI from '../../src/imageinsert/imageinsertui'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Link from '@ckeditor/ckeditor5-link/src/link'; -import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder'; -import { prepareIntegrations, createLabeledInputView } from '../../src/imageinsert/utils'; -import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; - -describe( 'Upload utils', () => { - describe( 'prepareIntegrations()', () => { - it( 'should return "insetImageViaUrl" and "openCKFinder" integrations', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Link, - Image, - ImageUploadUI, - CKFinderUploadAdapter, - CKFinder - ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } - } - } ); - - const openCKFinderExtendedView = Object.values( prepareIntegrations( editor ) )[ 1 ]; - - expect( openCKFinderExtendedView.class ).contains( 'ck-image-insert__ck-finder-button' ); - expect( openCKFinderExtendedView.label ).to.equal( 'Insert image or file' ); - expect( openCKFinderExtendedView.withText ).to.be.true; - - editor.destroy(); - editorElement.remove(); - } ); - - it( 'should return only "insertImageViaUrl" integration and throw warning ' + - 'for "image-upload-integrations-invalid-view" error', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Image, - ImageUploadUI - ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } - } - } ); - - expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 ); - - editor.destroy(); - editorElement.remove(); - } ); - - it( 'should return only "link" integration', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Link, - Image, - ImageUploadUI - ], - image: { - insert: { - integrations: [ - 'link' - ] - } - } - } ); - - expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 ); - expect( Object.values( prepareIntegrations( editor ) )[ 0 ].label ).to.equal( 'Link' ); - expect( Object.values( prepareIntegrations( editor ) )[ 0 ] ).to.be.instanceOf( ButtonView ); - - editor.destroy(); - editorElement.remove(); - } ); - - it( 'should return "insertImageViaUrl" integration, when no integrations were configured', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Image, - ImageUploadUI - ] - } ); - - expect( Object.keys( prepareIntegrations( editor ) ).length ).to.equal( 1 ); - - editor.destroy(); - editorElement.remove(); - } ); - } ); - - describe( 'createLabeledInputView()', () => { - describe( 'image URL input view', () => { - it( 'should have placeholder', () => { - const view = createLabeledInputView( { t: val => val } ); - expect( view.fieldView.placeholder ).to.equal( 'https://example.com/image.png' ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js b/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js index ad933ab7472..17dc548218c 100644 --- a/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js +++ b/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js @@ -6,10 +6,10 @@ /* globals document */ import ListPropertiesView from '../../../src/listproperties/ui/listpropertiesview'; -import CollapsibleView from '../../../src/listproperties/ui/collapsibleview'; import { ButtonView, + CollapsibleView, FocusCycler, LabeledFieldView, SwitchButtonView, diff --git a/packages/ckeditor5-list/tests/listproperties/ui/collapsibleview.js b/packages/ckeditor5-ui/tests/collapsible/collapsibleview.js similarity index 91% rename from packages/ckeditor5-list/tests/listproperties/ui/collapsibleview.js rename to packages/ckeditor5-ui/tests/collapsible/collapsibleview.js index 2a9a53d1be8..688bdff7b48 100644 --- a/packages/ckeditor5-list/tests/listproperties/ui/collapsibleview.js +++ b/packages/ckeditor5-ui/tests/collapsible/collapsibleview.js @@ -3,10 +3,11 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import CollapsibleView from '../../../src/listproperties/ui/collapsibleview'; +import CollapsibleView from '../../src/collapsible/collapsibleview'; +import ButtonView from '../../src/button/buttonview'; -import { ButtonView, ViewCollection } from '@ckeditor/ckeditor5-ui'; -import dropdownArrowIcon from '@ckeditor/ckeditor5-ui/theme/icons/dropdown-arrow.svg'; +import dropdownArrowIcon from '../../theme/icons/dropdown-arrow.svg'; +import ViewCollection from '../../src/viewcollection'; describe( 'CollapsibleView', () => { let view, locale; @@ -81,6 +82,16 @@ describe( 'CollapsibleView', () => { } ); } ); + describe( 'focus()', () => { + it( 'focuses the button', () => { + const spy = sinon.spy( view.buttonView, 'focus' ); + + view.focus(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + describe( 'DOM bindings', () => { describe( 'button label', () => { it( 'should react on view#label', () => { diff --git a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts index 92dd6c7c061..a1d812aba0d 100644 --- a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts +++ b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts @@ -23,9 +23,6 @@ import type { Locale } from '@ckeditor/ckeditor5-utils'; * view.set( { * acceptedType: 'image/*', * allowMultipleFiles: true - * } ); - * - * view.buttonView.set( { * label: t( 'Insert image' ), * icon: imageIcon, * tooltip: true @@ -39,11 +36,6 @@ import type { Locale } from '@ckeditor/ckeditor5-utils'; * ``` */ export default class FileDialogButtonView extends ButtonView { - /** - * The button view of the component. - */ - public buttonView: ButtonView; - /** * A hidden `` view used to execute file dialog. */ @@ -72,9 +64,6 @@ export default class FileDialogButtonView extends ButtonView { constructor( locale?: Locale ) { super( locale ); - // TODO should we leave this for backward compatibility? - this.buttonView = this; - this._fileInputView = new FileInputView( locale ); this._fileInputView.bind( 'acceptedType' ).to( this ); this._fileInputView.bind( 'allowMultipleFiles' ).to( this ); diff --git a/packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js b/packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js index c69909eee45..2efe4478f7c 100644 --- a/packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js +++ b/packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js @@ -24,14 +24,13 @@ describe( 'FileDialogButtonView', () => { describe( 'child views', () => { describe( 'button view', () => { it( 'should be rendered', () => { - expect( view.buttonView ).to.instanceof( ButtonView ); - expect( view.buttonView ).to.equal( view.template.children[ 0 ] ); + expect( view ).to.instanceof( ButtonView ); } ); it( 'should open file dialog on execute', () => { const spy = sinon.spy( view._fileInputView, 'open' ); const stub = sinon.stub( view._fileInputView.element, 'click' ); - view.buttonView.fire( 'execute' ); + view.fire( 'execute' ); sinon.assert.calledOnce( spy ); stub.restore(); @@ -41,7 +40,7 @@ describe( 'FileDialogButtonView', () => { describe( 'file dialog', () => { it( 'should be rendered', () => { expect( view._fileInputView ).to.instanceof( View ); - expect( view._fileInputView ).to.equal( view.template.children[ 1 ] ); + expect( view._fileInputView ).to.equal( view.children.get( 1 ) ); } ); it( 'should be bound to view#acceptedType', () => { @@ -68,14 +67,4 @@ describe( 'FileDialogButtonView', () => { } ); } ); } ); - - describe( 'focus()', () => { - it( 'should focus view#buttonView', () => { - const spy = sinon.spy( view.buttonView, 'focus' ); - - view.focus(); - - sinon.assert.calledOnce( spy ); - } ); - } ); } ); From 0680c8fcb5f5b64f11b3541fdd7d1e01ccda48f9 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 30 Nov 2023 17:53:34 +0100 Subject: [PATCH 36/53] Updating tests. --- .../ckeditor5-image/tests/imageupload/imageuploadui.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js index ea08d8f86ef..5dafea2c95f 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js @@ -83,11 +83,11 @@ describe( 'ImageUploadUI', () => { command.isEnabled = true; - expect( button.buttonView.isEnabled ).to.true; + expect( button.isEnabled ).to.true; command.isEnabled = false; - expect( button.buttonView.isEnabled ).to.false; + expect( button.isEnabled ).to.false; } ); // ckeditor5-upload/#77 @@ -98,11 +98,11 @@ describe( 'ImageUploadUI', () => { button.render(); - button.buttonView.on( 'execute', spy ); + button.on( 'execute', spy ); command.isEnabled = false; - button.buttonView.element.dispatchEvent( new Event( 'click' ) ); + button.element.dispatchEvent( new Event( 'click' ) ); sinon.assert.notCalled( spy ); } ); From d09b0921adf032ff93436326ac4a7205bdc0fca4 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 14:06:44 +0100 Subject: [PATCH 37/53] Removed outdated config. --- .../ckeditor5-clipboard/tests/manual/dragdrop-blocks.js | 7 +------ .../ckeditor5-image/tests/imageinsert/imageinsertui.js | 9 +-------- packages/ckeditor5-image/tests/manual/imageblock.js | 8 +------- packages/ckeditor5-image/tests/manual/imageinline.js | 8 +------- packages/ckeditor5-image/tests/manual/imagetypetoggle.js | 8 +------- packages/ckeditor5-image/tests/manual/picture.js | 7 +------ .../ckeditor5-show-blocks/tests/manual/showblocks.js | 7 +------ packages/ckeditor5-style/tests/manual/styledropdown.js | 7 +------ packages/ckeditor5-ui/tests/manual/toolbar/nested.js | 7 +------ tests/manual/all-features-dll.js | 7 +------ tests/manual/all-features.js | 7 +------ tests/manual/memory/memory-semi-automated.js | 7 +------ 12 files changed, 12 insertions(+), 77 deletions(-) diff --git a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js index 15c9dceb0f5..469497bf194 100644 --- a/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js +++ b/packages/ckeditor5-clipboard/tests/manual/dragdrop-blocks.js @@ -133,12 +133,7 @@ ClassicEditor 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, placeholder: 'Type the content here!', mention: { diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js index 06a4ca7d5f6..3c57c9af51d 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js @@ -50,14 +50,7 @@ describe( 'ImageInsertUI', () => { editor = await ClassicEditor.create( editorElement, { plugins: [ Paragraph, Image, ImageInsert, FileRepository, UploadAdapterPluginMock, Clipboard ], - toolbar: [ 'insertImage' ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } - } + toolbar: [ 'insertImage' ] } ); dropdown = editor.ui.view.toolbar.children.first.children.first; diff --git a/packages/ckeditor5-image/tests/manual/imageblock.js b/packages/ckeditor5-image/tests/manual/imageblock.js index 4cbf2e36f0a..9b057a77b6c 100644 --- a/packages/ckeditor5-image/tests/manual/imageblock.js +++ b/packages/ckeditor5-image/tests/manual/imageblock.js @@ -65,13 +65,7 @@ ClassicEditor 'redo' ], image: { - toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } + toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, ckfinder: { // eslint-disable-next-line max-len diff --git a/packages/ckeditor5-image/tests/manual/imageinline.js b/packages/ckeditor5-image/tests/manual/imageinline.js index 83f50205013..3633477567e 100644 --- a/packages/ckeditor5-image/tests/manual/imageinline.js +++ b/packages/ckeditor5-image/tests/manual/imageinline.js @@ -67,13 +67,7 @@ ClassicEditor 'redo' ], image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', '|', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } + toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', '|', 'imageTextAlternative' ] }, ckfinder: { // eslint-disable-next-line max-len diff --git a/packages/ckeditor5-image/tests/manual/imagetypetoggle.js b/packages/ckeditor5-image/tests/manual/imagetypetoggle.js index c6e2a80c5a2..4e2601c3f92 100644 --- a/packages/ckeditor5-image/tests/manual/imagetypetoggle.js +++ b/packages/ckeditor5-image/tests/manual/imagetypetoggle.js @@ -69,13 +69,7 @@ ClassicEditor 'redo' ], image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } + toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, ckfinder: { // eslint-disable-next-line max-len diff --git a/packages/ckeditor5-image/tests/manual/picture.js b/packages/ckeditor5-image/tests/manual/picture.js index 4538ea87093..c8473361d5f 100644 --- a/packages/ckeditor5-image/tests/manual/picture.js +++ b/packages/ckeditor5-image/tests/manual/picture.js @@ -78,12 +78,7 @@ ClassicEditor 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] } } ) .then( editor => { diff --git a/packages/ckeditor5-show-blocks/tests/manual/showblocks.js b/packages/ckeditor5-show-blocks/tests/manual/showblocks.js index a3c16dff738..87464ff0c6d 100644 --- a/packages/ckeditor5-show-blocks/tests/manual/showblocks.js +++ b/packages/ckeditor5-show-blocks/tests/manual/showblocks.js @@ -178,12 +178,7 @@ ClassicEditor 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, list: { properties: { diff --git a/packages/ckeditor5-style/tests/manual/styledropdown.js b/packages/ckeditor5-style/tests/manual/styledropdown.js index c3e9b1167db..2f345e2246d 100644 --- a/packages/ckeditor5-style/tests/manual/styledropdown.js +++ b/packages/ckeditor5-style/tests/manual/styledropdown.js @@ -171,12 +171,7 @@ const config = { 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, placeholder: 'Type the content here!', mention: { diff --git a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js index fbf4c6a692a..3c61929cee2 100644 --- a/packages/ckeditor5-ui/tests/manual/toolbar/nested.js +++ b/packages/ckeditor5-ui/tests/manual/toolbar/nested.js @@ -158,12 +158,7 @@ ClassicEditor image: { toolbar: [ 'imageTextAlternative' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] } } ) .then( editor => { diff --git a/tests/manual/all-features-dll.js b/tests/manual/all-features-dll.js index 11362ea8329..2c40faaf64b 100644 --- a/tests/manual/all-features-dll.js +++ b/tests/manual/all-features-dll.js @@ -196,12 +196,7 @@ const config = { 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, placeholder: 'Type the content here!', mention: { diff --git a/tests/manual/all-features.js b/tests/manual/all-features.js index 17569304afe..3bc74817105 100644 --- a/tests/manual/all-features.js +++ b/tests/manual/all-features.js @@ -122,12 +122,7 @@ ClassicEditor 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, placeholder: 'Type the content here!', mention: { diff --git a/tests/manual/memory/memory-semi-automated.js b/tests/manual/memory/memory-semi-automated.js index bb9645aed8f..36f3a520fa2 100644 --- a/tests/manual/memory/memory-semi-automated.js +++ b/tests/manual/memory/memory-semi-automated.js @@ -124,12 +124,7 @@ function initEditor() { 'imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', 'resizeImage' - ], - insert: { - integrations: [ - 'insertImageViaUrl' - ] - } + ] }, placeholder: 'Type the content here!', mention: { From a514d8285b2a538db21b433df7dc56fe8d0af096 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 14:29:49 +0100 Subject: [PATCH 38/53] Added tests for ImageInline and ImageBlock plugins. --- packages/ckeditor5-image/tests/imageblock.js | 53 +++++++++++++++++++ packages/ckeditor5-image/tests/imageinline.js | 53 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/ckeditor5-image/tests/imageblock.js create mode 100644 packages/ckeditor5-image/tests/imageinline.js diff --git a/packages/ckeditor5-image/tests/imageblock.js b/packages/ckeditor5-image/tests/imageblock.js new file mode 100644 index 00000000000..0f492f7cfc0 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageblock.js @@ -0,0 +1,53 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; + +import ImageBlock from '../src/imageblock'; +import ImageBlockEditing from '../src/image/imageblockediting'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; +import ImageTextAlternative from '../src/imagetextalternative'; +import ImageInsertUI from '../src/imageinsert/imageinsertui'; + +describe( 'ImageBlock', () => { + let editorElement, editor; + + beforeEach( async () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ ImageBlock, Paragraph ] + } ); + } ); + + afterEach( async () => { + editorElement.remove(); + await editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( ImageBlock ) ).to.instanceOf( ImageBlock ); + expect( editor.plugins.get( 'ImageBlock' ) ).to.instanceOf( ImageBlock ); + } ); + + it( 'should load ImageBlockEditing plugin', () => { + expect( editor.plugins.get( ImageBlockEditing ) ).to.instanceOf( ImageBlockEditing ); + } ); + + it( 'should load Widget plugin', () => { + expect( editor.plugins.get( Widget ) ).to.instanceOf( Widget ); + } ); + + it( 'should load ImageTextAlternative plugin', () => { + expect( editor.plugins.get( ImageTextAlternative ) ).to.instanceOf( ImageTextAlternative ); + } ); + + it( 'should load ImageInsertUI plugin', () => { + expect( editor.plugins.get( ImageInsertUI ) ).to.instanceOf( ImageInsertUI ); + } ); +} ); diff --git a/packages/ckeditor5-image/tests/imageinline.js b/packages/ckeditor5-image/tests/imageinline.js new file mode 100644 index 00000000000..f29d06a346b --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinline.js @@ -0,0 +1,53 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; + +import ImageInline from '../src/imageinline'; +import ImageInlineEditing from '../src/image/imageinlineediting'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; +import ImageTextAlternative from '../src/imagetextalternative'; +import ImageInsertUI from '../src/imageinsert/imageinsertui'; + +describe( 'ImageInline', () => { + let editorElement, editor; + + beforeEach( async () => { + editorElement = global.document.createElement( 'div' ); + global.document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ ImageInline, Paragraph ] + } ); + } ); + + afterEach( async () => { + editorElement.remove(); + await editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( ImageInline ) ).to.instanceOf( ImageInline ); + expect( editor.plugins.get( 'ImageInline' ) ).to.instanceOf( ImageInline ); + } ); + + it( 'should load ImageInlineEditing plugin', () => { + expect( editor.plugins.get( ImageInlineEditing ) ).to.instanceOf( ImageInlineEditing ); + } ); + + it( 'should load Widget plugin', () => { + expect( editor.plugins.get( Widget ) ).to.instanceOf( Widget ); + } ); + + it( 'should load ImageTextAlternative plugin', () => { + expect( editor.plugins.get( ImageTextAlternative ) ).to.instanceOf( ImageTextAlternative ); + } ); + + it( 'should load ImageInsertUI plugin', () => { + expect( editor.plugins.get( ImageInsertUI ) ).to.instanceOf( ImageInsertUI ); + } ); +} ); From 7ad07ddfb9b57f63e446a8f3ffa743dd9338223e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 14:32:52 +0100 Subject: [PATCH 39/53] Add test for ImageInsertViaUrl plugin. --- packages/ckeditor5-image/tests/imageinsertviaurl.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-image/tests/imageinsertviaurl.js b/packages/ckeditor5-image/tests/imageinsertviaurl.js index 2f760789851..8b614080785 100644 --- a/packages/ckeditor5-image/tests/imageinsertviaurl.js +++ b/packages/ckeditor5-image/tests/imageinsertviaurl.js @@ -6,6 +6,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import ImageInsertUI from '../src/imageinsert/imageinsertui'; import ImageInsertViaUrl from '../src/imageinsertviaurl'; +import ImageInsertViaUrlUI from '../src/imageinsert/imageinsertviaurlui'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -23,7 +24,6 @@ describe( 'ImageInsertViaUrl', () => { afterEach( async () => { editorElement.remove(); - await editor.destroy(); } ); @@ -35,6 +35,10 @@ describe( 'ImageInsertViaUrl', () => { expect( editor.plugins.get( 'ImageInsertUI' ) ).to.instanceOf( ImageInsertUI ); } ); + it( 'should load ImageInsertViaUrlUI plugin', () => { + expect( editor.plugins.get( 'ImageInsertViaUrlUI' ) ).to.instanceOf( ImageInsertViaUrlUI ); + } ); + it( 'should not load ImageUpload plugin', () => { expect( editor.plugins.has( 'ImageUpload' ) ).to.be.false; } ); From dd89a6097b875955d7df2d699554dea77152d085 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 14:53:05 +0100 Subject: [PATCH 40/53] Added tests for SplitButtonView with a custom action button. --- .../tests/dropdown/button/splitbuttonview.js | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js b/packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js index b5e6c1dbd67..203fbd73419 100644 --- a/packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js +++ b/packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js @@ -284,6 +284,154 @@ describe( 'SplitButtonView', () => { } ); } ); + describe( 'custom actionView button', () => { + let customButton; + + class CustomButtonView extends ButtonView {} + + beforeEach( () => { + customButton = new CustomButtonView( locale ); + view = new SplitButtonView( locale, customButton ); + + view.render(); + } ); + + it( 'creates custom view#actionView', () => { + expect( view.actionView ).to.be.instanceOf( CustomButtonView ); + expect( view.actionView ).to.equal( customButton ); + expect( view.actionView.element.classList.contains( 'ck-splitbutton__action' ) ).to.be.true; + } ); + + it( 'does not adds isToggleable to view#actionView', () => { + expect( view.actionView.isToggleable ).to.be.false; + + view.isToggleable = true; + + expect( view.actionView.isToggleable ).to.be.false; + } ); + + it( 'creates view#arrowView', () => { + expect( view.arrowView ).to.be.instanceOf( ButtonView ); + expect( view.arrowView.element.classList.contains( 'ck-splitbutton__arrow' ) ).to.be.true; + expect( view.arrowView.element.attributes[ 'aria-haspopup' ].value ).to.equal( 'true' ); + expect( view.arrowView.icon ).to.be.not.undefined; + expect( view.arrowView.tooltip ).to.equal( view.tooltip ); + expect( view.arrowView.label ).to.equal( view.label ); + } ); + + it( 'creates element from template', () => { + expect( view.element.tagName ).to.equal( 'DIV' ); + expect( view.element.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.classList.contains( 'ck-splitbutton' ) ).to.be.true; + } ); + + it( 'binds #isVisible to the template', () => { + expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.false; + + view.isVisible = false; + + expect( view.element.classList.contains( 'ck-hidden' ) ).to.be.true; + + // There should be no binding to the action view. Only the entire split button should react. + expect( view.actionView.element.classList.contains( 'ck-hidden' ) ).to.be.false; + } ); + + describe( 'bindings', () => { + it( 'delegates actionView#execute to view#execute', () => { + const spy = sinon.spy(); + + view.on( 'execute', spy ); + + view.actionView.fire( 'execute' ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'does not bind actionView#icon to view', () => { + expect( view.actionView.icon ).to.be.undefined; + + view.icon = 'foo'; + + expect( view.actionView.icon ).to.be.undefined; + } ); + + it( 'does not bind actionView#isEnabled to view', () => { + expect( view.actionView.isEnabled ).to.be.true; + + view.isEnabled = false; + + expect( view.actionView.isEnabled ).to.be.true; + } ); + + it( 'does not bind actionView#label to view', () => { + expect( view.actionView.label ).to.be.undefined; + + view.label = 'foo'; + + expect( view.actionView.label ).to.be.undefined; + } ); + + it( 'delegates arrowView#execute to view#open', () => { + const spy = sinon.spy(); + + view.on( 'open', spy ); + + view.arrowView.fire( 'execute' ); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'binds arrowView#isEnabled to view', () => { + expect( view.arrowView.isEnabled ).to.be.true; + + view.isEnabled = false; + + expect( view.arrowView.isEnabled ).to.be.false; + } ); + + it( 'does not bind actionView#tabindex to view', () => { + expect( view.actionView.tabindex ).to.equal( -1 ); + + view.tabindex = 1; + + expect( view.actionView.tabindex ).to.equal( -1 ); + } ); + + // Makes little sense for split button but the Button interface specifies it, so let's support it. + it( 'does not bind actionView#type to view', () => { + expect( view.actionView.type ).to.equal( 'button' ); + + view.type = 'submit'; + + expect( view.actionView.type ).to.equal( 'button' ); + } ); + + it( 'does not bind actionView#withText to view', () => { + expect( view.actionView.withText ).to.be.false; + + view.withText = true; + + expect( view.actionView.withText ).to.be.false; + } ); + + it( 'does not bind actionView#tooltip to view', () => { + expect( view.actionView.tooltip ).to.be.false; + + view.tooltip = true; + + expect( view.actionView.tooltip ).to.be.false; + } ); + + it( 'does not bind actionView#tooltipPosition to view', () => { + expect( view.actionView.tooltipPosition ).to.equal( 's' ); + + view.tooltipPosition = 'n'; + + expect( view.actionView.tooltipPosition ).to.equal( 's' ); + } ); + } ); + } ); + describe( 'focus()', () => { it( 'focuses the actionButton', () => { const spy = sinon.spy( view.actionView, 'focus' ); From 9feae7f293d68559dda4fe0b81b8ddaacb5a207d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 14:58:08 +0100 Subject: [PATCH 41/53] Added test for passing a button instance to createDropdown() helper. --- packages/ckeditor5-ui/tests/dropdown/utils.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/ckeditor5-ui/tests/dropdown/utils.js b/packages/ckeditor5-ui/tests/dropdown/utils.js index 455c724d8ee..d1c088d6839 100644 --- a/packages/ckeditor5-ui/tests/dropdown/utils.js +++ b/packages/ckeditor5-ui/tests/dropdown/utils.js @@ -68,6 +68,15 @@ describe( 'utils', () => { expect( dropdownView.buttonView ).to.be.instanceOf( SplitButtonView ); } ); + it( 'creates dropdown#buttonView out of passed ButtonView instance', () => { + const buttonView = new SplitButtonView( locale ); + + dropdownView = createDropdown( locale, buttonView ); + + expect( dropdownView.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdownView.buttonView ).to.equal( buttonView ); + } ); + it( 'binds #isEnabled to the buttonView', () => { dropdownView = createDropdown( locale ); From 3b3a7cc27fd7556960d318a063da933b425366ea Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 16:41:51 +0100 Subject: [PATCH 42/53] Added tests for ImageInsertUrlView. --- .../imageinsert/ui/imageinserturlview.js | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js new file mode 100644 index 00000000000..a8ee56ed608 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js @@ -0,0 +1,377 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; + +import ImageInsertUrlView from '../../../src/imageinsert/ui/imageinserturlview'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; +import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; +import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; +import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview'; +import { icons } from '@ckeditor/ckeditor5-core'; + +describe( 'ImageInsertUrlView', () => { + let view; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + view = new ImageInsertUrlView( { t: val => val } ); + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + describe( 'constructor()', () => { + it( 'should have #imageURLInputValue', () => { + expect( view.imageURLInputValue ).to.equal( '' ); + } ); + + it( 'should have #isImageSelected', () => { + expect( view.isImageSelected ).to.be.false; + } ); + + it( 'should have #isEnabled', () => { + expect( view.isEnabled ).to.be.true; + } ); + + it( 'should create #focusTracker instance', () => { + expect( view.focusTracker ).to.be.instanceOf( FocusTracker ); + } ); + + it( 'should create #keystrokes instance', () => { + expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler ); + } ); + + it( 'should create #focusCycler instance', () => { + expect( view.focusCycler ).to.be.instanceOf( FocusCycler ); + } ); + + it( 'should create #_focusables view collection', () => { + expect( view._focusables ).to.be.instanceOf( ViewCollection ); + } ); + } ); + + describe( 'template', () => { + it( 'should create element from the template', () => { + expect( view.element.tagName ).to.equal( 'DIV' ); + expect( view.element.classList.contains( 'ck' ) ).to.true; + expect( view.element.classList.contains( 'ck-image-insert-url' ) ).to.true; + + const childNodes = view.element.childNodes; + + expect( childNodes[ 0 ] ).to.equal( view.urlInputView.element ); + expect( childNodes[ 1 ].tagName ).to.equal( 'DIV' ); + expect( childNodes[ 1 ].classList.contains( 'ck' ) ).to.be.true; + expect( childNodes[ 1 ].classList.contains( 'ck-image-insert-url__action-row' ) ).to.be.true; + + const childNodes2 = childNodes[ 1 ].childNodes; + + expect( childNodes2[ 0 ] ).to.equal( view.insertButtonView.element ); + expect( childNodes2[ 1 ] ).to.equal( view.cancelButtonView.element ); + } ); + + it( 'should use dedicated views', () => { + expect( view.template.children[ 0 ] ).to.equal( view.urlInputView ); + expect( view.template.children[ 1 ].children[ 0 ] ).to.equal( view.insertButtonView ); + expect( view.template.children[ 1 ].children[ 1 ] ).to.equal( view.cancelButtonView ); + } ); + } ); + + describe( 'render()', () => { + it( 'should register child views in #_focusables', () => { + expect( view._focusables.map( f => f ) ).to.have.members( [ + view.urlInputView, + view.insertButtonView, + view.cancelButtonView + ] ); + } ); + + it( 'should register child views\' #element in #focusTracker', () => { + const view = new ImageInsertUrlView( { t: () => {} } ); + + const spy = sinon.spy( view.focusTracker, 'add' ); + view.render(); + + sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element ); + sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element ); + sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element ); + + view.destroy(); + } ); + + describe( 'activates keyboard navigation for the toolbar', () => { + it( 'so "tab" focuses the next focusable item', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + // Mock the url input is focused. + view.focusTracker.isFocused = true; + view.focusTracker.focusedElement = view.urlInputView.element; + + const spy = sinon.spy( view.insertButtonView, 'focus' ); + + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'so "shift + tab" focuses the previous focusable item', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + // Mock the cancel button is focused. + view.focusTracker.isFocused = true; + view.focusTracker.focusedElement = view.cancelButtonView.element; + + const spy = sinon.spy( view.insertButtonView, 'focus' ); + + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + sinon.assert.calledOnce( spy ); + } ); + } ); + } ); + + describe( 'destroy()', () => { + it( 'should destroy the FocusTracker instance', () => { + const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + + it( 'should destroy the KeystrokeHandler instance', () => { + const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + } ); + + describe( 'focus()', () => { + it( 'should focus the url input', () => { + const spy = sinon.spy( view.urlInputView, 'focus' ); + + view.focus(); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should focus the last focusable', () => { + const spy = sinon.spy( view.cancelButtonView, 'focus' ); + + view.focus( -1 ); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( '#urlInputView', () => { + it( 'should be an instance of the LabeledFieldView', () => { + expect( view.urlInputView ).to.be.instanceOf( LabeledFieldView ); + } ); + + it( 'should accept text', () => { + expect( view.urlInputView.fieldView ).to.be.instanceOf( InputTextView ); + } ); + + it( 'should bind label to #isImageSelected', () => { + view.isImageSelected = false; + + expect( view.urlInputView.label ).to.equal( 'Insert image via URL' ); + + view.isImageSelected = true; + + expect( view.urlInputView.label ).to.equal( 'Update image URL' ); + } ); + + it( 'should bind isEnabled to #isEnabled', () => { + view.isEnabled = false; + + expect( view.urlInputView.isEnabled ).to.be.false; + + view.isEnabled = true; + + expect( view.urlInputView.isEnabled ).to.be.true; + } ); + + it( 'should set placeholder', () => { + expect( view.urlInputView.placeholder ).to.equal( 'https://example.com/image.png' ); + expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com/image.png' ); + } ); + + it( 'should bind value to #imageURLInputValue', () => { + view.imageURLInputValue = 'abc'; + + expect( view.urlInputView.fieldView.value ).to.equal( 'abc' ); + + view.imageURLInputValue = null; + + expect( view.urlInputView.fieldView.value ).to.equal( '' ); + } ); + + it( 'should be bound with #imageURLInputValue', () => { + view.urlInputView.fieldView.element.value = 'abc'; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'abc' ); + + view.urlInputView.fieldView.element.value = 'xyz'; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'xyz' ); + } ); + + it( 'should trim input value', () => { + view.urlInputView.fieldView.element.value = ' '; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( '' ); + + view.urlInputView.fieldView.element.value = ' test '; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'test' ); + } ); + } ); + + describe( '#insertButtonView', () => { + it( 'should be an instance of the ButtonView', () => { + expect( view.insertButtonView ).to.be.instanceOf( ButtonView ); + } ); + + it( 'should have an icon', () => { + expect( view.insertButtonView.icon ).to.equal( icons.check ); + } ); + + it( 'should have a class', () => { + expect( view.insertButtonView.class ).to.equal( 'ck-button-save' ); + } ); + + it( 'should be a submit button', () => { + expect( view.insertButtonView.type ).to.equal( 'submit' ); + } ); + + it( 'should have text', () => { + expect( view.insertButtonView.withText ).to.be.true; + } ); + + it( 'should bind label to #isImageSelected', () => { + view.isImageSelected = false; + + expect( view.insertButtonView.label ).to.equal( 'Insert' ); + + view.isImageSelected = true; + + expect( view.insertButtonView.label ).to.equal( 'Update' ); + } ); + + it( 'should bind isEnabled to #isEnabled and #imageURLInputValue', () => { + view.isEnabled = false; + view.imageURLInputValue = ''; + + expect( view.insertButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + view.imageURLInputValue = 'abc'; + + expect( view.insertButtonView.isEnabled ).to.be.true; + + view.isEnabled = false; + view.imageURLInputValue = 'abc'; + + expect( view.insertButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + view.imageURLInputValue = ''; + + expect( view.insertButtonView.isEnabled ).to.be.false; + } ); + + it( 'should fire "submit" event on insertButtonView#execute', () => { + const spy = sinon.spy(); + + view.on( 'submit', spy ); + + view.insertButtonView.fire( 'execute' ); + + expect( spy.calledOnce ).to.true; + } ); + } ); + + describe( '#cancelButtonView', () => { + it( 'should be an instance of the ButtonView', () => { + expect( view.cancelButtonView ).to.be.instanceOf( ButtonView ); + } ); + + it( 'should have an icon', () => { + expect( view.cancelButtonView.icon ).to.equal( icons.cancel ); + } ); + + it( 'should have a class', () => { + expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' ); + } ); + + it( 'should be a plain button', () => { + expect( view.cancelButtonView.type ).to.equal( 'button' ); + } ); + + it( 'should have text', () => { + expect( view.cancelButtonView.withText ).to.be.true; + } ); + + it( 'should have label', () => { + expect( view.cancelButtonView.label ).to.equal( 'Cancel' ); + } ); + + it( 'should bind isEnabled to #isEnabled', () => { + view.isEnabled = false; + + expect( view.cancelButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + + expect( view.cancelButtonView.isEnabled ).to.be.true; + + view.isEnabled = false; + + expect( view.cancelButtonView.isEnabled ).to.be.false; + } ); + + it( 'should fire "cancel" event on cancelButtonView#execute', () => { + const spy = sinon.spy(); + + view.on( 'cancel', spy ); + + view.cancelButtonView.fire( 'execute' ); + + expect( spy.calledOnce ).to.true; + } ); + } ); +} ); From aa28412d1b718f3cfb61fa9a02ac49d265896cc9 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Sat, 2 Dec 2023 19:38:31 +0100 Subject: [PATCH 43/53] Added tests for ImageInsertFormView. --- .../imageinsert/ui/imageinsertformview.js | 622 ++++++++++++++++++ .../imageinsert/ui/imageinsertpanelview.js | 329 --------- 2 files changed, 622 insertions(+), 329 deletions(-) create mode 100644 packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js delete mode 100644 packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js new file mode 100644 index 00000000000..641d109bee8 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js @@ -0,0 +1,622 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document, Event */ + +import ImageInsertFormView from '../../../src/imageinsert/ui/imageinsertformview'; +import ImageInsertUrlView from '../../../src/imageinsert/ui/imageinserturlview'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; +import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; +import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; +import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; +import CollapsibleView from '@ckeditor/ckeditor5-ui/src/collapsible/collapsibleview'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +describe( 'ImageInsertFormView', () => { + let view; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val } ); + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + describe( 'constructor()', () => { + it( 'should create #focusTracker instance', () => { + expect( view.focusTracker ).to.be.instanceOf( FocusTracker ); + } ); + + it( 'should create #keystrokes instance', () => { + expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler ); + } ); + + it( 'should create #_focusCycler instance', () => { + expect( view._focusCycler ).to.be.instanceOf( FocusCycler ); + } ); + + it( 'should create #_focusables view collection', () => { + expect( view._focusables ).to.be.instanceOf( ViewCollection ); + } ); + + it( 'should have #children view collection', () => { + expect( view.children ).to.be.instanceOf( ViewCollection ); + } ); + } ); + + describe( 'integrations', () => { + it( 'single integrations', () => { + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + + const view = new ImageInsertFormView( { t: val => val }, [ + inputIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + inputIntegrationView + ] ); + } ); + + it( 'multiple integrations', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + + const view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + inputIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + inputIntegrationView + ] ); + } ); + + it( 'integrations with collapsible view', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + const collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ + inputIntegrationView + ] ); + + const view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + collapsibleIntegrationView, + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + } ); + } ); + + describe( 'template', () => { + it( 'should create element from the template', () => { + expect( view.element.tagName ).to.equal( 'FORM' ); + expect( view.element.classList.contains( 'ck' ) ).to.true; + expect( view.element.classList.contains( 'ck-image-insert-form' ) ).to.true; + expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); + } ); + + it( 'should bind #children', () => { + expect( view.template.children[ 0 ] ).to.equal( view.children ); + } ); + } ); + + describe( 'render()', () => { + it( 'should handle and delegate DOM submit event', () => { + const spy = sinon.spy(); + + view.on( 'submit', spy ); + view.element.dispatchEvent( new Event( 'submit' ) ); + + expect( spy.calledOnce ).to.true; + } ); + + it( 'should register focusables in #focusTracker', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + const collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ + inputIntegrationView + ] ); + + const view = new ImageInsertFormView( { t: () => {} }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + const spy = sinon.spy( view.focusTracker, 'add' ); + + view.render(); + + sinon.assert.calledWithExactly( spy.getCall( 0 ), buttonIntegrationView.element ); + sinon.assert.calledWithExactly( spy.getCall( 1 ), collapsibleIntegrationView.element ); + sinon.assert.calledWithExactly( spy.getCall( 2 ), inputIntegrationView.element ); + sinon.assert.calledThrice( spy ); + + view.destroy(); + } ); + + describe( 'activates keyboard navigation', () => { + let view, firstIntegrationView, secondIntegrationView; + + beforeEach( () => { + firstIntegrationView = new ButtonView( { t: val => val } ); + secondIntegrationView = new ButtonView( { t: val => val } ); + + view = new ImageInsertFormView( { t: () => {} }, [ + firstIntegrationView, + secondIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'so "tab" focuses the next focusable item', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + // Mock the first integration focused. + view.focusTracker.isFocused = true; + view.focusTracker.focusedElement = firstIntegrationView.element; + + const spy = sinon.spy( secondIntegrationView, 'focus' ); + + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'so "shift + tab" focuses the previous focusable item', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + // Mock the cancel button is focused. + view.focusTracker.isFocused = true; + view.focusTracker.focusedElement = secondIntegrationView.element; + + const spy = sinon.spy( firstIntegrationView, 'focus' ); + + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'intercepts the arrow* events and overrides the default toolbar behavior', () => { + const keyEvtData = { + stopPropagation: sinon.spy() + }; + + keyEvtData.keyCode = keyCodes.arrowdown; + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + + keyEvtData.keyCode = keyCodes.arrowup; + view.keystrokes.press( keyEvtData ); + sinon.assert.calledTwice( keyEvtData.stopPropagation ); + + keyEvtData.keyCode = keyCodes.arrowleft; + view.keystrokes.press( keyEvtData ); + sinon.assert.calledThrice( keyEvtData.stopPropagation ); + + keyEvtData.keyCode = keyCodes.arrowright; + view.keystrokes.press( keyEvtData ); + sinon.assert.callCount( keyEvtData.stopPropagation, 4 ); + } ); + } ); + } ); + + describe( 'destroy()', () => { + it( 'should destroy the FocusTracker instance', () => { + const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + + it( 'should destroy the KeystrokeHandler instance', () => { + const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + } ); + + describe( 'focus()', () => { + it( 'should focus first focusable', () => { + const spy = sinon.spy( view._focusCycler, 'focusFirst' ); + + view.focus(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'focus cycling', () => { + let view, inputIntegrationView, buttonIntegrationView, otherButtonIntegrationView, collapsibleIntegrationView; + + beforeEach( () => { + inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + buttonIntegrationView = new ButtonView( { t: val => val } ); + otherButtonIntegrationView = new ButtonView( { t: val => val } ); + } ); + + describe( 'single button integration', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + } ); + + describe( 'single URL input integration', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + inputIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + } ); + } ); + + describe( 'multiple button integrations', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + otherButtonIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( otherButtonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( otherButtonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + } ); + + describe( 'mixed integrations (button and URL input)', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + inputIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + } ); + + describe( 'mixed integrations (button and URL input inside a collapsible view)', () => { + beforeEach( () => { + collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ inputIntegrationView ] ); + + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); + expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); + expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js deleted file mode 100644 index 02daced955f..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js +++ /dev/null @@ -1,329 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals document */ - -import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; - -import ImageInsertFormView from '../../../src/imageinsert/ui/imageinsertformview'; -import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import Collection from '@ckeditor/ckeditor5-utils/src/collection'; - -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; -import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; -import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; -import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; -import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; -import View from '@ckeditor/ckeditor5-ui/src/view'; - -import { createLabeledInputView } from '../../../src/imageinsert/utils'; - -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; - -describe( 'ImageInsertFormView', () => { - let view; - - beforeEach( () => { - view = new ImageInsertFormView( { t: val => val }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) - } ); - view.render(); - document.body.appendChild( view.element ); - } ); - - afterEach( () => { - view.element.remove(); - view.destroy(); - sinon.restore(); - } ); - - describe( 'constructor()', () => { - it( 'should contain instance of ButtonView as #insertButtonView', () => { - expect( view.insertButtonView ).to.be.instanceOf( ButtonView ); - expect( view.insertButtonView.label ).to.equal( 'Insert' ); - } ); - - it( 'should contain instance of ButtonView as #cancelButtonView', () => { - expect( view.cancelButtonView ).to.be.instanceOf( ButtonView ); - expect( view.cancelButtonView.label ).to.equal( 'Cancel' ); - } ); - - it( 'should contain #imageURLInputValue', () => { - expect( view.imageURLInputValue ).to.equal( '' ); - } ); - - it( 'should contain #_integrations as an instance of Collection', () => { - expect( view._integrations ).to.be.instanceOf( Collection ); - } ); - - describe( 'integrations', () => { - it( 'should contain 2 integrations when they were passed to the ImageInsertFormView as integrations object', () => { - const view = new ImageInsertFormView( { t: val => val }, { - 'integration1': new View(), - 'integration2': new ButtonView() - } ); - - expect( view._integrations ).to.be.instanceOf( Collection ); - expect( view._integrations.length ).to.equal( 2 ); - } ); - - it( 'should contain insertImageViaUrl view when it is passed via integrations object', () => { - const view = new ImageInsertFormView( { t: val => val }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ), - 'integration1': new View(), - 'integration2': new ButtonView() - } ); - - expect( view._integrations ).to.be.instanceOf( Collection ); - expect( view._integrations.length ).to.equal( 3 ); - expect( view._integrations.first ).to.be.instanceOf( LabeledFieldView ); - } ); - - it( 'should contain no integrations when they were not provided', () => { - const view = new ImageInsertFormView( { t: val => val } ); - - expect( view._integrations ).to.be.instanceOf( Collection ); - expect( view._integrations.length ).to.equal( 0 ); - } ); - } ); - - it( 'should create #focusTracker instance', () => { - expect( view.focusTracker ).to.be.instanceOf( FocusTracker ); - } ); - - it( 'should create #keystrokes instance', () => { - expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler ); - } ); - - it( 'should create #_focusCycler instance', () => { - expect( view._focusCycler ).to.be.instanceOf( FocusCycler ); - } ); - - it( 'should create #_focusables view collection', () => { - expect( view._focusables ).to.be.instanceOf( ViewCollection ); - } ); - - describe( 'events', () => { - it( 'should fire "submit" event on insertButtonView#execute', () => { - const spy = sinon.spy(); - - view.on( 'submit', spy ); - - view.insertButtonView.fire( 'execute' ); - - expect( spy.calledOnce ).to.true; - } ); - - it( 'should fire "cancel" event on cancelButtonView#execute', () => { - const spy = sinon.spy(); - - view.on( 'cancel', spy ); - - view.cancelButtonView.fire( 'execute' ); - - expect( spy.calledOnce ).to.true; - } ); - } ); - } ); - - describe( 'template', () => { - it( 'should create element from the template', () => { - expect( view.element.classList.contains( 'ck' ) ).to.true; - expect( view.element.classList.contains( 'ck-image-insert-form' ) ).to.true; - expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); - } ); - - it( 'should have form row view with buttons', () => { - expect( view.template.children[ 1 ] ).to.be.instanceOf( ImageUploadFormRowView ); - expect( view.template.children[ 1 ].children.first ).to.equal( view.insertButtonView ); - expect( view.template.children[ 1 ].children.last ).to.equal( view.cancelButtonView ); - } ); - } ); - - describe( 'render()', () => { - it( 'should register child views in #_focusables', () => { - expect( view._focusables.map( f => f ) ).to.have.members( [ - ...view._integrations, - view.insertButtonView, - view.cancelButtonView - ] ); - } ); - - it( 'should register child views\' #element in #focusTracker with no integrations', () => { - const view = new ImageInsertFormView( { t: () => {} } ); - - const spy = testUtils.sinon.spy( view.focusTracker, 'add' ); - view.render(); - - sinon.assert.calledWithExactly( spy.getCall( 0 ), view.insertButtonView.element ); - sinon.assert.calledWithExactly( spy.getCall( 1 ), view.cancelButtonView.element ); - - view.destroy(); - } ); - - it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => { - const view = new ImageInsertFormView( { t: () => {} }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) - } ); - - const spy = testUtils.sinon.spy( view.focusTracker, 'add' ); - - view.render(); - - sinon.assert.calledWithExactly( spy.getCall( 0 ), view.getIntegration( 'insertImageViaUrl' ).element ); - sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element ); - sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element ); - - view.destroy(); - } ); - - it( 'starts listening for #keystrokes coming from #element', () => { - const view = new ImageInsertFormView( { t: () => {} } ); - - const spy = sinon.spy( view.keystrokes, 'listenTo' ); - - view.render(); - sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, view.element ); - - view.destroy(); - } ); - - it( 'intercepts the arrow* events and overrides the default toolbar behavior', () => { - const keyEvtData = { - stopPropagation: sinon.spy() - }; - - keyEvtData.keyCode = keyCodes.arrowdown; - view.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( keyEvtData.stopPropagation ); - - keyEvtData.keyCode = keyCodes.arrowup; - view.keystrokes.press( keyEvtData ); - sinon.assert.calledTwice( keyEvtData.stopPropagation ); - - keyEvtData.keyCode = keyCodes.arrowleft; - view.keystrokes.press( keyEvtData ); - sinon.assert.calledThrice( keyEvtData.stopPropagation ); - - keyEvtData.keyCode = keyCodes.arrowright; - view.keystrokes.press( keyEvtData ); - sinon.assert.callCount( keyEvtData.stopPropagation, 4 ); - } ); - - describe( 'activates keyboard navigation for the toolbar', () => { - it( 'so "tab" focuses the next focusable item', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - - // Mock the url input is focused. - view.focusTracker.isFocused = true; - view.focusTracker.focusedElement = view.getIntegration( 'insertImageViaUrl' ).element; - - const spy = sinon.spy( view.insertButtonView, 'focus' ); - - view.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( keyEvtData.preventDefault ); - sinon.assert.calledOnce( keyEvtData.stopPropagation ); - sinon.assert.calledOnce( spy ); - } ); - - it( 'so "shift + tab" focuses the previous focusable item', () => { - const keyEvtData = { - keyCode: keyCodes.tab, - shiftKey: true, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() - }; - - // Mock the cancel button is focused. - view.focusTracker.isFocused = true; - view.focusTracker.focusedElement = view.cancelButtonView.element; - - const spy = sinon.spy( view.insertButtonView, 'focus' ); - - view.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( keyEvtData.preventDefault ); - sinon.assert.calledOnce( keyEvtData.stopPropagation ); - sinon.assert.calledOnce( spy ); - } ); - } ); - } ); - - describe( 'destroy()', () => { - it( 'should destroy the FocusTracker instance', () => { - const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); - - view.destroy(); - - sinon.assert.calledOnce( destroySpy ); - } ); - - it( 'should destroy the KeystrokeHandler instance', () => { - const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); - - view.destroy(); - - sinon.assert.calledOnce( destroySpy ); - } ); - } ); - - describe( 'focus()', () => { - it( 'should focus on the first integration', () => { - const spy = sinon.spy( view.getIntegration( 'insertImageViaUrl' ), 'focus' ); - - view.focus(); - - sinon.assert.calledOnce( spy ); - } ); - } ); - - describe( 'Insert image via URL integration input', () => { - it( 'should be bound with #imageURLInputValue', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - - form.fieldView.element.value = 'abc'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'abc' ); - - form.fieldView.element.value = 'xyz'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'xyz' ); - } ); - - it( 'should trim input value', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - - form.fieldView.element.value = ' '; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( '' ); - - form.fieldView.element.value = ' test '; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'test' ); - } ); - - it( 'binds saveButtonView#isEnabled to URL input value', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - const saveButtonView = view.template.children[ 1 ].children.first; - - expect( saveButtonView.isEnabled ).to.be.false; - - form.fieldView.element.value = 'test'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'test' ); - expect( !!saveButtonView.isEnabled ).to.be.true; - } ); - } ); -} ); From 7d0d130d4965d541e65a5dce8d5dde093c08cb0e Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 16:19:56 +0100 Subject: [PATCH 44/53] Added tests for ImageInsertUI. --- .../src/imageinsert/imageinsertui.ts | 20 +- .../tests/imageinsert/imageinsertui.js | 773 +++++++----------- 2 files changed, 290 insertions(+), 503 deletions(-) diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index b26d49f7d36..6f2e2524c1c 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -86,21 +86,21 @@ export default class ImageInsertUI extends Plugin { * @inheritDoc */ public init(): void { + const editor = this.editor; + const selection = editor.model.document.selection; + const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); + this.set( 'isImageSelected', false ); - const editor = this.editor; + this.listenTo( editor.model.document, 'change', () => { + this.isImageSelected = imageUtils.isImage( selection.getSelectedElement() ); + } ); + const componentCreator = ( locale: Locale ) => this._createToolbarComponent( locale ); // Register `insertImage` dropdown and add `imageInsert` dropdown as an alias for backward compatibility. editor.ui.componentFactory.add( 'insertImage', componentCreator ); editor.ui.componentFactory.add( 'imageInsert', componentCreator ); - - this.listenTo( editor.model.document, 'change', () => { - const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); - const element = this.editor.model.document.selection.getSelectedElement(); - - this.isImageSelected = imageUtils.isImage( element ); - } ); } /** @@ -134,7 +134,7 @@ export default class ImageInsertUI extends Plugin { observable, buttonViewCreator, formViewCreator, - requiresForm + requiresForm: !!requiresForm } ); } @@ -253,5 +253,5 @@ type IntegrationData = { observable: Observable & { isEnabled: boolean }; buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; formViewCreator: ( isOnlyOne: boolean ) => FocusableView; - requiresForm?: boolean; + requiresForm: boolean; }; diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js index 3c57c9af51d..0e4550a8475 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js @@ -3,77 +3,64 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document */ +/* globals document, console */ -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; - -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import Image from '../../src/image'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview'; -import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview'; -import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository'; -import ImageInsert from '../../src/imageinsert'; -import ImageInsertViaUrl from '../../src/imageinsertviaurl'; -import ImageInsertUI from '../../src/imageinsert/imageinsertui'; -import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification'; -import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; -import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; -import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; -import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder'; -import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import DropdownButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/dropdownbuttonview'; import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview'; -import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks'; -import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; -import Link from '@ckeditor/ckeditor5-link/src/link'; - -describe( 'ImageInsertUI', () => { - let editor, editorElement, fileRepository, dropdown; - - describe( 'dropdown (with uploadImage command)', () => { - class UploadAdapterPluginMock extends Plugin { - init() { - fileRepository = this.editor.plugins.get( FileRepository ); - fileRepository.createUploadAdapter = loader => { - return new UploadAdapterMock( loader ); - }; - } - } +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; - beforeEach( async () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - editor = await ClassicEditor.create( editorElement, { - plugins: [ Paragraph, Image, ImageInsert, FileRepository, UploadAdapterPluginMock, Clipboard ], - toolbar: [ 'insertImage' ] - } ); +import Image from '../../src/image'; +import ImageInsertUI from '../../src/imageinsert/imageinsertui'; +import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview'; - dropdown = editor.ui.view.toolbar.children.first.children.first; +describe( 'ImageInsertUI', () => { + let editor, editorElement, insertImageUI; - // Hide all notifications (prevent alert() calls). - const notification = editor.plugins.get( Notification ); - notification.on( 'show', evt => evt.stop() ); - } ); + testUtils.createSinonSandbox(); - afterEach( async () => { + afterEach( async () => { + if ( editorElement ) { editorElement.remove(); + } + if ( editor ) { await editor.destroy(); + } + } ); + + it( 'should have pluginName', () => { + expect( ImageInsertUI.pluginName ).to.equal( 'ImageInsertUI' ); + } ); + + describe( '#constructor()', () => { + beforeEach( async () => { + await createEditor( { plugins: [ ImageInsertUI ] } ); } ); - it( 'should register the "insertImage" dropdown', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + it( 'should define config', () => { + expect( editor.config.get( 'image.insert.integrations' ) ).to.deep.equal( [ + 'upload', + 'assetManager', + 'url' + ] ); + } ); + } ); - expect( dropdown ).to.be.instanceOf( DropdownView ); + describe( '#init()', () => { + beforeEach( async () => { + await createEditor( { plugins: [ ImageInsertUI ] } ); } ); - it( 'should make the "insertImage" dropdown accessible via the property of the plugin', () => { - expect( editor.plugins.get( 'ImageInsertUI' ).dropdownView ).to.be.instanceOf( DropdownView ); + it( 'should register component in component factory', () => { + expect( editor.ui.componentFactory.has( 'insertImage' ) ).to.be.true; + expect( editor.ui.componentFactory.has( 'imageInsert' ) ).to.be.true; } ); it( 'should register "imageInsert" dropdown as an alias for the "insertImage" dropdown', () => { @@ -82,556 +69,356 @@ describe( 'ImageInsertUI', () => { expect( dropdownCreator.callback ).to.equal( dropdownAliasCreator.callback ); } ); + } ); - it( 'should register the "insertImage" dropdown with basic properties', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const dropdownButtonView = dropdown.buttonView; - - expect( dropdownButtonView ).to.have.property( 'label', 'Insert image' ); - expect( dropdownButtonView ).to.have.property( 'icon' ); - expect( dropdownButtonView ).to.have.property( 'tooltip', true ); + describe( '#isImageSelected', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ ImageInsertUI, Essentials, Paragraph, Image ] + } ); } ); - it( 'should bind the enabled state of the dropdown to the UploadImageCommand command', () => { - const command = editor.commands.get( 'uploadImage' ); + it( 'should be false if image is not selected', () => { + setData( editor.model, + '[foo]' + + '' + ); - expect( command.isEnabled, 'command state' ).to.be.true; - expect( dropdown.isEnabled, 'dropdown state #1' ).to.be.true; + expect( insertImageUI.isImageSelected ).to.be.false; - command.forceDisabled( 'foo' ); + editor.model.change( writer => { + writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); + } ); - expect( dropdown.isEnabled, 'dropdown state #2' ).to.be.false; + expect( insertImageUI.isImageSelected ).to.be.false; } ); - it( 'should insert panel view children on first dropdown open', () => { - expect( dropdown.panelView.children.length ).to.equal( 0 ); - - dropdown.isOpen = true; - - expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); - - dropdown.isOpen = false; - dropdown.isOpen = true; + it( 'should be true if block image is selected', () => { + setData( editor.model, + 'foo' + + '[]' + ); - // Make sure it happens only once. - expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); + expect( insertImageUI.isImageSelected ).to.be.true; } ); - describe( 'dropdown action button', () => { - it( 'should belong to a split button', () => { - expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); - } ); + it( 'should change on selection change', () => { + setData( editor.model, + 'foo[]' + + '' + ); - it( 'should be an instance of FileDialogButtonView', () => { - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + expect( insertImageUI.isImageSelected ).to.be.false; - expect( dropdown.buttonView.actionView ).to.be.instanceOf( FileDialogButtonView ); + editor.model.change( writer => { + writer.setSelection( editor.model.document.getRoot().getChild( 1 ), 'on' ); } ); - } ); - - describe( 'dropdown panel buttons', () => { - it( 'should have "Update" label on submit button when URL input is already filled', () => { - const viewDocument = editor.editing.view.document; - editor.setData( '
' ); + expect( insertImageUI.isImageSelected ).to.be.true; - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); - - const img = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( img, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' ); + editor.model.change( writer => { + writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); } ); - it( 'should have "Insert" label on submit button on uploading a new image', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '

test

' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' ); - } ); - - const el = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' ); - } ); + expect( insertImageUI.isImageSelected ).to.be.false; } ); + } ); - describe( 'dropdown panel integrations', () => { - describe( 'insert image via URL form', () => { - it( 'should have "Insert image via URL" label on inserting new image', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '

test

' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' ); - } ); - - const el = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' ); - } ); - - it( 'should have "Update image URL" label on updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '
' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); - - const el = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' ); - } ); - } ); + describe( '#registerIntegration()', () => { + beforeEach( async () => { + await createEditor( { plugins: [ ImageInsertUI ] } ); } ); - it( 'should remove all attributes from model except "src" when updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); - - const insertButtonView = dropdown.panelView.children.first.insertButtonView; - - editor.setData( '
' ); + it( 'should store the integration definition', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator } ); - const selectedElement = editor.model.document.selection.getSelectedElement(); + expect( insertImageUI._integrations.has( 'foobar' ) ).to.be.true; - expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true; + const integrationData = insertImageUI._integrations.get( 'foobar' ); - dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png'; - - dropdown.on( 'submit', submitSpy ); - - insertButtonView.fire( 'execute' ); - - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( submitSpy ); - expect( dropdown.isOpen ).to.be.false; - expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false; - expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false; + expect( integrationData.observable ).to.equal( observable ); + expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); + expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.requiresForm ).to.be.false; } ); - describe( 'events', () => { - it( 'should emit "submit" event when clicking on submit button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); - - dropdown.on( 'submit', submitSpy ); - - const insertButtonView = dropdown.panelView.children.first.insertButtonView; - - insertButtonView.fire( 'execute' ); - - expect( dropdown.isOpen ).to.be.false; - sinon.assert.calledOnce( commandSpy ); - sinon.assert.calledOnce( submitSpy ); + it( 'should store the integration definition (with optional data)', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; + + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - it( 'should emit "cancel" event when clicking on cancel button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const cancelSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); - - dropdown.on( 'cancel', cancelSpy ); + expect( insertImageUI._integrations.has( 'foobar' ) ).to.be.true; - const cancelButtonView = dropdown.panelView.children.first.cancelButtonView; + const integrationData = insertImageUI._integrations.get( 'foobar' ); - cancelButtonView.fire( 'execute' ); + expect( integrationData.observable ).to.equal( observable ); + expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); + expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.requiresForm ).to.be.true; + } ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( cancelSpy ); + it( 'should warn if multiple integrations with the same name are registered', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; + const warnStub = sinon.stub( console, 'warn' ); + + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - it( 'should focus on "insert image via URL" input after opening', () => { - let spy; - - // The ImageInsertFormView is added on first open. - // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 - dropdown.on( 'change:isOpen', () => { - const imageInsertFormView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertFormView, 'focus' ); - } ); + expect( warnStub.notCalled ).to.be.true; - dropdown.buttonView.fire( 'open' ); - sinon.assert.calledOnce( spy ); + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - } ); - - it( 'should inject integrations to the dropdown panel view from the config', async () => { - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Link, - Image, - CKFinderUploadAdapter, - CKFinder, - Paragraph, - ImageInsert, - ImageInsertUI, - FileRepository, - UploadAdapterPluginMock, - Clipboard - ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } - } - } ); - - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - - dropdown.isOpen = true; - - expect( dropdown.panelView.children.first._integrations.length ).to.equal( 2 ); - expect( dropdown.panelView.children.first._integrations.first ).to.be.instanceOf( LabeledFieldView ); - expect( dropdown.panelView.children.first._integrations.last ).to.be.instanceOf( ButtonView ); - editor.destroy(); + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integration-exists' ); } ); } ); - describe( 'dropdown (without uploadImage command)', () => { - beforeEach( async () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - editor = await ClassicEditor.create( editorElement, { - plugins: [ Paragraph, Image, ImageInsertViaUrl ], - toolbar: [ 'insertImage' ] - } ); + describe( 'integrations', () => { + let observableUpload, observableUrl; - dropdown = editor.ui.view.toolbar.children.first.children.first; + beforeEach( async () => { + await createEditor( { plugins: [ Image, Essentials, Paragraph ] } ); } ); - afterEach( async () => { - editorElement.remove(); - - await editor.destroy(); - } ); + it( 'should warn if empty list of integrations is configured', () => { + editor.config.set( 'image.insert.integrations', [] ); - it( 'should register the "insertImage" dropdown', () => { + const warnStub = sinon.stub( console, 'warn' ); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown ).to.be.null; + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-specified' ); } ); - it( 'should make the "insertImage" dropdown accessible via the property of the plugin', () => { - expect( editor.plugins.get( 'ImageInsertUI' ).dropdownView ).to.be.instanceOf( DropdownView ); - } ); + it( 'should warn if unknown integration is requested by config', () => { + editor.config.set( 'image.insert.integrations', [ 'foo' ] ); - it( 'should register "imageInsert" dropdown as an alias for the "insertImage" dropdown', () => { - const dropdownCreator = editor.ui.componentFactory._components.get( 'insertImage'.toLowerCase() ); - const dropdownAliasCreator = editor.ui.componentFactory._components.get( 'imageInsert'.toLowerCase() ); + const warnStub = sinon.stub( console, 'warn' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdownCreator.callback ).to.equal( dropdownAliasCreator.callback ); + expect( dropdown ).to.be.null; + expect( warnStub.calledTwice ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-unknown-integration' ); + expect( warnStub.firstCall.args[ 1 ].item ).to.equal( 'foo' ); + expect( warnStub.secondCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-registered' ); } ); - it( 'should bind the enabled state of the dropdown to the InsertImageCommand command', () => { - const command = editor.commands.get( 'insertImage' ); - - expect( command.isEnabled, 'command state' ).to.be.true; - expect( dropdown.isEnabled, 'dropdown state #1' ).to.be.true; + it( 'should not warn if known but not registered integration is requested by config', () => { + editor.config.set( 'image.insert.integrations', [ 'url', 'assetManager', 'upload' ] ); - command.forceDisabled( 'foo' ); + const warnStub = sinon.stub( console, 'warn' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdown.isEnabled, 'dropdown state #2' ).to.be.false; + expect( dropdown ).to.be.null; + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-registered' ); } ); - it( 'should not insert panel view children until dropdown is not open for the first time', () => { - expect( dropdown.panelView.children.length ).to.equal( 0 ); - - dropdown.buttonView.fire( 'open' ); + describe( 'single integration without form view required', () => { + beforeEach( async () => { + registerUploadIntegration(); + } ); - expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertFormView ); - } ); + it( 'should create a toolbar button', () => { + const button = editor.ui.componentFactory.create( 'insertImage' ); - describe( 'dropdown button', () => { - it( 'should be an instance of DropdownButtonView', () => { - expect( dropdown.buttonView ).to.be.instanceOf( DropdownButtonView ); + expect( button ).to.be.instanceOf( ButtonView ); + expect( button.label ).to.equal( 'button upload single' ); } ); } ); - describe( 'dropdown panel buttons', () => { - it( 'should have "Update" label on submit button when URL input is already filled', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '
' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); - - const img = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( img, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' ); + describe( 'single integration with form view required', () => { + beforeEach( async () => { + registerUrlIntegration(); } ); - it( 'should have "Insert" label on submit button on uploading a new image', () => { - const viewDocument = editor.editing.view.document; + it( 'should create a toolbar dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - editor.setData( '

test

' ); + expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown.buttonView.label ).to.equal( 'button url single' ); + expect( dropdown.isEnabled ).to.be.true; + } ); - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' ); - } ); + it( 'should bind isEnabled state to observable', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const el = viewDocument.selection.getSelectedElement(); + observableUrl.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); + observableUrl.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; + } ); - viewDocument.fire( 'click', domEventDataMock ); + it( 'should create panel view on dropdown first open', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + expect( dropdown.panelView.children.length ).to.equal( 0 ); - const inputValue = dropdown.panelView.children.first.imageURLInputValue; + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' ); + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 0 ).label ).to.equal( 'dropdown url single' ); } ); } ); - describe( 'dropdown panel integrations', () => { - describe( 'insert image via URL form', () => { - it( 'should have "Insert image via URL" label on inserting new image', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '

test

' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' ); - } ); - - const el = viewDocument.selection.getSelectedElement(); - - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); - - viewDocument.fire( 'click', domEventDataMock ); - - dropdown.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' ); - } ); - - it( 'should have "Update image URL" label on updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '
' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); + describe( 'multiple integrations', () => { + beforeEach( async () => { + registerUploadIntegration(); + registerUrlIntegration(); + } ); - const el = viewDocument.selection.getSelectedElement(); + it( 'should create a toolbar split button dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); + expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.actionView.label ).to.equal( 'button upload multiple' ); + expect( dropdown.isEnabled ).to.be.true; + } ); - viewDocument.fire( 'click', domEventDataMock ); + it( 'should bind split button label to #isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + expect( insertImageUI.isImageSelected ).to.be.false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' ); - } ); + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); } ); - } ); - it( 'should remove all attributes from model except "src" when updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); + it( 'should bind isEnabled state to observables', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + observableUrl.isEnabled = false; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; - const insertButtonView = dropdown.panelView.children.first.insertButtonView; + observableUrl.isEnabled = true; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.true; - editor.setData( '
' ); + observableUrl.isEnabled = false; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); + observableUrl.isEnabled = true; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; } ); - const selectedElement = editor.model.document.selection.getSelectedElement(); - - expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true; + it( 'should create panel view on dropdown first open', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png'; + expect( dropdown.panelView.children.length ).to.equal( 0 ); - dropdown.on( 'submit', submitSpy ); + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); - insertButtonView.fire( 'execute' ); + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( submitSpy ); - expect( dropdown.isOpen ).to.be.false; - expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false; - expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false; + expect( formView.children.length ).to.equal( 2 ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 0 ).label ).to.equal( 'dropdown upload multiple' ); + expect( formView.children.get( 1 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 1 ).label ).to.equal( 'dropdown url multiple' ); + } ); } ); - describe( 'events', () => { - it( 'should emit "submit" event when clicking on submit button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); + function registerUrlIntegration() { + observableUrl = new Model( { isEnabled: true } ); - dropdown.buttonView.fire( 'open' ); + insertImageUI.registerIntegration( { + name: 'url', + observable: observableUrl, + requiresForm: true, + buttonViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - dropdown.on( 'submit', submitSpy ); + button.label = 'button url ' + ( isOnlyOne ? 'single' : 'multiple' ); - const insertButtonView = dropdown.panelView.children.first.insertButtonView; + return button; + }, + formViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - insertButtonView.fire( 'execute' ); + button.label = 'dropdown url ' + ( isOnlyOne ? 'single' : 'multiple' ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.calledOnce( commandSpy ); - sinon.assert.calledOnce( submitSpy ); + return button; + } } ); + } - it( 'should emit "cancel" event when clicking on cancel button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const cancelSpy = sinon.spy(); + function registerUploadIntegration() { + observableUpload = new Model( { isEnabled: true } ); - dropdown.buttonView.fire( 'open' ); + insertImageUI.registerIntegration( { + name: 'upload', + observable: observableUpload, + buttonViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - dropdown.on( 'cancel', cancelSpy ); + button.label = 'button upload ' + ( isOnlyOne ? 'single' : 'multiple' ); - const cancelButtonView = dropdown.panelView.children.first.cancelButtonView; + return button; + }, + formViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - cancelButtonView.fire( 'execute' ); + button.label = 'dropdown upload ' + ( isOnlyOne ? 'single' : 'multiple' ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( cancelSpy ); + return button; + } } ); + } + } ); - it( 'should focus on "insert image via URL" input after opening', () => { - let spy; + async function createEditor( config ) { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); - // The ImageInsertFormView is added on first open. - // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 - dropdown.on( 'change:isOpen', () => { - const imageInsertFormView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertFormView, 'focus' ); - } ); + editor = await ClassicTestEditor.create( editorElement, config ); - dropdown.buttonView.fire( 'open' ); - sinon.assert.calledOnce( spy ); - } ); - } ); - } ); + insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + } } ); - -function fakeEventData() { - return { - preventDefault: sinon.spy() - }; -} From cf1db75edc4a3923856f11dfe2c3ebcf95c8c9a2 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 18:40:32 +0100 Subject: [PATCH 45/53] Added test for ImageInsertViaUrlUI. --- .../tests/imageinsert/imageinsertviaurlui.js | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js new file mode 100644 index 00000000000..195256990c9 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js @@ -0,0 +1,419 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview'; + +import { CollapsibleView, DropdownButtonView } from '@ckeditor/ckeditor5-ui'; +import { icons } from '@ckeditor/ckeditor5-core'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import Image from '../../src/image'; +import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview'; +import ImageInsertViaUrlUI from '../../src/imageinsert/imageinsertviaurlui'; +import { ImageInsertViaUrl } from '../../src'; +import ImageInsertUrlView from '../../src/imageinsert/ui/imageinserturlview'; + +describe( 'ImageInsertViaUrlUI', () => { + let editor, editorElement, insertImageUI; + + testUtils.createSinonSandbox(); + + afterEach( async () => { + if ( editorElement ) { + editorElement.remove(); + } + + if ( editor ) { + await editor.destroy(); + } + } ); + + it( 'should have pluginName', () => { + expect( ImageInsertViaUrlUI.pluginName ).to.equal( 'ImageInsertViaUrlUI' ); + } ); + + describe( 'single integration', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); + } ); + + it( 'should create toolbar dropdown button', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView ).to.be.instanceOf( DropdownButtonView ); + expect( dropdown.buttonView.icon ).to.equal( icons.imageUrl ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should create form view on first open of dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.panelView.children.length ).to.equal( 0 ); + + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); + + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.length ).to.equal( 1 ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); + } ); + + describe( 'form bindings', () => { + let dropdown, formView, urlView; + + beforeEach( () => { + dropdown = editor.ui.componentFactory.create( 'insertImage' ); + dropdown.isOpen = true; + formView = dropdown.panelView.children.get( 0 ); + urlView = formView.children.get( 0 ); + } ); + + it( 'should bind #isImageSelected', () => { + expect( urlView.isImageSelected ).to.be.false; + + insertImageUI.isImageSelected = true; + expect( urlView.isImageSelected ).to.be.true; + + insertImageUI.isImageSelected = false; + expect( urlView.isImageSelected ).to.be.false; + } ); + + it( 'should bind #isEnabled', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const insertImageCommand = editor.commands.get( 'insertImage' ); + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.false; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + } ); + + it( 'should set #imageURLInputValue at first open', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'foobar'; + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const urlView = formView.children.get( 0 ); + + expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); + } ); + + it( 'should reset #imageURLInputValue on dropdown reopen', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'abc'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( 'abc' ); + + replaceImageSourceCommand.value = '123'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '123' ); + + replaceImageSourceCommand.value = undefined; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '' ); + } ); + + it( 'should execute replaceImageSource command and close dropdown', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should execute insertImage command', () => { + const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should close dropdown', () => { + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + urlView.fire( 'cancel' ); + + expect( stubExecute.notCalled ).to.be.true; + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + } ); + + describe( 'multiple integrations', () => { + let observable; + + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); + + observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'foo', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + + editor.config.set( 'image.insert.integrations', [ 'url', 'foo' ] ); + } ); + + it( 'should create toolbar dropdown button', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.icon ).to.equal( icons.imageUrl ); + expect( dropdown.buttonView.actionView.tooltip ).to.be.true; + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should create form view on first open of dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.panelView.children.length ).to.equal( 0 ); + + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); + + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.length ).to.equal( 2 ); + + const collapsibleView = formView.children.get( 0 ); + expect( collapsibleView ).to.be.instanceOf( CollapsibleView ); + expect( collapsibleView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); + } ); + + describe( 'form bindings', () => { + let dropdown, formView, collapsibleView, urlView; + + beforeEach( () => { + dropdown = editor.ui.componentFactory.create( 'insertImage' ); + dropdown.isOpen = true; + formView = dropdown.panelView.children.get( 0 ); + collapsibleView = formView.children.get( 0 ); + urlView = collapsibleView.children.get( 0 ); + } ); + + it( 'should bind #isImageSelected', () => { + expect( urlView.isImageSelected ).to.be.false; + + insertImageUI.isImageSelected = true; + expect( urlView.isImageSelected ).to.be.true; + expect( collapsibleView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( urlView.isImageSelected ).to.be.false; + expect( collapsibleView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind #isEnabled', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const insertImageCommand = editor.commands.get( 'insertImage' ); + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.false; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + } ); + + it( 'should set #imageURLInputValue and CollapsibleView#isCollapsed at first open', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'foobar'; + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const collapsibleView = formView.children.get( 0 ); + const urlView = collapsibleView.children.get( 0 ); + + expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); + expect( collapsibleView.isCollapsed ).to.be.true; + } ); + + it( 'should reset #imageURLInputValue and CollapsibleView#isCollapsed on dropdown reopen', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'abc'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( 'abc' ); + expect( collapsibleView.isCollapsed ).to.be.true; + + replaceImageSourceCommand.value = '123'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '123' ); + expect( collapsibleView.isCollapsed ).to.be.true; + + replaceImageSourceCommand.value = undefined; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '' ); + expect( collapsibleView.isCollapsed ).to.be.true; + } ); + + it( 'should execute replaceImageSource command and close dropdown', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should execute insertImage command', () => { + const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should close dropdown', () => { + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + urlView.fire( 'cancel' ); + + expect( stubExecute.notCalled ).to.be.true; + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + } ); + + async function createEditor( config ) { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, config ); + + insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + } +} ); From 9ddbc4b213bb5ada7ccb716b0764334fc2805042 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 19:26:34 +0100 Subject: [PATCH 46/53] Added tests for ImageUploadUI integration. --- .../src/imageupload/imageuploadui.ts | 2 - .../tests/imageinsert/imageinsertviaurlui.js | 4 +- .../tests/imageupload/imageuploadui.js | 105 ++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index c0a41216085..a7e596134d2 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -81,7 +81,6 @@ export default class ImageUploadUI extends Plugin { buttonViewCreator: () => { const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; - uploadImageButton.icon = icons.imageUpload; uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace image from computer' ) : t( 'Upload image from computer' ) @@ -93,7 +92,6 @@ export default class ImageUploadUI extends Plugin { formViewCreator: () => { const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; - uploadImageButton.icon = icons.imageUpload; uploadImageButton.withText = true; uploadImageButton.bind( 'label' ).to( imageInsertUI, 'isImageSelected', isImageSelected => isImageSelected ? t( 'Replace from computer' ) : diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js index 195256990c9..24980785553 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js @@ -204,14 +204,12 @@ describe( 'ImageInsertViaUrlUI', () => { } ); describe( 'multiple integrations', () => { - let observable; - beforeEach( async () => { await createEditor( { plugins: [ Image, ImageInsertViaUrl ] } ); - observable = new Model( { isEnabled: true } ); + const observable = new Model( { isEnabled: true } ); insertImageUI.registerIntegration( { name: 'foo', diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js index 5dafea2c95f..7060b0782c2 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js @@ -16,13 +16,18 @@ import ImageUploadEditing from '../../src/imageupload/imageuploadediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; 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 testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; describe( 'ImageUploadUI', () => { let editor, model, editorElement, fileRepository; + testUtils.createSinonSandbox(); + class UploadAdapterPluginMock extends Plugin { init() { fileRepository = this.editor.plugins.get( FileRepository ); @@ -201,4 +206,104 @@ describe( 'ImageUploadUI', () => { expect( spy ).to.be.calledOnce; } ); + + describe( 'InsertImageUI integration', () => { + it( 'should create FileDialogButtonView in split button dropdown button', () => { + mockAssetManagerIntegration(); + + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const dropdownButton = dropdown.buttonView.actionView; + + expect( dropdownButton ).to.be.instanceOf( FileDialogButtonView ); + expect( dropdownButton.withText ).to.be.false; + + expect( spy.calledTwice ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( spy.secondCall.args[ 0 ] ).to.equal( 'uploadImage' ); + expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + } ); + + it( 'should create FileDialogButtonView in dropdown panel', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + expect( buttonView ).to.be.instanceOf( FileDialogButtonView ); + expect( buttonView.withText ).to.be.true; + + expect( spy.calledOnce ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'uploadImage' ); + expect( spy.firstCall.returnValue ).to.equal( buttonView ); + } ); + + it( 'should bind to #isImageSelected', () => { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const dropdownButton = dropdown.buttonView.actionView; + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + insertImageUI.isImageSelected = false; + expect( dropdownButton.label ).to.equal( 'Upload image from computer' ); + expect( buttonView.label ).to.equal( 'Upload from computer' ); + + insertImageUI.isImageSelected = true; + expect( dropdownButton.label ).to.equal( 'Replace image from computer' ); + expect( buttonView.label ).to.equal( 'Replace from computer' ); + } ); + + it( 'should close dropdown on execute', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + sinon.stub( editor, 'execute' ); + + buttonView.fire( 'execute' ); + + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + + function mockAssetManagerIntegration() { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + const observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'assetManager', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + } } ); From 0443c16304963d8fe931f79efafd9276a2093b21 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 19:52:20 +0100 Subject: [PATCH 47/53] Added tests for CKBoxUI integration. --- packages/ckeditor5-ckbox/tests/ckboxui.js | 103 ++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/ckeditor5-ckbox/tests/ckboxui.js b/packages/ckeditor5-ckbox/tests/ckboxui.js index 922f0b58139..0a5ce0d1c90 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxui.js +++ b/packages/ckeditor5-ckbox/tests/ckboxui.js @@ -15,6 +15,8 @@ import ImageInlineEditing from '@ckeditor/ckeditor5-image/src/image/imageinlinee import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import CloudServicesCoreMock from './_utils/cloudservicescoremock'; +import ImageInsertUI from '@ckeditor/ckeditor5-image/src/imageinsert/imageinsertui'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import CKBoxUI from '../src/ckboxui'; @@ -41,6 +43,7 @@ describe( 'CKBoxUI', () => { ImageInlineEditing, ImageUploadEditing, ImageUploadProgress, + ImageInsertUI, CloudServices, CKBoxUI, CKBoxEditing @@ -144,4 +147,104 @@ describe( 'CKBoxUI', () => { expect( executeSpy.args[ 0 ][ 0 ] ).to.equal( 'ckbox' ); } ); } ); + + describe( 'InsertImageUI integration', () => { + it( 'should create CKBox button in split button dropdown button', () => { + mockAssetManagerIntegration(); + + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const dropdownButton = dropdown.buttonView.actionView; + + expect( dropdownButton ).to.be.instanceOf( ButtonView ); + expect( dropdownButton.withText ).to.be.false; + + expect( spy.calledTwice ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( spy.secondCall.args[ 0 ] ).to.equal( 'ckbox' ); + expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + } ); + + it( 'should create CKBox button in dropdown panel', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + expect( buttonView ).to.be.instanceOf( ButtonView ); + expect( buttonView.withText ).to.be.true; + + expect( spy.calledOnce ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'ckbox' ); + expect( spy.firstCall.returnValue ).to.equal( buttonView ); + } ); + + it( 'should bind to #isImageSelected', () => { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const dropdownButton = dropdown.buttonView.actionView; + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + insertImageUI.isImageSelected = false; + expect( dropdownButton.label ).to.equal( 'Insert image with file manager' ); + expect( buttonView.label ).to.equal( 'Insert with file manager' ); + + insertImageUI.isImageSelected = true; + expect( dropdownButton.label ).to.equal( 'Replace image with file manager' ); + expect( buttonView.label ).to.equal( 'Replace with file manager' ); + } ); + + it( 'should close dropdown on execute', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + sinon.stub( editor, 'execute' ); + + buttonView.fire( 'execute' ); + + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + + function mockAssetManagerIntegration() { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + const observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'url', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + } } ); From 98ba510053ed24991a0d7041883f6f4072b17085 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 19:59:04 +0100 Subject: [PATCH 48/53] Added tests for integration with CKFinderUI. --- packages/ckeditor5-ckbox/tests/ckboxui.js | 3 + .../ckeditor5-ckfinder/tests/ckfinderui.js | 104 ++++++++++++++++++ .../tests/imageupload/imageuploadui.js | 3 + 3 files changed, 110 insertions(+) diff --git a/packages/ckeditor5-ckbox/tests/ckboxui.js b/packages/ckeditor5-ckbox/tests/ckboxui.js index 0a5ce0d1c90..6cecd91c595 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxui.js +++ b/packages/ckeditor5-ckbox/tests/ckboxui.js @@ -18,6 +18,7 @@ import CloudServicesCoreMock from './_utils/cloudservicescoremock'; import ImageInsertUI from '@ckeditor/ckeditor5-image/src/imageinsert/imageinsertui'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { icons } from 'ckeditor5/src/core'; import CKBoxUI from '../src/ckboxui'; import CKBoxEditing from '../src/ckboxediting'; @@ -158,6 +159,7 @@ describe( 'CKBoxUI', () => { expect( dropdownButton ).to.be.instanceOf( ButtonView ); expect( dropdownButton.withText ).to.be.false; + expect( dropdownButton.icon ).to.equal( icons.imageAssetManager ); expect( spy.calledTwice ).to.be.true; expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); @@ -178,6 +180,7 @@ describe( 'CKBoxUI', () => { expect( buttonView ).to.be.instanceOf( ButtonView ); expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); expect( spy.calledOnce ).to.be.true; expect( spy.firstCall.args[ 0 ] ).to.equal( 'ckbox' ); diff --git a/packages/ckeditor5-ckfinder/tests/ckfinderui.js b/packages/ckeditor5-ckfinder/tests/ckfinderui.js index 51de804ff6c..ff47a9f8e77 100644 --- a/packages/ckeditor5-ckfinder/tests/ckfinderui.js +++ b/packages/ckeditor5-ckfinder/tests/ckfinderui.js @@ -11,11 +11,13 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import Image from '@ckeditor/ckeditor5-image/src/image'; import Link from '@ckeditor/ckeditor5-link/src/link'; import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; +import { icons } from 'ckeditor5/src/core'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import CKFinder from '../src/ckfinder'; import browseFilesIcon from '../theme/icons/browse-files.svg'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; describe( 'CKFinderUI', () => { let editorElement, editor, button; @@ -82,4 +84,106 @@ describe( 'CKFinderUI', () => { sinon.assert.calledOnce( executeStub ); } ); } ); + + describe( 'InsertImageUI integration', () => { + it( 'should create CKFinder button in split button dropdown button', () => { + mockAssetManagerIntegration(); + + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const dropdownButton = dropdown.buttonView.actionView; + + expect( dropdownButton ).to.be.instanceOf( ButtonView ); + expect( dropdownButton.withText ).to.be.false; + expect( dropdownButton.icon ).to.equal( icons.imageAssetManager ); + + expect( spy.calledTwice ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( spy.secondCall.args[ 0 ] ).to.equal( 'ckfinder' ); + expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + } ); + + it( 'should create CKFinder button in dropdown panel', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + expect( buttonView ).to.be.instanceOf( ButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageAssetManager ); + + expect( spy.calledOnce ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'ckfinder' ); + expect( spy.firstCall.returnValue ).to.equal( buttonView ); + } ); + + it( 'should bind to #isImageSelected', () => { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const dropdownButton = dropdown.buttonView.actionView; + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + insertImageUI.isImageSelected = false; + expect( dropdownButton.label ).to.equal( 'Insert image with file manager' ); + expect( buttonView.label ).to.equal( 'Insert with file manager' ); + + insertImageUI.isImageSelected = true; + expect( dropdownButton.label ).to.equal( 'Replace image with file manager' ); + expect( buttonView.label ).to.equal( 'Replace with file manager' ); + } ); + + it( 'should close dropdown on execute', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + sinon.stub( editor, 'execute' ); + + buttonView.fire( 'execute' ); + + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + + function mockAssetManagerIntegration() { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + const observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'url', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + } } ); diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js index 7060b0782c2..b5cac6f247b 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js @@ -18,6 +18,7 @@ import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import Model from '@ckeditor/ckeditor5-ui/src/model'; +import { icons } from 'ckeditor5/src/core'; 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'; @@ -217,6 +218,7 @@ describe( 'ImageUploadUI', () => { expect( dropdownButton ).to.be.instanceOf( FileDialogButtonView ); expect( dropdownButton.withText ).to.be.false; + expect( dropdownButton.icon ).to.equal( icons.imageUpload ); expect( spy.calledTwice ).to.be.true; expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); @@ -237,6 +239,7 @@ describe( 'ImageUploadUI', () => { expect( buttonView ).to.be.instanceOf( FileDialogButtonView ); expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageUpload ); expect( spy.calledOnce ).to.be.true; expect( spy.firstCall.args[ 0 ] ).to.equal( 'uploadImage' ); From 1873b93743b44d6467332585ec6ee8b4aabb7f19 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 20:28:09 +0100 Subject: [PATCH 49/53] Optimized icons. --- packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg | 2 +- packages/ckeditor5-core/theme/icons/image-asset-manager.svg | 2 +- packages/ckeditor5-core/theme/icons/image-upload.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg b/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg index 8c702cd4cf4..9bfaba8f5ce 100644 --- a/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg +++ b/packages/ckeditor5-ckbox/theme/icons/ckbox-image-edit.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-asset-manager.svg b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg index 3aee54400b2..c24ae034fc9 100644 --- a/packages/ckeditor5-core/theme/icons/image-asset-manager.svg +++ b/packages/ckeditor5-core/theme/icons/image-asset-manager.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ckeditor5-core/theme/icons/image-upload.svg b/packages/ckeditor5-core/theme/icons/image-upload.svg index 2a789667920..6d784f751f9 100644 --- a/packages/ckeditor5-core/theme/icons/image-upload.svg +++ b/packages/ckeditor5-core/theme/icons/image-upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 0dc5353562f00f149f455bfd131695ab1b53cf53 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 4 Dec 2023 20:52:02 +0100 Subject: [PATCH 50/53] Added `FileDialogButtonView#buttonView` property for backward compatibility. --- .../ckeditor5-upload/src/ui/filedialogbuttonview.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts index a1d812aba0d..41f8b1456bd 100644 --- a/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts +++ b/packages/ckeditor5-upload/src/ui/filedialogbuttonview.ts @@ -36,6 +36,13 @@ import type { Locale } from '@ckeditor/ckeditor5-utils'; * ``` */ export default class FileDialogButtonView extends ButtonView { + /** + * The button view of the component. + * + * @deprecated + */ + public buttonView: ButtonView; + /** * A hidden `` view used to execute file dialog. */ @@ -64,6 +71,9 @@ export default class FileDialogButtonView extends ButtonView { constructor( locale?: Locale ) { super( locale ); + // For backward compatibility. + this.buttonView = this; + this._fileInputView = new FileInputView( locale ); this._fileInputView.bind( 'acceptedType' ).to( this ); this._fileInputView.bind( 'allowMultipleFiles' ).to( this ); From 794751eb449022a6e4a73b8c2dc7e17e1c2c5cec Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 5 Dec 2023 11:18:53 +0100 Subject: [PATCH 51/53] Updated to match upstream changes. --- .../src/ckboximageedit/ckboximageeditcommand.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts index 50a3731cfad..17ff55fd4b3 100644 --- a/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboximageedit/ckboximageeditcommand.ts @@ -180,8 +180,9 @@ export default class CKBoxImageEditCommand extends Command { this._wrapper.remove(); this._wrapper = null; - this.refresh(); this.editor.editing.view.focus(); + + this.refresh(); } /** From 8a11832ccda486a6d22a270dedfcccbad44b3274 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 5 Dec 2023 14:00:29 +0100 Subject: [PATCH 52/53] Removed old/invalid config from demos. --- .../docs/_snippets/features/block-quote-source.js | 5 ----- .../docs/_snippets/features/build-drag-drop-source.js | 5 ----- .../ckeditor5-heading/docs/_snippets/features/title.js | 5 ----- .../docs/_snippets/features/minimap.js | 5 ----- .../docs/_snippets/features/source-editing-imports.js | 5 ----- .../docs/_snippets/features/build-table-source.js | 5 ----- .../docs/_snippets/features/build-word-count-source.js | 10 ---------- 7 files changed, 40 deletions(-) diff --git a/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js b/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js index 05f5a5b8eb2..8619b7be28f 100644 --- a/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js +++ b/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js @@ -32,11 +32,6 @@ ClassicEditor.defaultConfig = { '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, ui: { viewportOffset: { top: window.getViewportTopOffsetConfig() diff --git a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js index 1ce771fbee9..92e5ff879a5 100644 --- a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js +++ b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js @@ -124,11 +124,6 @@ const defaultConfig = { table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, ui: { viewportOffset: { top: window.getViewportTopOffsetConfig() diff --git a/packages/ckeditor5-heading/docs/_snippets/features/title.js b/packages/ckeditor5-heading/docs/_snippets/features/title.js index b69c239d8f2..1d33e85566f 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/title.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/title.js @@ -107,11 +107,6 @@ BalloonEditor.defaultConfig = { 'mergeTableCells' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, // This value must be kept in sync with the language defined in webpack.config.js. language: 'en' }; diff --git a/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js b/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js index 782408bfbca..39612e79fdc 100644 --- a/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js +++ b/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js @@ -90,11 +90,6 @@ const config = { extraClasses: 'live-snippet formatted' }, cloudServices: CS_CONFIG, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, ui: { viewportOffset: { top: window.getViewportTopOffsetConfig() diff --git a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js index 25023665368..a1c89986aea 100644 --- a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js +++ b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js @@ -59,11 +59,6 @@ ClassicEditor.defaultConfig = { '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, ui: { viewportOffset: { top: window.getViewportTopOffsetConfig() diff --git a/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js b/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js index 78bfa1c92da..026cf034cfe 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js +++ b/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js @@ -33,11 +33,6 @@ ClassicEditor.defaultConfig = { '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, ui: { viewportOffset: { top: window.getViewportTopOffsetConfig() diff --git a/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js b/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js index c5b2bb575e0..16863832bb8 100644 --- a/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js +++ b/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js @@ -86,11 +86,6 @@ BalloonEditor.defaultConfig = { 'redo' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, image: { toolbar: [ 'imageStyle:inline', @@ -139,11 +134,6 @@ ClassicEditor.defaultConfig = { 'redo' ] }, - insert: { - integrations: [ - 'insertImageViaUrl' - ] - }, image: { toolbar: [ 'imageStyle:inline', From a809c4774dd409ae9bd42a1bb53e96f1acff275b Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 5 Dec 2023 15:10:39 +0100 Subject: [PATCH 53/53] Fixed failing test. --- .../tests/ckboximageedit/ckboximageeditcommand.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js b/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js index eb257d7a0b8..16dc6350d47 100644 --- a/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js +++ b/packages/ckeditor5-ckbox/tests/ckboximageedit/ckboximageeditcommand.js @@ -339,7 +339,7 @@ describe( 'CKBoxImageEditCommand', () => { sinon.assert.calledOnce( focusSpy ); } ); - it( 'should refresh the command after closing the CKBox Image Editor dialog', () => { + it( 'should refresh the command after closing the CKBox Image Editor dialog', async () => { const ckboxImageId = 'example-id'; setModelData( model, @@ -348,11 +348,11 @@ describe( 'CKBoxImageEditCommand', () => { const imageElement = editor.model.document.selection.getSelectedElement(); - const onClose = command._prepareOptions( { + const options = await command._prepareOptions( { element: imageElement, ckboxImageId, controller: new AbortController() - } ).onClose; + } ); const refreshSpy = testUtils.sinon.spy( command, 'refresh' ); @@ -361,7 +361,7 @@ describe( 'CKBoxImageEditCommand', () => { command.execute(); expect( command.value ).to.be.true; - onClose(); + options.onClose(); expect( command.value ).to.be.false; sinon.assert.calledOnce( refreshSpy ); } );