@elementor/editor-controls 3.32.0-95 → 3.33.0-101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "3.32.0-95",
4
+ "version": "3.33.0-101",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,21 +40,21 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "3.32.0-95",
44
- "@elementor/editor-elements": "3.32.0-95",
45
- "@elementor/editor-props": "3.32.0-95",
46
- "@elementor/editor-responsive": "3.32.0-95",
47
- "@elementor/editor-ui": "3.32.0-95",
48
- "@elementor/editor-v1-adapters": "3.32.0-95",
49
- "@elementor/env": "3.32.0-95",
50
- "@elementor/http-client": "3.32.0-95",
43
+ "@elementor/editor-current-user": "3.33.0-101",
44
+ "@elementor/editor-elements": "3.33.0-101",
45
+ "@elementor/editor-props": "3.33.0-101",
46
+ "@elementor/editor-responsive": "3.33.0-101",
47
+ "@elementor/editor-ui": "3.33.0-101",
48
+ "@elementor/editor-v1-adapters": "3.33.0-101",
49
+ "@elementor/env": "3.33.0-101",
50
+ "@elementor/http-client": "3.33.0-101",
51
51
  "@elementor/icons": "^1.51.1",
52
- "@elementor/locations": "3.32.0-95",
53
- "@elementor/query": "3.32.0-95",
54
- "@elementor/session": "3.32.0-95",
52
+ "@elementor/locations": "3.33.0-101",
53
+ "@elementor/query": "3.33.0-101",
54
+ "@elementor/session": "3.33.0-101",
55
55
  "@elementor/ui": "1.36.12",
56
- "@elementor/utils": "3.32.0-95",
57
- "@elementor/wp-media": "3.32.0-95",
56
+ "@elementor/utils": "3.33.0-101",
57
+ "@elementor/wp-media": "3.33.0-101",
58
58
  "@wordpress/i18n": "^5.13.0",
59
59
  "@monaco-editor/react": "^4.7.0"
60
60
  },
@@ -7,8 +7,15 @@ export const EditorWrapper = styled( Box )`
7
7
  position: relative;
8
8
  height: 200px;
9
9
 
10
- .monaco-editor .colorpicker-widget {
11
- z-index: 99999999 !important;
10
+ .monaco-editor .suggest-widget {
11
+ width: 220px !important;
12
+ max-width: 220px !important;
13
+ }
14
+
15
+ .visual-content-dimmed {
16
+ opacity: 0.6;
17
+ color: #aaa !important;
18
+ pointer-events: none;
12
19
  }
13
20
  `;
14
21
 
@@ -5,12 +5,13 @@ import { useTheme } from '@elementor/ui';
5
5
  import { Editor } from '@monaco-editor/react';
6
6
 
7
7
  import { EditorWrapper } from './css-editor.styles';
8
- import { setCustomSyntaxRules, validate } from './css-validation';
8
+ import { clearMarkersFromVisualContent, setCustomSyntaxRules, validate } from './css-validation';
9
9
  import { ResizeHandleComponent } from './resize-handle';
10
+ import { preventChangeOnVisualContent } from './visual-content-change-protection';
10
11
 
11
12
  type CssEditorProps = {
12
13
  value: string;
13
- onChange: ( value: string ) => void;
14
+ onChange: ( value: string, isValid: boolean ) => void;
14
15
  };
15
16
 
16
17
  const setVisualContent = ( value: string ): string => {
@@ -29,82 +30,23 @@ const getActual = ( value: string ): string => {
29
30
  .join( '\n' );
30
31
  };
31
32
 
32
- const preventChangeOnVisualContent = ( editor: editor.IStandaloneCodeEditor, monaco: MonacoEditor ) => {
33
- const model = editor.getModel();
34
- if ( ! model ) {
35
- return;
36
- }
37
-
38
- editor.onKeyDown( ( e ) => {
39
- const position = editor.getPosition();
40
- if ( ! position ) {
41
- return;
42
- }
43
-
44
- const totalLines = model.getLineCount();
45
- const isInProtectedRange = position.lineNumber === 1 || position.lineNumber === totalLines;
46
-
47
- if ( isInProtectedRange ) {
48
- const allowedKeys = [
49
- monaco.KeyCode.UpArrow,
50
- monaco.KeyCode.DownArrow,
51
- monaco.KeyCode.LeftArrow,
52
- monaco.KeyCode.RightArrow,
53
- monaco.KeyCode.Home,
54
- monaco.KeyCode.End,
55
- monaco.KeyCode.PageUp,
56
- monaco.KeyCode.PageDown,
57
- monaco.KeyCode.Tab,
58
- monaco.KeyCode.Escape,
59
- ];
60
-
61
- if ( ! allowedKeys.includes( e.keyCode ) ) {
62
- e.preventDefault();
63
- e.stopPropagation();
64
- }
65
- }
66
- } );
67
- };
68
-
69
33
  const createEditorDidMountHandler = (
70
34
  editorRef: React.MutableRefObject< editor.IStandaloneCodeEditor | null >,
71
- monacoRef: React.MutableRefObject< MonacoEditor | null >,
72
- debounceTimer: React.MutableRefObject< NodeJS.Timeout | null >,
73
- onChange: ( value: string ) => void
35
+ monacoRef: React.MutableRefObject< MonacoEditor | null >
74
36
  ) => {
75
37
  return ( editor: editor.IStandaloneCodeEditor, monaco: MonacoEditor ) => {
76
38
  editorRef.current = editor;
77
39
  monacoRef.current = monaco;
78
40
 
79
- preventChangeOnVisualContent( editor, monaco );
41
+ preventChangeOnVisualContent( editor );
80
42
 
81
43
  setCustomSyntaxRules( editor, monaco );
82
44
 
83
- editor.onDidChangeModelContent( () => {
84
- const code = editor.getModel()?.getValue() ?? '';
85
- const userContent = getActual( code );
86
-
87
- setCustomSyntaxRules( editor, monaco );
88
-
89
- const currentTimer = debounceTimer.current;
90
- if ( currentTimer ) {
91
- clearTimeout( currentTimer );
92
- }
93
-
94
- const newTimer = setTimeout( () => {
95
- if ( ! editorRef.current || ! monacoRef.current ) {
96
- return;
97
- }
98
-
99
- const hasNoErrors = validate( editorRef.current, monacoRef.current );
100
-
101
- if ( hasNoErrors ) {
102
- onChange( userContent );
103
- }
104
- }, 500 );
105
-
106
- debounceTimer.current = newTimer;
45
+ monaco.editor.onDidChangeMarkers( () => {
46
+ setTimeout( () => clearMarkersFromVisualContent( editor, monaco ), 0 );
107
47
  } );
48
+
49
+ editor.setPosition( { lineNumber: 2, column: ( editor.getModel()?.getLineContent( 2 ).length ?? 0 ) + 1 } );
108
50
  };
109
51
  };
110
52
 
@@ -126,7 +68,35 @@ export const CssEditor = ( { value, onChange }: CssEditorProps ) => {
126
68
  }
127
69
  }, [] );
128
70
 
129
- const handleEditorDidMount = createEditorDidMountHandler( editorRef, monacoRef, debounceTimer, onChange );
71
+ const handleEditorChange = () => {
72
+ if ( ! editorRef.current || ! monacoRef.current ) {
73
+ return;
74
+ }
75
+
76
+ const code = editorRef.current?.getModel()?.getValue() ?? '';
77
+ const userContent = getActual( code );
78
+
79
+ setCustomSyntaxRules( editorRef?.current, monacoRef.current );
80
+
81
+ const currentTimer = debounceTimer.current;
82
+ if ( currentTimer ) {
83
+ clearTimeout( currentTimer );
84
+ }
85
+
86
+ const newTimer = setTimeout( () => {
87
+ if ( ! editorRef.current || ! monacoRef.current ) {
88
+ return;
89
+ }
90
+
91
+ const hasNoErrors = validate( editorRef.current, monacoRef.current );
92
+
93
+ onChange( userContent, hasNoErrors );
94
+ }, 500 );
95
+
96
+ debounceTimer.current = newTimer;
97
+ };
98
+
99
+ const handleEditorDidMount = createEditorDidMountHandler( editorRef, monacoRef );
130
100
 
131
101
  React.useEffect( () => {
132
102
  const timerRef = debounceTimer;
@@ -145,18 +115,24 @@ export const CssEditor = ( { value, onChange }: CssEditorProps ) => {
145
115
  height="100%"
146
116
  language="css"
147
117
  theme={ theme.palette.mode === 'dark' ? 'vs-dark' : 'vs' }
148
- defaultValue={ setVisualContent( value ) }
118
+ value={ setVisualContent( value ) }
149
119
  onMount={ handleEditorDidMount }
120
+ onChange={ handleEditorChange }
150
121
  options={ {
151
- lineNumbers: 'off',
152
- folding: false,
153
- showFoldingControls: 'never',
122
+ lineNumbers: 'on',
123
+ folding: true,
154
124
  minimap: { enabled: false },
155
125
  fontFamily: 'Roboto, Arial, Helvetica, Verdana, sans-serif',
156
126
  fontSize: 12,
157
127
  renderLineHighlight: 'none',
158
128
  hideCursorInOverviewRuler: true,
159
129
  fixedOverflowWidgets: true,
130
+ suggestFontSize: 10,
131
+ suggestLineHeight: 14,
132
+ stickyScroll: {
133
+ enabled: false,
134
+ },
135
+ lineDecorationsWidth: 2,
160
136
  } }
161
137
  />
162
138
  <ResizeHandleComponent
@@ -60,3 +60,16 @@ export function validate( editor: editor.IStandaloneCodeEditor, monaco: MonacoEd
60
60
  const allMarkers = monaco.editor.getModelMarkers( { resource: model.uri } );
61
61
  return allMarkers.filter( ( marker ) => marker.severity === monaco.MarkerSeverity.Error ).length === 0;
62
62
  }
63
+
64
+ export function clearMarkersFromVisualContent( editor: editor.IStandaloneCodeEditor, monaco: MonacoEditor ): void {
65
+ const model = editor.getModel();
66
+
67
+ if ( ! model ) {
68
+ return;
69
+ }
70
+
71
+ const allMarkers = monaco.editor.getModelMarkers( { resource: model.uri } );
72
+ const filteredMarkers = allMarkers.filter( ( marker ) => marker.startLineNumber !== 1 );
73
+ const nonCustomMarkers = filteredMarkers.filter( ( m ) => m.source !== 'custom-css-rules' );
74
+ monaco.editor.setModelMarkers( model, 'css', nonCustomMarkers );
75
+ }
@@ -0,0 +1,69 @@
1
+ import type { editor } from 'monaco-types';
2
+
3
+ export const preventChangeOnVisualContent = ( editor: editor.IStandaloneCodeEditor ) => {
4
+ const model = editor.getModel();
5
+ if ( ! model ) {
6
+ return;
7
+ }
8
+
9
+ const decorationsCollection = editor.createDecorationsCollection();
10
+
11
+ const applyVisualContentStyling = () => {
12
+ const totalLines = model.getLineCount();
13
+ const decorations = [];
14
+
15
+ decorations.push( {
16
+ range: {
17
+ startLineNumber: 1,
18
+ startColumn: 1,
19
+ endLineNumber: 1,
20
+ endColumn: model.getLineContent( 1 ).length + 1,
21
+ },
22
+ options: {
23
+ inlineClassName: 'visual-content-dimmed',
24
+ isWholeLine: false,
25
+ },
26
+ } );
27
+
28
+ if ( totalLines > 1 ) {
29
+ decorations.push( {
30
+ range: {
31
+ startLineNumber: totalLines,
32
+ startColumn: 1,
33
+ endLineNumber: totalLines,
34
+ endColumn: model.getLineContent( totalLines ).length + 1,
35
+ },
36
+ options: {
37
+ inlineClassName: 'visual-content-dimmed',
38
+ isWholeLine: false,
39
+ },
40
+ } );
41
+ }
42
+
43
+ decorationsCollection.set( decorations );
44
+ };
45
+
46
+ applyVisualContentStyling();
47
+
48
+ model.onDidChangeContent( () => {
49
+ applyVisualContentStyling();
50
+ } );
51
+
52
+ const originalPushEditOperations = model.pushEditOperations;
53
+ model.pushEditOperations = function ( beforeCursorState, editOperations, cursorStateComputer ) {
54
+ const totalLines = model.getLineCount();
55
+
56
+ const filteredOperations = editOperations.filter( ( operation ) => {
57
+ const range = operation.range;
58
+ const affectsProtectedLine =
59
+ range.startLineNumber === 1 ||
60
+ range.endLineNumber === 1 ||
61
+ range.startLineNumber === totalLines ||
62
+ range.endLineNumber === totalLines;
63
+
64
+ return ! affectsProtectedLine;
65
+ } );
66
+
67
+ return originalPushEditOperations.call( this, beforeCursorState, filteredOperations, cursorStateComputer );
68
+ };
69
+ };
@@ -22,6 +22,7 @@ type ItemSelectorProps = {
22
22
  itemStyle?: ( item: SelectableItem ) => React.CSSProperties;
23
23
  onDebounce?: ( name: string ) => void;
24
24
  icon: React.ElementType< { fontSize: string } >;
25
+ disabledItems?: string[];
25
26
  };
26
27
 
27
28
  export const ItemSelector = ( {
@@ -34,10 +35,11 @@ export const ItemSelector = ( {
34
35
  itemStyle = () => ( {} ),
35
36
  onDebounce = () => {},
36
37
  icon,
38
+ disabledItems,
37
39
  }: ItemSelectorProps ) => {
38
40
  const [ searchValue, setSearchValue ] = useState( '' );
39
41
 
40
- const filteredItemsList = useFilteredItemsList( itemsList, searchValue );
42
+ const filteredItemsList = useFilteredItemsList( itemsList, searchValue, disabledItems );
41
43
 
42
44
  const IconComponent = icon;
43
45
 
@@ -128,6 +130,7 @@ type ItemListProps = {
128
130
  selectedItem: string | null;
129
131
  itemStyle?: ( item: SelectableItem ) => React.CSSProperties;
130
132
  onDebounce?: ( name: string ) => void;
133
+ disabledItems?: string[];
131
134
  };
132
135
 
133
136
  const ItemList = ( {
@@ -5,6 +5,7 @@ import { type PopupState, usePopupState } from '@elementor/ui';
5
5
 
6
6
  import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
7
7
  import { useSyncExternalState } from '../../../hooks/use-sync-external-state';
8
+ import { eventBus } from '../../../services/event-bus';
8
9
  import { type Item, type RepeatablePropValue } from '../types';
9
10
 
10
11
  type SetterFn< T > = ( prevItems: T ) => T;
@@ -46,7 +47,11 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
46
47
  children,
47
48
  initial,
48
49
  propTypeUtil,
49
- }: React.PropsWithChildren< { initial: T; propTypeUtil: PropTypeUtil< string, T[] >; isSortable?: boolean } > ) => {
50
+ }: React.PropsWithChildren< {
51
+ initial: T;
52
+ propTypeUtil: PropTypeUtil< string, T[] >;
53
+ isSortable?: boolean;
54
+ } > ) => {
50
55
  const { value: repeaterValues, setValue: setRepeaterValues } = useBoundProp( propTypeUtil );
51
56
 
52
57
  const [ items, setItems ] = useSyncExternalState( {
@@ -92,10 +97,20 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
92
97
 
93
98
  setOpenItemIndex( newIndex );
94
99
  popoverState.open( rowRef ?? ev );
100
+
101
+ eventBus.emit( `${ propTypeUtil.key }-item-added`, {
102
+ itemValue: initial.value,
103
+ } );
95
104
  };
96
105
 
97
106
  const removeItem = ( index: number ) => {
107
+ const itemToRemove = items[ index ];
108
+
98
109
  setItems( items.filter( ( _, pos ) => pos !== index ) );
110
+
111
+ eventBus.emit( `${ propTypeUtil.key }-item-removed`, {
112
+ itemValue: itemToRemove?.value,
113
+ } );
99
114
  };
100
115
 
101
116
  const updateItem = ( updatedItem: T, index: number ) => {
@@ -0,0 +1,28 @@
1
+ import { getSelectedElements } from '@elementor/editor-elements';
2
+ import { sendMixpanelEvent } from '@elementor/utils';
3
+
4
+ import { eventBus } from '../../services/event-bus';
5
+ import { type initialTransitionValue } from './data';
6
+
7
+ type TransitionItemValue = typeof initialTransitionValue;
8
+
9
+ const transitionRepeaterMixpanelEvent = {
10
+ eventName: 'click_added_transition',
11
+ location: 'V4 Style Tab',
12
+ secondaryLocation: 'Transition control',
13
+ trigger: 'click',
14
+ };
15
+
16
+ export function subscribeToTransitionEvent() {
17
+ eventBus.subscribe( 'transition-item-added', ( data ) => {
18
+ const payload = data as { itemValue?: TransitionItemValue };
19
+ const value = payload?.itemValue?.selection?.value?.value?.value;
20
+ const selectedElements = getSelectedElements();
21
+ const widgetType = selectedElements[ 0 ]?.type ?? null;
22
+ sendMixpanelEvent( {
23
+ transition_type: value ?? 'unknown',
24
+ ...transitionRepeaterMixpanelEvent,
25
+ widget_type: widgetType,
26
+ } );
27
+ } );
28
+ }
@@ -1,15 +1,17 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState } from 'react';
3
- import { selectionSizePropTypeUtil } from '@elementor/editor-props';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { createArrayPropUtils, selectionSizePropTypeUtil, type SelectionSizePropValue } from '@elementor/editor-props';
4
4
  import { type StyleDefinitionState } from '@elementor/editor-styles';
5
5
  import { InfoCircleFilledIcon } from '@elementor/icons';
6
6
  import { Alert, AlertTitle, Box, Typography } from '@elementor/ui';
7
7
  import { __ } from '@wordpress/i18n';
8
8
 
9
+ import { useBoundProp } from '../../bound-prop-context';
9
10
  import { createControl } from '../../create-control';
10
11
  import { RepeatableControl } from '../repeatable-control';
11
12
  import { SelectionSizeControl } from '../selection-size-control';
12
13
  import { initialTransitionValue, transitionProperties } from './data';
14
+ import { subscribeToTransitionEvent } from './trainsition-events';
13
15
  import { TransitionSelector } from './transition-selector';
14
16
 
15
17
  const DURATION_CONFIG = {
@@ -20,7 +22,7 @@ const DURATION_CONFIG = {
20
22
 
21
23
  // this config needs to be loaded at runtime/render since it's the transitionProperties object will be mutated by the pro plugin.
22
24
  // See: https://elementor.atlassian.net/browse/ED-20285
23
- const getSelectionSizeProps = ( recentlyUsedList: string[] ) => {
25
+ const getSelectionSizeProps = ( recentlyUsedList: string[], disabledItems?: string[] ) => {
24
26
  return {
25
27
  selectionLabel: __( 'Type', 'elementor' ),
26
28
  sizeLabel: __( 'Duration', 'elementor' ),
@@ -28,6 +30,7 @@ const getSelectionSizeProps = ( recentlyUsedList: string[] ) => {
28
30
  component: TransitionSelector,
29
31
  props: {
30
32
  recentlyUsedList,
33
+ disabledItems,
31
34
  },
32
35
  },
33
36
  sizeConfigMap: {
@@ -44,11 +47,11 @@ const getSelectionSizeProps = ( recentlyUsedList: string[] ) => {
44
47
  };
45
48
  };
46
49
 
47
- function getChildControlConfig( recentlyUsedList: string[] ) {
50
+ function getChildControlConfig( recentlyUsedList: string[], disabledItems?: string[] ) {
48
51
  return {
49
52
  propTypeUtil: selectionSizePropTypeUtil,
50
53
  component: SelectionSizeControl as unknown as React.ComponentType< Record< string, unknown > >,
51
- props: getSelectionSizeProps( recentlyUsedList ),
54
+ props: getSelectionSizeProps( recentlyUsedList, disabledItems ),
52
55
  };
53
56
  }
54
57
 
@@ -70,6 +73,16 @@ const disableAddItemTooltipContent = (
70
73
  </Alert>
71
74
  );
72
75
 
76
+ subscribeToTransitionEvent();
77
+
78
+ const getTransitionLabel = ( item: SelectionSizePropValue ) => {
79
+ return ( item.value.selection.value as { key: { value: string } } )?.key?.value ?? '';
80
+ };
81
+
82
+ const getDisabledItems = ( value: SelectionSizePropValue[] | null | undefined ) => {
83
+ return value?.map( getTransitionLabel ) ?? [];
84
+ };
85
+
73
86
  export const TransitionRepeaterControl = createControl(
74
87
  ( {
75
88
  recentlyUsedListGetter,
@@ -81,6 +94,14 @@ export const TransitionRepeaterControl = createControl(
81
94
  const currentStyleIsNormal = currentStyleState === null;
82
95
  const [ recentlyUsedList, setRecentlyUsedList ] = useState< string[] >( [] );
83
96
 
97
+ const childArrayPropTypeUtil = useMemo(
98
+ () => createArrayPropUtils( selectionSizePropTypeUtil.key, selectionSizePropTypeUtil.schema, 'transition' ),
99
+ []
100
+ );
101
+
102
+ const { value } = useBoundProp( childArrayPropTypeUtil );
103
+ const disabledItems = useMemo( () => getDisabledItems( value ), [ value ] );
104
+
84
105
  useEffect( () => {
85
106
  recentlyUsedListGetter().then( setRecentlyUsedList );
86
107
  }, [ recentlyUsedListGetter ] );
@@ -94,7 +115,7 @@ export const TransitionRepeaterControl = createControl(
94
115
  showDuplicate={ false }
95
116
  showToggle={ true }
96
117
  initialValues={ initialTransitionValue }
97
- childControlConfig={ getChildControlConfig( recentlyUsedList ) }
118
+ childControlConfig={ getChildControlConfig( recentlyUsedList, disabledItems ) }
98
119
  propKey="transition"
99
120
  addItemTooltipProps={ {
100
121
  disabled: ! currentStyleIsNormal,
@@ -32,7 +32,13 @@ const findByValue = ( value: string ) => {
32
32
  }
33
33
  };
34
34
 
35
- export const TransitionSelector = ( { recentlyUsedList = [] }: { recentlyUsedList: string[] } ) => {
35
+ export const TransitionSelector = ( {
36
+ recentlyUsedList = [],
37
+ disabledItems = [],
38
+ }: {
39
+ recentlyUsedList: string[];
40
+ disabledItems?: string[];
41
+ } ) => {
36
42
  const { value, setValue } = useBoundProp( keyValuePropTypeUtil );
37
43
  const {
38
44
  key: { value: transitionLabel },
@@ -113,6 +119,7 @@ export const TransitionSelector = ( { recentlyUsedList = [] }: { recentlyUsedLis
113
119
  sectionWidth={ 268 }
114
120
  title={ __( 'Transition Property', 'elementor' ) }
115
121
  icon={ VariationsIcon as React.ElementType< { fontSize: string } > }
122
+ disabledItems={ disabledItems }
116
123
  />
117
124
  </Popover>
118
125
  </Box>
@@ -3,9 +3,10 @@ import { type Category } from '../components/item-selector';
3
3
  export type SelectableItem = {
4
4
  type: 'item' | 'category';
5
5
  value: string;
6
+ disabled?: boolean;
6
7
  };
7
8
 
8
- export const useFilteredItemsList = ( itemsList: Category[], searchValue: string ) => {
9
+ export const useFilteredItemsList = ( itemsList: Category[], searchValue: string, disabledItems?: string[] ) => {
9
10
  return itemsList.reduce< SelectableItem[] >( ( acc, category ) => {
10
11
  const filteredItems = category.items.filter( ( item ) =>
11
12
  item.toLowerCase().includes( searchValue.toLowerCase() )
@@ -15,7 +16,7 @@ export const useFilteredItemsList = ( itemsList: Category[], searchValue: string
15
16
  acc.push( { type: 'category', value: category.label } );
16
17
 
17
18
  filteredItems.forEach( ( item ) => {
18
- acc.push( { type: 'item', value: item } );
19
+ acc.push( { type: 'item', value: item, disabled: disabledItems?.includes( item ) ?? false } );
19
20
  } );
20
21
  }
21
22
 
@@ -0,0 +1,38 @@
1
+ class EventBus {
2
+ private listeners = new Map< string, Set< ( data?: unknown ) => void > >();
3
+
4
+ subscribe( eventName: string, callback: ( data?: unknown ) => void ) {
5
+ if ( ! this.listeners.has( eventName ) ) {
6
+ this.listeners.set( eventName, new Set() );
7
+ }
8
+ const eventListeners = this.listeners.get( eventName );
9
+ if ( eventListeners ) {
10
+ eventListeners.add( callback );
11
+ }
12
+ }
13
+
14
+ unsubscribe( eventName: string, callback: ( data?: unknown ) => void ) {
15
+ const eventListeners = this.listeners.get( eventName );
16
+ if ( ! eventListeners ) {
17
+ return;
18
+ }
19
+
20
+ eventListeners.delete( callback );
21
+ if ( eventListeners.size === 0 ) {
22
+ this.listeners.delete( eventName );
23
+ }
24
+ }
25
+
26
+ emit( eventName: string, data?: unknown ) {
27
+ const eventListeners = this.listeners.get( eventName );
28
+ if ( eventListeners ) {
29
+ eventListeners.forEach( ( callback ) => callback( data ) );
30
+ }
31
+ }
32
+
33
+ clearAll(): void {
34
+ this.listeners.clear();
35
+ }
36
+ }
37
+
38
+ export const eventBus = new EventBus();