1# SelectionMenu
2
3
4文本选择菜单,适用于富文本组件通过[bindSelectionMenu](./ts-basic-components-richeditor.md#bindselectionmenu)绑定自定义文本选择菜单,建议绑定鼠标右键或者鼠标选中方式弹出,不支持作为普通组件单独使用。
5
6
7> **说明:**
8>
9> 该组件从API Version 11开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。
10
11
12## 导入模块
13
14```
15import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI'
16```
17
18## 子组件
19
20无。
21
22## SelectionMenu
23
24SelectionMenu(options: SelectionMenuOptions): void
25
26入参为空时,文本选择菜单组件SelectionMenu内容区大小及组件大小为零。表现例如,富文本组件[RichEditor](ts-basic-components-richeditor.md)使用[bindSelectionMenu](ts-basic-components-richeditor.md#bindselectionmenu)接口绑定一个SelectionMenu的右键菜单,则右键富文本组件区域时无任何菜单弹出。
27
28**装饰器类型:**\@Builder
29
30**原子化服务API:** 从API version 12开始,该接口支持在原子化服务中使用。
31
32**系统能力:** SystemCapability.ArkUI.ArkUI.Full
33
34**参数:**
35
36| 参数名 | 类型 | 必填 | 说明 |
37| -------- | -------- | -------- | -------- |
38| options | [SelectionMenuOptions](#selectionmenuoptions) | 是 | 文本选择菜单可选项。 |
39
40## SelectionMenuOptions
41
42SelectionMenuOptions定义SelectionMenu的可选菜单类型项及其具体配置参数。
43
44**原子化服务API:** 从API version 12开始,该接口支持在原子化服务中使用。
45
46**系统能力:** SystemCapability.ArkUI.ArkUI.Full
47
48| 名称 | 类型 | 必填 | 说明 |
49| -------- | -------- | -------- | -------- |
50| editorMenuOptions | Array&lt;[EditorMenuOptions](#editormenuoptions)&gt; | 否 | 编辑菜单。<br/>editorMenuOptions未配置时,不显示编辑菜单。<br/>同时配置EditorMenuOptions中action和builder时,点击图标会同时响应。<br/>点击编辑菜单图标默认不关闭整个菜单,应用可以通过action接口配置RichEditorController的closeSelectionMenu主动关闭菜单。 |
51| expandedMenuOptions | Array&lt;[ExpandedMenuOptions](#expandedmenuoptions)&gt; | 否 | 扩展下拉菜单。<br/>expandedMenuOptions参数为空时无更多按钮,不显示扩展下拉菜单。<br/>expandedMenuOptions参数不为空时显示更多按钮,配置菜单项收起在更多按钮中,点击更多按钮展示。 |
52| controller | [RichEditorController](ts-basic-components-richeditor.md#richeditorcontroller) | 否 | 富文本控制器不为空时显示默认系统菜单(包含剪切复制粘贴等部分)且默认菜单功能内置。<br/>controller为空时不显示更多按钮,expandedMenuOptions参数不为空则显示下拉菜单中。<br/>系统默认只支持复制粘贴富文本文本内容,图文混排需要应用自定义onCopy、onPaste接口。应用自行配置onCopy \| onPaste接口时,系统菜单默认复制粘贴失效,调用应用自定义函数。 <br/>**说明:**<br/> 点击自定义文本选择菜单内置复制功能选项后,自定义菜单消失选中文本高亮保留。<br/> 点击自定义文本选择菜单内置全选功能选项后,自定义菜单消失文本全选高亮。<br/> 点击自定义文本选择菜单内置粘贴功能选项后,空白处粘贴或者选中文本替换粘贴均是保留被复制文本的样式。<br/> 当富文本组件[RichEditor](ts-basic-components-richeditor.md)的copyOptions属性设置为`CopyOptions.None`时,内置的复制剪切功能不会被限制。|
53| onCopy | (event?: [EditorEventInfo](#editoreventinfo))&nbsp;=&gt;&nbsp;void | 否 | 替代内置系统菜单复制项的事件回调。<br/>生效前提是一定要有controller参数,有系统默认菜单才能替换内置复制功能。<br/>**说明:**<br/> event为返回信息。|
54| onPaste | (event?: [EditorEventInfo](#editoreventinfo))&nbsp;=&gt;&nbsp;void | 否 | 替代内置系统菜单粘贴项的事件回调。<br/>生效前提是一定要有controller参数,有系统默认菜单才能替换内置粘贴功能。<br/>**说明:**<br/> event为返回信息。 |
55| onCut | (event?: [EditorEventInfo](#editoreventinfo))&nbsp;=&gt;&nbsp;void | 否 | 替代内置系统菜单剪切项的事件回调。<br/>生效前提是一定要有controller参数,有系统默认菜单才能替换内置剪切功能。<br/>**说明:**<br/>event为返回信息。|
56| onSelectAll | (event?: [EditorEventInfo](#editoreventinfo))&nbsp;=&gt;&nbsp;void | 否 | 替代内置系统菜单全选项的事件回调。<br/>生效前提是一定要有controller参数,有系统默认菜单才能替换内置全选功能。<br/>**说明:**<br/>event为返回信息。|
57
58
59## EditorMenuOptions
60
61编辑菜单选项。
62
63**原子化服务API:** 从API version 12开始,该接口支持在原子化服务中使用。
64
65**系统能力:** SystemCapability.ArkUI.ArkUI.Full
66
67| 名称 | 类型 | 必填 | 说明 |
68| -------- | -------- | -------- | -------- |
69| icon | [ResourceStr](ts-types.md#resourcestr) | 是 | 图标资源。 |
70| builder | ()&nbsp;=&gt;&nbsp;void | 否 | 点击时显示用户自定义组件,自定义组件在构造时结合@Builder使用。 |
71| action | ()&nbsp;=&gt;&nbsp;void | 否 | 点击菜单项的事件回调。 |
72
73
74## ExpandedMenuOptions
75
76扩展下拉菜单。
77
78继承于[MenuItemOptions](ts-basic-components-menuitem.md#menuitemoptions对象说明)。
79
80**原子化服务API:** 从API version 12开始,该接口支持在原子化服务中使用。
81
82**系统能力:** SystemCapability.ArkUI.ArkUI.Full
83
84| 名称 | 类型 | 必填 | 说明 |
85| -------- | -------- | -------- | -------- |
86| action | ()&nbsp;=&gt;&nbsp;void | 否 | 点击菜单项的事件回调。 |
87
88## EditorEventInfo
89
90选中内容信息。
91
92**原子化服务API:** 从API version 12开始,该接口支持在原子化服务中使用。
93
94**系统能力:** SystemCapability.ArkUI.ArkUI.Full
95
96| 名称 | 类型 | 必填 | 说明 |
97| -------- | -------- | -------- | -------- |
98| content | [RichEditorSelection](ts-basic-components-richeditor.md#richeditorselection) | 否 | 选中内容信息。|
99
100## 属性
101
102不支持[通用属性](ts-universal-attributes-size.md),宽度默认224vp, 高度自适应内容。
103
104## 事件
105不支持[通用事件](ts-universal-events-click.md)。
106
107## 示例
108
109该示例展示了文本绑定不同触发方式的自定义文本选择菜单的效果。
110
111```ts
112import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI'
113
114@Entry
115@Component
116struct Index {
117  @State select: boolean = true
118  controller: RichEditorController = new RichEditorController();
119  options: RichEditorOptions = { controller: this.controller }
120  @State message: string = 'Hello world'
121  @State textSize: number = 30
122  @State fontWeight: FontWeight = FontWeight.Normal
123  @State start: number = -1
124  @State end: number = -1
125  @State visibleValue: Visibility = Visibility.Visible
126  @State colorTransparent: Color = Color.Transparent
127  @State textStyle: RichEditorTextStyle = {}
128  private editorMenuOptions: Array<EditorMenuOptions> =
129    [
130      { icon: $r("app.media.ic_notepad_textbold"), action: () => {
131        if (this.controller) {
132          let selection = this.controller.getSelection();
133          let spans = selection.spans
134          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
135            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
136              let span = item as RichEditorTextSpanResult
137              this.textStyle = span.textStyle
138              let start = span.offsetInSpan[0]
139              let end = span.offsetInSpan[1]
140              let offset = span.spanPosition.spanRange[0]
141              if (this.textStyle.fontWeight != 11) {
142                this.textStyle.fontWeight = FontWeight.Bolder
143              } else {
144                this.textStyle.fontWeight = FontWeight.Normal
145              }
146              this.controller.updateSpanStyle({
147                start: offset + start,
148                end: offset + end,
149                textStyle: this.textStyle
150              })
151            }
152          })
153        }
154      } },
155      { icon: $r("app.media.ic_notepad_texttilt"), action: () => {
156        if (this.controller) {
157          let selection = this.controller.getSelection();
158          let spans = selection.spans
159          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
160            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
161              let span = item as RichEditorTextSpanResult
162              this.textStyle = span.textStyle
163              let start = span.offsetInSpan[0]
164              let end = span.offsetInSpan[1]
165              let offset = span.spanPosition.spanRange[0]
166              if (this.textStyle.fontStyle == FontStyle.Italic) {
167                this.textStyle.fontStyle = FontStyle.Normal
168              } else {
169                this.textStyle.fontStyle = FontStyle.Italic
170              }
171              this.controller.updateSpanStyle({
172                start: offset + start,
173                end: offset + end,
174                textStyle: this.textStyle
175              })
176            }
177          })
178        }
179      } },
180      { icon: $r("app.media.ic_notepad_underline"),
181        action: () => {
182          if (this.controller) {
183            let selection = this.controller.getSelection();
184            let spans = selection.spans
185            spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
186              if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
187                let span = item as RichEditorTextSpanResult
188                this.textStyle = span.textStyle
189                let start = span.offsetInSpan[0]
190                let end = span.offsetInSpan[1]
191                let offset = span.spanPosition.spanRange[0]
192                if (this.textStyle.decoration) {
193                  if (this.textStyle.decoration.type == TextDecorationType.Underline) {
194                    this.textStyle.decoration.type = TextDecorationType.None
195                  } else {
196                    this.textStyle.decoration.type = TextDecorationType.Underline
197                  }
198                } else {
199                  this.textStyle.decoration = { type: TextDecorationType.Underline, color: Color.Black }
200                }
201                this.controller.updateSpanStyle({
202                  start: offset + start,
203                  end: offset + end,
204                  textStyle: this.textStyle
205                })
206              }
207            })
208          }
209        }
210      },
211      { icon: $r("app.media.app_icon"), action: () => {
212      }, builder: (): void => this.sliderPanel() },
213      { icon: $r("app.media.ic_notepad_textcolor"), action: () => {
214        if (this.controller) {
215          let selection = this.controller.getSelection();
216          let spans = selection.spans
217          spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
218            if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
219              let span = item as RichEditorTextSpanResult
220              this.textStyle = span.textStyle
221              let start = span.offsetInSpan[0]
222              let end = span.offsetInSpan[1]
223              let offset = span.spanPosition.spanRange[0]
224              if (this.textStyle.fontColor == Color.Orange || this.textStyle.fontColor == '#FFFFA500') {
225                this.textStyle.fontColor = Color.Black
226              } else {
227                this.textStyle.fontColor = Color.Orange
228              }
229              this.controller.updateSpanStyle({
230                start: offset + start,
231                end: offset + end,
232                textStyle: this.textStyle
233              })
234            }
235          })
236        }
237      } }]
238  private expandedMenuOptions: Array<ExpandedMenuOptions> =
239    [{ startIcon: $r("app.media.icon"), content: '词典', action: () => {
240    } }, { startIcon: $r("app.media.icon"), content: '翻译', action: () => {
241    } }, { startIcon: $r("app.media.icon"), content: '搜索', action: () => {
242    } }]
243  private expandedMenuOptions1: Array<ExpandedMenuOptions> = []
244  private editorMenuOptions1: Array<EditorMenuOptions> = []
245  private selectionMenuOptions: SelectionMenuOptions = {
246    editorMenuOptions: this.editorMenuOptions,
247    expandedMenuOptions: this.expandedMenuOptions,
248    controller: this.controller,
249    onCut: (event?: EditorEventInfo) => {
250      if (event && event.content) {
251        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
252          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
253            let span = item as RichEditorTextSpanResult
254            console.info('test cut' + span.value)
255            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
256          }
257        })
258      }
259    },
260    onPaste: (event?: EditorEventInfo) => {
261      if (event && event.content) {
262        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
263          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
264            let span = item as RichEditorTextSpanResult
265            console.info('test onPaste' + span.value)
266            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
267          }
268        })
269      }
270    },
271    onCopy: (event?: EditorEventInfo) => {
272      if (event && event.content) {
273        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
274          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
275            let span = item as RichEditorTextSpanResult
276            console.info('test cut' + span.value)
277            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
278          }
279        })
280      }
281    },
282    onSelectAll: (event?: EditorEventInfo) => {
283      if (event && event.content) {
284        event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
285          if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
286            let span = item as RichEditorTextSpanResult
287            console.info('test onPaste' + span.value)
288            console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1])
289          }
290        })
291      }
292    }
293  }
294
295  @Builder sliderPanel() {
296    Column() {
297      Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
298        Text('A').fontSize(15)
299        Slider({ value: this.textSize, step: 10, style: SliderStyle.InSet })
300          .width(210)
301          .onChange((value: number, mode: SliderChangeMode) => {
302            if (this.controller) {
303              let selection = this.controller.getSelection();
304              if (mode == SliderChangeMode.End) {
305                if (this.textSize == undefined) {
306                  this.textSize = 0
307                }
308                let spans = selection.spans
309                spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => {
310                  if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') {
311                    this.textSize = Math.max(this.textSize, (item as RichEditorTextSpanResult).textStyle.fontSize)
312                  }
313                })
314              }
315              if (mode == SliderChangeMode.Moving || mode == SliderChangeMode.Click) {
316                this.start = selection.selection[0]
317                this.end = selection.selection[1]
318                this.textSize = value
319                this.controller.updateSpanStyle({
320                  start: this.start,
321                  end: this.end,
322                  textStyle: { fontSize: this.textSize }
323                })
324              }
325            }
326          })
327        Text('A').fontSize(20).fontWeight(FontWeight.Medium)
328      }.borderRadius($r('sys.float.ohos_id_corner_radius_card'))
329    }
330    .shadow(ShadowStyle.OUTER_DEFAULT_MD)
331    .backgroundColor(Color.White)
332    .borderRadius($r('sys.float.ohos_id_corner_radius_card'))
333    .padding(15)
334    .height(48)
335  }
336
337  @Builder
338  MyMenu() {
339    Column() {
340      SelectionMenu(this.selectionMenuOptions)
341    }
342    .width(256)
343    .backgroundColor(Color.Transparent)
344  }
345
346  @Builder
347  MyMenu2() {
348    Column() {
349      SelectionMenu({
350        editorMenuOptions: this.editorMenuOptions,
351        expandedMenuOptions: this.expandedMenuOptions1,
352        controller: this.controller,
353      })
354    }
355    .width(256)
356    .backgroundColor(Color.Transparent)
357  }
358
359  @Builder
360  MyMenu3() {
361    Column() {
362      SelectionMenu({
363        editorMenuOptions: this.editorMenuOptions1,
364        expandedMenuOptions: this.expandedMenuOptions,
365        controller: this.controller,
366      })
367    }
368    .width(256)
369    .backgroundColor(Color.Transparent)
370  }
371
372  build() {
373    Column() {
374      Button("SetSelection")
375        .onClick((event: ClickEvent) => {
376          if (this.controller) {
377            this.controller.setSelection(0, 2)
378          }
379        })
380
381      RichEditor(this.options)
382        .onReady(() => {
383          this.controller.addTextSpan(this.message, { style: { fontColor: Color.Orange, fontSize: 30 } })
384          this.controller.addTextSpan(this.message, { style: { fontColor: Color.Black, fontSize: 25 } })
385        })
386        .onSelect((value: RichEditorSelection) => {
387          if (value.selection[0] == -1 && value.selection[1] == -1) {
388            return
389          }
390          this.start = value.selection[0]
391          this.end = value.selection[1]
392        })
393        .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu3(), RichEditorResponseType.RIGHT_CLICK)
394        .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu2(), RichEditorResponseType.SELECT)
395        .borderWidth(1)
396        .borderColor(Color.Red)
397        .width(200)
398        .height(200)
399    }
400  }
401}
402```
403> **说明:**
404>
405> 系统暂未预置加粗、斜体等图标,示例代码使用本地资源图标,开发者使用时需自行替换editorMenuOptions中icon项的资源。
406
407![selectionmenu](figures/selectionmenu.jpeg)
408