1# SelectionMenu
2
3
4The **SelectionMenu** component is a context menu designed for use with the [RichEditor](ts-basic-components-richeditor.md) component, allowing you to bind a custom context menu on selection through the [bindSelectionMenu](./ts-basic-components-richeditor.md#bindselectionmenu) API. This component is not intended for standalone use, and you are advised to display it by right-clicking or by selecting text with a mouse device.
5
6
7> **NOTE**
8>
9> This component is supported since API version 11. Updates will be marked with a superscript to indicate their earliest API version.
10
11
12## Modules to Import
13
14```
15import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI'
16```
17
18## Child Components
19
20Not supported
21
22## SelectionMenu
23
24SelectionMenu(options: SelectionMenuOptions)
25
26Defines a custom context menu on selection. When the input parameter is empty, the sizes of the menu and its content area are 0, making the menu invisible. In this case, for example, if a right-click context menu is bound to the **RichEditor** component, it will not be displayed when the component is right-clicked.
27
28**Decorator**: @Builder
29
30**Atomic service API**: This API can be used in atomic services since API version 12.
31
32**System capability**: SystemCapability.ArkUI.ArkUI.Full
33
34**Parameters**
35
36| Name| Type| Mandatory| Description|
37| -------- | -------- | -------- | -------- |
38| options | [SelectionMenuOptions](#selectionmenuoptions) | Yes| Options of the context menu on selection.|
39
40## SelectionMenuOptions
41
42Defines the options of the context menu on selection.
43
44**Atomic service API**: This API can be used in atomic services since API version 12.
45
46**System capability**: SystemCapability.ArkUI.ArkUI.Full
47
48| Name| Type| Mandatory| Description|
49| -------- | -------- | -------- | -------- |
50| editorMenuOptions | Array&lt;[EditorMenuOptions](#editormenuoptions)&gt; | No| Edit menu.<br>If **editorMenuOptions** is not set, the edit menu is not displayed.<br>When both **action** and **builder** in **EditorMenuOptions** are configured, clicking the edit icon will trigger both.<br>By default, the context menu is not closed when the edit menu icon is clicked. You can configure **closeSelectionMenu** of **RichEditorController** in **action** to enable the menu to be closed.|
51| expandedMenuOptions | Array&lt;[ExpandedMenuOptions](#expandedmenuoptions)&gt; | No| Expanded drop-down menu options.<br>If this parameter is left empty, the expanded drop-down menu is not displayed.<br>The options configured for **ExpandedMenuOptions** are displayed in the **More** menu option, and clicking **More** shows the expanded drop-down menu.|
52| controller | [RichEditorController](ts-basic-components-richeditor.md#richeditorcontroller) | No| Rich text editor controller. If **controller** is set, the default system menu (including the cut, copy, and paste options) is displayed, and the preset menu features are provided.<br>If **controller** is left empty, the **More** menu option is not displayed. If **expandedMenuOptions** is not empty, the expanded drop-down menu is displayed.<br>By default, the copy and paste feature is only available for rich text. To use the feature for content that includes both text and images, define custom **onCopy** and **onPaste** APIs. If a custom **onCopy** \| **onPaste** API is defined, the default copy and paste feature is ineffective, and the custom API is called instead.<br>**NOTE**<br> When the preset copy option is selected, the custom context menu on selection is hidden, while the selected text is still highlighted.<br> When the preset select-all option is selected, the custom context menu on selection is hidden, while all text is highlighted.<br> When the preset paste option is selected, the style of the copied text is retained, whether the text is pasted to a blank area or not.<br> When the **copyOptions** attribute of the [RichEditor](ts-basic-components-richeditor.md) component is set to **CopyOptions.None**, the preset copy and cut features are not restricted.|
53| onCopy | (event?: [EditorEventInfo](#editoreventinfo)) =&gt; void | No| Event callback to take the place of the preset copy menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br> **event** indicates the returned information.|
54| onPaste | (event?: [EditorEventInfo](#editoreventinfo)) =&gt; void | No| Event callback to take the place of the preset paste menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br> **event** indicates the returned information.|
55| onCut | (event?: [EditorEventInfo](#editoreventinfo)) =&gt; void | No| Event callback to take the place of the preset cut menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br>**event** indicates the returned information.|
56| onSelectAll | (event?: [EditorEventInfo](#editoreventinfo)) =&gt; void | No| Event callback to take the place of the preset select-all menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br>**event** indicates the returned information.|
57
58
59## EditorMenuOptions
60
61Describes the edit menu options.
62
63**Atomic service API**: This API can be used in atomic services since API version 12.
64
65**System capability**: SystemCapability.ArkUI.ArkUI.Full
66
67| Name| Type| Mandatory| Description|
68| -------- | -------- | -------- | -------- |
69| icon | [ResourceStr](ts-types.md#resourcestr) | Yes| Icon.|
70| builder | () =&gt; void | No| Builder of the custom component displayed upon click. It must be used with @Builder for building the custom component.|
71| action | () =&gt; void | No| Action triggered when the menu option is clicked.|
72
73
74## ExpandedMenuOptions
75
76Describes the expanded drop-down menu options.
77
78Inherits from [MenuItemOptions](ts-basic-components-menuitem.md#menuitemoptions).
79
80**Atomic service API**: This API can be used in atomic services since API version 12.
81
82**System capability**: SystemCapability.ArkUI.ArkUI.Full
83
84| Name| Type| Mandatory| Description|
85| -------- | -------- | -------- | -------- |
86| action | () =&gt; void | No| Action triggered when the menu option is clicked.|
87
88## EditorEventInfo
89
90Provides the information about the selected content.
91
92**Atomic service API**: This API can be used in atomic services since API version 12.
93
94**System capability**: SystemCapability.ArkUI.ArkUI.Full
95
96| Name| Type| Mandatory| Description|
97| -------- | -------- | -------- | -------- |
98| content | [RichEditorSelection](ts-basic-components-richeditor.md#richeditorselection) | No| Information about the selected content.|
99
100## Attributes
101
102The [universal attributes](ts-universal-attributes-size.md) are not supported. The default width is 256 vp, and the height is adaptive.
103
104## Events
105The [universal events](ts-universal-events-click.md) are not supported.
106
107## Example
108
109```ts
110import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI'
111
112@Entry
113@Component
114struct Index {
115  @State select: boolean = true
116  controller: RichEditorController = new RichEditorController();
117  options: RichEditorOptions = { controller: this.controller }
118  @State message: string = 'Hello word'
119  @State textSize: number = 30
120  @State fontWeight: FontWeight = FontWeight.Normal
121  @State start: number = -1
122  @State end: number = -1
123  @State visibleValue: Visibility = Visibility.Visible
124  @State colorTransparent: Color = Color.Transparent
125  @State textStyle: RichEditorTextStyle = {}
126  private editorMenuOptions: Array<EditorMenuOptions> =
127    [
128      { icon: $r("app.media.ic_notepad_textbold"), action: () => {
129        if (this.controller) {
130          let selection = this.controller.getSelection();
131          let spans = selection.spans
132          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
133            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
134              let span = item as RichEditorTextSpanResult
135              this.textStyle = span.textStyle
136              let start = span.offsetInSpan[0]
137              let end = span.offsetInSpan[1]
138              let offset = span.spanPosition.spanRange[0]
139              if (this.textStyle.fontWeight != 11) {
140                this.textStyle.fontWeight = FontWeight.Bolder
141              } else {
142                this.textStyle.fontWeight = FontWeight.Normal
143              }
144              this.controller.updateSpanStyle({
145                start: offset + start,
146                end: offset + end,
147                textStyle: this.textStyle
148              })
149            }
150          })
151        }
152      } },
153      { icon: $r("app.media.ic_notepad_texttilt"), action: () => {
154        if (this.controller) {
155          let selection = this.controller.getSelection();
156          let spans = selection.spans
157          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
158            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
159              let span = item as RichEditorTextSpanResult
160              this.textStyle = span.textStyle
161              let start = span.offsetInSpan[0]
162              let end = span.offsetInSpan[1]
163              let offset = span.spanPosition.spanRange[0]
164              if (this.textStyle.fontStyle == FontStyle.Italic) {
165                this.textStyle.fontStyle = FontStyle.Normal
166              } else {
167                this.textStyle.fontStyle = FontStyle.Italic
168              }
169              this.controller.updateSpanStyle({
170                start: offset + start,
171                end: offset + end,
172                textStyle: this.textStyle
173              })
174            }
175          })
176        }
177      } },
178      { icon: $r("app.media.ic_notepad_underline"),
179        action: () => {
180          if (this.controller) {
181            let selection = this.controller.getSelection();
182            let spans = selection.spans
183            spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
184              if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
185                let span = item as RichEditorTextSpanResult
186                this.textStyle = span.textStyle
187                let start = span.offsetInSpan[0]
188                let end = span.offsetInSpan[1]
189                let offset = span.spanPosition.spanRange[0]
190                if (this.textStyle.decoration) {
191                  if (this.textStyle.decoration.type == TextDecorationType.Underline) {
192                    this.textStyle.decoration.type = TextDecorationType.None
193                  } else {
194                    this.textStyle.decoration.type = TextDecorationType.Underline
195                  }
196                } else {
197                  this.textStyle.decoration = { type: TextDecorationType.Underline, color: Color.Black }
198                }
199                this.controller.updateSpanStyle({
200                  start: offset + start,
201                  end: offset + end,
202                  textStyle: this.textStyle
203                })
204              }
205            })
206          }
207        }
208      },
209      { icon: $r("app.media.app_icon"), action: () => {
210      }, builder: (): void => this.sliderPanel() },
211      { icon: $r("app.media.ic_notepad_textcolor"), action: () => {
212        if (this.controller) {
213          let selection = this.controller.getSelection();
214          let spans = selection.spans
215          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
216            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
217              let span = item as RichEditorTextSpanResult
218              this.textStyle = span.textStyle
219              let start = span.offsetInSpan[0]
220              let end = span.offsetInSpan[1]
221              let offset = span.spanPosition.spanRange[0]
222              if (this.textStyle.fontColor == Color.Orange || this.textStyle.fontColor == '#FFFFA500') {
223                this.textStyle.fontColor = Color.Black
224              } else {
225                this.textStyle.fontColor = Color.Orange
226              }
227              this.controller.updateSpanStyle({
228                start: offset + start,
229                end: offset + end,
230                textStyle: this.textStyle
231              })
232            }
233          })
234        }
235      } }]
236  private expandedMenuOptions: Array<ExpandedMenuOptions> =
237    [{ startIcon: $r("app.media.icon"), content: 'Dictionary', action: () => {
238    } }, { startIcon: $r("app.media.icon"), content: 'Translate', action: () => {
239    } }, { startIcon: $r("app.media.icon"), content: 'Search', action: () => {
240    } }]
241  private expandedMenuOptions1: Array<ExpandedMenuOptions> = []
242  private editorMenuOptions1: Array<EditorMenuOptions> = []
243  private selectionMenuOptions: SelectionMenuOptions = {
244    editorMenuOptions: this.editorMenuOptions,
245    expandedMenuOptions: this.expandedMenuOptions,
246    controller: this.controller,
247    onCut: (event?: EditorEventInfo) => {
248      if (event && event.content) {
249        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
250          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
251            let span = item as RichEditorTextSpanResult
252            console.info('test cut' + span.value)
253            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
254          }
255        })
256      }
257    },
258    onPaste: (event?: EditorEventInfo) => {
259      if (event && event.content) {
260        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
261          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
262            let span = item as RichEditorTextSpanResult
263            console.info('test onPaste' + span.value)
264            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
265          }
266        })
267      }
268    },
269    onCopy: (event?: EditorEventInfo) => {
270      if (event && event.content) {
271        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
272          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
273            let span = item as RichEditorTextSpanResult
274            console.info('test cut' + span.value)
275            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
276          }
277        })
278      }
279    },
280    onSelectAll: (event?: EditorEventInfo) => {
281      if (event && event.content) {
282        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
283          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
284            let span = item as RichEditorTextSpanResult
285            console.info('test onPaste' + span.value)
286            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
287          }
288        })
289      }
290    }
291  }
292
293  @Builder sliderPanel() {
294    Column() {
295      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
296        Text('A').fontSize(15)
297        Slider({ value: this.textSize, step: 10, style: SliderStyle.InSet })
298          .width(210)
299          .onChange((value: number, mode: SliderChangeMode) => {
300            if (this.controller) {
301              let selection = this.controller.getSelection();
302              if (mode == SliderChangeMode.End) {
303                if (this.textSize == undefined) {
304                  this.textSize = 0
305                }
306                let spans = selection.spans
307                spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
308                  if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
309                    this.textSize = Math.max(this.textSize, (item as RichEditorTextSpanResult).textStyle.fontSize)
310                  }
311                })
312              }
313              if (mode == SliderChangeMode.Moving || mode == SliderChangeMode.Click) {
314                this.start = selection.selection[0]
315                this.end = selection.selection[1]
316                this.textSize = value
317                this.controller.updateSpanStyle({
318                  start: this.start,
319                  end: this.end,
320                  textStyle: { fontSize: this.textSize }
321                })
322              }
323            }
324          })
325        Text('A').fontSize(20).fontWeight(FontWeight.Medium)
326      }.borderRadius($r('sys.float.ohos_id_corner_radius_card'))
327    }
328    .shadow(ShadowStyle.OUTER_DEFAULT_MD)
329    .backgroundColor(Color.White)
330    .borderRadius($r('sys.float.ohos_id_corner_radius_card'))
331    .padding(15)
332    .height(48)
333  }
334
335  @Builder
336  MyMenu() {
337    Column() {
338      SelectionMenu(this.selectionMenuOptions)
339    }
340    .width(256)
341    .backgroundColor(Color.Transparent)
342  }
343
344  @Builder
345  MyMenu2() {
346    Column() {
347      SelectionMenu({
348        editorMenuOptions: this.editorMenuOptions,
349        expandedMenuOptions: this.expandedMenuOptions1,
350        controller: this.controller,
351      })
352    }
353    .width(256)
354    .backgroundColor(Color.Transparent)
355  }
356
357  @Builder
358  MyMenu3() {
359    Column() {
360      SelectionMenu({
361        editorMenuOptions: this.editorMenuOptions1,
362        expandedMenuOptions: this.expandedMenuOptions,
363        controller: this.controller,
364      })
365    }
366    .width(256)
367    .backgroundColor(Color.Transparent)
368  }
369
370  build() {
371    Column() {
372      Button("SetSelection")
373        .onClick((event: ClickEvent) => {
374          if (this.controller) {
375            this.controller.setSelection(0, 2)
376          }
377        })
378
379      RichEditor(this.options)
380        .onReady(() => {
381          this.controller.addTextSpan(this.message, { style: { fontColor: Color.Orange, fontSize: 30 } })
382          this.controller.addTextSpan(this.message, { style: { fontColor: Color.Black, fontSize: 25 } })
383        })
384        .onSelect((value: RichEditorSelection) => {
385          if (value.selection[0] == -1 && value.selection[1] == -1) {
386            return
387          }
388          this.start = value.selection[0]
389          this.end = value.selection[1]
390        })
391        .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu3(), RichEditorResponseType.RIGHT_CLICK)
392        .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu2(), RichEditorResponseType.SELECT)
393        .borderWidth(1)
394        .borderColor(Color.Red)
395        .width(200)
396        .height(200)
397    }
398  }
399}
400```
401> **NOTE**
402>
403> Icons in bold and italics are not preset in the system. The sample code uses the default icons. You need to replace the icons in **editorMenuOptions** with the desired icons.
404
405![selectionmenu](figures/selectionmenu.jpeg)
406