From 3a729b03c384bf1e5bd66c04cd8667a97a08a44a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 11 Aug 2023 17:02:08 +0200 Subject: [PATCH 01/84] Poc of groups within ListView component instances. --- .../ckeditor5-ui/components/list/list.css | 11 +++ packages/ckeditor5-ui/src/dropdown/utils.ts | 94 +++++++++++++------ packages/ckeditor5-ui/src/index.ts | 1 + .../src/list/listitemgroupview.ts | 88 +++++++++++++++++ packages/ckeditor5-ui/src/list/listview.ts | 54 +++++++++-- .../tests/manual/dropdown/dropdown.html | 5 + .../tests/manual/dropdown/dropdown.js | 76 +++++++++++++++ .../ckeditor5-ui/tests/manual/list/list.html | 13 +++ .../ckeditor5-ui/tests/manual/list/list.js | 60 ++++++++++++ .../ckeditor5-ui/tests/manual/list/list.md | 1 + 10 files changed, 370 insertions(+), 33 deletions(-) create mode 100644 packages/ckeditor5-ui/src/list/listitemgroupview.ts create mode 100644 packages/ckeditor5-ui/tests/manual/list/list.html create mode 100644 packages/ckeditor5-ui/tests/manual/list/list.js create mode 100644 packages/ckeditor5-ui/tests/manual/list/list.md 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 9199dd73572..88b45aac15e 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 @@ -77,6 +77,17 @@ } } +.ck-list .ck-list__group { + padding-top: var(--ck-spacing-medium); + border-top: 1px solid var(--ck-color-base-border); + + & > li:first-child > span { + font-size: 11px; + font-weight: bold; + padding: var(--ck-spacing-medium); + } +} + .ck.ck-list__separator { height: 1px; width: 100%; diff --git a/packages/ckeditor5-ui/src/dropdown/utils.ts b/packages/ckeditor5-ui/src/dropdown/utils.ts index 8a4130c88fa..1981aa8d318 100644 --- a/packages/ckeditor5-ui/src/dropdown/utils.ts +++ b/packages/ckeditor5-ui/src/dropdown/utils.ts @@ -39,6 +39,7 @@ import { import '../../theme/components/dropdown/toolbardropdown.css'; import '../../theme/components/dropdown/listdropdown.css'; +import ListItemGroupView from '../list/listitemgroupview'; /** * A helper for creating dropdowns. It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown}, @@ -345,38 +346,14 @@ function addListToOpenDropdown( role?: string; } ): void { - const locale = dropdownView.locale; - + const locale = dropdownView.locale!; const listView = dropdownView.listView = new ListView( locale ); const items = typeof itemsOrCallback == 'function' ? itemsOrCallback() : itemsOrCallback; listView.ariaLabel = options.ariaLabel; listView.role = options.role; - listView.items.bindTo( items ).using( def => { - if ( def.type === 'separator' ) { - return new ListSeparatorView( locale ); - } else if ( def.type === 'button' || def.type === 'switchbutton' ) { - const listItemView = new ListItemView( locale ); - let buttonView; - - if ( def.type === 'button' ) { - buttonView = new ButtonView( locale ); - } else { - buttonView = new SwitchButtonView( locale ); - } - - // Bind all model properties to the button view. - buttonView.bind( ...Object.keys( def.model ) as Array ).to( def.model ); - buttonView.delegate( 'execute' ).to( listItemView ); - - listItemView.children.add( buttonView ); - - return listItemView; - } - - return null; - } ); + bindViewCollectionItemsToDefinitions( dropdownView, listView.items, items, locale ); dropdownView.panelView.children.add( listView ); @@ -547,11 +524,60 @@ function focusDropdownPanelOnOpen( dropdownView: DropdownView ) { }, { priority: 'low' } ); } +/** + * TODO + * + * @param items + * @param definitions + * @param locale + */ +function bindViewCollectionItemsToDefinitions( + dropdownView: DropdownView, + items: ViewCollection, + definitions: Collection, + locale: Locale +) { + items.bindTo( definitions ).using( def => { + if ( def.type === 'separator' ) { + return new ListSeparatorView( locale ); + } else if ( def.type === 'group' ) { + const groupView = new ListItemGroupView( locale ); + + groupView.set( { label: def.label } ); + + bindViewCollectionItemsToDefinitions( dropdownView, groupView.items, def.items, locale ); + + groupView.items.delegate( 'execute' ).to( dropdownView ); + + return groupView; + } else if ( def.type === 'button' || def.type === 'switchbutton' ) { + const listItemView = new ListItemView( locale ); + let buttonView; + + if ( def.type === 'button' ) { + buttonView = new ButtonView( locale ); + } else { + buttonView = new SwitchButtonView( locale ); + } + + // Bind all model properties to the button view. + buttonView.bind( ...Object.keys( def.model ) as Array ).to( def.model ); + buttonView.delegate( 'execute' ).to( listItemView ); + + listItemView.children.add( buttonView ); + + return listItemView; + } + + return null; + } ); +} + /** * A definition of the list item used by the {@link module:ui/dropdown/utils~addListToDropdown} * utility. */ -export type ListDropdownItemDefinition = ListDropdownSeparatorDefinition | ListDropdownButtonDefinition; +export type ListDropdownItemDefinition = ListDropdownSeparatorDefinition | ListDropdownButtonDefinition | ListDropdownGroupDefinition; /** * A definition of the 'separator' list item. @@ -571,3 +597,17 @@ export type ListDropdownButtonDefinition = { */ model: Model; }; + +export type ListDropdownGroupDefinition = { + type: 'group'; + + /** + * TODO + */ + label: string; + + /** + * TODO + */ + items: Collection; +}; diff --git a/packages/ckeditor5-ui/src/index.ts b/packages/ckeditor5-ui/src/index.ts index 36185b2cace..d506c00cf67 100644 --- a/packages/ckeditor5-ui/src/index.ts +++ b/packages/ckeditor5-ui/src/index.ts @@ -60,6 +60,7 @@ export { default as LabelView } from './label/labelview'; export { default as LabeledFieldView } from './labeledfield/labeledfieldview'; export * from './labeledfield/utils'; +export { default as ListItemGroupView } from './list/listitemgroupview'; export { default as ListItemView } from './list/listitemview'; export { default as ListView } from './list/listview'; diff --git a/packages/ckeditor5-ui/src/list/listitemgroupview.ts b/packages/ckeditor5-ui/src/list/listitemgroupview.ts new file mode 100644 index 00000000000..15adbdcc537 --- /dev/null +++ b/packages/ckeditor5-ui/src/list/listitemgroupview.ts @@ -0,0 +1,88 @@ +/** + * @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 ui/list/listitemgroupview + */ + +import View from '../view'; + +import type { FocusableView } from '../focuscycler'; +import type ViewCollection from '../viewcollection'; + +import { uid, type Locale } from '@ckeditor/ckeditor5-utils'; +import type ListItemView from './listitemview'; + +/** + * TODO + */ +export default class ListItemGroupView extends View { + /** + * TODO + */ + declare public label: string; + + /** + * Collection of the child views inside of the {@link #element}. + */ + public readonly items: ViewCollection; + + /** + * @inheritDoc + */ + constructor( locale?: Locale ) { + super( locale ); + + const bind = this.bindTemplate; + + this.items = this.createCollection(); + + const groupLabelId = `ck-editor__label_${ uid() }`; + + this.setTemplate( { + tag: 'li', + + attributes: { + class: [ + 'ck', + 'ck-list__group' + ] + }, + + children: [ + { + tag: 'li', + attributes: { + role: 'presentation', + id: groupLabelId + }, + children: [ + { + tag: 'span', + children: [ + { text: bind.to( 'label' ) } + ] + }, + { + tag: 'ul', + attributes: { + role: 'group', + 'aria-labelledby': groupLabelId + }, + children: this.items + } + ] + } + ] + } ); + } + + /** + * Focuses the list item. + */ + public focus(): void { + ( this.items.first as FocusableView ).focus(); + } +} diff --git a/packages/ckeditor5-ui/src/list/listview.ts b/packages/ckeditor5-ui/src/list/listview.ts index e53152cb5de..5efd0deb5a0 100644 --- a/packages/ckeditor5-ui/src/list/listview.ts +++ b/packages/ckeditor5-ui/src/list/listview.ts @@ -10,9 +10,10 @@ import View from '../view'; import FocusCycler from '../focuscycler'; -import type ListItemView from './listitemview'; +import ListItemView from './listitemview'; +import ListItemGroupView from './listitemgroupview'; import type DropdownPanelFocusable from '../dropdown/dropdownpanelfocusable'; -import type ViewCollection from '../viewcollection'; +import ViewCollection from '../viewcollection'; import { FocusTracker, @@ -28,6 +29,11 @@ import '../../theme/components/list/list.css'; * The list view class. */ export default class ListView extends View implements DropdownPanelFocusable { + /** + * TODO + */ + public readonly focusables: ViewCollection; + /** * Collection of the child list views. */ @@ -70,12 +76,13 @@ export default class ListView extends View implements Dropdown const bind = this.bindTemplate; + this.focusables = new ViewCollection(); this.items = this.createCollection(); this.focusTracker = new FocusTracker(); this.keystrokes = new KeystrokeHandler(); this._focusCycler = new FocusCycler( { - focusables: this.items, + focusables: this.focusables, focusTracker: this.focusTracker, keystrokeHandler: this.keystrokes, actions: { @@ -113,17 +120,52 @@ export default class ListView extends View implements Dropdown public override render(): void { super.render(); + const registerItem = ( item: ListItemView ) => { + if ( this.focusables.has( item ) ) { + return; + } + + this.focusTracker.add( item.element! ); + this.focusables.add( item ); + }; + + const registerGroupItems = ( group: ListItemGroupView ) => { + for ( const child of group.items ) { + registerItem( child ); + } + }; + // Items added before rendering should be known to the #focusTracker. for ( const item of this.items ) { - this.focusTracker.add( item.element! ); + if ( item instanceof ListItemView ) { + registerItem( item ); + } else if ( item instanceof ListItemGroupView ) { + registerGroupItems( item ); + } } this.items.on>( 'add', ( evt, item ) => { - this.focusTracker.add( item.element! ); + if ( item instanceof ListItemView ) { + registerItem( item ); + } + + if ( item instanceof ListItemGroupView ) { + registerGroupItems( item ); + } } ); this.items.on>( 'remove', ( evt, item ) => { - this.focusTracker.remove( item.element! ); + if ( item instanceof ListItemView ) { + this.focusTracker.remove( item.element! ); + this.focusables.remove( item ); + } + + if ( item instanceof ListItemGroupView ) { + for ( const child of item.items ) { + this.focusTracker.remove( child.element! ); + this.focusables.remove( child ); + } + } } ); // Start listening for the keystrokes coming from #element. diff --git a/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.html b/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.html index 70104b6ac55..049b8aa85e8 100644 --- a/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.html +++ b/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.html @@ -6,6 +6,11 @@

Dropdown with ListView

+

Dropdown with ListView (and groups)

+ +
+ +

Long label (truncated)

diff --git a/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.js b/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.js index 707a46d3e48..b152c0d5a8d 100644 --- a/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.js +++ b/packages/ckeditor5-ui/tests/manual/dropdown/dropdown.js @@ -21,6 +21,7 @@ import { createDropdown, addToolbarToDropdown, addListToDropdown } from '../../. const ui = testUtils.createTestUIView( { dropdown: '#dropdown', listDropdown: '#list-dropdown', + listDropdownWithGroups: '#list-dropdown-with-groups', dropdownLabel: '#dropdown-label', toolbarDropdown: '#dropdown-toolbar', splitButton: '#dropdown-splitbutton' @@ -76,6 +77,80 @@ function testList() { window.Model = Model; } +function testListWithGroups() { + const collection = new Collection( { idProperty: 'label' } ); + + collection.addMany( [ + { + type: 'button', + model: new Model( { + label: 'Item 1', + withText: true + } ) + }, + { + type: 'group', + label: 'Group 1', + items: new Collection( [ + { + type: 'button', + model: new Model( { + label: 'Group 1, Item 1', + withText: true + } ) + }, + { + type: 'button', + model: new Model( { + label: 'Group 1, Item 1', + withText: true + } ) + } + ] ) + }, + { + type: 'group', + label: 'Group 2', + items: new Collection( [ + { + type: 'button', + model: new Model( { + label: 'Group 2, Item 1', + withText: true + } ) + }, + { + type: 'button', + model: new Model( { + label: 'Group 2, Item 1', + withText: true + } ) + } + ] ) + } + ] ); + + const dropdownView = createDropdown( {} ); + + dropdownView.buttonView.set( { + label: 'ListDropdown (with groups)', + isEnabled: true, + isOn: false, + withText: true + } ); + + addListToDropdown( dropdownView, collection ); + + dropdownView.on( 'execute', evt => { + console.log( 'List#execute:', evt.source.label ); + } ); + + ui.listDropdownWithGroups.add( dropdownView ); + + window.listDropdownWithGroupsCollection = collection; + window.Model = Model; +} + function testLongLabel() { const dropdownView = createDropdown( {} ); @@ -160,6 +235,7 @@ function testSplitButton() { testEmpty(); testList(); +testListWithGroups(); testLongLabel(); testToolbar(); testSplitButton(); diff --git a/packages/ckeditor5-ui/tests/manual/list/list.html b/packages/ckeditor5-ui/tests/manual/list/list.html new file mode 100644 index 00000000000..6be06b12b32 --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/list/list.html @@ -0,0 +1,13 @@ +
+ + diff --git a/packages/ckeditor5-ui/tests/manual/list/list.js b/packages/ckeditor5-ui/tests/manual/list/list.js new file mode 100644 index 00000000000..f95c894b91f --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/list/list.js @@ -0,0 +1,60 @@ +/** + * @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 + */ + +/* global document */ + +import { ButtonView, ListItemGroupView, ListItemView, ListView } from '../../../src'; + +const playground = document.querySelector( '#playground' ); + +const defaultView = new ListView(); +defaultView.render(); +playground.appendChild( defaultView.element ); + +const grouppedView = new ListView(); +grouppedView.render(); +playground.appendChild( grouppedView.element ); + +defaultView.items.addMany( [ + createItem( 'Item 1' ), + createItem( 'Item 2' ), + createItem( 'Item 3' ), + createItem( 'Item 4' ), + createItem( 'Item 5' ) +] ); + +grouppedView.items.addMany( [ + createItem( 'Item 1' ), + createItem( 'Item 2' ), + createGroup( 'Items group 1', [ + createItem( 'Item 1.1' ), + createItem( 'Item 1.2' ) + ] ), + createGroup( 'Items group 2', [ + createItem( 'Item 2.1' ), + createItem( 'Item 2.2' ) + ] ) +] ); + +function createItem( label ) { + const item = new ListItemView(); + const button = new ButtonView(); + + item.children.add( button ); + + button.set( { label, withText: true } ); + + return item; +} + +function createGroup( label, items ) { + const groupView = new ListItemGroupView(); + + groupView.label = label; + groupView.items.addMany( items ); + + return groupView; +} + diff --git a/packages/ckeditor5-ui/tests/manual/list/list.md b/packages/ckeditor5-ui/tests/manual/list/list.md new file mode 100644 index 00000000000..1333ed77b7e --- /dev/null +++ b/packages/ckeditor5-ui/tests/manual/list/list.md @@ -0,0 +1 @@ +TODO From 8896812ea79cba120b7d167966ac35f4cb874e7b Mon Sep 17 00:00:00 2001 From: DawidKossowskii Date: Thu, 31 Aug 2023 09:12:49 +0200 Subject: [PATCH 02/84] Implementing textarea component --- packages/ckeditor5-ui/src/index.ts | 2 + packages/ckeditor5-ui/src/input/inputbase.ts | 170 ++++++++++++++++ packages/ckeditor5-ui/src/input/inputview.ts | 150 +------------- .../ckeditor5-ui/src/textarea/textareaview.ts | 65 ++++++ .../ckeditor5-ui/tests/input/inputbase.js | 97 +++++++++ .../ckeditor5-ui/tests/input/inputview.js | 63 ------ .../ckeditor5-ui/tests/textarea/textarea.js | 186 ++++++++++++++++++ 7 files changed, 523 insertions(+), 210 deletions(-) create mode 100644 packages/ckeditor5-ui/src/input/inputbase.ts create mode 100644 packages/ckeditor5-ui/src/textarea/textareaview.ts create mode 100644 packages/ckeditor5-ui/tests/input/inputbase.js create mode 100644 packages/ckeditor5-ui/tests/textarea/textarea.js diff --git a/packages/ckeditor5-ui/src/index.ts b/packages/ckeditor5-ui/src/index.ts index d506c00cf67..74375d83000 100644 --- a/packages/ckeditor5-ui/src/index.ts +++ b/packages/ckeditor5-ui/src/index.ts @@ -54,6 +54,8 @@ export { default as InputView } from './input/inputview'; export { default as InputTextView } from './inputtext/inputtextview'; export { default as InputNumberView } from './inputnumber/inputnumberview'; +export { default as TextareaView } from './textarea/textareaview'; + export { default as IframeView } from './iframe/iframeview'; export { default as LabelView } from './label/labelview'; diff --git a/packages/ckeditor5-ui/src/input/inputbase.ts b/packages/ckeditor5-ui/src/input/inputbase.ts new file mode 100644 index 00000000000..d32a27839b3 --- /dev/null +++ b/packages/ckeditor5-ui/src/input/inputbase.ts @@ -0,0 +1,170 @@ +/** + * @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 ui/input/inputbase + */ + +import View from '../view'; + +import { + FocusTracker, + type Locale, + type ObservableChangeEvent +} from '@ckeditor/ckeditor5-utils'; + +/** + * The base input view class. + */ +export default abstract class InputBase extends View { + /** + * Stores information about the editor UI focus and propagates it so various plugins and components + * are unified as a focus group. + */ + public readonly focusTracker: FocusTracker; + + /** + * The value of the input. + * + * @observable + */ + declare public value: string | undefined; + + /** + * The `id` attribute of the input (i.e. to pair with a `