Skip to content

Commit 5125f01

Browse files
feat: nested popover (#2649)
* Move popover types to separate file * tmp * open top * Fix bug with keyboard navigation * Fix bug with scroll * Fix mobile * Add popover header class * Display nested items on mobile * Refactor history * Fix positioning on desktop * Fix tests * Fix child popover indent left * Fix ts errors in popover files * Move files * Rename cn to bem * Clarify comments and rename method * Refactor popover css classes * Rename cls to css * Split popover desktop and mobile classes * Add ability to open popover to the left if not enough space to open to the right * Add nested popover test * Add popover test for mobile screens * Fix tests * Add union type for both popovers * Add global window resize event * Multiple fixes * Move nodes initialization to constructor * Rename handleShowingNestedItems to showNestedItems * Replace WindowResize with EditorMobileLayoutToggled * New doze of fixes * Review fixes * Fixes * Fixes * Make each nested popover decide itself if it should open top * Update changelog * Update changelog * Update changelog
1 parent ecdd733 commit 5125f01

32 files changed

+1770
-760
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 2.30.1
4+
5+
`New` – Block Tunes now supports nesting items
6+
37
### 2.30.0
48

59
- `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig)

src/components/dom.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ export default class Dom {
5252
* @param {object} [attributes] - any attributes
5353
* @returns {HTMLElement}
5454
*/
55-
public static make(tagName: string, classNames: string | string[] | null = null, attributes: object = {}): HTMLElement {
55+
public static make(tagName: string, classNames: string | (string | undefined)[] | null = null, attributes: object = {}): HTMLElement {
5656
const el = document.createElement(tagName);
5757

5858
if (Array.isArray(classNames)) {
59-
el.classList.add(...classNames);
59+
const validClassnames = classNames.filter(className => className !== undefined) as string[];
60+
61+
el.classList.add(...validClassnames);
6062
} else if (classNames) {
6163
el.classList.add(classNames);
6264
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Fired when editor mobile layout toggled
3+
*/
4+
export const EditorMobileLayoutToggled = 'editor mobile layout toggled';
5+
6+
/**
7+
* Payload that will be passed with the event
8+
*/
9+
export interface EditorMobileLayoutToggledPayload {
10+
/**
11+
* True, if mobile layout enabled
12+
*/
13+
isEnabled: boolean;
14+
}
15+

src/components/events/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BlockChanged, BlockChangedPayload } from './BlockChanged';
33
import { BlockHovered, BlockHoveredPayload } from './BlockHovered';
44
import { FakeCursorAboutToBeToggled, FakeCursorAboutToBeToggledPayload } from './FakeCursorAboutToBeToggled';
55
import { FakeCursorHaveBeenSet, FakeCursorHaveBeenSetPayload } from './FakeCursorHaveBeenSet';
6+
import { EditorMobileLayoutToggled, EditorMobileLayoutToggledPayload } from './EditorMobileLayoutToggled';
67

78
/**
89
* Events fired by Editor Event Dispatcher
@@ -11,7 +12,8 @@ export {
1112
RedactorDomChanged,
1213
BlockChanged,
1314
FakeCursorAboutToBeToggled,
14-
FakeCursorHaveBeenSet
15+
FakeCursorHaveBeenSet,
16+
EditorMobileLayoutToggled
1517
};
1618

1719
/**
@@ -23,4 +25,5 @@ export interface EditorEventMap {
2325
[BlockChanged]: BlockChangedPayload;
2426
[FakeCursorAboutToBeToggled]: FakeCursorAboutToBeToggledPayload;
2527
[FakeCursorHaveBeenSet]: FakeCursorHaveBeenSetPayload;
28+
[EditorMobileLayoutToggled]: EditorMobileLayoutToggledPayload
2629
}

src/components/flipper.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,11 @@ export default class Flipper {
4949

5050
/**
5151
* Instance of flipper iterator
52-
*
53-
* @type {DomIterator|null}
5452
*/
55-
private readonly iterator: DomIterator = null;
53+
private readonly iterator: DomIterator | null = null;
5654

5755
/**
5856
* Flag that defines activation status
59-
*
60-
* @type {boolean}
6157
*/
6258
private activated = false;
6359

@@ -77,7 +73,7 @@ export default class Flipper {
7773
private flipCallbacks: Array<() => void> = [];
7874

7975
/**
80-
* @param {FlipperOptions} options - different constructing settings
76+
* @param options - different constructing settings
8177
*/
8278
constructor(options: FlipperOptions) {
8379
this.iterator = new DomIterator(options.items, options.focusedItemClass);
@@ -110,7 +106,6 @@ export default class Flipper {
110106
*/
111107
public activate(items?: HTMLElement[], cursorPosition?: number): void {
112108
this.activated = true;
113-
114109
if (items) {
115110
this.iterator.setItems(items);
116111
}

src/components/modules/toolbar/blockSettings.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
77
import Flipper from '../../flipper';
88
import { TunesMenuConfigItem } from '../../../../types/tools';
99
import { resolveAliases } from '../../utils/resolve-aliases';
10-
import Popover, { PopoverEvent } from '../../utils/popover';
10+
import { type Popover, PopoverDesktop, PopoverMobile } from '../../utils/popover';
11+
import { PopoverEvent } from '../../utils/popover/popover.types';
12+
import { isMobileScreen } from '../../utils';
13+
import { EditorMobileLayoutToggled } from '../../events';
1114

1215
/**
1316
* HTML Elements that used for BlockSettings
@@ -27,8 +30,6 @@ interface BlockSettingsNodes {
2730
export default class BlockSettings extends Module<BlockSettingsNodes> {
2831
/**
2932
* Module Events
30-
*
31-
* @returns {{opened: string, closed: string}}
3233
*/
3334
public get events(): { opened: string; closed: string } {
3435
return {
@@ -56,8 +57,12 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
5657
*
5758
* @todo remove once BlockSettings becomes standalone non-module class
5859
*/
59-
public get flipper(): Flipper {
60-
return this.popover?.flipper;
60+
public get flipper(): Flipper | undefined {
61+
if (this.popover === null) {
62+
return;
63+
}
64+
65+
return 'flipper' in this.popover ? this.popover?.flipper : undefined;
6166
}
6267

6368
/**
@@ -67,9 +72,9 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
6772

6873
/**
6974
* Popover instance. There is a util for vertical lists.
75+
* Null until popover is not initialized
7076
*/
71-
private popover: Popover | undefined;
72-
77+
private popover: Popover | null = null;
7378

7479
/**
7580
* Panel with block settings with 2 sections:
@@ -82,13 +87,17 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
8287
if (import.meta.env.MODE === 'test') {
8388
this.nodes.wrapper.setAttribute('data-cy', 'block-tunes');
8489
}
90+
91+
this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close);
8592
}
8693

8794
/**
8895
* Destroys module
8996
*/
9097
public destroy(): void {
9198
this.removeAllNodes();
99+
this.listeners.destroy();
100+
this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close);
92101
}
93102

94103
/**
@@ -118,7 +127,10 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
118127

119128
/** Tell to subscribers that block settings is opened */
120129
this.eventsDispatcher.emit(this.events.opened);
121-
this.popover = new Popover({
130+
131+
const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
132+
133+
this.popover = new PopoverClass({
122134
searchable: true,
123135
items: tunesItems.map(tune => this.resolveTuneAliases(tune)),
124136
customContent: customHtmlTunesContainer,
@@ -132,22 +144,22 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
132144

133145
this.popover.on(PopoverEvent.Close, this.onPopoverClose);
134146

135-
this.nodes.wrapper.append(this.popover.getElement());
147+
this.nodes.wrapper?.append(this.popover.getElement());
136148

137149
this.popover.show();
138150
}
139151

140152
/**
141153
* Returns root block settings element
142154
*/
143-
public getElement(): HTMLElement {
155+
public getElement(): HTMLElement | undefined {
144156
return this.nodes.wrapper;
145157
}
146158

147159
/**
148160
* Close Block Settings pane
149161
*/
150-
public close(): void {
162+
public close = (): void => {
151163
if (!this.opened) {
152164
return;
153165
}
@@ -183,7 +195,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
183195
this.popover.getElement().remove();
184196
this.popover = null;
185197
}
186-
}
198+
};
187199

188200
/**
189201
* Handles popover close event

src/components/modules/toolbar/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
220220
};
221221
}
222222

223+
223224
/**
224225
* Toggles read-only mode
225226
*
@@ -479,9 +480,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
479480
}
480481
});
481482

482-
return this.toolboxInstance.make();
483+
return this.toolboxInstance.getElement();
483484
}
484485

486+
485487
/**
486488
* Handler for Plus Button
487489
*/

src/components/modules/ui.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { mobileScreenBreakpoint } from '../utils';
1616
import styles from '../../styles/main.css?inline';
1717
import { BlockHovered } from '../events/BlockHovered';
1818
import { selectionChangeDebounceTimeout } from '../constants';
19+
import { EditorMobileLayoutToggled } from '../events';
1920
/**
2021
* HTML Elements used for UI
2122
*/
@@ -121,7 +122,7 @@ export default class UI extends Module<UINodes> {
121122
/**
122123
* Detect mobile version
123124
*/
124-
this.checkIsMobile();
125+
this.setIsMobile();
125126

126127
/**
127128
* Make main UI elements
@@ -234,10 +235,21 @@ export default class UI extends Module<UINodes> {
234235
}
235236

236237
/**
237-
* Check for mobile mode and cache a result
238+
* Check for mobile mode and save the result
238239
*/
239-
private checkIsMobile(): void {
240-
this.isMobile = window.innerWidth < mobileScreenBreakpoint;
240+
private setIsMobile(): void {
241+
const isMobile = window.innerWidth < mobileScreenBreakpoint;
242+
243+
if (isMobile !== this.isMobile) {
244+
/**
245+
* Dispatch global event
246+
*/
247+
this.eventsDispatcher.emit(EditorMobileLayoutToggled, {
248+
isEnabled: this.isMobile,
249+
});
250+
}
251+
252+
this.isMobile = isMobile;
241253
}
242254

243255
/**
@@ -426,7 +438,7 @@ export default class UI extends Module<UINodes> {
426438
/**
427439
* Detect mobile version
428440
*/
429-
this.checkIsMobile();
441+
this.setIsMobile();
430442
}
431443

432444
/**

0 commit comments

Comments
 (0)