From 0778e60d0b1d0b35029544a274ee1d03d0f89975 Mon Sep 17 00:00:00 2001 From: Kagol Date: Tue, 21 May 2024 15:31:50 +0800 Subject: [PATCH] feat(base-select): [base-select,select] add base-select (#1632) * refactor(base-select): add base-select * refactor(base-select): remove tree/grid and e2e tests * refactor(base-select): remove tree/grid in the base-select/pc.vue * refactor(base-select): remove tree/grid in the renderless/index.ts|vue.ts * docs(base-select): remove tree/grid demos --- examples/sites/demos/apis/base-select.js | 886 ++++++++ .../base-select/all-text-composition-api.vue | 28 + .../demos/pc/app/base-select/all-text.spec.ts | 13 + .../demos/pc/app/base-select/all-text.vue | 36 + .../allow-create-composition-api.vue | 102 + .../pc/app/base-select/allow-create.spec.ts | 50 + .../demos/pc/app/base-select/allow-create.vue | 105 + .../automatic-dropdown-composition-api.vue | 51 + .../base-select/automatic-dropdown.spec.ts | 31 + .../pc/app/base-select/automatic-dropdown.vue | 59 + .../basic-usage-composition-api.vue | 38 + .../pc/app/base-select/basic-usage.spec.ts | 47 + .../demos/pc/app/base-select/basic-usage.vue | 47 + .../binding-obj-composition-api.vue | 34 + .../pc/app/base-select/binding-obj.spec.ts | 21 + .../demos/pc/app/base-select/binding-obj.vue | 43 + .../cache-usage-composition-api.vue | 45 + .../pc/app/base-select/cache-usage.spec.ts | 19 + .../demos/pc/app/base-select/cache-usage.vue | 55 + .../clear-no-match-value-composition-api.vue | 43 + .../base-select/clear-no-match-value.spec.ts | 24 + .../app/base-select/clear-no-match-value.vue | 51 + .../base-select/clearable-composition-api.vue | 25 + .../pc/app/base-select/clearable.spec.ts | 21 + .../demos/pc/app/base-select/clearable.vue | 34 + .../collapse-tags-composition-api.vue | 48 + .../pc/app/base-select/collapse-tags.spec.ts | 32 + .../pc/app/base-select/collapse-tags.vue | 56 + .../copy-multi-composition-api.vue | 44 + .../pc/app/base-select/copy-multi.spec.ts | 63 + .../demos/pc/app/base-select/copy-multi.vue | 54 + .../copy-single-composition-api.vue | 150 ++ .../pc/app/base-select/copy-single.spec.ts | 76 + .../demos/pc/app/base-select/copy-single.vue | 156 ++ .../base-select/disabled-composition-api.vue | 100 + .../demos/pc/app/base-select/disabled.spec.ts | 88 + .../demos/pc/app/base-select/disabled.vue | 107 + .../base-select/events-composition-api.vue | 106 + .../demos/pc/app/base-select/events.spec.ts | 72 + .../sites/demos/pc/app/base-select/events.vue | 112 + .../filter-method-composition-api.vue | 72 + .../pc/app/base-select/filter-method.spec.ts | 89 + .../pc/app/base-select/filter-method.vue | 82 + .../filter-mode-composition-api.vue | 42 + .../demos/pc/app/base-select/filter-mode.vue | 51 + .../base-select/hide-drop-composition-api.vue | 25 + .../pc/app/base-select/hide-drop.spec.ts | 12 + .../demos/pc/app/base-select/hide-drop.vue | 34 + .../input-box-type-composition-api.vue | 43 + .../pc/app/base-select/input-box-type.spec.ts | 72 + .../pc/app/base-select/input-box-type.vue | 52 + .../is-drop-inherit-width-composition-api.vue | 39 + .../base-select/is-drop-inherit-width.spec.ts | 34 + .../app/base-select/is-drop-inherit-width.vue | 48 + .../manual-focus-blur-composition-api.vue | 88 + .../app/base-select/manual-focus-blur.spec.ts | 18 + .../pc/app/base-select/manual-focus-blur.vue | 90 + .../base-select/map-field-composition-api.vue | 33 + .../pc/app/base-select/map-field.spec.ts | 21 + .../demos/pc/app/base-select/map-field.vue | 65 + .../memoize-usage-composition-api.vue | 51 + .../pc/app/base-select/memoize-usage.spec.ts | 19 + .../pc/app/base-select/memoize-usage.vue | 65 + .../base-select/multiple-composition-api.vue | 91 + .../multiple-mix-composition-api.vue | 147 ++ .../demos/pc/app/base-select/multiple-mix.vue | 152 ++ .../demos/pc/app/base-select/multiple.spec.ts | 49 + .../demos/pc/app/base-select/multiple.vue | 98 + .../native-properties-composition-api.vue | 25 + .../app/base-select/native-properties.spec.ts | 16 + .../pc/app/base-select/native-properties.vue | 34 + .../no-data-text-composition-api.vue | 34 + .../pc/app/base-select/no-data-text.spec.ts | 39 + .../demos/pc/app/base-select/no-data-text.vue | 43 + .../optimization-composition-api.vue | 45 + .../pc/app/base-select/optimization.spec.ts | 46 + .../demos/pc/app/base-select/optimization.vue | 54 + .../option-group-composition-api.vue | 96 + .../pc/app/base-select/option-group.spec.ts | 22 + .../demos/pc/app/base-select/option-group.vue | 105 + .../popup-style-position-composition-api.vue | 32 + .../base-select/popup-style-position.spec.ts | 16 + .../app/base-select/popup-style-position.vue | 41 + .../remote-method-composition-api.vue | 283 +++ .../pc/app/base-select/remote-method.spec.ts | 68 + .../pc/app/base-select/remote-method.vue | 285 +++ .../searchable-composition-api.vue | 46 + .../pc/app/base-select/searchable.spec.ts | 66 + .../demos/pc/app/base-select/searchable.vue | 55 + .../show-alloption-composition-api.vue | 25 + .../pc/app/base-select/show-alloption.spec.ts | 15 + .../pc/app/base-select/show-alloption.vue | 34 + .../base-select/show-tip-composition-api.vue | 19 + .../demos/pc/app/base-select/show-tip.vue | 28 + .../app/base-select/size-composition-api.vue | 39 + .../demos/pc/app/base-select/size.spec.ts | 58 + .../sites/demos/pc/app/base-select/size.vue | 47 + .../slot-default-composition-api.vue | 88 + .../pc/app/base-select/slot-default.spec.ts | 18 + .../demos/pc/app/base-select/slot-default.vue | 92 + .../slot-empty-composition-api.vue | 36 + .../pc/app/base-select/slot-empty.spec.ts | 16 + .../demos/pc/app/base-select/slot-empty.vue | 45 + .../slot-footer-composition-api.vue | 35 + .../pc/app/base-select/slot-footer.spec.ts | 16 + .../demos/pc/app/base-select/slot-footer.vue | 45 + .../slot-label-composition-api.vue | 77 + .../demos/pc/app/base-select/slot-label.vue | 85 + .../slot-prefix-composition-api.vue | 31 + .../pc/app/base-select/slot-prefix.spec.ts | 18 + .../demos/pc/app/base-select/slot-prefix.vue | 39 + .../slot-reference-composition-api.vue | 25 + .../pc/app/base-select/slot-reference.spec.ts | 16 + .../pc/app/base-select/slot-reference.vue | 33 + .../base-select/tag-type-composition-api.vue | 27 + .../demos/pc/app/base-select/tag-type.spec.ts | 13 + .../demos/pc/app/base-select/tag-type.vue | 36 + .../app/base-select/webdoc/base-select.cn.md | 7 + .../app/base-select/webdoc/base-select.en.md | 7 + .../pc/app/base-select/webdoc/base-select.js | 535 +++++ examples/sites/demos/pc/menus.js | 1 + packages/modules.json | 8 + packages/renderless/src/base-select/index.ts | 2022 +++++++++++++++++ packages/renderless/src/base-select/vue.ts | 587 +++++ .../theme/src/base-select/aurora-theme.js | 13 + packages/theme/src/base-select/index.less | 442 ++++ packages/theme/src/base-select/smb-theme.js | 18 + packages/theme/src/base-select/vars.less | 80 + packages/vue/package.json | 1 + packages/vue/src/base-select/index.ts | 35 + packages/vue/src/base-select/package.json | 37 + packages/vue/src/base-select/src/index.ts | 352 +++ .../vue/src/base-select/src/mobile-first.vue | 705 ++++++ packages/vue/src/base-select/src/pc.vue | 713 ++++++ packages/vue/src/base-select/src/token.ts | 8 + 135 files changed, 12932 insertions(+) create mode 100644 examples/sites/demos/apis/base-select.js create mode 100644 examples/sites/demos/pc/app/base-select/all-text-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/all-text.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/all-text.vue create mode 100644 examples/sites/demos/pc/app/base-select/allow-create-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/allow-create.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/allow-create.vue create mode 100644 examples/sites/demos/pc/app/base-select/automatic-dropdown-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/automatic-dropdown.vue create mode 100644 examples/sites/demos/pc/app/base-select/basic-usage-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/basic-usage.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/basic-usage.vue create mode 100644 examples/sites/demos/pc/app/base-select/binding-obj-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/binding-obj.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/binding-obj.vue create mode 100644 examples/sites/demos/pc/app/base-select/cache-usage-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/cache-usage.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/cache-usage.vue create mode 100644 examples/sites/demos/pc/app/base-select/clear-no-match-value-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/clear-no-match-value.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/clear-no-match-value.vue create mode 100644 examples/sites/demos/pc/app/base-select/clearable-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/clearable.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/clearable.vue create mode 100644 examples/sites/demos/pc/app/base-select/collapse-tags-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/collapse-tags.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/collapse-tags.vue create mode 100644 examples/sites/demos/pc/app/base-select/copy-multi-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/copy-multi.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/copy-multi.vue create mode 100644 examples/sites/demos/pc/app/base-select/copy-single-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/copy-single.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/copy-single.vue create mode 100644 examples/sites/demos/pc/app/base-select/disabled-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/disabled.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/disabled.vue create mode 100644 examples/sites/demos/pc/app/base-select/events-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/events.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/events.vue create mode 100644 examples/sites/demos/pc/app/base-select/filter-method-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/filter-method.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/filter-method.vue create mode 100644 examples/sites/demos/pc/app/base-select/filter-mode-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/filter-mode.vue create mode 100644 examples/sites/demos/pc/app/base-select/hide-drop-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/hide-drop.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/hide-drop.vue create mode 100644 examples/sites/demos/pc/app/base-select/input-box-type-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/input-box-type.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/input-box-type.vue create mode 100644 examples/sites/demos/pc/app/base-select/is-drop-inherit-width-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/is-drop-inherit-width.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/is-drop-inherit-width.vue create mode 100644 examples/sites/demos/pc/app/base-select/manual-focus-blur-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/manual-focus-blur.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/manual-focus-blur.vue create mode 100644 examples/sites/demos/pc/app/base-select/map-field-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/map-field.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/map-field.vue create mode 100644 examples/sites/demos/pc/app/base-select/memoize-usage-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/memoize-usage.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/memoize-usage.vue create mode 100644 examples/sites/demos/pc/app/base-select/multiple-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/multiple-mix-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/multiple-mix.vue create mode 100644 examples/sites/demos/pc/app/base-select/multiple.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/multiple.vue create mode 100644 examples/sites/demos/pc/app/base-select/native-properties-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/native-properties.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/native-properties.vue create mode 100644 examples/sites/demos/pc/app/base-select/no-data-text-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/no-data-text.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/no-data-text.vue create mode 100644 examples/sites/demos/pc/app/base-select/optimization-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/optimization.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/optimization.vue create mode 100644 examples/sites/demos/pc/app/base-select/option-group-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/option-group.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/option-group.vue create mode 100644 examples/sites/demos/pc/app/base-select/popup-style-position-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/popup-style-position.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/popup-style-position.vue create mode 100644 examples/sites/demos/pc/app/base-select/remote-method-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/remote-method.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/remote-method.vue create mode 100644 examples/sites/demos/pc/app/base-select/searchable-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/searchable.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/searchable.vue create mode 100644 examples/sites/demos/pc/app/base-select/show-alloption-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/show-alloption.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/show-alloption.vue create mode 100644 examples/sites/demos/pc/app/base-select/show-tip-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/show-tip.vue create mode 100644 examples/sites/demos/pc/app/base-select/size-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/size.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/size.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-default-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-default.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/slot-default.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-empty-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-empty.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/slot-empty.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-footer-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-footer.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/slot-footer.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-label-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-label.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-prefix-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-prefix.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/slot-prefix.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-reference-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/slot-reference.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/slot-reference.vue create mode 100644 examples/sites/demos/pc/app/base-select/tag-type-composition-api.vue create mode 100644 examples/sites/demos/pc/app/base-select/tag-type.spec.ts create mode 100644 examples/sites/demos/pc/app/base-select/tag-type.vue create mode 100644 examples/sites/demos/pc/app/base-select/webdoc/base-select.cn.md create mode 100644 examples/sites/demos/pc/app/base-select/webdoc/base-select.en.md create mode 100644 examples/sites/demos/pc/app/base-select/webdoc/base-select.js create mode 100644 packages/renderless/src/base-select/index.ts create mode 100644 packages/renderless/src/base-select/vue.ts create mode 100644 packages/theme/src/base-select/aurora-theme.js create mode 100644 packages/theme/src/base-select/index.less create mode 100644 packages/theme/src/base-select/smb-theme.js create mode 100644 packages/theme/src/base-select/vars.less create mode 100644 packages/vue/src/base-select/index.ts create mode 100644 packages/vue/src/base-select/package.json create mode 100644 packages/vue/src/base-select/src/index.ts create mode 100644 packages/vue/src/base-select/src/mobile-first.vue create mode 100644 packages/vue/src/base-select/src/pc.vue create mode 100644 packages/vue/src/base-select/src/token.ts diff --git a/examples/sites/demos/apis/base-select.js b/examples/sites/demos/apis/base-select.js new file mode 100644 index 0000000000..9439ca8a1e --- /dev/null +++ b/examples/sites/demos/apis/base-select.js @@ -0,0 +1,886 @@ +export default { + mode: ['pc', 'mobile-first'], + apis: [ + { + name: 'base-select', + type: 'component', + props: [ + { + name: 'all-text', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '当下拉中显示全部时,自定义全部的显示文本。不指定时,则默认显示"全部"', + 'en-US': + 'When all is displayed in the drop-down list, you can customize the display text of all. If this parameter is not specified, All is displayed by default.' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'all-text', + mfDemo: 'all-text' + }, + { + name: 'allow-copy', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否允许复制输入框的内容,适用单选可搜索场景', + 'en-US': + 'Is it allowed to copy the content of the input box, applicable to single choice searchable scenarios' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'copy-single', + mfDemo: 'copy-single' + }, + { + name: 'allow-create', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否允许创建新条目,需配合 filterable 使用。若搜索字段不在选项列表中,可创建为新的选项', + 'en-US': + 'Is it allowed to create new entries? It needs to be used in conjunction with filterable. If the search field is not in the option list, it can be created as a new option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'allow-create', + mfDemo: 'allow-create' + }, + { + name: 'autocomplete', + type: 'string', + defaultValue: "'off'", + desc: { + 'zh-CN': '输入框的原生 autocomplete 属性', + 'en-US': 'The native autocomplete attribute of the input box' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'native-properties', + mfDemo: 'native-properties' + }, + { + name: 'cache-op', + typeAnchorName: 'ICacheOp', + type: 'ICacheOp', + defaultValue: + "
\n{\n  key: '',\n  sortBy: 'frequency',\n  sort: 'desc',\n  dataKey: 'value',\n  highlightClass: \n  'memorize-highlight',\n  highlightNum: Infinity,\n  cacheNum: Infinity,\n  serialize: JSON.stringify\n  deserialize: JSON.parse\n}\n
", + desc: { + 'zh-CN': '启用本地缓存已选项的功能配置(根据用户点击选择的次数、最后时间继续存储排序)', + 'en-US': 'Set the component type when Grid or Tree is embedded in the drop-down list box.' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'cache-usage', + mfDemo: 'cache-usage' + }, + { + name: 'clear-no-match-value', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否自动清空无法在 options 中找到匹配项的值', + 'en-US': 'Automatically clear values that cannot find matching items in options' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'clear-no-match-value', + mfDemo: 'clear-no-match-value' + }, + { + name: 'clearable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否启用一键清除的功能', + 'en-US': 'Whether to display the one click clear button, only applicable to radio selection' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'clearable', + mfDemo: 'clearable' + }, + { + name: 'click-expend', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '点击可展开或收起显示不全的选项。仅用于多选', + 'en-US': 'Click to expand or collapse options. Only applicable to multiple selections' + } + }, + { + name: 'collapse-tags', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否将多个标签折叠显示。仅适用多选', + 'en-US': 'Whether to collapse multiple labels for display. Only applicable to multiple selections' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'collapse-tags', + mfDemo: 'collapse-tags' + }, + { + name: 'copyable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否启用一键复制的功能。点击复制按钮一键复制所有标签的文本内容并以逗号分隔,仅适用于多选', + 'en-US': + 'Is the one click copy function enabled. Click the copy button to copy the text content of all labels with one click, separated by commas, only applicable to multiple selections' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'copy-multi', + mfDemo: 'copy-multi' + }, + { + name: 'default-first-option', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否启用按 Enter 键选择第一个匹配项的功能。需配合 filterable 或 remote 使用', + 'en-US': + 'Whether to enable the function of pressing the Enter key to select the first match. Must be used in conjunction with filterable or remote' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'allow-create', + mfDemo: 'allow-create' + }, + { + name: 'disabled', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否禁用', + 'en-US': 'Is it disabled' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'disabled', + mfDemo: 'disabled' + }, + { + name: 'dropdown-icon', + type: 'Component', + defaultValue: '', + desc: { + 'zh-CN': '自定义下拉图标', + 'en-US': 'Custom drop-down icon' + }, + mode: ['pc'], + pcDemo: 'multiple' + }, + { + name: 'dropdown-style', + type: 'String', + defaultValue: '', + desc: { + 'zh-CN': '自定义下拉选项样式', + 'en-US': 'Custom drop-down options style' + }, + mode: ['pc'], + pcDemo: 'multiple' + }, + { + name: 'filter-method', + type: '(query: string) => void', + defaultValue: '', + desc: { + 'zh-CN': '自定义过滤方法', + 'en-US': 'Custom filtering method' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'filter-method', + mfDemo: 'filter-method' + }, + { + name: 'filterable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否可搜索', + 'en-US': 'Is it searchable' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'filter-method', + mfDemo: 'filter-method' + }, + { + name: 'input-box-type', + type: "'input' | 'underline'", + defaultValue: "'input'", + desc: { + 'zh-CN': '输入框的显示类型', + 'en-US': 'Display type of input box' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'input-box-type', + mfDemo: 'input-box-type' + }, + { + name: 'is-drop-inherit-width', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '下拉弹框的宽度是否跟输入框保持一致。默认超出输入框宽度时由内容撑开', + 'en-US': + 'Is the width of the dropdown box consistent with the input box. By default, when the width of the input box is exceeded, it is supported by the content' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'is-drop-inherit-width', + mfDemo: 'is-drop-inherit-width' + }, + { + name: 'loading', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否加载中,适用于远程搜索场景', + 'en-US': 'Loading or not, suitable for remote search scenarios' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'remote-method', + mfDemo: 'remote-method' + }, + { + name: 'loading-text', + type: 'string', + defaultValue: "'加载中'", + desc: { + 'zh-CN': '远程加载时显示的文本', + 'en-US': 'Text displayed during remote loading' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'remote-method', + mfDemo: 'remote-method' + }, + { + name: 'max-visible-rows', + type: 'number', + defaultValue: '1', + desc: { + 'zh-CN': '多行默认最大显示行数,超出后选项自动隐藏', + 'en-US': + 'Default maximum display lines for multiple lines, with automatic hiding option for exceeding lines' + }, + mode: ['pc'], + pcDemo: 'collapse-tags' + }, + { + name: 'modelValue / v-model', + type: 'string | number | Array', + defaultValue: '', + desc: { + 'zh-CN': '绑定值', + 'en-US': 'Bind value' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'multiple', + mfDemo: 'multiple' + }, + { + name: 'multiple', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否允许选择多个选项', + 'en-US': 'Allow multiple options to be selected' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'multiple', + mfDemo: 'multiple' + }, + { + name: 'multiple-limit', + type: 'number', + defaultValue: '0', + desc: { + 'zh-CN': '多选时最多可选择的个数,默认为 0 不限制', + 'en-US': + 'When selecting multiple options, the maximum number of options available is 0, with no limit by default' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'multiple-limit', + mfDemo: 'multiple-limit' + }, + { + name: 'name', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '输入框的原生 name 属性', + 'en-US': 'The native name attribute of the input box' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'native-properties', + mfDemo: 'native-properties' + }, + { + name: 'no-data-text', + type: 'string', + defaultValue: "'暂无相关数据'", + desc: { + 'zh-CN': '选项列表为空时显示的文本,也可以使用 empty 插槽设置', + 'en-US': 'The text displayed when the option list is empty can also be set using empty slots' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'no-data-text', + mfDemo: 'no-data-text' + }, + { + name: 'no-match-text', + type: 'string', + defaultValue: "'无匹配数据'", + desc: { + 'zh-CN': '搜索条件无匹配时显示的文本,也可以使用 empty 插槽设置', + 'en-US': + 'The text displayed when there is no match for the search criteria can also be set using empty slots' + }, + mode: ['pc'], + pcDemo: 'filter-method' + }, + { + name: 'options', + typeAnchorName: 'IOption', + type: 'IOption[]', + defaultValue: '', + desc: { + 'zh-CN': '选项列表配置,使用后不需要再配置 tiny-option', + 'en-US': 'Option list configuration, no need to configure tiny options after use' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'map-field', + mfDemo: 'map-field' + }, + { + name: 'placeholder', + type: 'string', + defaultValue: "'请选择'", + desc: { + 'zh-CN': '占位符', + 'en-US': 'Placeholder' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'native-properties', + mfDemo: 'native-properties' + }, + { + name: 'placement', + typeAnchorName: 'IPlacement', + type: 'IPlacement', + defaultValue: "'bottom-start'", + desc: { + 'zh-CN': '下拉弹框相对于触发源的弹出位置', + 'en-US': 'The pop-up position of the pull-down pop-up box relative to the trigger source' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'popup-style-position', + mfDemo: 'popup-style-position' + }, + { + name: 'popper-append-to-body', + type: 'boolean', + defaultValue: 'true', + desc: { + 'zh-CN': '是否将弹出框的 dom 元素插入至 body 元素', + 'en-US': 'Whether to insert the dom element of the pop-up box into the body element' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'popup-style-position', + mfDemo: 'popup-style-position' + }, + { + name: 'popper-class', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '自定义下拉框的类名,用于自定义样式', + 'en-US': 'The class name of the custom dropdown box, used for customizing styles' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'popup-style-position', + mfDemo: 'popup-style-position' + }, + { + name: 'remote', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否为远程搜索', + 'en-US': 'Is it a remote search' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'remote-method', + mfDemo: 'remote-method' + }, + { + name: 'remote-method', + type: '(query:string) => void', + defaultValue: '', + desc: { + 'zh-CN': '远程搜索的方法', + 'en-US': 'Remote search methods' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'remote-method', + mfDemo: 'remote-method' + }, + { + name: 'reserve-keyword', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '多选可搜索时,是否在选中一个选项后仍然保留当前的搜索关键词', + 'en-US': + 'When selecting multiple searchable options, do you still keep the current search keywords after selecting one option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'remote-method', + mfDemo: 'remote-method' + }, + { + name: 'searchable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否启用下拉面板搜索', + 'en-US': 'Whether to allow users to create new items. This parameter must be used together with filterable.' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'searchable', + mfDemo: 'searchable' + }, + { + name: 'show-alloption', + type: 'boolean', + defaultValue: 'true', + desc: { + 'zh-CN': '是否展示 “全选” 选项', + 'en-US': 'Whether to display the "Select All" option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'show-alloption', + mfDemo: 'show-alloption' + }, + { + name: 'show-empty-image', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否显示空数据图片', + 'en-US': 'Display empty data image' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'no-data-text', + mfDemo: 'no-data-text' + }, + { + name: 'size', + type: "'medium' | 'small' | 'mini'", + defaultValue: '', + desc: { + 'zh-CN': '输入框尺寸。', + 'en-US': 'Text box size' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'size', + mfDemo: 'size' + }, + { + name: 'tag-selectable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '输入框中的标签是否可通过鼠标选中复制', + 'en-US': 'Can the label in the input box be copied by selecting it with the mouse' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'copy-multi', + mfDemo: 'copy-multi' + }, + { + name: 'tag-type', + type: "'success' | 'info' | 'warning' | 'danger'", + defaultValue: '', + desc: { + 'zh-CN': '标签类型,仅多选适用。使用 aurora 主题时设置该属性为 info', + 'en-US': + 'Label type, only applicable for multiple choices. Set this property to info when using the aurora theme' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'tag-type', + mfDemo: 'tag-type' + }, + { + name: 'text-field', + type: 'string', + defaultValue: "'label'", + desc: { + 'zh-CN': '显示值字段', + 'en-US': 'Show Value Fields' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'map-field', + mfDemo: 'map-field' + }, + { + name: 'text-split', + type: 'string', + defaultValue: "','", + desc: { + 'zh-CN': '自定义复制文本的分隔符,需结合 copyable 属性使用', + 'en-US': 'The separator for custom copied text needs to be used in conjunction with the copyable attribute' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'copy-multi', + mfDemo: 'copy-multi' + }, + { + name: 'top-create', + type: 'boolean', + defaultValue: '', + desc: { + 'zh-CN': '是否显示下拉框顶部新增按钮,点击按钮会抛出一个 top-create-click 事件,可以在事件中自定义一些行为', + 'en-US': + 'Indicates whether to display a new button on the top of the drop-down list box. When a button is clicked, a top-create-click event is thrown. You can customize some behaviors in the event' + }, + mode: ['pc'], + pcDemo: 'allow-create' + }, + { + name: 'value-field', + type: 'string', + defaultValue: "'value'", + desc: { + 'zh-CN': '绑定值字段', + 'en-US': 'Bind Value Field' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'map-field', + mfDemo: 'map-field' + }, + { + name: 'value-key', + type: 'string', + defaultValue: "'value'", + desc: { + 'zh-CN': '作为 value 唯一标识的键名,绑定值为对象类型时必填', + 'en-US': + 'The key name that uniquely identifies the value must be filled in when the binding value is of object type' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'binding-obj', + mfDemo: 'binding-obj' + }, + { + name: 'show-proportion', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否展示多选框选中条数和总条数的占比的文字提示', + 'en-US': + 'Display the proportion of the number of selected items and the total number of items in the multiple-choice box' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'collapse-tags', + mfDemo: 'collapse-tags' + }, + { + name: 'show-limit-text', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': + '是否展示多选框开启多选限制选择数量时,选中条数和限制总条数的占比的文字提示。 该属性的优先级大于show-proportion 属性,同时设置只', + 'en-US': + 'Display the proportion of the number of selected items and the total number of items in the multiple-choice box' + }, + mode: ['pc'], + pcDemo: 'multiple' + } + ], + events: [ + { + name: 'blur', + type: '(event:MouseEvent) => void', + defaultValue: '', + desc: { + 'zh-CN': '监听输入框失去焦点事件', + 'en-US': 'Listening for input box lose focus event' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + }, + { + name: 'change', + type: '(value:string|number|Array, list:Array) => void', + defaultValue: '', + desc: { + 'zh-CN': '监听绑定值变更事件', + 'en-US': 'Listening for binding value change events' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + }, + { + name: 'clear', + type: '() => void', + defaultValue: '', + desc: { + 'zh-CN': '监听一键清除事件', + 'en-US': 'Listening for one click clear events' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + }, + { + name: 'focus', + type: '(event:MouseEvent) => void', + defaultValue: '', + desc: { + 'zh-CN': '监听输入框获取焦点事件', + 'en-US': 'Listening to input boxes to obtain focus events' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + }, + { + name: 'remove-tag', + type: '(tag:string|number) => void', + defaultValue: '', + desc: { + 'zh-CN': '监听多选时移除标签事件', + 'en-US': 'Remove label events when listening for multiple selections' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + }, + { + name: 'top-create-click', + type: '() => void', + defaultValue: '', + desc: { + 'zh-CN': '监听顶部新增按钮点击事件,同 top-create 属性一起使用', + 'en-US': + 'Listens to the click event of a new button on the top. This parameter is used together with the top-create attribute' + }, + mode: ['pc'], + pcDemo: 'events' + }, + { + name: 'visible-change', + type: '(status:boolean) => void', + defaultValue: '', + desc: { + 'zh-CN': '监听下拉弹框的显示隐藏状态', + 'en-US': 'Monitor the display and hidden status of dropdown pop ups' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'events', + mfDemo: 'events' + } + ], + methods: [ + { + name: 'blur', + type: '() => void', + defaultValue: '', + desc: { + 'zh-CN': '使输入框失去焦点', + 'en-US': 'Causes the input box to lose focus' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'manual-focus-blur', + mfDemo: 'manual-focus-blur' + }, + { + name: 'focus', + type: '() => void', + defaultValue: '', + desc: { + 'zh-CN': '使输入框获取焦点', + 'en-US': 'Bring the input box to focus' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'manual-focus-blur', + mfDemo: 'manual-focus-blur' + } + ], + slots: [ + { + name: 'default', + type: '', + defaultValue: '', + desc: { + 'zh-CN': '选项默认插槽', + 'en-US': 'Option default slot' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'slot-default', + mfDemo: 'slot-default' + }, + { + name: 'empty', + type: '', + defaultValue: '', + desc: { + 'zh-CN': '空数据插槽', + 'en-US': 'Empty data slot' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'slot-empty', + mfDemo: 'slot-empty' + }, + { + name: 'footer', + type: '', + defaultValue: '', + desc: { + 'zh-CN': '下拉弹框底部插槽', + 'en-US': 'Pull down the bottom slot of the pop-up box' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'slot-footer', + mfDemo: 'slot-footer' + }, + { + name: 'prefix', + type: '', + defaultValue: '', + desc: { + 'zh-CN': '输入框前缀插槽', + 'en-US': 'Input box prefix slot' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'slot-prefix', + mfDemo: 'slot-prefix' + }, + { + name: 'reference', + type: '', + defaultValue: '', + desc: { + 'zh-CN': '触发源插槽', + 'en-US': 'Trigger Source Slot' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'slot-reference', + mfDemo: 'slot-reference' + } + ] + }, + { + name: 'option', + type: 'component', + props: [ + { + name: 'disabled', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '选项是否禁用', + 'en-US': 'Is the option disabled' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'disabled', + mfDemo: 'disabled' + }, + { + name: 'icon', + type: 'Component', + defaultValue: '', + desc: { + 'zh-CN': '自定义选项的图标', + 'en-US': 'Customize icons for options' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: 'basic-usage' + }, + { + name: 'label', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '选项的显示文本', + 'en-US': 'Display text for option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: 'basic-usage' + }, + { + name: 'required', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '选项是否必选', + 'en-US': 'Is it mandatory to select an option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: '', + mfDemo: '' + }, + { + name: 'value', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '选项的值', + 'en-US': 'Value for option' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: 'basic-usage' + } + ], + events: [], + methods: [], + slots: [] + } + ], + types: [ + { + name: 'IOption', + type: 'interface', + code: ` +interface IOption { + value?: string | number + label?: string + disabled?: boolean + icon?: Component + required?:boolean +} +` + }, + { + name: 'ICacheOp', + type: 'interface', + code: ` +interface ICacheItem { + frequency: number + key: string + time: number +} + +interface ICacheOp { + key: string // 本地缓存的唯一 key 值 + sortBy?: 'frequency' | 'time' // 排序的字段,默认 frequency (频次) + sort?: 'desc' | 'asc' // 排序方式,默认 desc (降序) + dataKey?: string // 数据中的唯一标识的 key 名称,默认 value + highlightClass?: string // 个性化高亮 class 名称,默认:memorize-highlight + highlightNum?: number // 高亮个性化的条数,默认:Infinity + cacheNum?: number // 存储个性化的条数,默认:Infinity + serialize?: ()=> string // 本地存储序列化方法,默认:JSON.stringify + deserialize?: ()=> ICacheItem[] // 本地存储序反列化方法,默认:JSON.parse +} +` + }, + { + name: 'IPlacement', + type: 'type', + code: ` +type IPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end' +` + } + ] +} diff --git a/examples/sites/demos/pc/app/base-select/all-text-composition-api.vue b/examples/sites/demos/pc/app/base-select/all-text-composition-api.vue new file mode 100644 index 0000000000..5d51123303 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/all-text-composition-api.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/all-text.spec.ts b/examples/sites/demos/pc/app/base-select/all-text.spec.ts new file mode 100644 index 0000000000..39905a355b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/all-text.spec.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test' + +test('多选时自定义全部的文本', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#all-text') + const wrap = page.locator('#all-text') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await select.locator('.tiny-input__suffix').click() + await expect(option.filter({ hasText: '全部小吃' })).toHaveCount(1) +}) diff --git a/examples/sites/demos/pc/app/base-select/all-text.vue b/examples/sites/demos/pc/app/base-select/all-text.vue new file mode 100644 index 0000000000..ccd74b4662 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/all-text.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/allow-create-composition-api.vue b/examples/sites/demos/pc/app/base-select/allow-create-composition-api.vue new file mode 100644 index 0000000000..1fd7223fbe --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/allow-create-composition-api.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/allow-create.spec.ts b/examples/sites/demos/pc/app/base-select/allow-create.spec.ts new file mode 100644 index 0000000000..a1289ba5e7 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/allow-create.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test' + +test('点击选中', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.waitForTimeout(300) + await page.goto('base-select#allow-create') + + const wrap = page.locator('#allow-create') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = select.locator('.tiny-input__inner') + + await input.click() + await input.fill('测试allow-create') + const KeyboardEvent = await page.evaluateHandle(() => new KeyboardEvent('keyup')) + await input.dispatchEvent('keyup', { KeyboardEvent }) + + await expect(input).toHaveValue('测试allow-create') + await dropdown.getByRole('listitem').filter({ hasText: '测试allow-create' }).click() + await expect(input).toHaveValue('测试allow-create') + + await input.click() + await expect(input).toHaveValue('') + await expect(dropdown.getByRole('listitem').filter({ hasText: '测试allow-create' })).toHaveClass(/selected/) +}) + +test('enter 选中', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.waitForTimeout(300) + await page.goto('base-select#allow-create') + + const wrap = page.locator('#allow-create') + const select = wrap.locator('.tiny-base-select').nth(1) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = select.locator('.tiny-input__inner') + + await input.click() + await input.press('a') + await input.press('b') + await page.waitForTimeout(300) + await input.press('Enter') + + await expect(dropdown).toBeHidden() + await expect(input).toHaveValue('ab') + + await input.click() + + await expect(input).toHaveValue('') + await expect(dropdown.getByRole('listitem').filter({ hasText: 'ab' })).toHaveClass(/selected/) +}) diff --git a/examples/sites/demos/pc/app/base-select/allow-create.vue b/examples/sites/demos/pc/app/base-select/allow-create.vue new file mode 100644 index 0000000000..18299f53c5 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/allow-create.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/automatic-dropdown-composition-api.vue b/examples/sites/demos/pc/app/base-select/automatic-dropdown-composition-api.vue new file mode 100644 index 0000000000..e86522b7b1 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/automatic-dropdown-composition-api.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts b/examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts new file mode 100644 index 0000000000..914f68dc25 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/automatic-dropdown.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test' + +test('不可搜索时,获取焦点不下拉', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#automatic-dropdown') + const wrap = page.locator('#automatic-dropdown') + const input = wrap.locator('.tiny-input__inner').first() + const dropdown = page.locator('.tiny-select-dropdown').first() + + await wrap.getByRole('button').first().click() + // 聚焦高亮 + await expect(input).toHaveCSS('border-color', 'rgb(94, 124, 224)') + // 不下拉 + await expect(dropdown).toBeHidden() +}) + +test('可搜索时,获取焦点自动下拉', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#automatic-dropdown') + const wrap = page.locator('#automatic-dropdown') + const input = wrap.locator('.tiny-input__inner').nth(1) + const dropdown = page.locator('.tiny-select-dropdown').nth(1) + + await wrap.getByRole('button').nth(1).click() + // 聚焦下拉 + await dropdown.getByRole('listitem').filter({ hasText: '上海' }).click() + await expect(input).toHaveValue('上海') + // 验证选中 + await input.click() + await expect(page.getByRole('listitem').filter({ hasText: '上海' })).toHaveClass(/selected/) +}) diff --git a/examples/sites/demos/pc/app/base-select/automatic-dropdown.vue b/examples/sites/demos/pc/app/base-select/automatic-dropdown.vue new file mode 100644 index 0000000000..8ce56e852a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/automatic-dropdown.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/base-select/basic-usage-composition-api.vue new file mode 100644 index 0000000000..d32cd127f3 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/basic-usage-composition-api.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts b/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts new file mode 100644 index 0000000000..ff1e532181 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test' + +test('基础用法标签式', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#basic-usage') + const wrap = page.locator('#basic-usage') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await option.filter({ hasText: '天津' }).click() + await expect(input).toHaveValue('天津') + await select.locator('.tiny-input__suffix svg').click() + await expect(page.getByRole('listitem').filter({ hasText: '天津' })).toHaveClass(/selected/) + await option.filter({ hasText: '深圳' }).click() + await expect(input).toHaveValue('深圳') + await input.click() + await expect(option.filter({ hasText: '深圳' })).toHaveClass(/selected/) + await expect(option.locator('.tiny-option__icon')).toHaveCount(5) + await option.nth(0).click() + await expect(dropdown).toBeHidden() +}) + +test('基础用法配置式', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#basic-usage') + const wrap = page.locator('#basic-usage') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await option.filter({ hasText: '天津' }).click() + await expect(input).toHaveValue('天津') + await select.locator('.tiny-input__suffix svg').click() + await expect(page.getByRole('listitem').filter({ hasText: '天津' })).toHaveClass(/selected/) + await option.filter({ hasText: '深圳' }).click() + await expect(input).toHaveValue('深圳') + await input.click() + await expect(option.filter({ hasText: '深圳' })).toHaveClass(/selected/) + await expect(option.locator('.tiny-option__icon')).toHaveCount(5) + await option.nth(0).click() + await expect(dropdown).toBeHidden() +}) diff --git a/examples/sites/demos/pc/app/base-select/basic-usage.vue b/examples/sites/demos/pc/app/base-select/basic-usage.vue new file mode 100644 index 0000000000..add4b8033d --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/basic-usage.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/binding-obj-composition-api.vue b/examples/sites/demos/pc/app/base-select/binding-obj-composition-api.vue new file mode 100644 index 0000000000..991904adc7 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/binding-obj-composition-api.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/binding-obj.spec.ts b/examples/sites/demos/pc/app/base-select/binding-obj.spec.ts new file mode 100644 index 0000000000..34ad2f1187 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/binding-obj.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test' + +test('binding-obj', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#binding-obj') + + const wrap = page.locator('#binding-obj') + const input = wrap.locator('.tiny-input__inner') + const dropdown = page.locator('.tiny-select-dropdown') + const valueLocator = wrap.locator('.value') + + await input.click() + await dropdown.getByRole('listitem').filter({ hasText: '重庆' }).click() + await expect(input).toHaveValue('重庆') + await expect(valueLocator).toHaveText('{ "val": "选项4", "id": 4 }') + + await input.click() + await dropdown.getByRole('listitem').filter({ hasText: '天津' }).click() + await expect(input).toHaveValue('天津') + await expect(valueLocator).toHaveText('{ "val": "选项3", "id": 3 }') +}) diff --git a/examples/sites/demos/pc/app/base-select/binding-obj.vue b/examples/sites/demos/pc/app/base-select/binding-obj.vue new file mode 100644 index 0000000000..342d16d4fa --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/binding-obj.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/cache-usage-composition-api.vue b/examples/sites/demos/pc/app/base-select/cache-usage-composition-api.vue new file mode 100644 index 0000000000..08aca15af7 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/cache-usage-composition-api.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/cache-usage.spec.ts b/examples/sites/demos/pc/app/base-select/cache-usage.spec.ts new file mode 100644 index 0000000000..5b32cb0055 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/cache-usage.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test' + +test('cache-op', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#cache-usage') + + const wrap = page.locator('#cache-usage') + const dropdown = page.locator('.tiny-select-dropdown') + const input = wrap.locator('.tiny-input__inner') + const cacheValue = wrap.locator('.cache-value') + + await input.click() + await dropdown.getByRole('listitem').filter({ hasText: '北京' }).click() + await expect(cacheValue).toContainText(['选项1']) + + await input.click() + await dropdown.getByRole('listitem').filter({ hasText: '上海' }).click() + await expect(cacheValue).toContainText(['选项2']) +}) diff --git a/examples/sites/demos/pc/app/base-select/cache-usage.vue b/examples/sites/demos/pc/app/base-select/cache-usage.vue new file mode 100644 index 0000000000..54aca34e20 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/cache-usage.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/clear-no-match-value-composition-api.vue b/examples/sites/demos/pc/app/base-select/clear-no-match-value-composition-api.vue new file mode 100644 index 0000000000..08e422df27 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clear-no-match-value-composition-api.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/clear-no-match-value.spec.ts b/examples/sites/demos/pc/app/base-select/clear-no-match-value.spec.ts new file mode 100644 index 0000000000..1f8ab8884c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clear-no-match-value.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test' + +test('单选找不到匹配值', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#clear-no-match-value') + const wrap = page.locator('#clear-no-match-value') + const input = wrap.locator('.tiny-input__inner').nth(0) + + // 验证是否清空不匹配的值 + await expect(input).toHaveValue('') + await expect(wrap.locator('.val')).toHaveText('') +}) + +test('多选找不到匹配值', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#clear-no-match-value') + const wrap = page.locator('#clear-no-match-value') + const tag = wrap.locator('.tiny-base-select').nth(1).locator('.tiny-tag') + + // 验证是否清空不匹配的值 + await expect(tag).toHaveCount(1) + await expect(tag).toHaveText('上海') + await expect(wrap.locator('.multi-val')).toHaveText('[ "选项2" ]') +}) diff --git a/examples/sites/demos/pc/app/base-select/clear-no-match-value.vue b/examples/sites/demos/pc/app/base-select/clear-no-match-value.vue new file mode 100644 index 0000000000..ec76936c92 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clear-no-match-value.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/clearable-composition-api.vue b/examples/sites/demos/pc/app/base-select/clearable-composition-api.vue new file mode 100644 index 0000000000..f301d6c70c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clearable-composition-api.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/clearable.spec.ts b/examples/sites/demos/pc/app/base-select/clearable.spec.ts new file mode 100644 index 0000000000..7906b0ed2a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clearable.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test' + +test('clearable', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#clearable') + const wrap = page.locator('#clearable') + const dropdown = page.locator('.tiny-select-dropdown') + const input = wrap.locator('.tiny-input__inner') + const icon = wrap.locator('.tiny-input__suffix') + + // 验证默认值 + await expect(input).toHaveValue('天津') + // 验证清空 + await input.hover() + await icon.click() + await expect(input).toHaveValue('') + // 验证选中 + await icon.click() + await dropdown.getByRole('listitem').filter({ hasText: '上海' }).click() + await expect(input).toHaveValue('上海') +}) diff --git a/examples/sites/demos/pc/app/base-select/clearable.vue b/examples/sites/demos/pc/app/base-select/clearable.vue new file mode 100644 index 0000000000..7b6878f829 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/clearable.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/collapse-tags-composition-api.vue b/examples/sites/demos/pc/app/base-select/collapse-tags-composition-api.vue new file mode 100644 index 0000000000..1c6b672d37 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/collapse-tags-composition-api.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/collapse-tags.spec.ts b/examples/sites/demos/pc/app/base-select/collapse-tags.spec.ts new file mode 100644 index 0000000000..0072f2686b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/collapse-tags.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test' + +test('collapse-tags', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#collapse-tags') + const wrap = page.locator('#collapse-tags') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const tag = select.locator('.tiny-tag') + const option = dropdown.locator('.tiny-option') + + // 验证默认值的折叠标签显示 + await expect(tag).toHaveCount(2) + await expect(tag.filter({ hasText: '北京' })).toBeVisible() + await expect(tag.filter({ hasText: '+ 1' })).toBeVisible() + + // 点击下拉后选中效果 + await tag.first().click() + await expect(option.filter({ hasText: '北京' })).toHaveClass(/selected/) + await expect(option.filter({ hasText: '上海' })).toHaveClass(/selected/) + + // 取消选中一个 + await option.filter({ hasText: '北京' }).locator('span').nth(2).click() + await expect(tag.filter({ hasText: '+ 1' })).toBeHidden() + await expect(tag).toHaveCount(1) + + // 再选中2个 + await option.filter({ hasText: '天津' }).locator('span').nth(2).click() + await option.filter({ hasText: '深圳' }).locator('span').nth(2).click() + await expect(tag.filter({ hasText: '+ 2' })).toBeVisible() + await expect(tag).toHaveCount(2) +}) diff --git a/examples/sites/demos/pc/app/base-select/collapse-tags.vue b/examples/sites/demos/pc/app/base-select/collapse-tags.vue new file mode 100644 index 0000000000..b980f4ddba --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/collapse-tags.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/copy-multi-composition-api.vue b/examples/sites/demos/pc/app/base-select/copy-multi-composition-api.vue new file mode 100644 index 0000000000..691d263b1c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-multi-composition-api.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/copy-multi.spec.ts b/examples/sites/demos/pc/app/base-select/copy-multi.spec.ts new file mode 100644 index 0000000000..5c5083b1b4 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-multi.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' + +test('多选复制单个标签', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#copy-multi') + + const wrap = page.locator('#copy-multi') + const select = wrap.locator('.tiny-base-select').nth(0) + const tag = select.locator('.tiny-tag').nth(0) + + await expect(tag).toContainText('北京') + await page.waitForTimeout(200) + const tagBox = await tag.locator('span').boundingBox() + const x = tagBox.x + tagBox.width + const y = tagBox.y + tagBox.height - 5 + + await page.mouse.move(tagBox.x, tagBox.y) + await page.waitForTimeout(200) + await page.mouse.down() + await page.waitForTimeout(200) + await page.mouse.move(x, y) + await page.waitForTimeout(200) + await page.mouse.up() + + await page.keyboard.press('Control+C') + await page.waitForTimeout(200) + const valueInput = page.locator('.copy-value .tiny-input__inner') + await expect(valueInput).toHaveValue('') + await valueInput.focus() + await page.keyboard.press('Control+V') + await page.waitForTimeout(200) + await expect(valueInput).toHaveValue('北京') +}) + +test('多选一键复制所有标签', async ({ page }) => { + await page.goto('base-select#copy-multi') + + const wrap = page.locator('#copy-multi') + const select = wrap.locator('.tiny-base-select').nth(1) + const copyValueInput = wrap.locator('.copy-value .tiny-input__inner') + + await page.waitForTimeout(200) + await select.hover() + await select.locator('.tiny-base-select__copy > .tiny-svg').click() + + await copyValueInput.press('Control+V') + await expect(copyValueInput).toHaveValue('北京,上海') +}) + +test('多选设置复制文本分隔符', async ({ page }) => { + await page.goto('base-select#copy-multi') + + const wrap = page.locator('#copy-multi') + const select = wrap.locator('.tiny-base-select').nth(2) + const copyValueInput = wrap.locator('.copy-value .tiny-input__inner') + + await page.waitForTimeout(200) + await select.hover() + await select.locator('.tiny-base-select__copy > .tiny-svg').click() + + await copyValueInput.press('Control+V') + await expect(copyValueInput).toHaveValue('北京/上海') +}) diff --git a/examples/sites/demos/pc/app/base-select/copy-multi.vue b/examples/sites/demos/pc/app/base-select/copy-multi.vue new file mode 100644 index 0000000000..2bb4a5e16e --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-multi.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/copy-single-composition-api.vue b/examples/sites/demos/pc/app/base-select/copy-single-composition-api.vue new file mode 100644 index 0000000000..6fd7094350 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-single-composition-api.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/copy-single.spec.ts b/examples/sites/demos/pc/app/base-select/copy-single.spec.ts new file mode 100644 index 0000000000..ce839fde7a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-single.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@playwright/test' + +test('单选无需配置可复制', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#copy-single') + + const wrap = page.locator('#copy-single') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const valueInput = wrap.locator('.custom .tiny-input__inner') + + await page.waitForTimeout(200) + const inputBox = await input.boundingBox() + + await page.mouse.move(inputBox.x + inputBox?.width / 2, inputBox.y + inputBox?.height / 2) + await page.mouse.down() + await page.mouse.move(inputBox.x - 2, inputBox.y + inputBox?.height / 2) + await page.mouse.up() + await page.keyboard.press('Control+C') + await expect(valueInput).toHaveValue('') + await valueInput.focus() + await page.keyboard.press('Control+V') + await page.waitForTimeout(200) + await expect(valueInput).toHaveValue('北京') +}) + +test('单选可搜索配置 allow-copy 可复制', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#copy-single') + + const wrap = page.locator('#copy-single') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input__inner') + const valueInput = wrap.locator('.custom .tiny-input__inner') + + await expect(input).toHaveValue('北京') + await page.waitForTimeout(200) + const inputBox = await input.boundingBox() + + await page.mouse.move(inputBox.x + inputBox?.width / 2, inputBox.y + inputBox?.height / 2) + await page.mouse.down() + await page.mouse.move(inputBox.x - 2, inputBox.y + inputBox?.height / 2) + await page.mouse.up() + await page.waitForTimeout(800) + + await page.keyboard.press('Control+C') + await expect(valueInput).toHaveValue('') + await valueInput.click() + await page.keyboard.press('Control+V') + await page.waitForTimeout(200) + await expect(valueInput).toHaveValue('北京') +}) + +test('单选远程搜索配置 allow-copy 可复制', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#copy-single') + + const wrap = page.locator('#copy-single') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input__inner') + const valueInput = wrap.locator('.custom .tiny-input__inner') + + await page.waitForTimeout(100) + const inputBox = await input.boundingBox() + + await page.mouse.move(inputBox.x + inputBox?.width / 2, inputBox.y + inputBox?.height / 2) + await page.mouse.down() + await page.mouse.move(inputBox.x - 2, inputBox.y + inputBox?.height / 2) + await page.mouse.up() + await page.keyboard.press('Control+C') + await expect(valueInput).toHaveValue('') + await valueInput.focus() + await page.keyboard.press('Control+V') + await page.waitForTimeout(200) + await expect(valueInput).toHaveValue('Alabama') +}) diff --git a/examples/sites/demos/pc/app/base-select/copy-single.vue b/examples/sites/demos/pc/app/base-select/copy-single.vue new file mode 100644 index 0000000000..340ed0cd89 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/copy-single.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/disabled-composition-api.vue b/examples/sites/demos/pc/app/base-select/disabled-composition-api.vue new file mode 100644 index 0000000000..8ee34bcd96 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/disabled-composition-api.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/disabled.spec.ts b/examples/sites/demos/pc/app/base-select/disabled.spec.ts new file mode 100644 index 0000000000..e4eece8691 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/disabled.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test' + +test('下拉禁用', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#disabled') + const wrap = page.locator('#disabled') + const input = wrap.locator('.tiny-input__inner').first() + + const hasDisabled = await input.evaluate((input) => input.hasAttribute('disabled')) + await expect(hasDisabled).toBe(true) +}) + +test('多选某项禁用', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#disabled') + const wrap = page.locator('#disabled') + const select = wrap.locator('.tiny-base-select').nth(1) + const dropdown = page.locator('body > .tiny-select-dropdown') + const tag = select.locator('.tiny-tag') + const option = dropdown.locator('.tiny-option') + + await expect(tag).toHaveCount(1) + await select.click() + await expect(option.filter({ hasText: '上海' })).toHaveClass(/is-disabled/) + + await option.filter({ hasText: '上海' }).click() + await expect(tag).toHaveCount(1) + + await option.filter({ hasText: '北京' }).click() + await expect(tag).toHaveCount(2) + await expect(tag.filter({ hasText: '北京' })).toHaveCount(1) +}) + +test('单选某项禁用', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#disabled') + const wrap = page.locator('#disabled') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await select.click() + await expect(option.filter({ hasText: '全部' })).toHaveCount(0) + await expect(option.filter({ hasText: '上海' })).toHaveClass(/is-disabled/) + + await option.filter({ hasText: '上海' }).click() + await expect(dropdown).toBeVisible() + await expect(input).toHaveValue('') + + await option.filter({ hasText: '北京' }).click() + await expect(dropdown).toBeHidden() + await expect(input).toHaveValue('北京') +}) + +test('多选,禁用项默认选中', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#disabled') + const wrap = page.locator('#disabled') + const select = wrap.locator('.tiny-base-select').nth(3) + const dropdown = page.locator('body > .tiny-select-dropdown') + const tag = select.locator('.tiny-tag') + const option = dropdown.locator('.tiny-option') + + // 默认值显示tag数 + await expect(tag).toHaveCount(2) + // 禁用项默认选中不显示关闭图标 + await expect(tag.filter({ hasText: '上海' }).locator('svg')).toHaveCount(0) + // 非禁用项显示关闭图标 + await expect(tag.filter({ hasText: '天津' }).locator('svg')).toHaveCount(1) + + // 下拉禁用和默认选中的效果 + await select.click() + await expect(option.filter({ hasText: '全部' })).toHaveCount(1) + await expect(option.filter({ hasText: '上海' })).toHaveClass(/is-disabled/) + await expect(option.filter({ hasText: '上海' })).toHaveClass(/selected/) + await expect(option.filter({ hasText: '天津' })).toHaveClass(/selected/) + + // 点击禁用项不取消选中 + await option.filter({ hasText: '上海' }).click() + await expect(dropdown).toBeVisible() + await expect(tag).toHaveCount(2) + + // 选中其他项 + await option.filter({ hasText: '北京' }).click() + await expect(dropdown).toBeVisible() + await expect(tag).toHaveCount(3) +}) diff --git a/examples/sites/demos/pc/app/base-select/disabled.vue b/examples/sites/demos/pc/app/base-select/disabled.vue new file mode 100644 index 0000000000..b3454b885b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/disabled.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/events-composition-api.vue b/examples/sites/demos/pc/app/base-select/events-composition-api.vue new file mode 100644 index 0000000000..535d997a58 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/events-composition-api.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/events.spec.ts b/examples/sites/demos/pc/app/base-select/events.spec.ts new file mode 100644 index 0000000000..f29d291e75 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/events.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test' + +test('单选事件', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#events') + const wrap = page.locator('#events') + const select = wrap.locator('.tiny-base-select').first() + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const model = page.locator('.tiny-modal') + + await input.click() + await page.waitForTimeout(500) + await expect(model.filter({ hasText: '触发 focus 事件' })).toHaveCount(1) + await expect(model.filter({ hasText: '触发 visible-change 事件' })).toHaveCount(1) + + await option.first().click() + await expect(input).toHaveValue('北京') + await expect(model.filter({ hasText: '触发 change 事件' })).toHaveCount(1) + await expect(model.filter({ hasText: '触发 visible-change 事件' })).toHaveCount(2) + + await page.waitForTimeout(500) + await wrap.click() + await expect(model.filter({ hasText: '触发 blur 事件' })).toHaveCount(1) + + await page.waitForTimeout(200) + await input.hover() + await select.locator('.tiny-base-select__caret.icon-close').click() + await page.waitForTimeout(500) + await expect(input).toHaveValue('') + await expect(model.filter({ hasText: '触发 clear 事件' })).toHaveCount(1) +}) + +test('多选事件', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#events') + const wrap = page.locator('#events') + const select = wrap.locator('.tiny-base-select').nth(1) + const tag = wrap.locator('.tiny-tag') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const model = page.locator('.tiny-modal') + + await page.waitForTimeout(500) + await select.click() + await expect(model.filter({ hasText: '触发 visible-change 事件' })).toHaveCount(1) + + await option.nth(1).click() + await expect(tag).toHaveCount(1) + await expect(model.filter({ hasText: '触发 change 事件' })).toHaveCount(1) + + await option.nth(0).click() + await expect(model.filter({ hasText: '触发 change 事件' })).toHaveCount(2) + await expect(tag).toHaveCount(5) + + await page.waitForTimeout(500) + await tag.first().locator('.tiny-tag__close').click() + await expect(model.filter({ hasText: '触发 change 事件' })).toHaveCount(1) + await expect(model.filter({ hasText: '触发 remove-tag 事件' })).toHaveCount(1) + await expect(tag).toHaveCount(4) + + await wrap.click() + await expect(model.filter({ hasText: '触发 visible-change 事件' })).toHaveCount(1) + + await page.waitForTimeout(200) + await select.hover() + await select.locator('.tiny-base-select__caret.icon-close').click() + + await expect(tag).toHaveCount(0) + await expect(model.filter({ hasText: '触发 change 事件' })).toHaveCount(1) +}) diff --git a/examples/sites/demos/pc/app/base-select/events.vue b/examples/sites/demos/pc/app/base-select/events.vue new file mode 100644 index 0000000000..466cfb353d --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/events.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/filter-method-composition-api.vue b/examples/sites/demos/pc/app/base-select/filter-method-composition-api.vue new file mode 100644 index 0000000000..0236ec70e9 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/filter-method-composition-api.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/filter-method.spec.ts b/examples/sites/demos/pc/app/base-select/filter-method.spec.ts new file mode 100644 index 0000000000..cff2496376 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/filter-method.spec.ts @@ -0,0 +1,89 @@ +import { expect, test } from '@playwright/test' + +test('默认搜索', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#filter-method') + const wrap = page.locator('#filter-method') + const select = wrap.locator('.tiny-base-select').first() + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + // 1.1 没有过滤到内容 + await input.click() + // 1.1.1 验证 no-match-text + await expect(page.getByText('No Match')).toBeHidden() + await input.press('1') + await expect(input).toHaveValue('1') + await input.press('Enter') + await expect(page.getByText('No Match')).toBeVisible() + + await page.waitForTimeout(500) + let allListItems = await option.all() + allListItems.forEach(async (item) => { + await expect(item).toHaveCSS('display', 'none') + }) + + // 1.2 过滤到内容 + await input.fill('上海') + await expect(input).toHaveValue('上海') + await input.press('Enter') + + await page.waitForTimeout(200) + + allListItems.forEach(async (item) => { + const isVisibleItem = (await item.innerText()) === '上海' + if (isVisibleItem) { + await expect(item).toHaveCSS('display', 'flex') + } else { + await expect(item).toHaveCSS('display', 'none') + } + }) + await expect(option.filter({ hasText: '上海' })).toBeVisible() + await option.filter({ hasText: '上海' }).click() + await expect(input).toHaveValue('上海') +}) + +test('自定义过滤', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#filter-method') + const wrap = page.locator('#filter-method') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + // 1.1 没有过滤到内容 + await input.click() + // 1.1.1 验证 no-match-text + await expect(page.getByText('No Match')).toBeHidden() + await input.press('1') + await expect(input).toHaveValue('1') + await input.press('Enter') + await expect(page.getByText('No Match')).toBeVisible() + + await page.waitForTimeout(500) + let allListItems = await option.all() + allListItems.forEach(async (item) => { + await expect(item).toHaveCSS('display', 'none') + }) + + // 1.2 过滤到内容 + await input.fill('上海') + await expect(input).toHaveValue('上海') + await input.press('Enter') + + await page.waitForTimeout(200) + + allListItems.forEach(async (item) => { + const isVisibleItem = (await item.innerText()) === '上海' + if (isVisibleItem) { + await expect(item).toHaveCSS('display', 'flex') + } else { + await expect(item).toHaveCSS('display', 'none') + } + }) + await expect(option.filter({ hasText: '上海' })).toBeVisible() + await option.filter({ hasText: '上海' }).click() + await expect(input).toHaveValue('上海') +}) diff --git a/examples/sites/demos/pc/app/base-select/filter-method.vue b/examples/sites/demos/pc/app/base-select/filter-method.vue new file mode 100644 index 0000000000..4703e0f769 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/filter-method.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/filter-mode-composition-api.vue b/examples/sites/demos/pc/app/base-select/filter-mode-composition-api.vue new file mode 100644 index 0000000000..855d833f3f --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/filter-mode-composition-api.vue @@ -0,0 +1,42 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/filter-mode.vue b/examples/sites/demos/pc/app/base-select/filter-mode.vue new file mode 100644 index 0000000000..6d94947c7d --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/filter-mode.vue @@ -0,0 +1,51 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/hide-drop-composition-api.vue b/examples/sites/demos/pc/app/base-select/hide-drop-composition-api.vue new file mode 100644 index 0000000000..a2cdd21488 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/hide-drop-composition-api.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/hide-drop.spec.ts b/examples/sites/demos/pc/app/base-select/hide-drop.spec.ts new file mode 100644 index 0000000000..d245939f27 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/hide-drop.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' + +test('hidedrop', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#hide-drop') + const wrap = page.locator('#hide-drop') + const select = wrap.locator('.tiny-base-select') + const dropdown = page.locator('body > .tiny-select-dropdown') + + await select.click() + await expect(dropdown).toBeHidden() +}) diff --git a/examples/sites/demos/pc/app/base-select/hide-drop.vue b/examples/sites/demos/pc/app/base-select/hide-drop.vue new file mode 100644 index 0000000000..bb2c0c4ff4 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/hide-drop.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/input-box-type-composition-api.vue b/examples/sites/demos/pc/app/base-select/input-box-type-composition-api.vue new file mode 100644 index 0000000000..171756ec40 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/input-box-type-composition-api.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts b/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts new file mode 100644 index 0000000000..09b58fb7d9 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test' + +test('下划线默认', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#input-box-type') + const wrap = page.locator('#input-box-type') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await expect(select).toHaveClass(/tiny-base-select__underline/) + await expect(input).toHaveCSS('border-top-width', '0px') + await expect(input).toHaveCSS('border-left-width', '0px') + await expect(input).toHaveCSS('border-right-width', '0px') + await expect(input).toHaveCSS('border-color', 'rgb(173, 176, 184)') + await expect(select.locator('svg')).toHaveCSS('fill', 'rgb(87, 93, 108)') + + await select.click() + await option.first().click() + await expect(dropdown).toBeHidden() + await expect(input).toHaveValue('北京') +}) + +test('下划线禁用', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#input-box-type') + const wrap = page.locator('#input-box-type') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + + await expect(select).toHaveClass(/tiny-base-select__underline/) + await expect(input).toHaveCSS('border-top-width', '0px') + await expect(input).toHaveCSS('border-left-width', '0px') + await expect(input).toHaveCSS('border-right-width', '0px') + await expect(input).toHaveCSS('border-color', 'rgb(223, 225, 230)') + await expect(input).toHaveCSS('cursor', 'not-allowed') + await expect(select.locator('svg')).toHaveCSS('fill', 'rgb(173, 176, 184)') + const hasDisabled = await input.evaluate((input) => input.hasAttribute('disabled')) + await expect(hasDisabled).toBe(true) + + await select.click() + await expect(dropdown).toBeHidden() + await expect(input).toHaveValue('') +}) + +test('下划线多选', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#input-box-type') + const wrap = page.locator('#input-box-type') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input__inner') + const tag = wrap.locator('.tiny-tag') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await expect(select).toHaveClass(/tiny-base-select__underline/) + await expect(input).toHaveCSS('border-top-width', '0px') + await expect(input).toHaveCSS('border-left-width', '0px') + await expect(input).toHaveCSS('border-right-width', '0px') + await expect(input).toHaveCSS('border-color', 'rgb(173, 176, 184)') + await expect(select.locator('.tiny-base-select__caret')).toHaveCSS('fill', 'rgb(87, 93, 108)') + + await select.click() + await expect(dropdown).toBeVisible() + await option.first().click() + await expect(tag).toHaveCount(5) + + await expect(select).toHaveClass(/tiny-base-select__underline/) + await expect(select).toHaveClass(/tiny-base-select__multiple/) +}) diff --git a/examples/sites/demos/pc/app/base-select/input-box-type.vue b/examples/sites/demos/pc/app/base-select/input-box-type.vue new file mode 100644 index 0000000000..70b81ad97e --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/input-box-type.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/is-drop-inherit-width-composition-api.vue b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width-composition-api.vue new file mode 100644 index 0000000000..ef88543db5 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width-composition-api.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.spec.ts b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.spec.ts new file mode 100644 index 0000000000..b160c88266 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test' + +test('默认下拉弹框宽度由内容撑开', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#is-drop-inherit-width') + + const wrap = page.locator('#is-drop-inherit-width') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = select.locator('.tiny-input__inner') + const option = dropdown.locator('.tiny-option') + + await select.click() + const inputBox = await input.boundingBox() + const listitemBox = await option.first().boundingBox() + + const result = listitemBox.width > inputBox.width + await expect(result).toBe(true) +}) + +test('下拉弹框宽度与输入框一致', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#is-drop-inherit-width') + const wrap = page.locator('#is-drop-inherit-width') + const select = wrap.locator('.tiny-base-select').nth(1) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = select.locator('.tiny-input__inner') + + await select.click() + const inputBox = await input.boundingBox() + const dropdownBox = await dropdown.boundingBox() + const result = dropdownBox.width - inputBox.width + await expect(result < 1).toBe(true) +}) diff --git a/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.vue b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.vue new file mode 100644 index 0000000000..05a57af408 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/is-drop-inherit-width.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/manual-focus-blur-composition-api.vue b/examples/sites/demos/pc/app/base-select/manual-focus-blur-composition-api.vue new file mode 100644 index 0000000000..1aad187f74 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/manual-focus-blur-composition-api.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/manual-focus-blur.spec.ts b/examples/sites/demos/pc/app/base-select/manual-focus-blur.spec.ts new file mode 100644 index 0000000000..1da8d29442 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/manual-focus-blur.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test' + +test('可搜索+手动聚焦失焦', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#manual-focus-blur') + const wrap = page.locator('#manual-focus-blur') + const dropdown = page.locator('body > .tiny-select-dropdown') + const button = wrap.locator('.demo2 .tiny-button') + + await page.waitForTimeout(2000) + // 手动聚焦下拉 + await button.nth(0).click() + await expect(dropdown).toBeVisible() + + // 手动失焦收起 + await button.nth(1).click() + await expect(dropdown).toBeHidden() +}) diff --git a/examples/sites/demos/pc/app/base-select/manual-focus-blur.vue b/examples/sites/demos/pc/app/base-select/manual-focus-blur.vue new file mode 100644 index 0000000000..4360f350c4 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/manual-focus-blur.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/map-field-composition-api.vue b/examples/sites/demos/pc/app/base-select/map-field-composition-api.vue new file mode 100644 index 0000000000..8e3a90d928 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/map-field-composition-api.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/map-field.spec.ts b/examples/sites/demos/pc/app/base-select/map-field.spec.ts new file mode 100644 index 0000000000..07840ebbb9 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/map-field.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +test('配置式配置映射字段', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('select#map-field') + const wrap = page.locator('#map-field') + const select = wrap.locator('.tiny-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const suffix = select.locator('.tiny-input__suffix') + const tag = select.locator('.tiny-tag') + + await expect(tag).toHaveCount(2) + await expect(tag.first()).toHaveText('北京') + await expect(tag.nth(1)).toHaveText('上海') + await suffix.click() + await page.waitForTimeout(500) + await expect(dropdown).toBeVisible() + await expect(option.filter({ hasText: '北京' })).toHaveClass(/selected/) + await expect(option.filter({ hasText: '上海' })).toHaveClass(/selected/) +}) diff --git a/examples/sites/demos/pc/app/base-select/map-field.vue b/examples/sites/demos/pc/app/base-select/map-field.vue new file mode 100644 index 0000000000..e88c886f47 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/map-field.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/memoize-usage-composition-api.vue b/examples/sites/demos/pc/app/base-select/memoize-usage-composition-api.vue new file mode 100644 index 0000000000..ed7b8e6248 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/memoize-usage-composition-api.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/memoize-usage.spec.ts b/examples/sites/demos/pc/app/base-select/memoize-usage.spec.ts new file mode 100644 index 0000000000..7e18cdabfa --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/memoize-usage.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test' + +test('手动缓存', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#memoize-usage') + const wrap = page.locator('#memoize-usage') + const select = wrap.locator('.tiny-base-select') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const cacheValue = wrap.locator('.cache-value') + + await select.click() + await option.filter({ hasText: '北京' }).click() + await expect(cacheValue).toContainText(['选项1']) + + await select.click() + await option.filter({ hasText: '上海' }).click() + await expect(cacheValue).toContainText(['选项2']) +}) diff --git a/examples/sites/demos/pc/app/base-select/memoize-usage.vue b/examples/sites/demos/pc/app/base-select/memoize-usage.vue new file mode 100644 index 0000000000..d004cfb645 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/memoize-usage.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/multiple-composition-api.vue b/examples/sites/demos/pc/app/base-select/multiple-composition-api.vue new file mode 100644 index 0000000000..001abf60d8 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/multiple-composition-api.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/multiple-mix-composition-api.vue b/examples/sites/demos/pc/app/base-select/multiple-mix-composition-api.vue new file mode 100644 index 0000000000..c2777a2151 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/multiple-mix-composition-api.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/multiple-mix.vue b/examples/sites/demos/pc/app/base-select/multiple-mix.vue new file mode 100644 index 0000000000..248db9f069 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/multiple-mix.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/multiple.spec.ts b/examples/sites/demos/pc/app/base-select/multiple.spec.ts new file mode 100644 index 0000000000..81a1c43b90 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/multiple.spec.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@playwright/test' + +test('多选时取远端数据与当前已选数据的并集', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#multiple') + const wrap = page.locator('#multiple') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const tag = select.locator('.tiny-tag') + + await expect(tag).toHaveCount(2) + await select.locator('.tiny-input__suffix').click() + await option.filter({ hasText: '全部' }).click() + await expect(tag).toHaveCount(7) + await option.filter({ hasText: '全部' }).click() + await expect(tag).toHaveCount(0) + await option.filter({ hasText: '北京' }).click() + await expect(tag).toHaveCount(1) + await option.filter({ hasText: '上海' }).click() + await expect(tag).toHaveCount(2) + await tag.filter({ hasText: '上海' }).locator('.tiny-tag__close').click() + await expect(tag).toHaveCount(1) +}) + +test('multiple-limit', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#multiple') + const wrap = page.locator('#multiple') + const select = wrap.locator('.tiny-base-select').nth(3) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const tag = select.locator('.tiny-tag') + + await select.click() + await option.nth(0).click() + await option.nth(1).click() + await expect(tag).toHaveCount(2) + await expect(option.filter({ hasText: '全部' })).toHaveCount(0) + + const list = await option.all() + list.forEach(async (item, index) => { + if (index <= 1) { + await expect(item).toHaveClass(/selected/) + } else { + await expect(item).toHaveClass(/is-disabled/) + } + }) +}) diff --git a/examples/sites/demos/pc/app/base-select/multiple.vue b/examples/sites/demos/pc/app/base-select/multiple.vue new file mode 100644 index 0000000000..d4d741dfb8 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/multiple.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/native-properties-composition-api.vue b/examples/sites/demos/pc/app/base-select/native-properties-composition-api.vue new file mode 100644 index 0000000000..23d150f465 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/native-properties-composition-api.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/native-properties.spec.ts b/examples/sites/demos/pc/app/base-select/native-properties.spec.ts new file mode 100644 index 0000000000..ee56d5e20c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/native-properties.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test' + +test('原生属性', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#native-properties') + + const wrap = page.locator('#native-properties') + const select = wrap.locator('.tiny-base-select') + const input = select.locator('.tiny-input__inner') + + await expect(input).toHaveAttribute('name', 'inputName') + await expect(input).toHaveAttribute('placeholder', '自定义 placeholder') + + const isHasAutocomplete = await input.evaluate((input) => input.hasAttribute('autocomplete')) + await expect(isHasAutocomplete).toBe(true) +}) diff --git a/examples/sites/demos/pc/app/base-select/native-properties.vue b/examples/sites/demos/pc/app/base-select/native-properties.vue new file mode 100644 index 0000000000..b38ad9bcc3 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/native-properties.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/no-data-text-composition-api.vue b/examples/sites/demos/pc/app/base-select/no-data-text-composition-api.vue new file mode 100644 index 0000000000..d6bd3d2f7a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/no-data-text-composition-api.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/no-data-text.spec.ts b/examples/sites/demos/pc/app/base-select/no-data-text.spec.ts new file mode 100644 index 0000000000..18595682f8 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/no-data-text.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' + +test('默认空数据文本', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#no-data-text') + + const wrap = page.locator('#no-data-text') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + + await input.click() + await expect(dropdown.locator('.tiny-select-dropdown__empty')).toHaveText('暂无相关数据') +}) + +test('自定义空数据文本', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#no-data-text') + + const wrap = page.locator('#no-data-text') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + + await input.click() + await expect(dropdown.locator('.tiny-select-dropdown__empty')).toHaveText('暂无数据') +}) + +test('显示空数据图片', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#no-data-text') + const wrap = page.locator('#no-data-text') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + + await input.click() + await expect(dropdown.locator('.tiny-select-dropdown__empty-images')).toBeVisible() +}) diff --git a/examples/sites/demos/pc/app/base-select/no-data-text.vue b/examples/sites/demos/pc/app/base-select/no-data-text.vue new file mode 100644 index 0000000000..3a04eeb495 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/no-data-text.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/optimization-composition-api.vue b/examples/sites/demos/pc/app/base-select/optimization-composition-api.vue new file mode 100644 index 0000000000..849b0f4191 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/optimization-composition-api.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/optimization.spec.ts b/examples/sites/demos/pc/app/base-select/optimization.spec.ts new file mode 100644 index 0000000000..6b3418c500 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/optimization.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test' + +test('单选虚拟滚动', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#optimization') + + const wrap = page.locator('#optimization') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await select.click() + await expect((await option.all()).length).toBeLessThan(20) // 新虚拟滚动,预加载行数不一样了 + await expect(option.filter({ hasText: '北京17' })).toBeHidden() + await option.nth(9).scrollIntoViewIfNeeded() + await page.waitForTimeout(1000) + await expect(option.filter({ hasText: '北京17' })).toBeVisible() // 现在预加载的行比较多,所以17行已经可以看到了 +}) + +test('多选虚拟滚动', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#optimization') + const wrap = page.locator('#optimization') + const select = wrap.locator('.tiny-base-select').nth(1) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const tag = select.locator('.tiny-tag') + + await select.click() + await expect((await option.all()).length).toBeLessThan(20) // 新虚拟滚动,预加载行数不一样了 + await expect(option.filter({ hasText: '北京17' })).toBeHidden() + await expect(option.filter({ hasText: '北京16' })).toBeHidden() + await page.waitForTimeout(500) + await option.nth(9).scrollIntoViewIfNeeded() + await page.waitForTimeout(1000) + await option.nth(9).scrollIntoViewIfNeeded() + await expect(option.filter({ hasText: '北京16' })).toBeVisible() + await expect(option.filter({ hasText: '北京17' })).toBeVisible() + await option.filter({ hasText: '北京17' }).click() + await expect(tag.first()).toHaveText('北京17') + await expect((await tag.all()).length).toEqual(1) + await option.filter({ hasText: '北京16' }).click() + await expect((await tag.all()).length).toEqual(2) + await expect(tag.nth(1)).toHaveText('+ 1') +}) diff --git a/examples/sites/demos/pc/app/base-select/optimization.vue b/examples/sites/demos/pc/app/base-select/optimization.vue new file mode 100644 index 0000000000..da98bc48f5 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/optimization.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/option-group-composition-api.vue b/examples/sites/demos/pc/app/base-select/option-group-composition-api.vue new file mode 100644 index 0000000000..59b1404861 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/option-group-composition-api.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/option-group.spec.ts b/examples/sites/demos/pc/app/base-select/option-group.spec.ts new file mode 100644 index 0000000000..eef5c71c39 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/option-group.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test' + +test('option-group', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#option-group') + + const wrap = page.locator('#option-group') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const title = dropdown.locator('.tiny-option-group__title') + const group = dropdown.locator('.tiny-option-group ') + + await select.click() + await expect(title.nth(0)).toHaveText('热门城市') + await expect(title.nth(1)).toHaveText('城市名') + await expect(option.filter({ hasText: '上海' })).toHaveClass(/is-disabled/) + await expect(option.filter({ hasText: '北京' })).toHaveClass(/is-disabled/) + await expect(group.nth(0).locator('.tiny-option')).toHaveCount(2) + await expect(group.nth(1).locator('.tiny-option')).toHaveCount(8) + await expect((await group.all()).length).toEqual(2) +}) diff --git a/examples/sites/demos/pc/app/base-select/option-group.vue b/examples/sites/demos/pc/app/base-select/option-group.vue new file mode 100644 index 0000000000..bd0ea5ad75 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/option-group.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/popup-style-position-composition-api.vue b/examples/sites/demos/pc/app/base-select/popup-style-position-composition-api.vue new file mode 100644 index 0000000000..63cef96f2b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/popup-style-position-composition-api.vue @@ -0,0 +1,32 @@ + + + + + + + diff --git a/examples/sites/demos/pc/app/base-select/popup-style-position.spec.ts b/examples/sites/demos/pc/app/base-select/popup-style-position.spec.ts new file mode 100644 index 0000000000..ff7f3feab2 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/popup-style-position.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test' + +test('popup-style-position', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#popup-style-position') + + const wrap = page.locator('#popup-style-position') + const select = wrap.locator('.tiny-base-select') + const dropdown = select.locator('.tiny-base-select__tags-group > .tiny-select-dropdown') + + await select.click() + await expect(dropdown).toHaveCount(1) + await expect(dropdown).toHaveClass(/drop/) + await expect(dropdown).toHaveCSS('background-color', 'rgb(213, 232, 255)') + await expect(dropdown).toHaveAttribute('x-placement', 'top') +}) diff --git a/examples/sites/demos/pc/app/base-select/popup-style-position.vue b/examples/sites/demos/pc/app/base-select/popup-style-position.vue new file mode 100644 index 0000000000..5a82cb1456 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/popup-style-position.vue @@ -0,0 +1,41 @@ + + + + + + + diff --git a/examples/sites/demos/pc/app/base-select/remote-method-composition-api.vue b/examples/sites/demos/pc/app/base-select/remote-method-composition-api.vue new file mode 100644 index 0000000000..43857c1432 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/remote-method-composition-api.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/remote-method.spec.ts b/examples/sites/demos/pc/app/base-select/remote-method.spec.ts new file mode 100644 index 0000000000..c7f319d870 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/remote-method.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test' + +test('远程搜索单选', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#remote-method') + + const wrap = page.locator('#remote-method') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.focus() + await expect(option).toHaveCount(0) + await expect(dropdown).toBeHidden() + + await input.fill('al') + await input.press('Enter') + await page.waitForTimeout(800) + await expect((await option.all()).length).toEqual(3) + await option.filter({ hasText: 'Alaska' }).click() + await expect(input).toHaveValue('Alaska') +}) + +test('远程搜索多选 + 保留搜索关键字', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#remote-method') + + const wrap = page.locator('#remote-method') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-base-select__input') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const tag = select.locator('.tiny-tag') + + await input.focus() + await expect(option).toHaveCount(0) + await input.press('a') + await input.press('Enter') + await page.waitForTimeout(300) + await option.filter({ hasText: 'Alabama' }).click() + await expect(input).toHaveValue('a') + await expect((await tag.all()).length).toEqual(1) + await input.press('l') + await page.waitForTimeout(500) + + await expect((await option.all()).length).toEqual(3) + await option.filter({ hasText: 'Alaska' }).click() + await page.waitForTimeout(300) + await expect((await tag.all()).length).toEqual(2) + await expect(input).toHaveValue('al') +}) + +test('获焦时触发远程搜索', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#remote-method') + + const wrap = page.locator('#remote-method') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await expect(option).toHaveCount(0) + await page.waitForTimeout(300) + await expect(option).toHaveCount(50) +}) diff --git a/examples/sites/demos/pc/app/base-select/remote-method.vue b/examples/sites/demos/pc/app/base-select/remote-method.vue new file mode 100644 index 0000000000..a2b6df6c9e --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/remote-method.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/searchable-composition-api.vue b/examples/sites/demos/pc/app/base-select/searchable-composition-api.vue new file mode 100644 index 0000000000..69bc2e9b2a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/searchable-composition-api.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/searchable.spec.ts b/examples/sites/demos/pc/app/base-select/searchable.spec.ts new file mode 100644 index 0000000000..6c8a7a615c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/searchable.spec.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test' + +test('searchable-single', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#searchable') + + const wrap = page.locator('#searchable') + const select = wrap.locator('.tiny-base-select').nth(0) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = dropdown.locator('.tiny-input__inner') + const option = dropdown.locator('.tiny-option') + + await expect(input).toBeHidden() + await select.click() + await page.waitForTimeout(500) + await expect(input).toBeVisible() + await input.fill('上海') + await input.press('Enter') + await page.waitForTimeout(500) + const list = await option.all() + list.forEach(async (item) => { + const text = await item.innerText() + const isVisibleItem = text === '上海' || text === '全部' + if (isVisibleItem) { + await expect(item).toHaveCSS('display', 'flex') + } else { + await expect(item).toHaveCSS('display', 'none') + } + }) + await option.filter({ hasText: '上海' }).click() + await page.waitForTimeout(500) + await expect(input).toHaveValue('') +}) + +test('searchable-multiple', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#searchable') + + const wrap = page.locator('#searchable') + const select = wrap.locator('.tiny-base-select').nth(1) + const dropdown = page.locator('body > .tiny-select-dropdown') + const input = dropdown.locator('.tiny-input__inner') + const option = dropdown.locator('.tiny-option') + const tags = select.locator('.tiny-tag') + + await expect(input).toBeHidden() + await select.click() + await page.waitForTimeout(500) + await expect(input).toBeVisible() + await input.fill('上海') + await input.press('Enter') + await page.waitForTimeout(500) + const list = await option.all() + list.forEach(async (item) => { + const text = await item.innerText() + const isVisibleItem = text === '上海' || text === '全部' + if (isVisibleItem) { + await expect(item).toHaveCSS('display', 'flex') + } else { + await expect(item).toHaveCSS('display', 'none') + } + }) + await option.filter({ hasText: '上海' }).click() + await page.waitForTimeout(500) + await expect((await tags.all()).length).toEqual(1) +}) diff --git a/examples/sites/demos/pc/app/base-select/searchable.vue b/examples/sites/demos/pc/app/base-select/searchable.vue new file mode 100644 index 0000000000..6eccd84cc4 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/searchable.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/show-alloption-composition-api.vue b/examples/sites/demos/pc/app/base-select/show-alloption-composition-api.vue new file mode 100644 index 0000000000..b996442d0a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/show-alloption-composition-api.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/show-alloption.spec.ts b/examples/sites/demos/pc/app/base-select/show-alloption.spec.ts new file mode 100644 index 0000000000..67e37afa79 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/show-alloption.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test' + +test('show-alloption', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#show-alloption') + + const wrap = page.locator('#show-alloption') + const select = wrap.locator('.tiny-base-select') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await select.click() + await page.waitForTimeout(500) + await expect(option.filter({ hasText: '全部' })).toBeHidden() +}) diff --git a/examples/sites/demos/pc/app/base-select/show-alloption.vue b/examples/sites/demos/pc/app/base-select/show-alloption.vue new file mode 100644 index 0000000000..cd9760a98b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/show-alloption.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/show-tip-composition-api.vue b/examples/sites/demos/pc/app/base-select/show-tip-composition-api.vue new file mode 100644 index 0000000000..65d2c44233 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/show-tip-composition-api.vue @@ -0,0 +1,19 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/show-tip.vue b/examples/sites/demos/pc/app/base-select/show-tip.vue new file mode 100644 index 0000000000..81c5e7042c --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/show-tip.vue @@ -0,0 +1,28 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/size-composition-api.vue b/examples/sites/demos/pc/app/base-select/size-composition-api.vue new file mode 100644 index 0000000000..4698dce084 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/size-composition-api.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/size.spec.ts b/examples/sites/demos/pc/app/base-select/size.spec.ts new file mode 100644 index 0000000000..df4298ae8b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/size.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test' + +test('默认尺寸', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#size') + + const wrap = page.locator('#size') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input') + const tag = select.locator('.tiny-tag') + + await expect(input.locator('.tiny-input__inner')).toHaveCSS('height', '30px') + await expect(tag.nth(0)).toHaveClass(/tiny-tag--light/) +}) + +test('medium 尺寸', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#size') + + const wrap = page.locator('#size') + const select = wrap.locator('.tiny-base-select').nth(1) + const input = select.locator('.tiny-input') + const tag = select.locator('.tiny-tag') + + await expect(input).toHaveClass(/tiny-input-medium/) + await expect(input.locator('.tiny-input__inner')).toHaveCSS('height', '40px') + await expect(tag.nth(0)).toHaveClass(/tiny-tag--medium tiny-tag--light/) +}) + +test('small 尺寸', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#size') + + const wrap = page.locator('#size') + const select = wrap.locator('.tiny-base-select').nth(2) + const input = select.locator('.tiny-input') + const tag = select.locator('.tiny-tag') + const { height } = await input.locator('.tiny-input__inner').boundingBox() + + await expect(input).toHaveClass(/tiny-input-small/) + await expect(tag.nth(0)).toHaveClass(/tiny-tag--small tiny-tag--light/) + expect(height).toBeCloseTo(32, 1) +}) + +test('mini 尺寸', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#size') + + const wrap = page.locator('#size') + const select = wrap.locator('.tiny-base-select').nth(3) + const input = select.locator('.tiny-input') + const tag = select.locator('.tiny-tag') + const { height } = await input.locator('.tiny-input__inner').boundingBox() + + await expect(input).toHaveClass(/tiny-input-mini/) + await expect(tag.nth(0)).toHaveClass(/tiny-tag--mini tiny-tag--light/) + expect(height).toBeCloseTo(24, 1) +}) diff --git a/examples/sites/demos/pc/app/base-select/size.vue b/examples/sites/demos/pc/app/base-select/size.vue new file mode 100644 index 0000000000..4d75c40b4a --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/size.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-default-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-default-composition-api.vue new file mode 100644 index 0000000000..47bda37e5f --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-default-composition-api.vue @@ -0,0 +1,88 @@ + + + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-default.spec.ts b/examples/sites/demos/pc/app/base-select/slot-default.spec.ts new file mode 100644 index 0000000000..7c9421fb25 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-default.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test' + +test('选项插槽', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#slot-default') + + const wrap = page.locator('#slot-default') + const select = wrap.locator('.tiny-base-select').nth(0) + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await expect(option.filter({ hasText: '北京' })).toBeVisible() + await expect(option.filter({ hasText: '北京' }).locator('.tiny-tag')).toHaveText('New') + await option.filter({ hasText: '北京' }).hover() + await expect(page.locator('body > .tiny-tooltip').filter({ hasText: '自定义提示' })).toBeVisible() +}) diff --git a/examples/sites/demos/pc/app/base-select/slot-default.vue b/examples/sites/demos/pc/app/base-select/slot-default.vue new file mode 100644 index 0000000000..88555da48b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-default.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-empty-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-empty-composition-api.vue new file mode 100644 index 0000000000..07edc4cc38 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-empty-composition-api.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-empty.spec.ts b/examples/sites/demos/pc/app/base-select/slot-empty.spec.ts new file mode 100644 index 0000000000..e0c977da15 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-empty.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test' + +test('空数据插槽', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#slot-empty') + + const wrap = page.locator('#slot-empty') + const select = wrap.locator('.tiny-base-select') + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await expect((await option.all()).length).toEqual(0) + await expect(page.locator('.tiny-select-dropdown')).toHaveText('APIG 网关异常重新加载') +}) diff --git a/examples/sites/demos/pc/app/base-select/slot-empty.vue b/examples/sites/demos/pc/app/base-select/slot-empty.vue new file mode 100644 index 0000000000..b36c6a5e59 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-empty.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-footer-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-footer-composition-api.vue new file mode 100644 index 0000000000..234c21597e --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-footer-composition-api.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-footer.spec.ts b/examples/sites/demos/pc/app/base-select/slot-footer.spec.ts new file mode 100644 index 0000000000..eded63c28f --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-footer.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test' + +test('底部插槽', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#slot-footer') + + const wrap = page.locator('#slot-footer') + const select = wrap.locator('.tiny-base-select') + const input = select.locator('.tiny-input__inner') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + + await input.click() + await expect((await option.all()).length).toEqual(5) + await expect(page.locator('.select-footer')).toHaveText('底部插槽') +}) diff --git a/examples/sites/demos/pc/app/base-select/slot-footer.vue b/examples/sites/demos/pc/app/base-select/slot-footer.vue new file mode 100644 index 0000000000..d9fc137a46 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-footer.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-label-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-label-composition-api.vue new file mode 100644 index 0000000000..c67305acc0 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-label-composition-api.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-label.vue b/examples/sites/demos/pc/app/base-select/slot-label.vue new file mode 100644 index 0000000000..1aa17ab9c2 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-label.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-prefix-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-prefix-composition-api.vue new file mode 100644 index 0000000000..99095fb086 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-prefix-composition-api.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-prefix.spec.ts b/examples/sites/demos/pc/app/base-select/slot-prefix.spec.ts new file mode 100644 index 0000000000..9ce28564bf --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-prefix.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test' + +test('输入框前缀插槽', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#slot-prefix') + + const wrap = page.locator('#slot-prefix') + const select = wrap.locator('.tiny-base-select') + const dropdown = page.locator('body > .tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const prefix = select.locator('.tiny-input .tiny-input__prefix .tiny-svg') + const tag = select.locator('.tiny-tag') + + await expect(prefix).toBeVisible() + await select.click() + await option.filter({ hasText: '北京' }).click() + await expect(tag.filter({ hasText: '北京' })).toBeVisible() +}) diff --git a/examples/sites/demos/pc/app/base-select/slot-prefix.vue b/examples/sites/demos/pc/app/base-select/slot-prefix.vue new file mode 100644 index 0000000000..dd1fba2148 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-prefix.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-reference-composition-api.vue b/examples/sites/demos/pc/app/base-select/slot-reference-composition-api.vue new file mode 100644 index 0000000000..b8f558cac0 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-reference-composition-api.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/slot-reference.spec.ts b/examples/sites/demos/pc/app/base-select/slot-reference.spec.ts new file mode 100644 index 0000000000..a5d5a81581 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-reference.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test' + +test('custom-reference', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#slot-reference') + + const wrap = page.locator('#slot-reference') + const select = wrap.locator('.tiny-base-select') + const dropdown = page.locator('.tiny-select-dropdown') + const option = dropdown.locator('.tiny-option') + const reference = select.locator('.custom-reference') + + await expect(option).toHaveCount(0) + await reference.click() + await option.filter({ hasText: '北京' }).click() +}) diff --git a/examples/sites/demos/pc/app/base-select/slot-reference.vue b/examples/sites/demos/pc/app/base-select/slot-reference.vue new file mode 100644 index 0000000000..c6deaf6c1d --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/slot-reference.vue @@ -0,0 +1,33 @@ + + + diff --git a/examples/sites/demos/pc/app/base-select/tag-type-composition-api.vue b/examples/sites/demos/pc/app/base-select/tag-type-composition-api.vue new file mode 100644 index 0000000000..95a855665e --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/tag-type-composition-api.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/tag-type.spec.ts b/examples/sites/demos/pc/app/base-select/tag-type.spec.ts new file mode 100644 index 0000000000..3721fee3ca --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/tag-type.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@playwright/test' + +test('标签类型', async ({ page }) => { + page.on('pageerror', (exception) => expect(exception).toBeNull()) + await page.goto('base-select#tag-type') + + const wrap = page.locator('#tag-type') + const select = wrap.locator('.tiny-base-select') + const tag = select.locator('.tiny-tag') + + // 验证是否有对应类型的类名 + await expect(tag.nth(0)).toHaveClass(/tiny-tag--warning/) +}) diff --git a/examples/sites/demos/pc/app/base-select/tag-type.vue b/examples/sites/demos/pc/app/base-select/tag-type.vue new file mode 100644 index 0000000000..b27f9707fc --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/tag-type.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/examples/sites/demos/pc/app/base-select/webdoc/base-select.cn.md b/examples/sites/demos/pc/app/base-select/webdoc/base-select.cn.md new file mode 100644 index 0000000000..9edb42039b --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/webdoc/base-select.cn.md @@ -0,0 +1,7 @@ +--- +title: BaseSelect 选择器 +--- + +# BaseSelect 选择器 + +BaseSelect 选择器是 Select 组件的基础版本,用法和 Select 一样,但是不包含下拉树和下拉表格功能。 diff --git a/examples/sites/demos/pc/app/base-select/webdoc/base-select.en.md b/examples/sites/demos/pc/app/base-select/webdoc/base-select.en.md new file mode 100644 index 0000000000..f6b9dc7330 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/webdoc/base-select.en.md @@ -0,0 +1,7 @@ +--- +title: BaseSelect +--- + +# BaseSelect + +BaseSelect is a UI component that displays and selects data from a drop-down list box. diff --git a/examples/sites/demos/pc/app/base-select/webdoc/base-select.js b/examples/sites/demos/pc/app/base-select/webdoc/base-select.js new file mode 100644 index 0000000000..5e323b68a0 --- /dev/null +++ b/examples/sites/demos/pc/app/base-select/webdoc/base-select.js @@ -0,0 +1,535 @@ +export default { + column: '2', + owner: '', + demos: [ + { + demoId: 'basic-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic Usage' + }, + desc: { + 'zh-CN': + '

通过 v-model 设置被选中的 tiny-optionvalue 属性值。

\n', + 'en-US': + '

Set the value attribute value of the selected tiny-option by v-model.

\n' + }, + codeFiles: ['basic-usage.vue'] + }, + { + demoId: 'multiple', + name: { + 'zh-CN': '多选', + 'en-US': 'Multiple' + }, + desc: { + 'zh-CN': ` + 通过 multiple 属性启用多选功能,此时 v-model 的值为当前选中值所组成的数组。默认选中值会以标签(Tag 组件)展示。
+ 通过 multiple-limit 属性限制最多可选择的个数,默认为 0 不限制。
+ 通过 show-limit-text 属性限制最多可选择的个数,默认为 0 不限制。
+ 多选时,通过给 option 标签配置 required 或者在 options 配置项中添加 required 属性,来设置必选选项。
+ 通过 dropdown-icon 属性可自定义下拉图标,drop-style 属性可自定义下拉选项样式。
+ `, + 'en-US': ` + Use the multiple attribute to enable the multi-selection function. In this case, the value of v-model is an array of selected values. By default, the selected value is displayed as a tag (Tag component).
+ The multiple-limit attribute is used to limit the maximum number of users that can be selected. The default value is 0. + The show-limit-text attribute is used to limit the maximum number of users that can be selected. The default value is 0, which is not limited.
+ When multiple options are selected, you can set required for the option tag or add the required attribute to the options configuration item to set mandatory options.
+ You can use the dropdown-icon attribute to customize the drop-down icon, and the drop-style attribute to customize the style of the drop-down options.
+ ` + }, + codeFiles: ['multiple.vue'] + }, + { + demoId: 'collapse-tags', + name: { + 'zh-CN': '折叠标签', + 'en-US': 'Collapse tags' + }, + desc: { + 'zh-CN': + '

通过 collapse-tags 属性设置选中多个选项时,多个标签缩略展示。设置 show-proportion 可展示当前选中条数和总条数占比,默认值为 false 。设置 hover-expandtrue ,默认折叠标签, hover 时展示所有标签。标签内容超长时超出省略,hover 标签时展示 tooltip

\n', + 'en-US': + '

When multiple options are selected through the collapse-tags attribute settings, multiple tags are displayed in a thumbnail. Set show-proportion to display the current number of selected items and the proportion of total items, with a default value of false . By setting hover-expand to true , the tags are collapsed by default, and all tags are displayed when hovering. If the content of the tag is too long, it should be omitted. When hovering the tag, a tooltip should be displayed

' + }, + codeFiles: ['collapse-tags.vue'] + }, + { + demoId: 'multiple-mix', + name: { + 'zh-CN': '仅显示', + 'en-US': 'Display only' + }, + desc: { + 'zh-CN': + '

Form 表单内 Select 组件不同尺寸设置 hover-expanddisplay-only 属性的综合应用。

\n', + 'en-US': + '

Comprehensive application of the hover-expand and display-only attributes of the Select component in the form.

\n' + }, + codeFiles: ['multiple-mix.vue'] + }, + { + demoId: 'tag-type', + name: { + 'zh-CN': '标签类型', + 'en-US': 'Tag type' + }, + desc: { + 'zh-CN': + '

通过 tag-type 属性设置标签类型,同 Tag 组件的 type 属性。可选值:success / info / warning / danger 。

\n', + 'en-US': + '

Set the label type through the tag-type attribute, which is the same as the type attribute of the Tag component. Optional values: success/info/warning/danger.

\n' + }, + codeFiles: ['tag-type.vue'] + }, + { + demoId: 'size', + name: { + 'zh-CN': '尺寸', + 'en-US': 'Size' + }, + desc: { + 'zh-CN': '

通过 size 属性设置输入框尺寸,可选值:medium / small / mini 。

', + 'en-US': + '

Set the input box size through the size attribute, with optional values of medium / small / mini.

' + }, + codeFiles: ['size.vue'] + }, + { + demoId: 'disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'Disabled' + }, + desc: { + 'zh-CN': '

通过 disabled 属性设置下拉或者下拉项的禁用状态。

\n', + 'en-US': + '

Set the disabled status of the dropdown or dropdown item through the disabled attribute.

\n' + }, + codeFiles: ['disabled.vue'] + }, + { + demoId: 'clearable', + name: { + 'zh-CN': '可清除', + 'en-US': 'Clearable' + }, + desc: { + 'zh-CN': '

通过 clearable 属性启用一键清除选中值的功能。仅适用于单选。

\n', + 'en-US': + '

Enable the function of clearing selected values with one click through theclearable attribute. Only applicable for single selection.

\n' + }, + codeFiles: ['clearable.vue'] + }, + { + demoId: 'filter-method', + name: { + 'zh-CN': '可搜索', + 'en-US': 'Filterable' + }, + desc: { + 'zh-CN': + '

通过 filterable 属性启用搜索功能。filter-method 自定义过滤方法。 no-match-text 属性自定义与搜索条件无匹配项时显示的文字。

\n', + 'en-US': + '

Enable search functionality through the filterable attribute filter-method customize the filtering method no-match-text the text displayed when there is no match between attribute customization and search criteria.

\n' + }, + codeFiles: ['filter-method.vue'] + }, + { + demoId: 'remote-method', + name: { + 'zh-CN': '远程搜索', + 'en-US': 'Remote search' + }, + desc: { + 'zh-CN': + '

通过 filterableremoteremote-method 这三个属性同时使用设置远程搜索。通过 reserve-keyword 属性设置多选可搜索时,可以在选中一个选项后保留当前的搜索关键词。

\n

通过 trim 属性去除双向数据绑定值空格。

', + 'en-US': + '

Set remote search through the use of three attributes:filterable,remote, and remote-method. When setting multiple searchable options through the reserve-keyword attribute, the current search keyword can be retained after selecting an option.

\n

Removes spaces from bidirectional data binding values through the trim attribute.

' + }, + codeFiles: ['remote-method.vue'] + }, + { + demoId: 'searchable', + name: { + 'zh-CN': '下拉面板可搜索', + 'en-US': 'Panel search' + }, + desc: { + 'zh-CN': '

通过 searchable 属性设置下拉面板显示搜索框,默认不显示。

\n', + 'en-US': + '

The search box is displayed through the searchable attribute setting drop-down panel, which is not displayed by default.

\n' + }, + codeFiles: ['searchable.vue'] + }, + { + demoId: 'allow-create', + name: { + 'zh-CN': '创建条目', + 'en-US': 'Create Entry' + }, + desc: { + 'zh-CN': + '

通过 allow-createfilterable 属性,设置当搜索字段不在已有选项中时,可创建为新的条目。结合 default-first-option 属性,可以按 Enter 键选中第一个匹配项。

\n

设置 top-create 属性后,Select 下拉框中会显示新增按钮,点击按钮会抛出一个 top-create-click 事件,可以在事件中自定义一些行为。

', + 'en-US': + '

By using the allow-create and filterable attributes, the search field can be created as a new entry when it is not in an existing option. By combining the default-first-option attribute, you can press the Enter key to select the first matching option.

\n' + }, + codeFiles: ['allow-create.vue'] + }, + { + demoId: 'map-field', + name: { + 'zh-CN': '映射字段', + 'en-US': 'Map Fields' + }, + desc: { + 'zh-CN': '通过 text-field 设置显示文本字段,value-field设置绑定值字段。', + 'en-US': + '

Set the display text field by text-field, and set the binding value field by value-field.

\n' + }, + codeFiles: ['map-field.vue'] + }, + { + demoId: 'popup-style-position', + name: { + 'zh-CN': '弹框样式与定位', + 'en-US': 'Bullet Box Style and Positioning' + }, + desc: { + 'zh-CN': + '

通过 popper-class 属性设置下拉弹框的类名,可自定义样式。placement设置弹出位置。popper-append-to-body 设置是否将弹框 dom 元素插入至 body 元素,默认为 true。

\n', + 'en-US': + '

You can customize the style by setting the class name of the dropdown pop-up box through the popper-class attribute placement set the pop-up position popper-append-to-body set whether to insert the pop-up dom element into the body element, default to true.

\n' + }, + codeFiles: ['popup-style-position.vue'] + }, + { + demoId: 'input-box-type', + name: { + 'zh-CN': '输入框类型', + 'en-US': 'Input box type' + }, + desc: { + 'zh-CN': '

通过 input-box-type 属性设置输入框类型。可选值:input / underline。

\n', + 'en-US': + 'The

Set the input box type through the input-box-type attribute. Optional values: input / underline.

\n' + }, + codeFiles: ['input-box-type.vue'] + }, + { + demoId: 'show-alloption', + name: { + 'zh-CN': '不展示全选', + 'en-US': 'Hide Select All' + }, + desc: { + 'zh-CN': '

通过 show-alloption 属性设置多选时不展示 全选 选项,默认展示 。

\n', + 'en-US': + '

By setting the show-allocation attribute, do not display the select all option when multiple selections are made, and display by default.

\n' + }, + codeFiles: ['show-alloption.vue'] + }, + { + demoId: 'clear-no-match-value', + name: { + 'zh-CN': '自动清除不匹配的值', + 'en-US': 'Clear mismatch value' + }, + desc: { + 'zh-CN': + '

通过 clear-no-match-value 属性设置 v-model 的值在 options 中无法找到匹配项的值会被自动清除,默认不清除。

\n', + 'en-US': + '

By setting the value of the v-model through the clear-no-match-valueattribute, if a matching value cannot be found in the options, it will be automatically cleared and will not be cleared by default.

\n' + }, + codeFiles: ['clear-no-match-value.vue'] + }, + { + demoId: 'optimization', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'Virtual scrolling' + }, + desc: { + 'zh-CN': + '

通过 optimization 开启大数据虚拟滚动功能。仅配置式(使用 options 属性)时支持。\n多选模式下,最大选中项数 multiple-limit 默认值为 20,如果选中项比较多,建议开启 collapse-tags 进行折叠显示。

\n', + 'en-US': + '

Enable the big data virtual scrolling function through optimization. Supported only when configuring (using the options attribute). In n multiple selection mode, the maximum number of selected items is multiple-limit with a default value of 20. If there are many selected items, it is recommended to turn on collapse-tags for collapsed display.

\n' + }, + codeFiles: ['optimization.vue'] + }, + { + demoId: 'option-group', + name: { + 'zh-CN': '分组', + 'en-US': 'Group' + }, + desc: { + 'zh-CN': + '

使用 tiny-option-group 组件对备选项进行分组。通过 label 属性设置分组名,disabled 属性设置该分组下所有选项为禁用。

\n', + 'en-US': + '

Use the tiny-option-group component to group alternative options. Set the group name through the label attribute, and set all options under the group to disabled through the disabled attribute.

\n' + }, + codeFiles: ['option-group.vue'] + }, + { + demoId: 'copy-single', + name: { + 'zh-CN': '单选可复制', + 'en-US': 'Single choice replicable' + }, + desc: { + 'zh-CN': '

通过 allow-copy 属性设置单选可搜索时,鼠标可滑动选中并复制输入框的内容。

\n', + 'en-US': + '

When setting radio searchable through the allow-copy attribute, the mouse can slide to select and copy the content of the input box.

\n' + }, + codeFiles: ['copy-single.vue'] + }, + { + demoId: 'copy-multi', + name: { + 'zh-CN': '多选可复制', + 'en-US': 'Multiple choices can be copied' + }, + desc: { + 'zh-CN': + '

通过 tag-selectable 属性设置输入框中标签可通过鼠标选择,然后按 Ctrl + C 或右键进行复制。copyable 属性设置启用一键复制所有标签的文本内容并以逗号分隔。

\n', + 'en-US': + '

By setting the tag-selectable attribute in the input box, the label can be selected with the mouse, and then copied by pressing Ctrl+C or right-click copyable attribute settings enable one click copying of all label text content separated by commas.

\n' + }, + codeFiles: ['copy-multi.vue'] + }, + { + demoId: 'native-properties', + name: { + 'zh-CN': '原生属性', + 'en-US': 'Native properties' + }, + desc: { + 'zh-CN': + '

通过 name / placeholder / autocomplete 属性设置下拉组件内置 Input 的原生属性。

\n', + 'en-US': + '

Set the native properties of the built-in Input in the dropdown component through the name / placeholder / autocomplete attribute settings.

\n' + }, + codeFiles: ['native-properties.vue'] + }, + { + demoId: 'binding-obj', + name: { + 'zh-CN': '绑定值为对象', + 'en-US': 'Bind value as object' + }, + desc: { + 'zh-CN': '

通过 value-key 属性设置 value 唯一标识的键名,绑定值可以设置为对象。

\n', + 'en-US': + '

By using the value-key attribute to set the key name uniquely identified by value, the binding value can be set as an object.

\n' + }, + codeFiles: ['binding-obj.vue'] + }, + { + demoId: 'no-data-text', + name: { + 'zh-CN': '空数据文本', + 'en-US': 'Empty data text' + }, + desc: { + 'zh-CN': + '

通过 no-data-text 属性设置选项为空时显示的文本,show-empty-image 属性设置是否显示空数据图片,默认不显示。

\n', + 'en-US': + '

By setting theno-data-text attribute to display text when the option is empty, and by setting the show-empty-image attribute to display empty data images, it is not displayed by default.

\n' + }, + codeFiles: ['no-data-text.vue'] + }, + { + demoId: 'manual-focus-blur', + name: { + 'zh-CN': '手动聚焦失焦', + 'en-US': 'Manual focusing out of focus' + }, + desc: { + 'zh-CN': '

通过 focus() 方法聚焦,blur()方法失焦。

\n', + 'en-US': + '

Focusing is achieved through the focus() method, while the blur() method is out of focus.

\n' + }, + codeFiles: ['manual-focus-blur.vue'] + }, + { + demoId: 'automatic-dropdown', + name: { + 'zh-CN': '获焦即弹出', + 'en-US': 'Eject upon capture of focus' + }, + desc: { + 'zh-CN': '

通过 automatic-dropdown 设置不可搜索的 select 获得焦点并自动弹出选项菜单。

\n', + 'en-US': + '

Set non searchable select to obtain focus and automatically pop up an option menu through automatic-dropdown.

\n' + }, + codeFiles: ['automatic-dropdown.vue'] + }, + { + demoId: 'is-drop-inherit-width', + name: { + 'zh-CN': '继承宽度', + 'en-US': 'Inherit width' + }, + desc: { + 'zh-CN': + '

通过 is-drop-inherit-width 属性设置下拉弹框的宽度是否跟输入框保持一致。默认超出输入框宽度时由内容撑开。

\n', + 'en-US': + '

Set whether the width of the dropdown pop-up box is consistent with the input box through the is-drop-inherit-width attribute. By default, when the width of the input box is exceeded, it is supported by the content.

\n' + }, + codeFiles: ['is-drop-inherit-width.vue'] + }, + { + demoId: 'hide-drop', + name: { + 'zh-CN': '隐藏下拉', + 'en-US': 'Hide drop' + }, + desc: { + 'zh-CN': '

通过 hide-drop 属性设置下拉列表不显示。

\n', + 'en-US': '

Set the drop-down list to not display through the hide-drop attribute.

' + }, + codeFiles: ['hide-drop.vue'] + }, + { + demoId: 'filter-mode', + name: { + 'zh-CN': '过滤器模式', + 'en-US': 'Filter mode' + }, + desc: { + 'zh-CN': + '

通过 shape 属性设置为 filter 切换至过滤器模式。 过滤器模式下可传入 label 显示标题,tip 显示提示信息,clearable 是否显示清除按钮,placeholder 显示占位符。

\n

通过 blank 属性将过滤器背景设置为透明。

', + 'en-US': + '

Set the shape attribute to filter to switch to filter mode. In filter mode, you can transfer the label display title, tip display prompt information, clearable whether to display the clear button, and placeholder display placeholder.

\n

Set the filter background to transparent with the blank attribute.

' + }, + codeFiles: ['filter-mode.vue'] + }, + { + demoId: 'cache-usage', + name: { + 'zh-CN': '自动缓存', + 'en-US': 'Automatic caching' + }, + desc: { + 'zh-CN': '

通过 cache-op 开启缓存功能,仅配置式生效。

\n', + 'en-US': '

Enable cache function through cache-op, only configuration mode takes effect

' + }, + codeFiles: ['cache-usage.vue'] + }, + { + demoId: 'memoize-usage', + name: { + 'zh-CN': '手动缓存', + 'en-US': 'Manual caching' + }, + desc: { + 'zh-CN': '

使用 tiny-option 组件,则需要手动加入缓存功能。

\n', + 'en-US': '

If using the tiny-option component, you need to manually add caching functionality.

' + }, + codeFiles: ['memoize-usage.vue'] + }, + { + demoId: 'slot-default', + name: { + 'zh-CN': '选项插槽', + 'en-US': 'Option slot' + }, + desc: { + 'zh-CN': '

通过 tiny-option 的 default 插槽自定义选项的 HTML 模板。

\n', + 'en-US': '

HTML template for customizing options through the default slot of tiny-option.

' + }, + codeFiles: ['slot-default.vue'] + }, + { + demoId: 'slot-footer', + name: { + 'zh-CN': '底部插槽', + 'en-US': 'Footer slot' + }, + desc: { + 'zh-CN': '

通过 footer 插槽自定义下拉弹框底部的 HTML 模板。

\n', + 'en-US': + '

Customize the HTML template at the bottom of the dropdown pop-up box through the footer slot.

' + }, + codeFiles: ['slot-footer.vue'] + }, + { + demoId: 'slot-empty', + name: { + 'zh-CN': '空数据插槽', + 'en-US': 'Empty data slot' + }, + desc: { + 'zh-CN': '

通过 empty 插槽自定义没有选项列表时显示的 HTML 模板。

\n', + 'en-US': + '

Customize the HTML template displayed when there is no option list through the empty slot.

' + }, + codeFiles: ['slot-empty.vue'] + }, + { + demoId: 'slot-prefix', + name: { + 'zh-CN': '输入框前缀插槽', + 'en-US': 'Predix slot' + }, + desc: { + 'zh-CN': '

通过 prefix 插槽自定义输入框前缀的 HTML 模板。

\n', + 'en-US': '

Customize the HTML template for the input box prefix through the prefix slot.

\n' + }, + codeFiles: ['slot-prefix.vue'] + }, + { + demoId: 'slot-reference', + name: { + 'zh-CN': '触发源插槽', + 'en-US': 'Reference slot' + }, + desc: { + 'zh-CN': '

通过 reference 插槽自定义触发源的 HTML 模板。

\n', + 'en-US': '

Customize the HTML template of the trigger source through the reference slot.

' + }, + codeFiles: ['slot-reference.vue'] + }, + { + demoId: 'slot-label', + name: { + 'zh-CN': '标签插槽', + 'en-US': 'Reference slot' + }, + desc: { + 'zh-CN': '

通过 label 插槽自定义多选选中标签的 HTML 模板。

\n', + 'en-US': + '

Customize the HTML template for multiple-choice selected labels through the label slot.

' + }, + codeFiles: ['slot-label.vue'] + }, + { + demoId: 'all-text', + name: { + 'zh-CN': '自定义全部文本', + 'en-US': 'Custom All Text' + }, + desc: { + 'zh-CN': '当下拉中显示全部时,通过all-text 属性自定义全部的显示文本', + 'en-US': + 'Use the all-text attribute to customize all displayed text when all is displayed in the drop-down list box.' + }, + codeFiles: ['all-text.vue'] + }, + { + demoId: 'events', + name: { + 'zh-CN': '事件', + 'en-US': 'Events' + }, + desc: { + 'zh-CN': + '

事件说明

\n

change:监听 v-model 的值发生变化。

\n

clear:监听单选时,点击清空按钮。

\n

blur:监听 input 失去焦点。

\n

focus:监听 input 获得焦点。

\n

visible-change:监听下拉框可见状态的变化。

\n

remove-tag:监听多选移除选中的标签。

\n

dropdown-click:监听下拉图标的点击事件。

\n
\n', + 'en-US': + '

Event Description

\n

change:Listen for changes in the value of the v-model.

clear:When listening to radio selection, click the clear button.

\n

blur:Listening to input losing focus.

\n

focus:Listening for input to gain focus.

\n

visible-change: Listen for changes in the visible status of the dropdown box

\n

remove-tag:Listen for multiple selections to remove selected tags.

\n

dropdown-click:Listens to the click event of the drop-down icon.

\n
\n' + }, + codeFiles: ['events.vue'] + } + ] +} diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js index 213f8e438f..759eebcc61 100644 --- a/examples/sites/demos/pc/menus.js +++ b/examples/sites/demos/pc/menus.js @@ -141,6 +141,7 @@ export const cmpMenus = [ } }, { 'nameCn': '搜索', 'name': 'Search', 'key': 'search' }, + { 'nameCn': '选择器', 'name': 'BaseSelect', 'key': 'base-select' }, { 'nameCn': '选择器', 'name': 'Select', 'key': 'select' }, { 'nameCn': '滑块', 'name': 'Slider', 'key': 'slider' }, { 'nameCn': '开关', 'name': 'Switch', 'key': 'switch' }, diff --git a/packages/modules.json b/packages/modules.json index 6c0f834115..5e3ffa6f90 100644 --- a/packages/modules.json +++ b/packages/modules.json @@ -2329,6 +2329,14 @@ "pc" ] }, + "BaseSelect": { + "path": "vue/src/base-select/index.ts", + "type": "component", + "exclude": false, + "mode": [ + "pc" + ] + }, "SelectDropdown": { "path": "vue/src/select-dropdown/index.ts", "type": "component", diff --git a/packages/renderless/src/base-select/index.ts b/packages/renderless/src/base-select/index.ts new file mode 100644 index 0000000000..1657e6ddbf --- /dev/null +++ b/packages/renderless/src/base-select/index.ts @@ -0,0 +1,2022 @@ +import { find } from '../common/array' +import { getObj, isEqual } from '../common/object' +import { isKorean } from '../common/string' +import scrollIntoView from '../common/deps/scroll-into-view' +import PopupManager from '../common/deps/popup-manager' +import debounce from '../common/deps/debounce' +import { getDataset } from '../common/dataset' +import Memorize from '../common/deps/memorize' +import { isEmptyObject } from '../common/type' +import { addResizeListener, removeResizeListener } from '../common/deps/resize-event' +import { extend } from '../common/object' +import { BROWSER_NAME } from '../common' +import browserInfo from '../common/browser' +import { isNull } from '../common/type' +import { fastdom } from '../common/deps/fastdom' +import { deepClone } from '../picker-column' +import { escapeRegexpString } from '../option' + +export const handleComposition = + ({ api, nextTick, state }) => + (event) => { + const text = event.target.value + + if (event.type === 'compositionend') { + state.isOnComposition = false + const isChange = false + const isInput = true + + nextTick(() => api.handleQueryChange(text, isChange, isInput)) + } else { + const lastCharacter = text[text.length - 1] || '' + + state.isOnComposition = !isKorean(lastCharacter) + } + } + +export const showTip = + ({ props, state, vm }) => + (show) => { + if (!props.showOverflowTooltip) { + return + } + + let overflow + + if (!show) { + clearTimeout(state.tipTimer) + + state.tipTimer = setTimeout(() => { + state.showTip = state.tipHover + }, vm.$refs.popover.closeDelay) + } else { + if (!props.multiple) { + const reference = vm.$refs.reference.$el + overflow = reference.querySelector('input').scrollWidth > reference.scrollWidth + } else { + overflow = vm.$refs.tags.scrollHeight > vm.$refs.tags.getBoundingClientRect().height + } + + state.showTip = show && overflow && !!state.tips && !state.visible + } + } + +export const gridOnQueryChange = + ({ props, vm, constants, state }) => + (value) => { + const { multiple, valueField, filterMethod, filterable, remote, remoteMethod } = props + + if (filterable && typeof filterMethod === 'function') { + const table = vm.$refs.selectGrid.$refs.tinyTable + const fullData = table.afterFullData + + vm.$refs.selectGrid.scrollTo(null, 0) + + table.afterFullData = filterMethod(value, fullData) || [] + + vm.$refs.selectGrid + .handleTableData(!value) + .then(() => state.selectEmitter.emit(constants.EVENT_NAME.updatePopper)) + + state.previousQuery = value + } else if (remote && typeof remoteMethod === 'function') { + state.previousQuery = value + remoteMethod(value, props.extraQueryParams).then((data) => { + // 多选时取远端数据与当前已选数据的并集 + if (multiple) { + const selectedIds = state.selected.map((sel) => sel[valueField]) + vm.$refs.selectGrid.clearSelection() + vm.$refs.selectGrid.setSelection( + data.filter((row) => ~selectedIds.indexOf(row[valueField])), + true + ) + state.remoteData = data.filter((row) => !~selectedIds.indexOf(row[valueField])).concat(state.selected) + } else { + vm.$refs.selectGrid.clearRadioRow() + vm.$refs.selectGrid.setRadioRow(find(data, (item) => props.modelValue === item[props.valueField])) + state.remoteData = data + } + + vm.$refs.selectGrid.$refs.tinyTable.lastScrollTop = 0 + vm.$refs.selectGrid.loadData(data) + vm.$refs.selectGrid + .handleTableData(!value) + .then(() => state.selectEmitter.emit(constants.EVENT_NAME.updatePopper)) + }) + } + } + +export const defaultOnQueryChange = + ({ props, state, constants, api, nextTick }) => + (value, isInput) => { + if (props.remote && (typeof props.remoteMethod === 'function' || typeof props.initQuery === 'function')) { + state.hoverIndex = -1 + props.remoteMethod && props.remoteMethod(value, props.extraQueryParams) + } else if (typeof props.filterMethod === 'function') { + props.filterMethod(value) + state.selectEmitter.emit(constants.COMPONENT_NAME.OptionGroup, constants.EVENT_NAME.queryChange) + } else { + api.queryChange(value, isInput) + } + setFilteredSelectCls(nextTick, state, props) + api.getOptionIndexArr() + + state.magicKey = state.magicKey > 0 ? -1 : 1 + } + +export const queryChange = + ({ props, state, constants }) => + (value, isInput) => { + if (props.optimization && isInput) { + const filterDatas = state.initDatas.filter((item) => new RegExp(escapeRegexpString(value), 'i').test(item.label)) + state.datas = filterDatas + } else { + state.selectEmitter.emit(constants.EVENT_NAME.queryChange, value) + } + } + +const setFilteredSelectCls = (nextTick, state, props) => { + nextTick(() => { + if (props.multiple && props.showAlloption && props.filterable && state.query && !props.remote) { + const filterSelectedVal = state.options + .filter((item) => item.state.visible && item.state.itemSelected) + .map((opt) => opt.value) + const visibleOptions = state.options.filter((item) => item.state.visible) + if (filterSelectedVal.length === visibleOptions.length) { + state.filteredSelectCls = 'checked-sur' + } else if (filterSelectedVal.length === 0) { + state.filteredSelectCls = 'check' + } else { + state.filteredSelectCls = 'halfselect' + } + } + }) +} + +export const handleQueryChange = + ({ api, constants, nextTick, props, vm, state }) => + (value, isChange = false, isInput = false) => { + if ((state.previousQuery === value && !isChange) || state.isOnComposition) { + return + } + + if ( + state.previousQuery === null && + !isChange && + (typeof props.filterMethod === 'function' || + typeof props.remoteMethod === 'function' || + typeof props.initQuery === 'function') + ) { + state.previousQuery = value + return + } + + state.query = value + state.previousQuery = value + + window.requestAnimationFrame(() => { + if (state.visible) { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper) + state.showWarper = true + } + }) + + state.hoverIndex = -1 + + if (props.multiple && props.filterable && !props.shape) { + nextTick(() => { + const length = vm.$refs.input.value.length * 15 + 20 + state.inputLength = state.collapseTags ? Math.min(50, length) : length + api.managePlaceholder() + api.resetInputHeight() + }) + } + + state.triggerSearch = true + + api.defaultOnQueryChange(value, isInput) + } + +export const scrollToOption = + ({ vm, constants }) => + (option) => { + const target = + Array.isArray(option) && option[0] && option[0].state ? option[0].state.el : option.state ? option.state.el : '' + if (vm.$refs.popper && target) { + const menu = vm.$refs.popper.$el.querySelector(constants.CLASS.SelectDropdownWrap) + setTimeout(() => scrollIntoView(menu, target)) + } + + vm.$refs.scrollbar && vm.$refs.scrollbar.handleScroll() + } + +export const handleMenuEnter = + ({ api, nextTick, state, props }) => + () => { + if (!props.optimization) { + nextTick(() => api.scrollToOption(state.selected)) + } + } + +export const emitChange = + ({ emit, props, state, constants }) => + (value, changed) => { + if (state.device === 'mb' && props.multiple && !changed) return + + if (!isEqual(props.modelValue, state.compareValue)) { + emit('change', value) + } + } + +export const directEmitChange = + ({ emit, props, state }) => + (value, key) => { + if (state.device === 'mb' && props.multiple) return + + emit('change', value, key) + } + +export const getOption = + ({ props, state, api }) => + (value) => { + let option + const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]' + const isNull = Object.prototype.toString.call(value).toLowerCase() === '[object null]' + const isUndefined = Object.prototype.toString.call(value).toLowerCase() === '[object undefined]' + + for (let i = state.cachedOptions.length - 1; i >= 0; i--) { + const cachedOption = state.cachedOptions[i] + const isEqual = isObject + ? getObj(cachedOption.value, props.valueKey) === getObj(value, props.valueKey) + : cachedOption.value === value + + if (isEqual) { + option = cachedOption + break + } + } + + if (option) { + return option + } + + if (props.optimization) { + option = api.getSelectedOption(value) + if (option) { + return { value: option.value, currentLabel: option.label || option.currentLabel } + } + + option = state.datas.find((v) => getObj(v, props.valueKey) === value) + if (option) { + return { value: option.value, currentLabel: option.label || option.currentLabel } + } + } + // tiny 新增 clearNoMatchValue的条件 + const label = !isObject && !isNull && !isUndefined && !props.clearNoMatchValue ? value : '' + let newOption = { value, currentLabel: label } + + if (props.multiple) { + newOption.hitState = false + } + + return newOption + } + +export const getSelectedOption = + ({ props, state }) => + (value) => { + let option + if (props.multiple) { + option = state.selected.find((v) => getObj(v, props.valueKey) === value) + } else { + if (!isEmptyObject(state.selected) && getObj(state.selected, props.valueKey) === value) { + option = state.selected + } + } + + return option + } + +// 单选,获取匹配的option +const getOptionOfSetSelected = ({ api, props }) => { + const option = api.getOption(props.modelValue) || {} + + if (!option.state) { + option.state = {} + } + + if (option.created) { + option.createdLabel = option.state.currentLabel + option.createdSelected = true + } else { + option.createdSelected = false + } + + // tiny 新增 + if (!option.currentLabel) { + api.clearNoMatchValue('') + } + + return option +} + +// 多选,获取匹配的option +const getResultOfSetSelected = ({ state, api, props }) => { + let result = [] + const newModelValue = [] // tiny 新增,用于 clearNoMatchValue + + if (Array.isArray(state.modelValue)) { + state.modelValue.forEach((value) => { + // tiny 新增 + const option = api.getOption(value) + if (!props.clearNoMatchValue || (props.clearNoMatchValue && option.label)) { + result.push(option) + newModelValue.push(value) + } + }) + } + // tiny 新增 + api.clearNoMatchValue(newModelValue) + + return result +} + +export const setSelected = + ({ api, nextTick, props, vm, state }) => + () => { + if (!props.multiple) { + const option = getOptionOfSetSelected({ api, props }) + state.selected = option + state.selectedLabel = option.state.currentLabel || option.currentLabel + props.filterable && !props.shape && (state.query = state.selectedLabel) + } else { + const result = getResultOfSetSelected({ state, props, api }) + state.selectCls = result.length + ? result.length === state.options.length + ? 'checked-sur' + : 'halfselect' + : 'check' + state.selected = result + vm.$refs.selectTree && vm.$refs.selectTree.setCheckedNodes && vm.$refs.selectTree.setCheckedNodes(state.selected) + state.tips = state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join(',') + + setFilteredSelectCls(nextTick, state, props) + nextTick(api.resetInputHeight) + } + } + +// 多选,树/表格,获取匹配option +export const getPluginOption = + ({ props, state }) => + (value) => { + const isRemote = + props.filterable && + props.remote && + (typeof props.remoteMethod === 'function' || typeof props.initQuery === 'function') + const { textField, valueField } = props + const sourceData = isRemote ? state.remoteData : state.gridData + const selNode = find(sourceData, (item) => item[valueField] === value) + const items = [] + + if (selNode) { + selNode.currentLabel = selNode[textField] + items.push(selNode) + } + + return items + } + +export const toggleCheckAll = + ({ api, state }) => + (filtered) => { + let value = [] + // 1. 需要控制勾选或去勾选的项 + const enabledValues = state.options + .filter((op) => !op.state.disabled && !op.state.groupDisabled && !op.required && op.state.visible) + .map((op) => op.value) + + if (filtered) { + if (state.filteredSelectCls === 'check' || state.filteredSelectCls === 'halfselect') { + value = [...new Set([...state.modelValue, ...enabledValues])] + } else { + value = state.modelValue.filter((val) => !enabledValues.includes(val)) + } + } else { + if (state.selectCls === 'check') { + value = enabledValues + } else if (state.selectCls === 'halfselect') { + const unchecked = state.options.filter((item) => !item.state.disabled && item.state.selectCls === 'check') + + unchecked.length ? (value = enabledValues) : (value = []) + } else if (state.selectCls === 'checked-sur') { + value = [] + } + } + // 2. 必选项 + const requiredValue = state.options.filter((op) => op.required).map((op) => op.value) + + // 3. 禁用且已设置为勾选的项 + const disabledSelectedValues = state.options + .filter((op) => (op.state.disabled || op.state.groupDisabled) && op.state.selectCls === 'checked-sur') + .map((op) => op.value) + + value = [...value, ...requiredValue, ...disabledSelectedValues] + + api.setSoftFocus() + + state.isSilentBlur = true + api.updateModelValue(value) + api.directEmitChange(value) + } + +export const handleFocus = + ({ emit, props, state }) => + (event) => { + if (!state.softFocus) { + if (props.automaticDropdown || props.filterable) { + state.visible = true + state.softFocus = true + } + + emit('focus', event) + } else { + if (state.searchSingleCopy && state.selectedLabel) { + emit('focus', event) + } + + state.softFocus = false + } + } + +export const focus = + ({ vm, state }) => + () => { + if (!state.softFocus) { + vm.$refs.reference.focus() + } + } + +export const blur = + ({ vm, state }) => + () => { + state.visible = false + vm.$refs.reference.blur() + } + +export const handleBlur = + ({ constants, dispatch, emit, state, designConfig }) => + (event) => { + clearTimeout(state.timer) + state.timer = setTimeout(() => { + if (state.isSilentBlur) { + state.isSilentBlur = false + } else { + emit('blur', event) + } + if (designConfig?.state?.delayBlur) { + dispatch(constants.COMPONENT_NAME.FormItem, constants.EVENT_NAME.formBlur, event.target.value) + } + }, 200) + // tiny 新增: 表单校验不能异步,否则弹窗中嵌套表单会出现弹窗关闭后再出现校验提示的bug + if (!designConfig?.state?.delayBlur) { + dispatch(constants.COMPONENT_NAME.FormItem, constants.EVENT_NAME.formBlur, event.target.value) + } + + state.softFocus = false + } + +export const handleClearClick = (api) => (event) => { + api.deleteSelected(event) +} + +export const doDestroy = (vm) => () => { + // 解决在特殊场景(表格使用select编辑器),选中下拉数据的一瞬间select组件被销毁时控制台报错的问题 + if (vm?.$refs?.popper) { + vm.$refs.popper.doDestroy() + } +} + +export const handleClose = (state) => () => { + state.visible = false +} + +export const toggleLastOptionHitState = + ({ state }) => + (hit) => { + if (!Array.isArray(state.selected)) { + return + } + + const option = state.selected[state.selected.length - 1] + + if (!option) { + return + } + + if (option.required) { + return true + } + + const hitTarget = option.state || option + + if (hit === true || hit === false) { + hitTarget.hitState = hit + + return hit + } + + hitTarget.hitState = !hitTarget.hitState + + return hitTarget.hitState + } + +export const deletePrevTag = + ({ api, state }) => + (event) => { + if (event.target.value.length <= 0 && !api.toggleLastOptionHitState()) { + const value = state.modelValue.slice() + + value.pop() + + state.compareValue = deepClone(value) + + api.updateModelValue(value) + + api.emitChange(value) + } + } + +export const managePlaceholder = + ({ vm, state }) => + () => { + if (state.currentPlaceholder !== '') { + state.currentPlaceholder = vm.$refs.input.value ? '' : state.cachedPlaceHolder + } + } + +export const resetInputState = + ({ api, vm, state }) => + (event) => { + if (event.keyCode !== 8) { + api.toggleLastOptionHitState(false) + } + + state.inputLength = vm.$refs.input.value.length * 15 + 20 + api.resetInputHeight() + } + +export const resetInputHeight = + ({ constants, nextTick, props, vm, state, api, designConfig }) => + () => { + if (state.collapseTags && !props.filterable) { + return + } + + nextTick(() => { + if (!vm.$refs.reference) { + return + } + + let input = vm.$refs.reference.type === 'text' && vm.$refs.reference.$el.querySelector('input') + const tags = vm.$refs.tags + const limitText = vm.$refs.reference.$el.querySelector('span.tiny-select__limit-txt') + + if (!input) { + return + } + + if (!state.isDisplayOnly && (props.hoverExpand || props.clickExpand) && !props.disabled) { + api.calcCollapseTags() + } + + const sizeInMap = + designConfig?.state.initialInputHeight || state.initialInputHeight || (state.isSaaSTheme ? 28 : 30) + const noSelected = state.selected.length === 0 + // tiny 新增的spacing (design中配置:aui为4,smb为0,tiny 默认为0) + const spacingHeight = designConfig ? designConfig.state?.spacingHeight : constants.SPACING_HEIGHT + + if (!state.isDisplayOnly) { + if (!noSelected && tags) { + fastdom.measure(() => { + const tagsClientHeight = tags.clientHeight + + fastdom.mutate(() => { + input.style.height = Math.max(tagsClientHeight + spacingHeight, sizeInMap) + 'px' + }) + }) + } else { + input.style.height = noSelected ? sizeInMap + 'px' : Math.max(0, sizeInMap) + 'px' + } + } else { + input.style.height = 'auto' + } + + // tags给字数限制 + if (limitText && props.multipleLimit) { + const { width, marginLeft, marginRight } = getComputedStyle(limitText) + vm.$refs.tags.style.paddingRight = `${Math.ceil( + parseFloat(width) + parseFloat(marginLeft) + parseFloat(marginRight) + )}px` + } + + if (state.visible && state.emptyText !== false) { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper, true) + } + }) + } + +export const resetHoverIndex = + ({ props, state }) => + () => { + if (!props.showOverflowTooltip) { + state.hoverIndex = -1 + } else if (!props.multiple) { + state.hoverIndex = state.options.indexOf(state.selected) + } else { + if (state.selected.length > 0) { + state.hoverIndex = Math.min.apply( + null, + state.selected.map((item) => state.options.indexOf(item)) + ) + } else { + state.hoverIndex = -1 + } + } + } + +export const resetDatas = + ({ props, state }) => + () => { + if (props.optimization && !props.remote && !props.filterMethod) { + state.datas = state.initDatas + } + } + +export const handleOptionSelect = + ({ api, nextTick, props, vm, state }) => + (option, byClick) => { + state.memorize && state.memorize.updateByKey(option[state.memorize._dataKey] || option.value) + + if (props.multiple) { + const value = (state.modelValue || []).slice() + const optionIndex = api.getValueIndex(value, option.value) + + if (optionIndex > -1) { + value.splice(optionIndex, 1) + } else if (state.multipleLimit <= 0 || value.length < state.multipleLimit) { + value.push(option.value) + } + + state.compareValue = deepClone(value) + + api.updateModelValue(value) + + api.emitChange(value) + + if (option.created) { + const isChange = false + const isInput = true + + state.query = '' + api.handleQueryChange('', isChange, isInput) + + state.inputLength = 20 + } + + if (props.filterable) { + vm.$refs.input.focus() + } + + if (props.autoClose) { + state.visible = false + } + } else { + state.compareValue = deepClone(option.value) + + api.updateModelValue(option.value) + + api.emitChange(option.value) + + // tiny 新增 修复select组件,创建条目选中再创建选中,还是上一次的数据 + if (option.created) { + state.createdSelected = true + state.createdLabel = option.value + } + + state.visible = false + } + + state.isSilentBlur = byClick + + api.setSoftFocus() + + if (state.visible) { + return + } + + nextTick(() => { + api.scrollToOption(option) + }) + } + +export const initValue = + ({ state }) => + (vm) => { + const isExist = state.initValue.find((val) => val === vm.value) + !isExist && state.initValue.push(vm.value) + } + +export const setSoftFocus = + ({ vm, state }) => + () => { + state.softFocus = true + + const input = vm.$refs.input || vm.$refs.reference + + if (input) { + input.focus() + } + + // tiny 新增: 解决 reference 插槽时,选择数据后,需要点2次才能打开下拉面板 + state.softFocus = false + } + +export const getValueIndex = + (props) => + (arr = [], value = null) => { + const isObject = Object.prototype.toString.call(value).toLowerCase() === '[object object]' + + if (!isObject) { + return arr.indexOf(value) + } else { + const valueKey = props.valueKey + let index = -1 + + arr.some((item, i) => { + if (getObj(item, valueKey) === getObj(value, valueKey)) { + index = i + return true + } + return false + }) + + return index + } + } + +export const toggleMenu = + ({ vm, state, props, api }) => + (e) => { + if (props.keepFocus && state.visible && props.filterable) { + return + } + + const event = e || window.event + const enterCode = 13 + const nodeName = event.target && event.target.nodeName + const toggleVisible = props.ignoreEnter ? event.keyCode !== enterCode && nodeName === 'INPUT' : true + + if (!props.displayOnly) { + event.stopPropagation() + } + + if (!state.selectDisabled) { + toggleVisible && !state.softFocus && (state.visible = !state.visible) + state.softFocus = false + + if (state.visible) { + if (!(props.filterable && props.shape)) { + const dom = vm.$refs.input || vm.$refs.reference + dom?.focus && dom.focus() + api.setOptionHighlight() + } + } + } + } + +export const selectOption = + ({ api, state, props }) => + (e) => { + if (!state.visible || props.hideDrop) { + api.toggleMenu(e) + } else { + let option = '' + if (state.query || props.remote) { + option = state.options.find((item) => item.state.index === state.hoverValue) + } else { + option = state.options[state.hoverIndex] + } + option && api.handleOptionSelect(option) + } + } + +export const deleteSelected = + ({ api, emit, props, state }) => + (event) => { + event && event.stopPropagation() + + let selectedValue = [] + if (props.multiple) { + const requireOptions = state.options.filter((opt) => opt.required && opt.value) + selectedValue = state.modelValue.slice().filter((v) => requireOptions.find((opt) => opt.value === v)) + } + + const value = props.multiple ? selectedValue : '' + + state.showTip = false + state.compareValue = deepClone(value) + + api.updateModelValue(value, true) + + api.emitChange(value, true) + + state.visible = false + + emit('clear') + } + +export const deleteTag = + ({ api, constants, emit, props, state, nextTick, vm }) => + (event, tag) => { + if (tag.required) return + + const index = state.selected.indexOf(tag) + + if (index > -1 && !state.selectDisabled) { + const value = state.modelValue.slice() + value.splice(index, 1) + + state.compareValue = deepClone(value) + + api.updateModelValue(value) + + api.emitChange(value) + + emit(constants.EVENT_NAME.removeTag, tag[props.valueField]) + nextTick(() => state.key++) + } + + event && event.stopPropagation() + } + +export const onInputChange = + ({ api, props, state, constants, nextTick }) => + () => { + if (!props.delay) { + if (props.filterable && state.query !== state.selectedLabel) { + const isChange = false + const isInput = true + + state.query = state.selectedLabel + api.handleQueryChange(state.query, isChange, isInput) + + nextTick(() => { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper) + }) + } + } else { + api.debouncRquest() + } + + nextTick(() => { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper) + }) + } + +export const onOptionDestroy = (state) => (index) => { + if (index > -1) { + state.optionsCount-- + state.filteredOptionsCount-- + state.options.splice(index, 1) + } +} + +export const resetInputWidth = + ({ vm, state }) => + () => { + // tiny 新增:由于当有reference 插槽时, 就没有 vm.$refs.reference 对象了。 + if (vm.$refs.reference && vm.$refs.reference.$el) { + state.inputWidth = vm.$refs.reference.$el.getBoundingClientRect().width + } + } + +export const handleResize = + ({ api, props, state }) => + () => { + api.resetInputWidth() + + if (props.multiple && !state.isDisplayOnly) { + api.resetInputHeight() + } + } + +export const setOptionHighlight = (state) => () => { + for (let i = 0; i < state.options.length; ++i) { + const option = state.options[i] + + if ( + !option.disabled && + !option.groupDisabled && + !option.state.created && + option.state.visible && + option.state.itemSelected + ) { + state.hoverIndex = i + break + } + } +} + +export const checkDefaultFirstOption = (state) => () => { + state.hoverIndex = -1 + + let hasCreated = false + + const visibleOptions = state.options.filter((item) => item.visible && item.state.visible) + + for (let i = visibleOptions.length - 1; i >= 0; i--) { + if (visibleOptions[i].created) { + hasCreated = true + state.hoverIndex = i + state.hoverValue = state.optionIndexArr[i] + + break + } + } + + if (hasCreated) { + return + } + + for (let i = 0; i < visibleOptions.length; i++) { + const option = visibleOptions[i] + + if (state.query) { + if (!option.disabled && !option.groupDisabled && option.state.visible && option.visible) { + state.hoverIndex = i + state.hoverValue = state.optionIndexArr[i] + + break + } + } else { + if (option.itemSelected) { + state.hoverIndex = i + state.hoverValue = state.optionIndexArr[i] + + break + } + } + } +} + +export const getValueKey = (props) => (item) => { + if (!item) return + if (Object.prototype.toString.call(item.value).toLowerCase() !== '[object object]') { + return item.value || item[props.valueField] + } + + return getObj(item.value, props.valueKey) +} + +export const navigateOptions = + ({ api, state, props, nextTick }) => + (direction) => { + const { optimization } = props + + if (optimization) { + return + } + const len = state.options.filter((item) => item.visible && item.state.visible).length + + if (!state.visible) { + state.visible = true + return + } + + if (len === 0 || state.filteredOptionsCount === 0) { + return + } + + state.disabledOptionHover = true + setTimeout(() => { + state.disabledOptionHover = false + }, 100) + + if (state.hoverIndex < -1 || state.hoverIndex >= len) { + state.hoverIndex = 0 + } + + if (!state.optionsAllDisabled) { + if (direction === 'next') { + state.hoverIndex++ + + if (state.hoverIndex === len) { + state.hoverIndex = 0 + } + } else if (direction === 'prev') { + state.hoverIndex-- + + if (state.hoverIndex < 0) { + state.hoverIndex = len - 1 + } + } + + let option = {} + + state.hoverValue = state.optionIndexArr[state.hoverIndex] + + if (state.query || props.remote) { + option = state.options.find((item) => item.state.index === state.hoverValue) + } else { + option = state.options[state.hoverIndex] + } + + if ( + option.disabled === true || + option.groupDisabled === true || + !option.state.visible || + option.state.limitReached + ) { + api.navigateOptions(direction) + } + + nextTick(() => api.scrollToOption(state.hoverIndex === -9 ? {} : option || {})) + } + } + +export const emptyFlag = + ({ props, state }) => + () => { + if (props.optimization) { + if (props.allowCreate) { + return state.query === '' && state.datas.length === 0 + } else { + return state.datas.length === 0 + } + } else { + return state.options.length === 0 + } + } + +export const recycleScrollerHeight = + ({ state, props, recycle }) => + () => { + const { ITEM_HEIGHT, SAFE_MARGIN, SAAS_HEIGHT, AURORA_HEIGHT } = recycle + let length = state.datas.length + if (state.showNewOption) { + length += 1 + } + + if (length === 0 || props.loading) { + return 0 + } else if (length < 6) { + return length * ITEM_HEIGHT + (state.isSaaSTheme ? SAFE_MARGIN * 2 : 0) + } else { + return state.isSaaSTheme ? SAAS_HEIGHT : AURORA_HEIGHT + } + } + +export const emptyText = + ({ I18N, props, state, t, isMobileFirstMode }) => + () => { + if (props.loading) { + return props.loadingText || t(I18N.loading) + } + + if (props.remote && state.query === '') { + return remoteEmptyText(props, state) + } + + if (props.remote && state.query === '' && state.emptyFlag && !state.triggerSearch) { + return props.shape === 'filter' || isMobileFirstMode ? '' : false + } + + if ( + props.filterable && + state.query && + ((props.remote && state.emptyFlag) || !state.options.some((option) => option.visible && option.state.visible)) + ) { + return props.noMatchText || t(I18N.noMatch) + } + + if (state.emptyFlag) { + return props.noDataText || t(I18N.noData) + } + + return null + } + +const remoteEmptyText = function (props, state) { + if (props.multiple) { + return state.selected.length > 0 || state.remoteData.length >= 0 + } + + return state.selected[props.valueField] || state.remoteData.length >= 0 +} + +export const watchValue = + ({ api, constants, dispatch, props, vm, state }) => + (value, oldValue) => { + if (props.multiple) { + api.resetInputHeight() + + if ((value && value.length > 0) || (vm.$refs.input && state.query !== '')) { + state.currentPlaceholder = '' + } else { + state.currentPlaceholder = state.cachedPlaceHolder + } + + if (props.filterable && !props.reserveKeyword) { + // tiny 优化: 多选且props.reserveKeyword为false时, aui此处会多请求一次 + // searchable时,不清空query, 这样才能保持搜索结果 + !props.searchable && (state.query = '') + } + } + + api.setSelected() + + !state.isClickChoose && api.initQuery({ init: true }).then(() => api.setSelected()) + state.isClickChoose = false + + if (props.filterable && !props.multiple) { + state.inputLength = 20 + } + + if (state.completed && !isEqual(value, oldValue)) { + dispatch(constants.COMPONENT_NAME.FormItem, constants.EVENT_NAME.formChange, value) + } + + props.optimization && optmzApis.setValueIndex({ props, state }) + } + +export const calcOverFlow = + ({ vm, props, state }) => + (height) => { + if (props.multiple && props.showOverflowTooltip) { + state.overflow = false + + const tagDom = vm.$refs.tags + const tags = tagDom.querySelectorAll('[data-tag="tiny-tag"]') + + if (tags.length) { + tagDom.scrollTo && tagDom.scrollTo({ top: 0 }) + let { x, width } = tags[0].getBoundingClientRect() + + if (width >= tagDom.getBoundingClientRect().width) { + state.overflow = 0 + } else { + for (let i = 1; i < tags.length; i++) { + let tx = tags[i].getBoundingClientRect().x + + if (tx === x) { + state.overflow = i - 1 + break + } + } + } + } + + vm.$refs.select.style.height = vm.$refs.select.style.height || height + vm.$refs.reference.$el.style.position = 'absolute' + + const inputWidth = vm.$refs.select.getBoundingClientRect().width + const position = state.visible ? 'absolute' : '' + + state.selectFiexd = { + height, + position, + width: inputWidth + 'px', + zIndex: PopupManager.nextZIndex() + } + + state.inputWidth = inputWidth + } + } + +const postOperOfToVisible = ({ props, state, constants }) => { + if (props.multiple) { + return + } + + if (state.selected) { + if (props.filterable && props.allowCreate && state.createdSelected && state.createdLabel) { + state.selectedLabel = state.createdLabel + } else { + state.selectedLabel = state.selected.state.currentLabel || state.selected.currentLabel + } + + if (props.filterable) { + state.query = state.selectedLabel + } + + if (props.filterable) { + state.currentPlaceholder = state.cachedPlaceHolder + } + } +} + +export const toVisible = + ({ constants, state, props, vm, api, nextTick }) => + () => { + state.selectEmitter.emit(constants.EVENT_NAME.destroyPopper) + props.remote && props.dropOnlySearch && (state.showWarper = false) + + if (vm.$refs.input) { + vm.$refs.input.blur() + } + + state.query = '' + state.selectedLabel = '' + state.inputLength = 20 + state.previousQuery !== state.query && api.initQuery().then(() => api.setSelected()) + + state.previousQuery = null + + api.resetHoverIndex() + api.resetDatas() + + nextTick(() => { + if (vm.$refs.input && vm.$refs.input.value === '' && state.selected.length === 0) { + state.currentPlaceholder = state.cachedPlaceHolder + } + + if (vm.$refs.selectGrid) { + vm.$refs.selectGrid.clearScroll() + } + }) + + postOperOfToVisible({ props, state, constants }) + } + +export const toHide = + ({ constants, state, props, vm, api }) => + () => { + const { filterable, remote, remoteConfig, shape, multiple, valueField } = props + + state.selectEmitter.emit(constants.COMPONENT_NAME.SelectDropdown, constants.EVENT_NAME.updatePopper) + + if (filterable) { + state.query = remote || shape ? '' : state.selectedLabel + const isChange = remote && remoteConfig.autoSearch && (state.firstAutoSearch || remoteConfig.clearData) + state.firstAutoSearch = false + api.handleQueryChange(state.query, isChange) + + if (multiple) { + vm.$refs.input.focus() + } else { + if (!remote) { + state.selectEmitter.emit(constants.EVENT_NAME.queryChange, '') + + state.selectEmitter.emit(constants.COMPONENT_NAME.OptionGroup, constants.EVENT_NAME.queryChange) + } + + if (state.selectedLabel && !shape) { + state.currentPlaceholder = state.selectedLabel + state.selectedLabel = '' + } + } + } + + if (vm.$refs.selectGrid) { + let { fullData } = vm.$refs.selectGrid.getTableData() + if (multiple) { + const selectedIds = state.selected.map((sel) => sel[valueField]) + vm.$refs.selectGrid.clearSelection() + vm.$refs.selectGrid.setSelection( + fullData.filter((row) => ~selectedIds.indexOf(row[valueField])), + true + ) + } else { + vm.$refs.selectGrid.clearRadioRow() + vm.$refs.selectGrid.setRadioRow(find(fullData, (item) => props.modelValue === item[valueField])) + } + + if (filterable && typeof props.filterMethod === 'function') { + vm.$refs.selectGrid.handleTableData(true) + } else if ( + filterable && + remote && + (typeof props.remoteMethod === 'function' || typeof props.initQuery === 'function') + ) { + vm.$refs.selectGrid.handleTableData() + } + } + } + +export const watchVisible = + ({ api, constants, emit, state, vm, props }) => + (value) => { + if ((props.filterable || props.remote) && !value) { + vm.$refs.reference.blur() + } + + if (api.onCopying()) { + return + } + + if (value && props.multiple && state.device === 'mb') { + state.selectedCopy = state.selected.slice() + } + + setTimeout(() => { + if (!value && !state.selectedLabel) { + state.cachedOptions.forEach((item) => { + item.state.visible = true + }) + } + }, 200) + + value ? api.toHide() : api.toVisible() + + emit(constants.EVENT_NAME.visibleChange, value) + + setTimeout(() => { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper) + if (value && vm.$refs.scrollbar) { + if (props.optimization) { + optmzApis.setScrollTop({ refs: vm.$refs, state }) + vm.$refs.scrollbar.updateVisibleItems(true, true) + } else { + vm.$refs.scrollbar.handleScroll() + } + } + }, props.updateDelay) + + if (!value && props.shape === 'filter') { + state.softFocus = false + } + } + +export const watchOptions = + ({ api, constants, nextTick, parent, props, state }) => + () => { + if (typeof window === 'undefined') { + return + } + + nextTick(() => { + state.selectEmitter.emit(constants.EVENT_NAME.updatePopper) + }) + + if (props.multiple) { + api.resetInputHeight() + } + + nextTick(() => { + if (parent.$el.querySelector('input') !== document.activeElement) { + api.setSelected() + } + }) + + api.getOptionIndexArr() + } + +export const getOptionIndexArr = + ({ props, state, api }) => + () => { + setTimeout(() => { + state.optionIndexArr = api.queryVisibleOptions().map((item) => Number(item.getAttribute('data-index'))) + if (props.defaultFirstOption && (props.filterable || props.remote) && state.filteredOptionsCount) { + if (props.optimization) { + optmzApis.checkDefaultFirstOption({ state }) + } else { + api.checkDefaultFirstOption() + } + } + }) + } + +export const queryVisibleOptions = + ({ props, vm, isMobileFirstMode }) => + () => { + if (props.optimization) { + return optmzApis.queryVisibleOptions(vm, isMobileFirstMode) + } else { + return vm.$refs.scrollbar + ? Array.from(vm.$refs.scrollbar.$el.querySelectorAll('[data-index]:not([style*="display: none"])')) + : [] + } + } + +export const handleCopyClick = + ({ parent, props, state }) => + () => { + const input = document.createElement('input') + + input.style.height = 0 + input.style.border = 'none' + input.style.position = 'absolute' + + parent.$el.appendChild(input) + + input.value = state.selected + .map((item) => (item.state ? item.state.currentLabel : item.currentLabel)) + .join(props.textSplit) + + input.select() + document.execCommand('copy') + parent.$el.removeChild(input) + } + +export const getcheckedData = + ({ props, state }) => + () => { + const checkedKey = [] + + if (!Array.isArray(state.selected)) { + return props.modelValue ? [props.modelValue] : [state.selected[props.valueField]] + } else { + state.selected.length > 0 && + state.selected.forEach((item) => { + checkedKey.push(item[props.valueField]) + }) + + return checkedKey + } + } + +export const debouncRquest = ({ api, state, props }) => + debounce(props.delay, () => { + if (props.filterable && state.query !== state.selectedLabel) { + const isChange = false + const isInput = true + + state.query = state.selectedLabel + api.handleQueryChange(state.query, isChange, isInput) + } + }) + +export const getChildValue = () => (data, key) => { + const ids = [] + + const getChild = (nodes) => { + nodes.forEach((node) => { + ids.push(node.data[key]) + + if (node.childNodes.length > 0) { + getChild(node.childNodes) + } + }) + } + + getChild(data) + + return ids +} + +export const watchPropsOption = + ({ constants, parent, props, state }) => + () => { + const dataset = { + dataset: props.options || props.dataset, + service: parent.$service + } + getDataset(dataset).then((data) => { + let sortData = data.slice() + if (props.multiple) { + const requiredData = [] + + sortData = sortData.filter((item) => { + if (item.required) { + requiredData.push(item) + return false + } + return true + }) + + sortData = requiredData.concat(sortData) + } + if (props.cacheOp && props.cacheOp.key) { + state.memorize = new Memorize(props.cacheOp) + state.datas = state.memorize.assemble(sortData.slice()) + } else { + state.datas = sortData + state.initDatas = sortData + } + }) + } + +export const onMouseenterNative = + ({ state }) => + () => { + state.inputHovering = true + + if (state.searchSingleCopy && state.selectedLabel) { + state.softFocus = true + } + } + +export const onMouseleaveNative = + ({ state }) => + (e) => { + if (e.target === e.currentTarget) return + state.inputHovering = false + + if (state.searchSingleCopy && state.selectedLabel) { + state.softFocus = false + } + } + +export const onCopying = + ({ state, vm }) => + () => { + return ( + state.searchSingleCopy && + state.selectedLabel && + vm.$refs.reference && + vm.$refs.reference.hasSelection && + vm.$refs.reference.hasSelection() + ) + } + +export const watchHoverIndex = + ({ state }) => + (value) => { + if (value === -1 || value === -9) { + state.hoverValue = -1 + } else { + state.hoverValue = state.optionIndexArr[value] + } + } + +export const handleDropdownClick = + ({ vm, state, props, emit }) => + ($event) => { + if (props.allowCopy && vm.$refs.reference) { + vm.$refs.reference.$el.querySelector('input').selectionEnd = 0 + } + + state.softFocus = false + + emit('dropdown-click', $event) + } +export const handleEnterTag = + ({ state }) => + ($event, key) => { + const target = $event.target + if (target && target.scrollWidth > target.offsetWidth) { + state.tooltipContent = { ...state.tooltipContent, [key]: target.innerText } + } + } + +export const calcCollapseTags = + ({ state, vm, props }) => + () => { + if (state.inputHovering && !props.clickExpand) { + return (state.isHidden = true) + } + + const tags = vm.$refs.tags + const collapseTag = tags && tags.querySelector('[data-tag="tags-collapse"]') + + if (!collapseTag || !tags) { + return + } + + const { width: tagsContentWidth, paddingLeft, paddingRight } = window.getComputedStyle(tags) + const tagsWidth = parseFloat(tagsContentWidth) - parseFloat(paddingLeft) - parseFloat(paddingRight) + + const { width: collapseTagContentWidth, marginRight } = collapseTag && window.getComputedStyle(collapseTag) + const collapseTagWidth = collapseTag && parseFloat(collapseTagContentWidth) + parseFloat(marginRight) // 4为右margin值 + + const tagList = Array.from(tags.querySelectorAll('[data-tag="tiny-tag"]')) + + let [dom, idx, currentRowWidth, currentTagIndex] = [null, 0, 0, 0] + + for (let rowNum = 0; rowNum < props.maxVisibleRows; rowNum++) { + currentRowWidth = 0 + let currentTagWidth = 0 + for (currentTagIndex; currentTagIndex < tagList.length; currentTagIndex++) { + const tag = tagList[currentTagIndex] + if (tag !== collapseTag) { + const { width: tagContentWidth, marginRight, marginLeft } = tag && window.getComputedStyle(tag) + currentTagWidth = parseFloat(tagContentWidth) + parseFloat(marginRight) + parseFloat(marginLeft) + currentRowWidth += currentTagWidth + } + + // 找到第一个超出隐藏的tag + if (tag !== collapseTag && currentRowWidth > tagsWidth) { + if (!dom && rowNum === props.maxVisibleRows - 1) { + // 判断当前行能否显示折叠tag + if (currentRowWidth - currentTagWidth + collapseTagWidth < tagsWidth) { + dom = tag + idx = currentTagIndex + } else { + dom = tagList[currentTagIndex - 1] + idx = currentTagIndex - 1 + } + } + + break + } + } + + if (currentTagIndex === tagList.length - 1) { + break + } + } + + // 未超出最大显示行数 + if (idx === 0) { + state.exceedMaxVisibleRow = false + state.showCollapseTag = false + return (state.isHidden = true) + } + + if (dom) { + dom.before(collapseTag) + state.isHidden = false + } else { + state.isHidden = true + } + state.collapseTagsLength = tagList.length - idx + state.exceedMaxVisibleRow = true + state.toHideIndex = idx + } + +export const watchInputHover = + ({ vm }) => + (newVal) => { + const [inputHovering, visible] = newVal + if (!inputHovering && !visible) { + const tags = vm.$refs.tags + const content = vm.$refs['tags-content'] + tags && content && tags.scrollTo({ top: content }) + } + } + +export const initQuery = + ({ props, state, constants, vm }) => + ({ init } = {}) => { + const isRemote = + props.filterable && + props.remote && + (typeof props.remoteMethod === 'function' || typeof props.initQuery === 'function') + + let selected + if (isRemote && props.initQuery) { + let initData = props.initQuery(props.modelValue, props.extraQueryParams, !!init) + if (initData.then) { + return new Promise((resolve) => { + initData.then((selected) => { + state.remoteData = selected + vm.$refs.selectGrid.loadData(selected) + resolve(selected) + }) + }) + } + selected = initData + state.remoteData = selected + vm.$refs.selectGrid.loadData(selected) + } + + return Promise.resolve(selected) + } + +export const mounted = + ({ api, parent, state, props, vm, designConfig }) => + () => { + state.defaultCheckedKeys = state.gridCheckedData + const parentEl = parent.$el + const inputEl = parentEl.querySelector('input[data-tag="tiny-input-inner"]') + + const inputClientRect = (inputEl && inputEl.getBoundingClientRect()) || {} + + if (inputEl === document.activeElement) { + document.activeElement.blur() + } + + state.completed = true + + // tiny 新增: sizeMap适配不同主题 + const defaultSizeMap = { default: 28, mini: 24, small: 32, medium: 40 } + const sizeMap = designConfig?.state?.sizeMap || defaultSizeMap + + if (props.multiple && Array.isArray(props.modelValue) && props.modelValue.length > 0) { + state.currentPlaceholder = '' + } + + state.initialInputHeight = state.isDisplayOnly + ? sizeMap[state.selectSize || 'default'] // tiny 新增 : default, aui只处理了另3种情况,不传入时,要固定为default + : inputClientRect.height || sizeMap[state.selectSize] + + addResizeListener(parentEl, api.handleResize) + + if (vm.$refs.tags) { + addResizeListener(vm.$refs.tags, api.resetInputHeight) + } + + if (props.remote && props.multiple) { + api.resetInputHeight() + } + + state.inputWidth = inputClientRect.width + + api.initQuery({ init: true }).then(() => api.setSelected()) + + if (props.dataset) { + api.watchPropsOption() + } + } + +export const unMount = + ({ api, parent, vm, state }) => + () => { + if (parent.$el && api.handleResize) { + removeResizeListener(parent.$el, api.handleResize) + } + + if (vm.$refs.tags) { + removeResizeListener(vm.$refs.tags, api.resetInputHeight) + } + + state.popperElm = null + } + +const optmzApis = { + exist: (val, multiple) => (multiple ? Array.isArray(val) && val.length : val), + getValueIndex: (props) => { + const { options, valueField, modelValue, multiple } = props + const contain = (val, arr) => Array.isArray(arr) && ~arr.indexOf(val) + const equal = (val, opt) => (multiple ? contain(opt[valueField], [val]) : opt[valueField] === val) + let start = 0 + + if (optmzApis.exist(modelValue, multiple) && options) { + const lastVal = multiple ? modelValue[modelValue.length - 1] : modelValue + for (let i = 0; i < options.length; i++) { + if (!equal(lastVal, options[i])) continue + return i + } + } + + return start + }, + queryVisibleOptions: (vm, isMobileFirstMode) => { + const querySelectKey = isMobileFirstMode ? '.cursor-not-allowed' : '.is-disabled' + return Array.from( + vm.$refs.scrollbar.$el.querySelectorAll( + '.tiny-recycle-scroller__slot, .tiny-recycle-scroller__item-view:not([style*="transform: translateY(-9999px) translateX(0px)"])' + ) + ) + .map((item) => item.querySelector(`[data-tag="tiny-select-dropdown-item"]:not(${querySelectKey})`)) + .filter((v) => v) + }, + setScrollTop: ({ refs, state }) => { + const { optimizeStore } = state + + refs.scrollbar.scrollToItem(optimizeStore.valueIndex) + }, + setValueIndex: ({ props, state }) => { + state.optimizeStore.valueIndex = optmzApis.getValueIndex(props) + }, + natural: (val) => (val < 0 ? 0 : val), + checkDefaultFirstOption: ({ state }) => { + state.hoverIndex = 0 + state.hoverValue = state.optionIndexArr[0] + } +} + +export const computeOptimizeOpts = + ({ props, designConfig }) => + () => { + const { optimization } = props + // tiny 新增: aui 的默认值为 { optionHeight: 34, limit: 20 } + const baseOpts = designConfig?.baseOpts ? designConfig.baseOpts : { gt: 20, rSize: 10, optionHeight: 30, limit: 20 } + + let optOpts + + if (optimization) { + if (typeof optimization === 'boolean') { + optOpts = extend(true, {}, baseOpts) + } else { + optOpts = extend(true, {}, baseOpts, optimization) + } + + return optOpts + } + } + +export const watchOptimizeOpts = + ({ props, state }) => + () => { + const { optimizeOpts, optimizeStore } = state + if (optimizeOpts) { + if (props.optimization) { + optimizeStore.valueIndex = optmzApis.getValueIndex(props) + } + } + } + +export const computeCollapseTags = (props) => () => props.collapseTags + +export const computeMultipleLimit = + ({ props, state }) => + () => { + const { multipleLimit, multiple, optimization } = props + const { optimizeOpts } = state + + return optmzApis.natural(multiple && optimization ? multipleLimit || optimizeOpts.limit : multipleLimit) + } + +export const updateModelValue = + ({ props, emit, state }) => + (value, needUpdate) => { + state.isClickChoose = true + + if (state.device === 'mb' && props.multiple && !needUpdate) { + state.modelValue = value + } else { + emit('update:modelValue', value) + } + } + +export const getLabelSlotValue = + ({ props, state }) => + (item) => { + const datas = state.datas + const value = item.state ? item.state.currentValue : item.value + const data = datas.find((data) => data.value === value) + + const obj = { + ...data, + label: item.state ? item.state.currentLabel : item.currentLabel, + value + } + return obj + } + +export const computedTagsStyle = + ({ props, parent, state, vm }) => + () => { + const isReadonly = props.disabled || (parent.form || {}).disabled || props.displayOnly + let tagsStyle = { + 'max-width': isReadonly ? '' : state.inputWidth - state.inputPaddingRight + 'px', + width: '100%' + } + + // 当前全部所选项显示 + if ((props.clickExpand && !state.exceedMaxVisibleRow) || state.visible) { + Object.assign(tagsStyle, { height: 'auto' }) + } + + if (props.clickExpand && state.exceedMaxVisibleRow && !state.showCollapseTag) { + const tags = vm.$refs.tags + const { paddingTop: tagsPaddingTop, paddingBottom: tagsPaddingBottom } = window.getComputedStyle(tags) + const tagsPaddingVertical = parseFloat(tagsPaddingTop) + parseFloat(tagsPaddingBottom) + const tag = tags?.querySelector('[data-tag="tiny-tag"]') + if (tag) { + const { height: tagHeight, marginTop, marginBottom } = window.getComputedStyle(tag) + const rowHeight = + (parseFloat(tagHeight) + parseFloat(marginTop) + parseFloat(marginBottom)) * props.maxVisibleRows + Object.assign(tagsStyle, { 'height': `${rowHeight + tagsPaddingVertical}px` }) + } + } + + return tagsStyle + } + +export const computedReadonly = + ({ props, state }) => + () => + state.device === 'mb' || + props.readonly || + !props.filterable || + props.multiple || + (browserInfo.name !== BROWSER_NAME.IE && browserInfo.name !== BROWSER_NAME.Edge && !state.visible) + +export const computedShowClose = + ({ props, state }) => + () => + props.clearable && + !state.selectDisabled && + (state.inputHovering || (props.multiple && state.visible)) && + (props.multiple + ? Array.isArray(props.modelValue) && props.modelValue.length > 0 + : !isNull(props.modelValue) && props.modelValue !== '') + +// tiny 新增: aui有自己的逻辑,移至defineConfig中去了 +export const computedCollapseTagSize = (state) => () => state.selectSize + +export const computedShowNewOption = + ({ props, state }) => + () => { + const query = state.device === 'mb' ? state.queryValue : state.query + return ( + props.filterable && + props.allowCreate && + query && + !state.options.filter((option) => !option.created).some((option) => option.state.currentLabel === state.query) + ) + } + +export const computedShowCopy = + ({ props, state }) => + () => + props.multiple && props.copyable && state.inputHovering && state.selected.length + +export const computedOptionsAllDisabled = (state) => () => + state.options.filter((option) => option.visible).every((option) => option.disabled) + +export const computedDisabledTooltipContent = (state) => () => + state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join(';') + +export const computedSelectDisabled = + ({ props, parent }) => + () => + props.disabled || (parent.form || {}).disabled || props.displayOnly || (parent.form || {}).displayOnly + +export const computedIsExpand = + ({ props, state }) => + () => { + const hoverExpanded = (state.selectHover || state.visible) && props.hoverExpand && !props.disabled + const clickExpanded = props.clickExpand && state.exceedMaxVisibleRow && state.showCollapseTag + return hoverExpanded || clickExpanded + } + +export const watchInitValue = + ({ props, emit }) => + (value) => { + if (props.multiple) { + let modelValue = props.modelValue.slice() + + value.forEach((val) => { + modelValue = modelValue.filter((item) => item !== val) + }) + + emit('update:modelValue', value.concat(modelValue)) + } + } + +export const watchShowClose = + ({ nextTick, state, parent }) => + () => { + nextTick(() => { + const parentEl = parent.$el + const inputEl = parentEl.querySelector('input[data-tag="tiny-input-inner"]') + + if (inputEl) { + const { paddingRight } = getComputedStyle(inputEl) + + state.inputPaddingRight = parseFloat(paddingRight) + } + }) + } + +// 以下为tiny 新增功能 +export const computedGetIcon = + ({ designConfig, props }) => + () => { + return props.dropdownIcon + ? { icon: props.dropdownIcon } + : { + icon: designConfig?.icons.dropdownIcon || 'icon-delta-down', + isDefault: true + } + } + +export const computedGetTagType = + ({ designConfig, props }) => + () => { + if (designConfig?.props?.tagType && !props.tagType) { + return designConfig.props.tagType + } + return props.tagType + } +export const clearSearchText = + ({ state, api }) => + () => { + state.query = '' + state.previousQuery = undefined + api.handleQueryChange(state.query) + } +export const clearNoMatchValue = + ({ props, emit }) => + (newModelValue) => { + if (!props.clearNoMatchValue) { + return + } + + if ( + (props.multiple && props.modelValue.length !== newModelValue.length) || + (!props.multiple && props.modelValue !== newModelValue) + ) { + emit('update:modelValue', newModelValue) + } + } + +// 解决无界时,event.target 会变为 wujie_iframe的元素的bug +export const handleDebouncedQueryChange = ({ state, api }) => + debounce(state.debounce, (value) => { + api.handleQueryChange(value) + }) + +export const onClickCollapseTag = + ({ state, props, nextTick, api }) => + (event: MouseEvent) => { + event.stopPropagation() + if (props.clickExpand && !props.disabled && !state.isDisplayOnly) { + state.showCollapseTag = !state.showCollapseTag + + nextTick(api.resetInputHeight) + } + } diff --git a/packages/renderless/src/base-select/vue.ts b/packages/renderless/src/base-select/vue.ts new file mode 100644 index 0000000000..0162dc0242 --- /dev/null +++ b/packages/renderless/src/base-select/vue.ts @@ -0,0 +1,587 @@ +import { + debouncRquest, + getChildValue, + getcheckedData, + calcOverFlow, + toggleCheckAll, + handleCopyClick, + showTip, + handleComposition, + handleQueryChange, + scrollToOption, + handleMenuEnter, + emitChange, + directEmitChange, + getOption, + getSelectedOption, + setSelected, + handleFocus, + focus, + blur, + handleBlur, + handleClearClick, + doDestroy, + handleClose, + toggleLastOptionHitState, + deletePrevTag, + managePlaceholder, + resetInputState, + resetInputHeight, + resetHoverIndex, + resetDatas, + handleOptionSelect, + setSoftFocus, + getValueIndex, + toggleMenu, + selectOption, + deleteSelected, + deleteTag, + onInputChange, + onOptionDestroy, + resetInputWidth, + handleResize, + checkDefaultFirstOption, + setOptionHighlight, + getValueKey, + emptyText, + emptyFlag, + recycleScrollerHeight, + watchValue, + watchVisible, + watchOptions, + navigateOptions, + getPluginOption, + watchPropsOption, + onMouseenterNative, + onMouseleaveNative, + onCopying, + gridOnQueryChange, + defaultOnQueryChange, + queryChange, + toVisible, + toHide, + mounted, + unMount, + watchHoverIndex, + computeOptimizeOpts, + watchOptimizeOpts, + computeCollapseTags, + computeMultipleLimit, + handleDropdownClick, + handleEnterTag, + calcCollapseTags, + initValue, + watchInputHover, + initQuery, + updateModelValue, + getLabelSlotValue, + computedTagsStyle, + computedReadonly, + computedShowClose, + computedCollapseTagSize, + computedShowNewOption, + computedShowCopy, + computedOptionsAllDisabled, + computedDisabledTooltipContent, + computedSelectDisabled, + watchInitValue, + watchShowClose, + getOptionIndexArr, + queryVisibleOptions, + // tiny 新增 + computedGetIcon, + computedGetTagType, + clearSearchText, + clearNoMatchValue, + handleDebouncedQueryChange, + onClickCollapseTag, + computedIsExpand +} from './index' +import debounce from '../common/deps/debounce' +import { isNumber } from '../common/type' + +export const api = [ + 'state', + 'toggleCheckAll', + 'handleCopyClick', + 'focus', + 'blur', + 'showTip', + 'doDestroy', + 'getOption', + 'emitChange', + 'handleBlur', + 'toggleMenu', + 'getValueKey', + 'handleFocus', + 'handleClose', + 'setSoftFocus', + 'getValueIndex', + 'scrollToOption', + 'resetHoverIndex', + 'onOptionDestroy', + 'resetInputWidth', + 'resetInputHeight', + 'managePlaceholder', + 'checkDefaultFirstOption', + 'setOptionHighlight', + 'toggleLastOptionHitState', + 'deleteTag', + 'setSelected', + 'selectOption', + 'handleResize', + 'deletePrevTag', + 'onInputChange', + 'deleteSelected', + 'handleMenuEnter', + 'resetInputState', + 'handleClearClick', + 'handleComposition', + 'handleQueryChange', + 'handleOptionSelect', + 'debouncedOnInputChange', + 'debouncedQueryChange', + 'navigateOptions', + 'onMouseenterNative', + 'onMouseleaveNative', + 'onCopying', + 'handleDropdownClick', + 'handleEnterTag', + 'getLabelSlotValue', + 'updateModelValue', + 'clearSearchText', + 'onClickCollapseTag' +] + +const initState = ({ reactive, computed, props, api, emitter, parent, constants, useBreakpoint, vm, designConfig }) => { + const stateAdd = initStateAdd({ computed, props, api, parent }) + const state = reactive({ + ...stateAdd, + selectEmitter: emitter(), + datas: [], + initDatas: [], + query: '', + magicKey: 0, + options: [], + visible: false, + showCopy: computed(() => api.computedShowCopy()), + showWarper: true, // 显示下拉外层控制 + selected: props.multiple ? [] : {}, + softFocus: false, + hover: false, + triggerSearch: false, + firstAutoSearch: props.remoteConfig.autoSearch, + tagsStyle: computed(() => api.computedTagsStyle()), + readonly: computed(() => api.computedReadonly()), + iconClass: computed(() => (state.visible ? '' : constants.CLASS.IsReverse)), + showClose: computed(() => api.computedShowClose()), + optionsAllDisabled: computed(() => api.computedOptionsAllDisabled()), + collapseTagSize: computed(() => api.computedCollapseTagSize()), + showNewOption: computed(() => api.computedShowNewOption()), + selectSize: computed(() => props.size || state.formItemSize), + optimizeOpts: computed(() => api.computeOptimizeOpts()), + optimizeStore: { valueIndex: 0, recycleScrollerHeight: computed(() => api.recycleScrollerHeight()) }, + + collapseTags: computed(() => api.computeCollapseTags()), + multipleLimit: computed(() => api.computeMultipleLimit()), + disabledTooltipContent: computed(() => api.computedDisabledTooltipContent()), + isExpand: computed(() => api.computedIsExpand()), + collapseTagsLength: 0, + initValue: [], + key: 0, + device: '', + timer: null, + modelValue: [], + queryValue: '', + selectedCopy: [], + compareValue: null, + selectedVal: computed(() => + state.device === 'mb' && props.multiple && state.visible ? state.selectedCopy : state.selected + ), + displayOnlyContent: computed(() => + props.multiple && Array.isArray(state.selected) + ? state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join('; ') + : '' + ), + breakpoint: useBreakpoint ? useBreakpoint().current : '', + isSaaSTheme: vm.theme === 'saas', + disabledOptionHover: false, + hasClearSelection: false, + // tiny 新增 + getIcon: computed(() => api.computedGetIcon()), + getTagType: computed(() => api.computedGetTagType()), + isSelectAll: computed(() => state.selectCls === 'checked-sur'), + autoHideDownIcon: (() => { + if (designConfig?.state && 'autoHideDownIcon' in designConfig.state) { + return designConfig.state.autoHideDownIcon + } + return true // tiny 默认为true + })() + }) + + return state +} + +const initStateAdd = ({ computed, props, api, parent }) => { + return { + selectedTags: [], + tips: '', + showTip: false, + tipHover: false, + selectHover: false, + tipTimer: null, + selectCls: 'checked-sur', + filteredSelectCls: 'checked-sur', + overflow: null, + completed: false, + inputWidth: 0, + inputPaddingRight: 0, + hoverIndex: -1, + hoverValue: -1, + optionsIndex: -1, + inputLength: 20, + optionsCount: 0, + selectFiexd: {}, + createdLabel: null, + isSilentBlur: false, + cachedOptions: [], + selectedLabel: '', + previousQuery: null, + inputHovering: false, + createdSelected: false, + isOnComposition: false, + cachedPlaceHolder: props.placeholder, + inputHeight: 0, + initialInputHeight: 0, + currentPlaceholder: props.placeholder, + filteredOptionsCount: 0, + gridData: [], + treeData: [], + remoteData: [], + currentKey: props.modelValue, + updateId: '', + popperElm: null, + debounce: computed(() => (isNumber(props.queryDebounce) ? props.queryDebounce : props.remote ? 300 : 0)), + emptyText: computed(() => api.emptyText()), + emptyFlag: computed(() => api.emptyFlag()), + formItemSize: computed(() => (parent.formItem || { state: {} }).state.formItemSize), + selectDisabled: computed(() => api.computedSelectDisabled()), + isDisplayOnly: computed(() => props.displayOnly || (parent.form || {}).displayOnly), + gridCheckedData: computed(() => api.getcheckedData()), + searchSingleCopy: computed(() => props.allowCopy && !props.multiple && props.filterable), + childrenName: computed(() => 'children'), + tooltipContent: {}, + isHidden: false, + defaultCheckedKeys: [], + optionIndexArr: [], + showCollapseTag: false, + exceedMaxVisibleRow: false, // 是否超出默认最大显示行数 + toHideIndex: Infinity // 第一个超出被隐藏的索引 + } +} + +const initApi = ({ + api, + props, + state, + emit, + maskState, + constants, + parent, + nextTick, + dispatch, + t, + vm, + isMobileFirstMode, + designConfig +}) => { + Object.assign(api, { + state, + maskState, + doDestroy: doDestroy(vm), + blur: blur({ vm, state }), + focus: focus({ vm, state }), + getValueKey: getValueKey(props), + handleClose: handleClose(state), + getValueIndex: getValueIndex(props), + getChildValue: getChildValue(), + getOption: getOption({ props, state, api }), + getSelectedOption: getSelectedOption({ props, state }), + emitChange: emitChange({ emit, props, state, constants }), + directEmitChange: directEmitChange({ emit, props, state, constants }), + toggleMenu: toggleMenu({ vm, state, props, api, isMobileFirstMode }), + showTip: showTip({ props, state, vm }), + onOptionDestroy: onOptionDestroy(state), + setSoftFocus: setSoftFocus({ vm, state }), + getcheckedData: getcheckedData({ props, state }), + resetInputWidth: resetInputWidth({ vm, state }), + resetHoverIndex: resetHoverIndex({ props, state }), + resetDatas: resetDatas({ props, state }), + scrollToOption: scrollToOption({ vm, constants }), + handleCopyClick: handleCopyClick({ parent, props, state }), + managePlaceholder: managePlaceholder({ vm, state }), + checkDefaultFirstOption: checkDefaultFirstOption(state), + + setOptionHighlight: setOptionHighlight(state), + handleBlur: handleBlur({ constants, dispatch, emit, state, designConfig }), + toggleLastOptionHitState: toggleLastOptionHitState({ state }), + emptyText: emptyText({ I18N: constants.I18N, props, state, t, isMobileFirstMode }), + emptyFlag: emptyFlag({ props, state }), + getOptionIndexArr: getOptionIndexArr({ props, state, api }), + queryVisibleOptions: queryVisibleOptions({ props, vm, isMobileFirstMode }), + recycleScrollerHeight: recycleScrollerHeight({ state, props, recycle: constants.RECYCLE }), + watchPropsOption: watchPropsOption({ constants, parent, props, state }), + onMouseenterNative: onMouseenterNative({ state }), + onMouseleaveNative: onMouseleaveNative({ state }), + onCopying: onCopying({ state, vm }), + gridOnQueryChange: gridOnQueryChange({ props, vm, constants, state }), + watchHoverIndex: watchHoverIndex({ state }), + computeOptimizeOpts: computeOptimizeOpts({ props, designConfig }), + computeCollapseTags: computeCollapseTags(props), + computeMultipleLimit: computeMultipleLimit({ props, state }), + watchInputHover: watchInputHover({ vm }), + initQuery: initQuery({ props, state, constants, vm }), + updateModelValue: updateModelValue({ props, emit, state }), + computedTagsStyle: computedTagsStyle({ props, parent, state, vm }), + computedReadonly: computedReadonly({ props, state }), + computedShowClose: computedShowClose({ props, state }), + computedCollapseTagSize: computedCollapseTagSize(state), + computedShowNewOption: computedShowNewOption({ props, state }), + computedShowCopy: computedShowCopy({ props, state }), + computedOptionsAllDisabled: computedOptionsAllDisabled(state), + computedDisabledTooltipContent: computedDisabledTooltipContent(state), + + computedSelectDisabled: computedSelectDisabled({ props, parent }), + computedIsExpand: computedIsExpand({ props, state }), + watchInitValue: watchInitValue({ props, emit }), + watchShowClose: watchShowClose({ nextTick, state, parent }), + // tiny 新增 + computedGetIcon: computedGetIcon({ designConfig, props }), + computedGetTagType: computedGetTagType({ designConfig, props }), + clearSearchText: clearSearchText({ state, api }), + clearNoMatchValue: clearNoMatchValue({ props, emit }) + }) + + addApi({ api, props, state, emit, constants, parent, nextTick, dispatch, vm, isMobileFirstMode, designConfig }) +} + +const addApi = ({ + api, + props, + state, + emit, + constants, + parent, + nextTick, + dispatch, + vm, + isMobileFirstMode, + designConfig +}) => { + Object.assign(api, { + resetInputHeight: resetInputHeight({ api, constants, nextTick, props, vm, state, designConfig }), + calcOverFlow: calcOverFlow({ vm, props, state }), + handleFocus: handleFocus({ api, emit, props, state }), + deleteTag: deleteTag({ api, constants, emit, props, vm, nextTick, state }), + watchValue: watchValue({ api, constants, dispatch, props, vm, state }), + toHide: toHide({ constants, state, props, vm, api }), + toVisible: toVisible({ constants, state, props, vm, api, nextTick }), + setSelected: setSelected({ api, constants, nextTick, props, vm, state }), + selectOption: selectOption({ api, state, props }), + handleResize: handleResize({ api, props, state }), + watchOptions: watchOptions({ api, constants, nextTick, parent, props, state, vm }), + watchVisible: watchVisible({ api, constants, emit, state, vm, props, isMobileFirstMode }), + deletePrevTag: deletePrevTag({ api, constants, props, state, vm }), + onInputChange: onInputChange({ api, props, state, constants, nextTick }), + deleteSelected: deleteSelected({ api, constants, emit, props, vm, state }), + handleMenuEnter: handleMenuEnter({ api, nextTick, state, props }), + resetInputState: resetInputState({ api, vm, state }), + navigateOptions: navigateOptions({ api, state, props, nextTick }), + handleClearClick: handleClearClick(api), + handleComposition: handleComposition({ api, nextTick, state }), + handleQueryChange: handleQueryChange({ api, constants, nextTick, props, vm, state }), + handleOptionSelect: handleOptionSelect({ api, nextTick, props, vm, state }), + getPluginOption: getPluginOption({ api, props, state }), + toggleCheckAll: toggleCheckAll({ api, emit, state, props }), + handleDebouncedQueryChange: handleDebouncedQueryChange({ state, api }), + debouncedQueryChange: (event) => { + // 解决无界下的异常 + const value = props.shape ? event : event.target.value + api.handleDebouncedQueryChange(value) + }, + debouncedOnInputChange: debounce(state.debounce, () => { + api.onInputChange() + }), + debouncRquest: debouncRquest({ api, state, props }), + defaultOnQueryChange: defaultOnQueryChange({ props, state, constants, api, nextTick, vm }), + queryChange: queryChange({ props, state, constants, api, nextTick, vm }), + mounted: mounted({ api, parent, state, props, vm, designConfig }), + unMount: unMount({ api, parent, vm, state }), + watchOptimizeOpts: watchOptimizeOpts({ props, state }), + handleDropdownClick: handleDropdownClick({ props, vm, state, emit }), + handleEnterTag: handleEnterTag({ state }), + calcCollapseTags: calcCollapseTags({ state, vm, props }), + initValue: initValue({ state }), + getLabelSlotValue: getLabelSlotValue({ props, state }), + onClickCollapseTag: onClickCollapseTag({ state, props, nextTick, api }) + }) +} + +const initWatch = ({ watch, props, api, state, nextTick }) => { + watch( + () => state.selectDisabled, + () => nextTick(api.resetInputHeight) + ) + + watch( + () => props.placeholder, + (value) => { + state.cachedPlaceHolder = state.currentPlaceholder = value + } + ) + + watch( + () => props.modelValue, + () => { + if (props.multiple && Array.isArray(props.modelValue)) { + state.modelValue = [...props.modelValue] + } else { + state.modelValue = props.modelValue + } + }, + { immediate: true, deep: true } + ) + + watch(() => state.modelValue, api.watchValue) + + watch( + () => state.selectedLabel, + () => { + if (props.trim) { + state.selectedLabel = state.selectedLabel.trim() + } + } + ) + + watch( + () => props.extraQueryParams, + () => api.handleQueryChange(state.previousQuery, true), + { deep: true } + ) + + watch( + () => state.breakpoint, + (val) => { + if (val === 'default') { + state.device = 'mb' + } else { + state.device = 'pc' + } + }, + { immediate: true, deep: true } + ) + + watch( + () => state.device, + (newVal, oldVal) => { + if (oldVal !== '' && state.visible) { + api.updateModelValue(state.modelValue, true) + } + } + ) + + watch(() => state.visible, api.watchVisible) + + watch(() => state.initValue, api.watchInitValue, { deep: true }) + + addWatch({ watch, props, api, state, nextTick }) +} + +const addWatch = ({ watch, props, api, state, nextTick }) => { + watch(() => [...state.options], api.watchOptions) + + watch(() => state.hoverIndex, api.watchHoverIndex) + + props.options && watch(() => props.options, api.watchPropsOption, { immediate: true, deep: true }) + + props.optimization && watch(() => state.optimizeOpts, api.watchOptimizeOpts, { immediate: true }) + + watch([() => state.inputHovering, () => state.visible], api.watchInputHover) + + watch(() => state.showClose, api.watchShowClose, { immediate: true }) + + watch( + () => state.selectHover, + () => props.hoverExpand && !props.disabled && !state.isDisplayOnly && nextTick(api.resetInputHeight) + ) +} + +export const renderless = ( + props, + { computed, onBeforeUnmount, onMounted, reactive, watch, provide, inject }, + { vm, parent, emit, constants, nextTick, dispatch, t, emitter, isMobileFirstMode, useBreakpoint, designConfig } +) => { + const api: any = {} + const state = initState({ + reactive, + computed, + props, + api, + emitter, + parent, + constants, + useBreakpoint, + vm, + designConfig + }) + const dialog = inject('dialog', null) + + provide('selectEmitter', state.selectEmitter) + provide('selectVm', vm) + + const maskState = reactive({ width: '', height: '', top: '' }) + + initApi({ + api, + props, + state, + emit, + maskState, + constants, + parent, + nextTick, + dispatch, + t, + vm, + isMobileFirstMode, + designConfig + }) + + parent.$on('handle-clear', (event) => { + api.handleClearClick(event) + }) + + if (props.multiple && !Array.isArray(props.modelValue)) { + emit('update:modelValue', []) + } + + if (!props.multiple && Array.isArray(props.modelValue)) { + emit('update:modelValue', '') + } + + dialog && dialog.state.emitter.on('handleSelectClose', api.handleClose) + state.selectEmitter.on(constants.EVENT_NAME.handleOptionClick, api.handleOptionSelect) + state.selectEmitter.on(constants.EVENT_NAME.setSelected, api.setSelected) + state.selectEmitter.on(constants.EVENT_NAME.initValue, api.initValue) + + initWatch({ watch, props, api, state, nextTick }) + + onMounted(api.mounted) + + onBeforeUnmount(() => { + api.unMount() + dialog && dialog.state.emitter.off('handleSelectClose', api.handleClose) + }) + + return api +} diff --git a/packages/theme/src/base-select/aurora-theme.js b/packages/theme/src/base-select/aurora-theme.js new file mode 100644 index 0000000000..df0f8e0d6d --- /dev/null +++ b/packages/theme/src/base-select/aurora-theme.js @@ -0,0 +1,13 @@ +export const tinySelectAuroraTheme = { + 'ti-select-input-caret-font-size': 'var(--ti-common-font-size-1)', + 'ti-select-input-caret-icon-color': 'var(--ti-common-color-icon-normal)', + 'ti-select-tags-margin-top': '2px', + 'ti-select-tags-margin-right': '4px', + 'ti-select-tags-margin-bottom': '2px', + 'ti-select-tags-margin-left': 'var(--ti-common-space-0)', + 'ti-select-tags-wrap-padding-left': '8px', + 'ti-select-input-icon-close-margin-right': 'var(--ti-common-space-2x)', + 'ti-select-tags-height': '28px', + 'ti-select-input-icon-top': '50%', + 'ti-select-suffix-display': 'none' +} diff --git a/packages/theme/src/base-select/index.less b/packages/theme/src/base-select/index.less new file mode 100644 index 0000000000..7e1508e99a --- /dev/null +++ b/packages/theme/src/base-select/index.less @@ -0,0 +1,442 @@ +/** +* Copyright (c) 2022 - present TinyVue Authors. +* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ + +@import '../custom.less'; +@import './vars.less'; + +@base-select-prefix-cls: ~'@{css-prefix}base-select'; +@input-prefix-cls: ~'@{css-prefix}input'; +@tag-prefix-cls: ~'@{css-prefix}tag'; + +.@{base-select-prefix-cls} { + .component-css-vars-select(); + + display: inline-block; + position: relative; + width: 100%; + outline: 0; + + &&__multiple:not(&__collapse-tags):not(&__filterable) &__tags > span { + // 兼容ie10-ie11 + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + } + + // 兼容edge + @supports (-ms-ime-align: auto) { + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + } + } + + &&__collapse-tags { + .@{base-select-prefix-cls}__tags { + & > span { + display: flex; + width: 100%; + + & > span { + display: flex; + } + + & > span:not(:only-child):first-child { + max-width: 70%; + } + } + } + + &.@{base-select-prefix-cls}__filterable { + .@{base-select-prefix-cls}__tags { + & > span { + width: auto; + max-width: 76%; + + & > span:first-child { + flex: 1; + + // 兼容ie10-ie11 + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + flex-basis: auto; + } + } + + & > span:only-child, + & > span:not(:only-child):first-child { + max-width: 100%; + } + + & > span:not(:only-child):not(:first-child) { + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + // 兼容ie10-ie11 + flex-shrink: 0; + flex-basis: auto; + } + } + } + } + } + } + + &&__filterable { + .@{base-select-prefix-cls}__tags { + .@{base-select-prefix-cls}__input { + cursor: text; + } + } + } + + &__tags { + position: absolute; + line-height: normal; + white-space: normal; + padding-left: var(--ti-select-tags-wrap-padding-left); + padding-right: var(--ti-select-tags-wrap-padding-right); + padding-bottom: var(--ti-select-tags-wrap-padding-bottom); + padding-top: var(--ti-select-tags-wrap-padding-top); + z-index: 1; + top: 50%; + margin-left: 1px; + transform: translateY(-50%); + display: flex; + align-items: center; + flex-wrap: wrap; + cursor: pointer; + + & > span { + display: contents; + + > span { + font-size: 0; + } + } + + .not-visible { + visibility: hidden; + } + + /* 搜索框 */ + .@{base-select-prefix-cls}__input { + cursor: pointer; + border: none; + outline: 0; + padding: 0; + margin-left: 8px; + color: var(--ti-select-input-text-color); + font-size: var(--ti-select-input-font-size); + height: var(--ti-select-input-height); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: transparent; + + &.is-mini { + height: var(--ti-select-input-mini-height); + } + + &.is-small { + height: var(--ti-select-input-small-height); + } + + &.is-medium { + height: var(--ti-select-input-medium-height); + } + } + + &.is-showicon { + padding-left: 24px; + } + + .@{tag-prefix-cls} { + white-space: nowrap; + box-sizing: border-box; + border-color: transparent; + margin: var(--ti-select-tags-margin-top) var(--ti-select-tags-margin-right) var(--ti-select-tags-margin-bottom) + var(--ti-select-tags-margin-left); + text-overflow: ellipsis; + overflow: hidden; + display: inline-flex; + justify-content: flex-start; + align-items: center; + max-width: 160px; + } + + .@{base-select-prefix-cls}__tags-text { + display: inline-block; + width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + vertical-align: bottom; + + & + .@{tag-prefix-cls}__close { + flex-shrink: 0; + } + + &.is-disabled { + max-height: 24px; + display: inline-flex; + + > span { + color: red; + font-size: var(--ti-tag-font-size); + margin: var(--ti-select-tags-margin-top) var(--ti-select-tags-margin-right) + var(--ti-select-tags-margin-bottom) var(--ti-select-tags-margin-left); + display: inline-block; + width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + // 收起按钮 + .@{base-select-prefix-cls}__collapse-text { + display: inline-flex; + align-items: center; + margin-left: var(--ti-select-tags-margin-left); + font-size: var(--ti-select-collapse-button-font-size); + color: var(--ti-select-collapse-button-text-icon-color); + + .tiny-svg { + margin-left: var(--ti-select-collapse-button-icon-margin-left); + fill: var(--ti-select-collapse-button-text-icon-color); + } + } + } + + &.is-hover-expand, + &.is-click-expand { + vertical-align: top; + + .@{base-select-prefix-cls}__tags-group { + position: absolute; + top: 0; + left: 0; + right: 0; + } + + &.is-hover { + .is-expand { + z-index: 2; + } + } + + .@{base-select-prefix-cls}__tags { + height: var(--ti-select-tags-height); + overflow: hidden; + + &-collapse { + visibility: visible; + position: static; + } + + .is-hidden { + visibility: hidden; + position: absolute; + } + + > span > span { + width: 100%; + display: flex; + flex-wrap: wrap; + } + + .hidden { + visibility: hidden; + } + } + + &.is-hover, + &.collapse-tag-clicked { + .@{base-select-prefix-cls}__tags { + height: auto; + max-height: var(--ti-select-tags-max-height); + overflow-y: auto; + + &-collapse { + visibility: hidden; + position: absolute; + + &.is-hidden { + margin: 0; + } + } + + &.not-selected { + overflow-y: hidden; + } + } + } + + & .is-expand { + position: absolute; + width: 100%; + } + + &.@{base-select-prefix-cls}--small { + .@{base-select-prefix-cls}__tags { + padding-top: 2px; + } + } + + &.@{base-select-prefix-cls}--mini { + .@{base-select-prefix-cls}__tags { + padding-top: 2px; + } + } + } + + &.is-hover-expand.is-disabled { + .@{base-select-prefix-cls}__tags { + height: auto; + } + } + + &.is-click-expand .@{base-select-prefix-cls}__tags-collapse { + visibility: visible; + position: static; + + &.is-hidden { + visibility: hidden; + position: absolute; + } + } + + &.is-disabled { + cursor: not-allowed; + + .@{input-prefix-cls} { + &__inner { + padding-right: 12px; + } + + &__suffix { + display: var(--ti-select-suffix-display); + } + } + + .@{base-select-prefix-cls}__tags { + padding-right: 16px; + } + } + + &-tip &-tipcontent { + max-width: 300px; + } + + & .@{input-prefix-cls} { + display: block; + + .@{input-prefix-cls}__inner[readonly] { + cursor: pointer; + } + + .@{base-select-prefix-cls}__caret { + fill: var(--ti-select-input-caret-icon-color); + font-size: var(--ti-select-input-caret-font-size); + transition: transform 0.3s; + transform: rotateZ(180deg); + cursor: pointer; + + &.@{base-select-prefix-cls}__close { + margin-right: var(--ti-select-input-icon-close-margin-right); + } + + &:hover { + fill: var(--ti-select-input-caret-hover-icon-color); + } + + &.is-reverse { + transform: rotateZ(0); + } + } + + .@{base-select-prefix-cls}__limit-txt, .@{base-select-prefix-cls}__proportion-txt { + font-size: var(--ti-select-suffix-font-size); + line-height: 1; + margin: 0 var(--ti-select-suffix-icon-margin-right); + } + + .@{base-select-prefix-cls}__copy { + cursor: pointer; + margin-right: var(--ti-select-suffix-icon-margin-right); + } + + .@{input-prefix-cls}__suffix { + top: var(--ti-select-input-icon-top); + + &-inner { + font-size: 0; + } + } + + & .@{input-prefix-cls}__suffix-inner { + overflow: hidden; + } + + &-medium .@{input-prefix-cls}__suffix { + top: var(--ti-select-input-icon-top-medium); + } + + &-small .@{input-prefix-cls}__suffix { + top: var(--ti-select-input-icon-top-small); + } + + &-mini .@{input-prefix-cls}__suffix { + top: var(--ti-select-input-icon-top-mini); + } + + &.is-disabled { + .@{base-select-prefix-cls}__caret { + fill: var(--ti-select-input-disabled-caret-text-color); + cursor: not-allowed; + } + + .@{input-prefix-cls}__inner { + cursor: not-allowed; + + &:hover { + border-color: var(--ti-select-input-disabled-hover-border-color); + } + } + } + + &.is-focus .@{input-prefix-cls}__inner { + border-color: var(--ti-select-inner-border-color-active); + } + } + + &__underline { + .@{input-prefix-cls}, + .@{input-prefix-cls}.is-disabled { + .@{input-prefix-cls}__inner { + border-radius: 0; + border-top-width: 0px; + border-left-width: 0px; + border-right-width: 0px; + padding-left: 0px; + background-color: var(--ti-input-bg-color); + } + + &__suffix { + right: 0px; + } + } + } +} diff --git a/packages/theme/src/base-select/smb-theme.js b/packages/theme/src/base-select/smb-theme.js new file mode 100644 index 0000000000..9f68d2d205 --- /dev/null +++ b/packages/theme/src/base-select/smb-theme.js @@ -0,0 +1,18 @@ +export const tinySelectSmbTheme = { + 'ti-select-input-caret-font-size': 'var(--ti-common-font-size-2)', + 'ti-select-input-caret-hover-icon-color': 'var(--ti-common-color-icon-graybg-hover)', + 'ti-select-input-caret-icon-color': 'var(--ti-common-color-icon-normal)', + 'ti-select-tags-height': 'var(--ti-common-space-8x)', + 'ti-select-tags-wrap-padding-left': 'var(--ti-common-space-2)', + 'ti-select-tags-wrap-padding-top': 'var(--ti-common-space-2)', + 'ti-select-tags-wrap-padding-bottom': 'var(--ti-common-space-2)', + 'ti-select-input-icon-top-small': 'var(--ti-common-space-4x)', + 'ti-select-input-icon-top': 'var(--ti-common-space-4x)', + 'ti-select-tags-margin-left': 'var(--ti-common-space-2)', + 'ti-select-tags-margin-right': 'var(--ti-common-space-2)', + 'ti-select-tags-margin-top': 'var(--ti-common-space-2)', + 'ti-select-tags-margin-bottom': 'var(--ti-common-space-2)', + 'ti-select-tags-max-height': 'none', + 'ti-select-collapse-button-text-icon-color': 'var(--ti-common-color-text-link)', + 'ti-select-input-icon-top-mini': 'var(--ti-common-space-4x)' +} diff --git a/packages/theme/src/base-select/vars.less b/packages/theme/src/base-select/vars.less new file mode 100644 index 0000000000..3290ca4816 --- /dev/null +++ b/packages/theme/src/base-select/vars.less @@ -0,0 +1,80 @@ +/** +* Copyright (c) 2022 - present TinyVue Authors. +* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ + +.component-css-vars-select() { + // 输入框悬浮时的边框色(hide) + --ti-select-inner-border-color-active: var(--ti-common-color-line-active, #5e7ce0); + // 可搜索输入框文本色 + --ti-select-input-text-color: var(--ti-common-color-text-primary, #252b3a); + // 选择器输入框字号(hide) + --ti-select-input-font-size: var(--ti-common-font-size-base, 12px); + // 选择器尾部图标距离输入框的垂直距离 + --ti-select-input-icon-top: 14px; + // 中型选择器尾部图标距离输入框的垂直距离 + --ti-select-input-icon-top-medium: var(--ti-common-space-5x, 20px); + // 小型选择器尾部图标距离输入框的垂直距离 + --ti-select-input-icon-top-small: var(--ti-common-space-4x, 16px); + // 迷你型选择器尾部图标距离输入框的垂直距离 + --ti-select-input-icon-top-mini: var(--ti-common-space-3x, 12px); + // 选择器输入框尾部图标的颜色 + --ti-select-input-caret-icon-color: var(--ti-common-color-text-secondary, #575d6c); + // 选择器输入框尾部图标悬浮时的颜色 + --ti-select-input-caret-hover-icon-color: var(--ti-common-color-icon-hover, #5e7ce0); + // 选择器输入框尾部图标的尺寸 + --ti-select-input-caret-font-size: 10px; + // 选择器输入框尾部关闭图标右侧外边距 + --ti-select-input-icon-close-margin-right: var(--ti-common-space-0, 0px); + // 选择器输入框尾部图标禁用时的颜色(hide) + --ti-select-input-disabled-caret-text-color: var(--ti-common-color-text-disabled, #adb0b8); + // 输入框禁用且悬浮时的边框色 + --ti-select-input-disabled-hover-border-color: var(--ti-common-color-line-disabled, #dfe1e6); + // 选择器输入框高度(hide) + --ti-select-input-height: var(--ti-common-size-6x, 24px); + // 迷你型选择器输入框高度(hide) + --ti-select-input-mini-height: var(--ti-common-size-6x, 24px); + // 小型选择器输入框高度(hide) + --ti-select-input-small-height: var(--ti-common-size-7x, 28px); + // 中型选择器输入框高度(hide) + --ti-select-input-medium-height: var(--ti-common-size-42); + // 选择器多选标签容器的左侧内边距 + --ti-select-tags-wrap-padding-left: calc(var(--ti-common-space-1, 1px) * 2); + // 选择器多选标签容器的顶部内边距 + --ti-select-tags-wrap-padding-top: calc(var(--ti-common-space-1, 1px) * 2); + // 选择器多选标签容器的底部内边距 + --ti-select-tags-wrap-padding-bottom: calc(var(--ti-common-space-1, 1px) * 2); + // 选择器多选标签容器的右侧内边距 + --ti-select-tags-wrap-padding-right: var(--ti-common-space-0, 0px); + // 选择器多选标签顶部外边距 + --ti-select-tags-margin-top: var(--ti-common-space-1, 1px); + // 选择器多选标签右侧外边距 + --ti-select-tags-margin-right: var(--ti-common-space-1, 1px); + // 选择器多选标签底部外边距 + --ti-select-tags-margin-bottom: var(--ti-common-space-1, 1px); + // 选择器多选标签左侧外边距 + --ti-select-tags-margin-left: var(--ti-common-space-1, 1px); + // 选择器多选标签单行的高度 + --ti-select-tags-height: calc(var(--ti-common-size-base) * 7); + // 选择器多选标签最大高度 + --ti-select-tags-max-height: var(--ti-common-size-24x, 96px); + // 选择器后缀图标显示状态 + --ti-select-suffix-display: 'inline-block'; + // 选择器右侧图标间距 + --ti-select-suffix-icon-margin-right: var(--ti-common-space-base, 4px); + // 选择器suffix字号 + --ti-select-suffix-font-size: var(--ti-common-font-size-base, 12px); + // 收起按钮文本与图标色(hide) + --ti-select-collapse-button-text-icon-color: var(--ti-common-color-primary-normal, #5e7ce0); + // 收起按钮图标左边距(hide) + --ti-select-collapse-button-icon-margin-left: var(--ti-common-space-2, 2px); + // 收起按钮字号(hide) + --ti-select-collapse-button-font-size: var(--ti-common-font-size-base, 12px); +} diff --git a/packages/vue/package.json b/packages/vue/package.json index c5b8fc7c43..6fd4a9ad6c 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -40,6 +40,7 @@ "@opentiny/vue-avatar": "workspace:~", "@opentiny/vue-badge": "workspace:~", "@opentiny/vue-baidu-map": "workspace:~", + "@opentiny/vue-base-select": "workspace:~", "@opentiny/vue-breadcrumb": "workspace:~", "@opentiny/vue-breadcrumb-item": "workspace:~", "@opentiny/vue-bulletin-board": "workspace:~", diff --git a/packages/vue/src/base-select/index.ts b/packages/vue/src/base-select/index.ts new file mode 100644 index 0000000000..37be6be1ac --- /dev/null +++ b/packages/vue/src/base-select/index.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import BaseSelect from './src/index' +import '@opentiny/vue-theme/base-select/index.less' +import { version } from './package.json' + +BaseSelect.model = { + prop: 'modelValue', + event: 'update:modelValue' +} + +/* istanbul ignore next */ +BaseSelect.install = function (Vue) { + Vue.component(BaseSelect.name, BaseSelect) +} + +BaseSelect.version = version + +/* istanbul ignore next */ +if (process.env.BUILD_TARGET === 'runtime') { + if (typeof window !== 'undefined' && window.Vue) { + BaseSelect.install(window.Vue) + } +} + +export default BaseSelect diff --git a/packages/vue/src/base-select/package.json b/packages/vue/src/base-select/package.json new file mode 100644 index 0000000000..1cb8842a4f --- /dev/null +++ b/packages/vue/src/base-select/package.json @@ -0,0 +1,37 @@ +{ + "name": "@opentiny/vue-base-select", + "version": "3.16.0", + "description": "", + "main": "lib/index.js", + "module": "index.ts", + "sideEffects": false, + "type": "module", + "devDependencies": { + "@opentiny-internal/vue-test-utils": "workspace:*", + "vitest": "^0.31.0" + }, + "scripts": { + "build": "pnpm -w build:ui $npm_package_name", + "//postversion": "pnpm build" + }, + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/vue-common": "workspace:~", + "@opentiny/vue-locale": "workspace:~", + "@opentiny/vue-tag": "workspace:~", + "@opentiny/vue-input": "workspace:~", + "@opentiny/vue-option": "workspace:~", + "@opentiny/vue-scrollbar": "workspace:~", + "@opentiny/vue-icon": "workspace:~", + "@opentiny/vue-select-dropdown": "workspace:~", + "@opentiny/vue-grid": "workspace:~", + "@opentiny/vue-tree": "workspace:~", + "@opentiny/vue-tooltip": "workspace:~", + "@opentiny/vue-filter-box": "workspace:~", + "@opentiny/vue-checkbox": "workspace:~", + "@opentiny/vue-theme": "workspace:~", + "@opentiny/vue-recycle-scroller": "workspace:~", + "@opentiny/vue-button": "workspace:~" + }, + "license": "MIT" +} \ No newline at end of file diff --git a/packages/vue/src/base-select/src/index.ts b/packages/vue/src/base-select/src/index.ts new file mode 100644 index 0000000000..7257d68804 --- /dev/null +++ b/packages/vue/src/base-select/src/index.ts @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common' +import { t } from '@opentiny/vue-locale' +import template from 'virtual-template?pc|mobile-first' +import { IconChevronDown } from '@opentiny/vue-icon' + +const $constants = { + CLASS: { + SelectDropdownWrap: '.tiny-select-dropdown__wrap', + IsReverse: 'is-reverse' + }, + I18N: { + noData: 'ui.select.noData', + noMatch: 'ui.select.noMatch', + loading: 'ui.select.loading' + }, + COMPONENT_NAME: { + Form: 'Form', + Select: 'Select', + Option: 'Option', + FormItem: 'FormItem', + OptionGroup: 'OptionGroup', + SelectDropdown: 'SelectDropdown' + }, + EVENT_NAME: { + removeTag: 'remove-tag', + formChange: 'form.change', + formBlur: 'form.blur', + queryChange: 'queryChange', + setSelected: 'setSelected', + updatePopper: 'updatePopper', + destroyPopper: 'destroyPopper', + visibleChange: 'visible-change', + handleOptionClick: 'handleOptionClick', + handleGroupDisabled: 'handleGroupDisabled', + initValue: 'initValue' + }, + TYPE: { + Grid: 'grid', + Tree: 'tree' + }, + MAX_WIDTH: 132, + RECYCLE: { + SAAS_HEIGHT: 220, + AURORA_HEIGHT: 180, + ITEM_HEIGHT: 34, + SAFE_MARGIN: 4 + }, + SAAS_SIZE: { + mini: 24, + small: 28, + medium: 32 + }, + AURORA_SIZE: { + mini: 24, + small: 36, + medium: 42 + }, + SPACING_HEIGHT: 0, + MAX_VISIBLE_ROWS: 1 // 多选默认最大显示行数,超出后自动隐藏 +} + +export default defineComponent({ + name: $prefix + 'BaseSelect', + componentName: 'BaseSelect', + inject: { + form: { + default: '' + }, + formItem: { + default: '' + } + }, + provide() { + return { + select: this + } + }, + props: { + ...$props, + _constants: { + type: Object, + default: () => $constants + }, + id: [Number, String], + name: String, + size: String, + remote: Boolean, + remoteConfig: { + type: Object, + default() { + return { + showIcon: false, + clearData: false, + autoSearch: false + } + } + }, + title: String, + shape: String, + tip: String, + label: String, + loading: Boolean, + disabled: Boolean, + options: Array, + dataset: Object, + textField: { + type: String, + default: 'label' + }, + tabindex: { + type: String, + default: '1' + }, + valueField: { + type: String, + default: 'value' + }, + placement: { + type: String, + default: 'bottom-start' + }, + showCheck: { + type: Boolean, + default: true + }, + showAlloption: { + type: Boolean, + default: true + }, + multiple: Boolean, + clearable: Boolean, + noDataText: String, + filterable: Boolean, + loadingText: String, + noMatchText: String, + popperClass: String, + allowCreate: Boolean, + collapseTags: Boolean, + remoteMethod: Function, + filterMethod: Function, + reserveKeyword: Boolean, + automaticDropdown: Boolean, + defaultFirstOption: Boolean, + modelValue: {}, + valueKey: { + type: String, + default: 'value' + }, + placeholder: { + type: String, + default: () => t('ui.select.placeholder') + }, + searchPlaceholder: { + type: String, + default: () => t('ui.select.pleaseSearch') + }, + autocomplete: { + type: String, + default: 'off' + }, + multipleLimit: { + type: Number, + default: 0 + }, + popperAppendToBody: { + type: Boolean, + default: true + }, + hideDrop: { + type: Boolean, + default: false + }, + copyable: { + type: Boolean, + default: false + }, + renderType: String, + gridOp: { + type: Object, + default: () => ({}) + }, + treeOp: { + type: Object, + default: () => ({}) + }, + delay: { + type: Number, + default: 200 + }, + readonly: Boolean, + dropStyle: { + type: Object, + default: () => ({}) + }, + cacheOp: Object, + isDropInheritWidth: Boolean, + tagSelectable: { + type: Boolean, + default: false + }, + selectConfig: { + type: Object, + default() { + return { + checkMethod() { + return true + } + } + } + }, + radioConfig: { + type: Object, + default() { + return { + checkMethod() { + return true + } + } + } + }, + allowCopy: { + type: Boolean, + default: false + }, + textSplit: { + type: String, + default: ',' + }, + autoClose: Boolean, + queryDebounce: Number, + ignoreEnter: { + type: Boolean, + default: false + }, + dropdownIcon: { + type: [Object, String], + default: () => { + const defaultDropdownIcon = IconChevronDown() + defaultDropdownIcon.isDefault = true + return defaultDropdownIcon + } + }, + disabledTooltipContent: String, + hoverExpand: { + type: Boolean, + default: false + }, + optimization: [Boolean, Object], + displayOnly: { + type: Boolean, + default: false + }, + initQuery: Function, + extraQueryParams: { + type: [Object, String, Boolean, Array, Number], + default: '' + }, + updateDelay: { + type: Number, + default: 0 + }, + showTips: { + type: Boolean, + default: true + }, + closeByMask: { + type: Boolean, + default: true + }, + keepFocus: { + type: Boolean, + default: false + }, + popperOptions: { + type: Object, + default: () => ({ gpuAcceleration: false, boundariesPadding: 0 }) + }, + trim: { + type: Boolean, + default: false + }, + topCreate: { + type: Boolean, + default: false + }, + topCreateText: { + type: String, + default: () => t('ui.select.add') + }, + blank: { + type: Boolean, + default: false + }, + // 以下为 tiny 新增 + searchable: { + type: Boolean, + default: false + }, + showEmptyImage: { + type: Boolean, + default: false + }, + InputBoxType: { + type: String, + default: 'input', + validator: (value: string) => ['input', 'underline'].includes(value) + }, + tagType: { + type: String, + default: '' + }, + clearNoMatchValue: { + type: Boolean, + default: false + }, + showLimitText: { + type: Boolean, + default: false + }, + showProportion: { + type: Boolean, + default: false + }, + clickExpand: { + type: Boolean, + default: false + }, + maxVisibleRows: { + type: Number, + default: $constants.MAX_VISIBLE_ROWS + }, + allText: { + type: String, + default: '' + } + }, + setup(props, context) { + return $setup({ props, context, template }) + } +}) diff --git a/packages/vue/src/base-select/src/mobile-first.vue b/packages/vue/src/base-select/src/mobile-first.vue new file mode 100644 index 0000000000..cdf246cf29 --- /dev/null +++ b/packages/vue/src/base-select/src/mobile-first.vue @@ -0,0 +1,705 @@ + + + diff --git a/packages/vue/src/base-select/src/pc.vue b/packages/vue/src/base-select/src/pc.vue new file mode 100644 index 0000000000..8d7e82818b --- /dev/null +++ b/packages/vue/src/base-select/src/pc.vue @@ -0,0 +1,713 @@ + + + + + diff --git a/packages/vue/src/base-select/src/token.ts b/packages/vue/src/base-select/src/token.ts new file mode 100644 index 0000000000..eadff4c420 --- /dev/null +++ b/packages/vue/src/base-select/src/token.ts @@ -0,0 +1,8 @@ +export const classes = { + 'caret': 'text-base rotate-0 transition-transform duration-300 fill-color-text-placeholder cursor-pointer ', + 'select-tags': + 'absolute leading-normal whitespace-normal sm:pl-3 pr-2 z-[1] top-1/2 -translate-y-2/4 flex items-center flex-wrap [&_[data-tag=tiny-tag]]:my-0.5 [&_[data-tag=tiny-tag]]:h-7 [&_[data-tag=tiny-tag]]:text-sm [&_[data-tag=tiny-tag]]:sm:h-5 [&_[data-tag=tiny-tag]]:sm:text-sm [&_[data-tag=tiny-tag]_svg]:shrink-0', + 'tags-text': 'inline-block w-full whitespace-nowrap text-ellipsis overflow-hidden align-bottom', + 'tag-info': + 'whitespace-nowrap text-ellipsis overflow-hidden inline-flex justify-start items-center border-transparent text-color-text-primary' +}