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