• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15import { LengthMetrics, OperationOption } from '@kit.ArkUI';
16
17declare type OnSelectCallback = (index: number, selectValue: string) => void;
18declare type OnPasteCallback = (pasteValue: string, event: PasteEvent) => void;
19declare type OnTextSelectionChangeCallback = (selectionStart: number, selectionEnd: number) => void;
20declare type OnContentScrollCallback = (totalOffsetX: number, totalOffsetY: number) => void;
21
22const TEXT_SIZE_BODY1: Resource = $r(`sys.float.ohos_id_text_size_body1`);
23const COLOR_TEXT_SECONDARY = $r(`sys.color.ohos_id_color_text_secondary`);
24const ICON_COLOR_SECONDARY = $r('sys.color.ohos_id_color_secondary');
25const ATOMIC_SERVICE_SEARCH_BG_COLOR = $r(`sys.color.ohos_id_color_text_field_sub_bg`);
26const TEXT_COLOR_PRIMARY = $r(`sys.color.ohos_id_color_text_primary`);
27const FUNCTION_ICON_COLOR = $r(`sys.color.ohos_id_color_primary`);
28const EFFECT_COLOR = $r(`sys.color.ohos_id_color_click_effect`);
29const ICON_SIZE: number = 16;
30const SELECT_PADDING_LEFT: number = 6;
31const SELECT_MARGIN_LEFT: number = 2;
32const FLEX_SHRINK: number = 0;
33const DIVIDER_OPACITY: number = 0.5;
34const DIVIDER_MARGIN_LEFT: number = 2;
35const DIVIDER_MARGIN_RIGHT: number = 0;
36const ATOMIC_SERVICE_SEARCH_HEIGHT: number = 40;
37const ATOMIC_SELECT_HEIGHT: number = 36;
38const ATOMIC_SELECT_BORDER_RADIUS: number = 20;
39const ATOMIC_DIVIDER_HEIGHT: number = 20;
40const ICON_WIDTH_AND_HEIGTH: number = 24;
41const OPERATION_ITEM1_MARGIN_RIGHT: number = 2;
42const OPERATION_ITEM2_MARGIN_LEFT: number = 8;
43const SEARCH_OFFSET_X: number = -5;
44
45export interface InputFilterParams {
46  inputFilterValue: ResourceStr,
47  error?: Callback<string>
48}
49
50export interface SearchButtonParams {
51  searchButtonValue: ResourceStr,
52  options?: SearchButtonOptions
53}
54
55export interface MenuAlignParams {
56  alignType: MenuAlignType,
57  offset?: Offset
58}
59
60export interface SelectParams {
61  options?: Array<SelectOption>;
62  selected?: number;
63  selectValue?: ResourceStr;
64  onSelect?: OnSelectCallback;
65  menuItemContentModifier?: ContentModifier<MenuItemConfiguration>;
66  divider?: Optional<DividerOptions> | null;
67  font?: Font;
68  fontColor?: ResourceColor;
69  selectedOptionBgColor?: ResourceColor;
70  selectedOptionFont?: Font;
71  selectedOptionFontColor?: ResourceColor;
72  optionBgColor?: ResourceColor;
73  optionFont?: Font;
74  optionFontColor?: ResourceColor;
75  optionWidth?: Dimension | OptionWidthMode;
76  optionHeight?: Dimension;
77  space?: Length;
78  arrowPosition?: ArrowPosition;
79  menuAlign?: MenuAlignParams;
80  menuBackgroundColor?: ResourceColor;
81  menuBackgroundBlurStyle?: BlurStyle;
82}
83
84export interface SearchParams {
85  searchKey?: ResourceStr;
86  componentBackgroundColor?: ResourceColor;
87  pressedBackgroundColor?: ResourceColor;
88  searchButton?: SearchButtonParams;
89  placeholderColor?: ResourceColor;
90  placeholderFont?: Font;
91  textFont?: Font;
92  textAlign?: TextAlign;
93  copyOptions?: CopyOptions;
94  searchIcon?: IconOptions | SymbolGlyphModifier;
95  cancelIcon?: IconOptions;
96  fontColor?: ResourceColor;
97  caretStyle?: CaretStyle;
98  enableKeyboardOnFocus?: boolean;
99  hideSelectionMenu?: boolean;
100  type?: SearchType;
101  maxLength?: number;
102  enterKeyType?: EnterKeyType;
103  decoration?: TextDecorationOptions;
104  letterSpacing?: number | string | Resource;
105  fontFeature?: ResourceStr;
106  selectedBackgroundColor?: ResourceColor;
107  inputFilter?: InputFilterParams;
108  textIndent?: Dimension;
109  minFontSize?: number | string | Resource;
110  maxFontSize?: number | string | Resource;
111  editMenuOptions?: EditMenuOptions;
112  enablePreviewText?: boolean;
113  enableHapticFeedback?: boolean;
114  onSubmit?: Callback<string> | SearchSubmitCallback;
115  onChange?: EditableTextOnChangeCallback;
116  onCopy?: Callback<string>;
117  onCut?: Callback<string>;
118  onPaste?: OnPasteCallback;
119  onTextSelectionChange?: OnTextSelectionChangeCallback;
120  onContentScroll?: OnContentScrollCallback;
121  onEditChange?: Callback<boolean>;
122  onWillInsert?: Callback<InsertValue, boolean>;
123  onDidInsert?: Callback<InsertValue>;
124  onWillDelete?: Callback<DeleteValue, boolean>;
125  onDidDelete?: Callback<DeleteValue>;
126}
127
128export interface OperationParams {
129  auxiliaryItem?: OperationOption;
130  independentItem?: OperationOption;
131}
132
133@Component
134export struct AtomicServiceSearch {
135  @State private isFunction1Pressed: boolean = false;
136  @State private isFunction2Pressed: boolean = false;
137  @State private isSearchPressed: boolean = false;
138  @State private showImage: boolean = true;
139  @Prop @Watch('onParamsChange') value?: ResourceStr = '';
140  @Prop placeholder?: ResourceStr = 'Search';
141  @Prop @Watch('onSelectChange') select?: SelectParams = {};
142  @Prop @Watch('onSearchChange') search?: SearchParams = {
143    componentBackgroundColor: ATOMIC_SERVICE_SEARCH_BG_COLOR,
144    placeholderFont: {
145      size: TEXT_SIZE_BODY1,
146    },
147    placeholderColor: COLOR_TEXT_SECONDARY,
148    textFont: {
149      size: TEXT_SIZE_BODY1,
150    },
151    fontColor: COLOR_TEXT_SECONDARY,
152    searchIcon: {
153      size: ICON_SIZE,
154      color: ICON_COLOR_SECONDARY,
155    },
156    pressedBackgroundColor: EFFECT_COLOR
157  };
158  operation?: OperationParams;
159  controller?: SearchController = new SearchController();
160
161  aboutToAppear(): void {
162    this.showImage = this.value?.toString().length === 0 ? true : false;
163    this.initSelectStyle();
164    this.initSearchStyle();
165  }
166
167  private onParamsChange(): void {
168    this.showImage = this.value?.toString().length === 0 ? true : false;
169  }
170
171  private onSelectChange(): void {
172    this.initSelectStyle();
173  }
174
175  private onSearchChange(): void {
176    this.initSearchStyle();
177  }
178
179  private initSelectStyle(): void {
180    if (typeof this.select !== 'undefined') {
181      if (typeof this.select.selected === 'undefined') {
182        this.select.selected = -1;
183      }
184      if (typeof this.select.font === 'undefined') {
185        this.select.font = { size: TEXT_SIZE_BODY1 };
186      }
187      if (typeof this.select.fontColor === 'undefined') {
188        this.select.fontColor = TEXT_COLOR_PRIMARY;
189      }
190    }
191  }
192
193  private initSearchStyle(): void {
194    if (typeof this.search !== 'undefined') {
195      if (typeof this.search.componentBackgroundColor === 'undefined') {
196        this.search.componentBackgroundColor = ATOMIC_SERVICE_SEARCH_BG_COLOR;
197      }
198      if (typeof this.search.placeholderFont === 'undefined') {
199        this.search.placeholderFont = { size: TEXT_SIZE_BODY1 };
200      }
201      if (typeof this.search.placeholderColor === 'undefined') {
202        this.search.placeholderColor = COLOR_TEXT_SECONDARY;
203      }
204      if (typeof this.search.textFont === 'undefined') {
205        this.search.textFont = { size: TEXT_SIZE_BODY1 };
206      }
207      if (typeof this.search.fontColor === 'undefined') {
208        this.search.fontColor = COLOR_TEXT_SECONDARY;
209      }
210      if (typeof this.search.searchIcon === 'undefined') {
211        this.search.searchIcon = {
212          size: ICON_SIZE,
213          color: ICON_COLOR_SECONDARY,
214        }
215      }
216      if (typeof this.search.pressedBackgroundColor === 'undefined') {
217        this.search.pressedBackgroundColor = EFFECT_COLOR;
218      }
219    }
220  }
221
222  @Builder
223  renderSelect() {
224    if (typeof this.select !== 'undefined' && typeof this.select.options !== 'undefined') {
225      Row() {
226        Select(this.select?.options)
227          .value(this.select?.selectValue)
228          .selected(this.select?.selected)
229          .onSelect(this.select?.onSelect)
230          .menuItemContentModifier(this.select?.menuItemContentModifier)
231          .divider(this.select?.divider)
232          .font(this.select?.font)
233          .fontColor(this.select?.fontColor)
234          .selectedOptionBgColor(this.select?.selectedOptionBgColor)
235          .selectedOptionFont(this.select?.selectedOptionFont)
236          .selectedOptionFontColor(this.select?.selectedOptionFontColor)
237          .optionBgColor(this.select?.optionBgColor)
238          .optionFont(this.select?.optionFont)
239          .optionFontColor(this.select?.optionFontColor)
240          .space(this.select?.space)
241          .arrowPosition(this.select?.arrowPosition)
242          .menuAlign(this.select?.menuAlign?.alignType, this.select?.menuAlign?.offset)
243          .optionWidth(this.select?.optionWidth)
244          .optionHeight(this.select?.optionHeight)
245          .menuBackgroundColor(this.select?.menuBackgroundColor)
246          .menuBackgroundBlurStyle(this.select?.menuBackgroundBlurStyle)
247          .height(ATOMIC_SELECT_HEIGHT)
248          .borderRadius(ATOMIC_SELECT_BORDER_RADIUS)
249          .constraintSize({ minHeight: ATOMIC_SELECT_HEIGHT })
250          .padding({ start: LengthMetrics.vp(SELECT_PADDING_LEFT) })
251          .margin({ start: LengthMetrics.vp(SELECT_MARGIN_LEFT) })
252          .backgroundColor(Color.Transparent)
253      }
254      .flexShrink(FLEX_SHRINK)
255    }
256  }
257
258  @Builder
259  renderDivider() {
260    if (typeof this.select !== 'undefined' && typeof this.select.options !== 'undefined') {
261      Divider()
262        .vertical(true)
263        .color(Color.Black)
264        .height(ATOMIC_DIVIDER_HEIGHT)
265        .opacity(DIVIDER_OPACITY)
266        .margin({
267          start: LengthMetrics.vp(DIVIDER_MARGIN_LEFT),
268          end: LengthMetrics.vp(DIVIDER_MARGIN_RIGHT)
269        })
270    }
271  }
272
273  @Builder
274  renderSearch() {
275    Search({
276      value: this.value?.toString(),
277      placeholder: this.placeholder,
278      controller: this.controller
279    })
280      .key(this.search?.searchKey?.toString())
281      .margin({ start: LengthMetrics.vp(SEARCH_OFFSET_X) })
282      .backgroundColor(Color.Transparent)
283      .searchButton(this.search?.searchButton?.searchButtonValue.toString(), this.search?.searchButton?.options)
284      .placeholderColor(this.search?.placeholderColor)
285      .placeholderFont(this.search?.placeholderFont)
286      .textFont(this.search?.textFont)
287      .textAlign(this.search?.textAlign)
288      .copyOption(this.search?.copyOptions)
289      .searchIcon(this.search?.searchIcon)
290      .cancelButton({ icon: this.search?.cancelIcon })
291      .fontColor(this.search?.fontColor)
292      .caretStyle(this.search?.caretStyle)
293      .enableKeyboardOnFocus(this.search?.enableKeyboardOnFocus)
294      .selectionMenuHidden(this.search?.hideSelectionMenu)
295      .type(this.search?.type)
296      .maxLength(this.search?.maxLength)
297      .enterKeyType(this.search?.enterKeyType)
298      .decoration(this.search?.decoration)
299      .letterSpacing(this.search?.letterSpacing)
300      .fontFeature(this.search?.fontFeature?.toString())
301      .selectedBackgroundColor(this.search?.selectedBackgroundColor)
302      .inputFilter(this.search?.inputFilter?.inputFilterValue, this.search?.inputFilter?.error)
303      .textIndent(this.search?.textIndent)
304      .minFontSize(this.search?.minFontSize)
305      .maxFontSize(this.search?.maxFontSize)
306      .editMenuOptions(this.search?.editMenuOptions)
307      .enablePreviewText(this.search?.enablePreviewText)
308      .enableHapticFeedback(this.search?.enableHapticFeedback)
309      .placeholderFont(this.search?.placeholderFont)
310      .textFont(this.search?.textFont)
311      .searchIcon(this.search?.searchIcon)
312      .fontColor(this.search?.fontColor)
313      .onCut(this.search?.onCut)
314      .onCopy(this.search?.onCopy)
315      .onPaste(this.search?.onPaste)
316      .onSubmit(this.search?.onSubmit)
317      .onDidInsert(this.search?.onDidInsert)
318      .onDidDelete(this.search?.onDidDelete)
319      .onEditChange(this.search?.onEditChange)
320      .onWillInsert(this.search?.onWillInsert)
321      .onWillDelete(this.search?.onWillDelete)
322      .onContentScroll(this.search?.onContentScroll)
323      .onTextSelectionChange(this.search?.onTextSelectionChange)
324      .onChange((value: string, previewText?: PreviewText) => {
325        if (previewText?.value.length !== 0) {
326          this.value = previewText?.value;
327        } else {
328          this.value = value;
329        }
330        if (typeof this.search?.onChange !== 'undefined') {
331          this.search?.onChange(value, previewText);
332        }
333      })
334      .onTouch((event?: TouchEvent) => {
335        if (event && event.type === TouchType.Down) {
336          this.isSearchPressed = true;
337        } else if (event && event.type === TouchType.Up) {
338          this.isSearchPressed = false;
339        }
340      })
341  }
342
343  @Builder
344  renderAuxiliaryItem() {
345    if (typeof this.operation?.auxiliaryItem !== 'undefined' && this.showImage) {
346      Row() {
347        Image(this.operation?.auxiliaryItem?.value)
348          .objectFit(ImageFit.Contain)
349          .fillColor(FUNCTION_ICON_COLOR)
350          .width(ICON_WIDTH_AND_HEIGTH)
351          .height(ICON_WIDTH_AND_HEIGTH)
352      }
353      .onClick(this.operation?.auxiliaryItem.action)
354      .flexShrink(FLEX_SHRINK)
355      .borderRadius(ATOMIC_SELECT_BORDER_RADIUS)
356      .alignItems(VerticalAlign.Center)
357      .justifyContent(FlexAlign.Center)
358      .width(ATOMIC_SELECT_HEIGHT)
359      .height(ATOMIC_SELECT_HEIGHT)
360      .margin({ end: LengthMetrics.vp(OPERATION_ITEM1_MARGIN_RIGHT) })
361      .backgroundColor(this.isFunction1Pressed ? this.search?.pressedBackgroundColor : Color.Transparent)
362      .onTouch((event?: TouchEvent) => {
363        if (event && event.type === TouchType.Down) {
364          this.isFunction1Pressed = true;
365        } else if (event && event.type === TouchType.Up) {
366          this.isFunction1Pressed = false;
367        }
368      })
369    }
370  }
371
372  @Builder
373  renderIndependentItem() {
374    if (typeof this.operation?.independentItem !== 'undefined') {
375      Row() {
376        Image(this.operation?.independentItem.value)
377          .objectFit(ImageFit.Contain)
378          .fillColor(FUNCTION_ICON_COLOR)
379          .width(ICON_WIDTH_AND_HEIGTH)
380          .height(ICON_WIDTH_AND_HEIGTH)
381      }
382      .onClick(this.operation?.independentItem.action)
383      .flexShrink(FLEX_SHRINK)
384      .borderRadius(ATOMIC_SELECT_BORDER_RADIUS)
385      .alignItems(VerticalAlign.Center)
386      .justifyContent(FlexAlign.Center)
387      .width(ATOMIC_SERVICE_SEARCH_HEIGHT)
388      .height(ATOMIC_SERVICE_SEARCH_HEIGHT)
389      .margin({ start: LengthMetrics.vp(OPERATION_ITEM2_MARGIN_LEFT) })
390      .backgroundColor(this.isFunction2Pressed ?
391        this.search?.pressedBackgroundColor : this.search?.componentBackgroundColor)
392      .onTouch((event?: TouchEvent) => {
393        if (event && event.type === TouchType.Down) {
394          this.isFunction2Pressed = true;
395        } else if (event && event.type === TouchType.Up) {
396          this.isFunction2Pressed = false;
397        }
398      })
399    }
400  }
401
402  build() {
403    Row() {
404      Flex({
405        direction: FlexDirection.Row,
406        alignItems: ItemAlign.Center,
407        justifyContent: FlexAlign.Start
408      }) {
409        Stack() {
410          Flex({
411            direction: FlexDirection.Row,
412            alignItems: ItemAlign.Center,
413            justifyContent: FlexAlign.Start
414          }) {
415            this.renderSelect();
416            this.renderDivider();
417            this.renderSearch();
418          }
419
420          if (typeof this.search?.searchButton === 'undefined') {
421            this.renderAuxiliaryItem();
422          }
423        }
424        .alignContent(Alignment.End)
425        .borderRadius(ATOMIC_SELECT_BORDER_RADIUS)
426        .backgroundColor(this.isSearchPressed ?
427          this.search?.pressedBackgroundColor : this.search?.componentBackgroundColor)
428
429        this.renderIndependentItem();
430      }
431    }
432    .height(ATOMIC_SERVICE_SEARCH_HEIGHT)
433  }
434}
435