From 192ea982eec7cedc05a17b10274f997448312585 Mon Sep 17 00:00:00 2001 From: wangchangqi Date: Mon, 28 Nov 2022 15:26:56 +0800 Subject: [PATCH] =?UTF-8?q?(feature)=E6=B7=BB=E5=8A=A0star-select=E5=8F=8A?= =?UTF-8?q?star-select-dialog,=E6=B7=BB=E5=8A=A0star-li=20type=3D'selector?= =?UTF-8?q?',=E6=9B=B4=E6=96=B0star-icons.ttf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/base/global-style.ts | 4 + src/components/button-group/README.md | 6 + src/components/card/card.css.ts | 1 + src/components/card/card.ts | 3 + src/components/dialog/README.md | 155 ++++++++++- src/components/dialog/index.ts | 1 + src/components/dialog/select-dialog.css.ts | 32 +++ src/components/dialog/select-dialog.ts | 291 +++++++++++++++++++++ src/components/dialog/svg/close_lm.svg | 10 - src/components/fonts/star-icons.ttf | Bin 112816 -> 114912 bytes src/components/li/README-settings.md | 61 ++++- src/components/li/li.css.ts | 24 +- src/components/li/li.ts | 9 +- src/components/radio/radio-group.ts | 86 +++++- src/components/select/README.md | 149 ++++++++++- src/components/select/index.ts | 1 + src/components/select/package.json | 22 ++ src/components/select/select.css.ts | 51 ++++ src/components/select/select.ts | 181 +++++++++++++ src/test/panels/fonts/star-icons.ts | 15 +- src/test/panels/li/li.ts | 10 +- src/test/panels/root.ts | 6 + src/test/panels/select/select.ts | 219 ++++++++++++++++ 23 files changed, 1270 insertions(+), 67 deletions(-) create mode 100644 src/components/dialog/select-dialog.css.ts create mode 100644 src/components/dialog/select-dialog.ts delete mode 100644 src/components/dialog/svg/close_lm.svg create mode 100644 src/components/select/index.ts create mode 100644 src/components/select/package.json create mode 100644 src/components/select/select.css.ts create mode 100644 src/components/select/select.ts create mode 100644 src/test/panels/select/select.ts diff --git a/src/components/base/global-style.ts b/src/components/base/global-style.ts index ad5877f..2717357 100644 --- a/src/components/base/global-style.ts +++ b/src/components/base/global-style.ts @@ -456,6 +456,7 @@ const baseComponentStyle = css` --li-label: var(--font-main-black); --li-description: var(--font-sec-auxiliary-black); --li-square: #B3B3B3; + --li-value-warn: var(--theme-red); --li-value-primary: var(--theme-blue); --li-value-default: var(--font-sec-auxiliary-black); --li-link: var(--linear-icon32-black); @@ -468,6 +469,9 @@ const baseComponentStyle = css` --card-label: var(--font-main-black); --card-link: var(--linear-icon32-black); + /* Selector */ + --selector-icon-color: var(--linear-icon32-black); + /* Radio */ --bor-radio-off: var(--auto-3px) solid rgba(38, 38, 38, 0.25); --bor-radio-off: var(--auto-3px) solid var(--opacity-white-25); diff --git a/src/components/button-group/README.md b/src/components/button-group/README.md index 8bebb1c..c8d9ff6 100644 --- a/src/components/button-group/README.md +++ b/src/components/button-group/README.md @@ -10,6 +10,7 @@ | -------------- | ------- | -------- | -------------------------- | | vertical | Boolean | false | 用于指示是否显示垂直按钮组 | | split | Boolean | false | 用于指示是否显示分隔符 | +| inheritRadius | Boolean | false | 用于指示是否继承倒角 |
star-button-group组件支持的插槽
@@ -38,6 +39,11 @@ button-group 将 100% 填充父容器所给的空间,再均匀平铺\ + + + + + ``` ## 注意 diff --git a/src/components/card/card.css.ts b/src/components/card/card.css.ts index 307d02b..a1040c0 100644 --- a/src/components/card/card.css.ts +++ b/src/components/card/card.css.ts @@ -4,6 +4,7 @@ export default css` :host { --background-image-url: ''; flex: 1; + display: block; } div { diff --git a/src/components/card/card.ts b/src/components/card/card.ts index 9830d25..43cebfe 100644 --- a/src/components/card/card.ts +++ b/src/components/card/card.ts @@ -42,6 +42,9 @@ export class StarCard extends StarBaseElement { */ @property({type: Boolean, reflect: true}) checked!: boolean + /* 该value可供在扩展的 radio-group 中使用 */ + @property({type: String, reflect: true}) value!: string + @property({type: String}) type = 'base' // @property({type: String}) size = "medium" @property({type: String}) heading = '' diff --git a/src/components/dialog/README.md b/src/components/dialog/README.md index bb91160..9fd8cc1 100644 --- a/src/components/dialog/README.md +++ b/src/components/dialog/README.md @@ -14,17 +14,48 @@ 基类采用的三段式结构: ``` -------------------- -| 图标/标题 | -------------------- -| 内容 | -| 纯文本/富文本/表单 | -------------------- -| 按钮/按钮组 | -------------------- +┌─────────────────────────────────────────────────┐ +| Header | +├─────────────────────────────────────────────────| +| ┌─────────────────────────────────────────────┐ | +| │ Icon(star-icons/svg-icon/png...) │ | +| └─────────────────────────────────────────────┘ | +| ┌─────────────────────────────────────────────┐ | +| │ Title(alert/confirm/prompt/select/Custom... │ | +| └─────────────────────────────────────────────┘ | +└─────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────┐ +| Content | +├─────────────────────────────────────────────────| +│ ┌──────┐ ┌───────┐ │ +│ │ Text │ │ Image │ │ +│ └──────┘ └───────┘ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Form │ │ +│ │ ┌─────────────────┐ ┌──────────────┐ │ │ +│ │ │ Input(Password) │ │ ActionButton │ │ │ +│ │ └─────────────────┘ └──────────────┘ │ │ +│ └──────────────────────────────────────┘ │ +│ ┌────────────────────────────────┐ │ +│ │ Select (Multiple)(Group)(Size) │ │ +│ │ ┌────────────┐ │ │ +│ │ │ RadioGroup │ │ │ +│ │ └────────────┘ │ │ +│ └────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────┐ +| Footer | +├─────────────────────────────────────────────────| +│ ┌─────────────────────────────────────────────┐ │ +│ │ StarButtonGroup │ │ +│ │ ┌────────────┐ ┌────────────┐ │ │ +│ │ │ StarButton │ │ StarButton │ ... │ │ +│ │ └────────────┘ └────────────┘ │ │ +│ └─────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ ``` -## 系统弹窗(alert/confirm/prompt) +## 系统模态弹窗(alert/confirm/prompt) ``` alert @@ -53,6 +84,112 @@ prompt ------------------- ``` +## 系统用户代理弹窗(select) + +### 值选择器 + +对应使用的原生标签: + +```html + + + + + + + + + + + +``` + +### 时间日期选择器 + +```html + + + + + + + +``` + +### UI 设计内容 + +触发形态: + +``` + +``` + +``` +┌───────────┐ +| Header | +├───────────| +| ┌───────┐ | +| │ Title │ | +| └───────┘ | +└───────────┘ +┌─────────────────────────────────────────────────┐ +| Content | +├─────────────────────────────────────────────────| +│ ┌─single select──────────────────┐ │ +│ │ │ │ +│ │ ┌────────────┐ │ │ +│ │ │ RadioGroup │ │ │ +│ │ └────────────┘ │ │ +│ └────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +┌───────────────────────────────────┐ +| Footer | +├───────────────────────────────────| +│ ┌───────────────────────────────┐ │ +│ │ StarButtonGroup │ │ +│ ├─────────single select─────────| │ +│ │ ┌────────────┐ │ │ +│ │ │ StarButton │ │ │ +│ │ └────────────┘ │ │ +│ ├───────multiple select─────────| │ +│ │ ┌────────────┐ ┌────────────┐ │ │ +│ │ │ StarButton │ │ StarButton │ │ │ +│ │ └────────────┘ └────────────┘ │ │ +│ └───────────────────────────────┘ │ +└───────────────────────────────────┘ +``` + +### 转换 + ## TBD: 使用用户代理支持的 使用浏览器用户代理支持的时,可以省去自行设定模态的步骤,并且其自身已经拥有了默认排版,省去设置样式的行为。 diff --git a/src/components/dialog/index.ts b/src/components/dialog/index.ts index 63744e1..21b250c 100644 --- a/src/components/dialog/index.ts +++ b/src/components/dialog/index.ts @@ -1,3 +1,4 @@ export {StarAlertDialog} from './alert-dialog.js' export {StarConfirmDialog} from './confirm-dialog.js' export {StarPromptDialog} from './prompt-dialog.js' +export {StarSelectDialog} from './select-dialog.js' diff --git a/src/components/dialog/select-dialog.css.ts b/src/components/dialog/select-dialog.css.ts new file mode 100644 index 0000000..8153925 --- /dev/null +++ b/src/components/dialog/select-dialog.css.ts @@ -0,0 +1,32 @@ +import {css} from 'lit' + +export default css` + .star-select-title { + color: var(--dialog-heading); + font-size: var(--auto-28px); + font-weight: bold; + text-align: center; + line-height: var(--auto-36px); + max-height: var(--auto-524px); + } + + main { + --oc-li-padding-inline: var(--auto-60px); + --oc-li-min-height: var(--auto-90px); + padding: 0; + } + + star-radio-group { + width: 100%; + overflow: auto; + max-height: calc(var(--auto-90px) * 4); + } + + star-radio-group[multiple] star-li[checked] { + --li-label: var(--theme-blue); + } + + star-card { + padding: 10px 0; + } +` diff --git a/src/components/dialog/select-dialog.ts b/src/components/dialog/select-dialog.ts new file mode 100644 index 0000000..f24504c --- /dev/null +++ b/src/components/dialog/select-dialog.ts @@ -0,0 +1,291 @@ +import { + customElement, + html, + nothing, + query, + state, + CSSResultArray, + TemplateResult, +} from '@star-web-components/base/star-base-element.js' +import StarBaseDialog from './base-dialog.js' +import selectDialogStyles from './select-dialog.css.js' +import '@star-web-components/button/button.js' +import '@star-web-components/button-group/button-group.js' +import '@star-web-components/radio' +import {StarButton} from '@star-web-components/button' +import {StarLi} from '@star-web-components/li' +import {StarRadioGroup} from '@star-web-components/radio' + +interface SelectDialogOptions { + title?: string + type: string | 'select' + subtitle?: string + image?: string + cancel?: string + confirm?: string + oncancel?: () => void + onconfirm?: (e: Event) => void + onselect?: (e: Event) => void + multiple?: boolean + optionsSlot: StarLi[] | TemplateResult +} + +/** + * 租借子节点 + * + * 将一组元素租给一个目标元素,在该组元素的原位置使用注释占位符进行替代, + * 并返回一个归还节点的方法. + * + * 原理: 剖析单个元素的租借过程 + * 1. 找到元素的父节点(parentElement 或 getRootNode()) + * 2. 在元素的父节点处将元素替换为注释占位符的节点拷贝 + * 3. 在目标位置插入租借来的元素 + * 4. 返回归还租借的子节点的方法 + */ +export const reparentChildren = ( + srcElements: T[], + destination: Element, + { + position, + prepareCallback, + }: { + position: InsertPosition + prepareCallback?: (el: T) => ((el: T) => void) | void + } = { + position: 'beforeend', + } +): (() => T[]) => { + let {length} = srcElements + if (length === 0) return () => srcElements + + let step = 1 + let index = 0 + + if (position === 'afterbegin' || position === 'afterend') { + step = -1 + index = length - 1 + } + + const placeholderItems = new Array(length) + const cleanupCallbacks = new Array<(el: T) => void>(length) + const placeholderTemplate: Comment = document.createComment( + 'placeholder for reparented element' + ) + + do { + const srcElement = srcElements[index] + if (prepareCallback) { + cleanupCallbacks[index] = prepareCallback(srcElement) as (el: T) => void + } + placeholderItems[index] = placeholderTemplate.cloneNode() as Comment + + const parentElement = srcElement.parentElement || srcElement.getRootNode() + if (parentElement && parentElement !== srcElement) { + parentElement.replaceChild(placeholderItems[index], srcElement) + } + destination.insertAdjacentElement(position, srcElement) + index += step + } while (--length > 0) + + return (): T[] => + restoreChildren(placeholderItems, srcElements, cleanupCallbacks) +} + +/** + * 归还子节点 + * + * 是租借子节点的逆过程, 将注释占位符替换成子节点, 再删除注释占位符 + */ +export const restoreChildren = ( + placeholderItems: Comment[], + srcElements: T[], + cleanupCallbacks: ((el: T) => void)[] = [] +): T[] => { + for (let index = 0; index < srcElements.length; ++index) { + const srcElement = srcElements[index] + const placeholderItem = placeholderItems[index] + const parentElement = + placeholderItem.parentElement || placeholderItem.getRootNode() + cleanupCallbacks[index]?.(srcElement) + if (parentElement && parentElement !== placeholderItem) { + parentElement.replaceChild(srcElement, placeholderItem) + } + delete placeholderItems[index] + } + return srcElements +} + +@customElement('star-select-dialog') +export class StarSelectDialog extends StarBaseDialog { + constructor(obj: SelectDialogOptions) { + super() + this.title = obj.title || this.title + this.type = obj.type + this.cancel = obj.cancel || '取消' + this.confirm = obj.confirm || '确定' + this.oncancel = obj.oncancel || (() => {}) + this.onconfirm = obj.onconfirm || (() => {}) + this.onselect = obj.onselect || (() => {}) + this.optionsSlot = obj.optionsSlot + this.multiple = obj.multiple || false + } + + public static override get styles(): CSSResultArray { + return [super.styles, selectDialogStyles] + } + + @state() type!: string + + @state() multiple = false + + @state() title = '提示' + + @query('star-radio-group') radioGroup!: StarRadioGroup + + @query("star-button[variant='primary']") confirmButton!: StarButton + + text!: string + + optionsSlot!: StarLi[] | TemplateResult + + /* 记录初始化的选择项,用于取消时重置 */ + initSelected!: string + + reparentThenRestore!: Function + + @state() optionsTemplateResult: TemplateResult | typeof nothing = nothing + + protected override get headerContent(): TemplateResult { + return html` +
+
${this.title}
+
+ ` + } + + /** + * 针对用户代理的select, 填充基本款: + * + * + * ... + * + * + * 针对自定义的select, 从来源处租借符合radio特征的slot插槽中的选择项: + * + * + * ... + * + */ + protected override get mainContent(): TemplateResult { + return html` + + ${Array.isArray(this.optionsSlot) ? nothing : this.optionsSlot} + + ` + } + + /** + * 针对不同的选择器类型:使用不同的底部按钮组 + * + * + */ + protected override get bottomContent(): TemplateResult { + switch (this.type) { + case 'select': { + if (this.multiple === false) { + return html` + + + + ` + } else { + return html` + + + + + ` + } + } + default: + throw new Error("Unhandled type in SelectDialog' bottom content") + } + } + + /** + * 从 multiple 模式中退出时,重置 StarRadioGroup 中的选择项 + */ + handleCancel(e: Event) { + if (this.multiple === true) { + this.radioGroup.setSelected(this.initSelected) + } + super.handleCancel(e) + this.reparentThenRestore?.() + } + + override handleConfirm(e: Event) { + super.handleConfirm(e) + this.reparentThenRestore?.() + } + + handleSelect(e: Event) { + this.onselect?.(e) + if (this.multiple === false) { + super.handleCancel(e) + this.reparentThenRestore?.() + } else { + if (this.radioGroup.getSelected() === '') { + this.confirmButton.setAttribute('disabled', '') + } else { + this.confirmButton.removeAttribute('disabled') + } + } + } + + protected firstUpdated(_: any) { + super.firstUpdated(_) + if (Array.isArray(this.optionsSlot)) { + this.reparentThenRestore = reparentChildren( + this.optionsSlot, + this.radioGroup + ) + } else { + // 再将 star-radio-group 下的内容解析为数组,重置于 optionsSlot 上 + this.optionsSlot = [ + ...this.querySelectorAll('star-radio-group *'), + ] as StarLi[] + } + + for (const radio of this.optionsSlot) { + if (radio.checked === true) { + this.updateComplete.then(() => { + // 平滑滚动至默认选择项 + radio.scrollIntoView({behavior: 'smooth'}) + // 保存初始化的选项 + this.initSelected = this.radioGroup.getSelected() + }) + return + } + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'star-select-dialog': StarSelectDialog + } +} diff --git a/src/components/dialog/svg/close_lm.svg b/src/components/dialog/svg/close_lm.svg deleted file mode 100644 index 14e2ae4..0000000 --- a/src/components/dialog/svg/close_lm.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/components/fonts/star-icons.ttf b/src/components/fonts/star-icons.ttf index 869a2ac37add8355e6f1d3dd87551aeeb3a1fcd7..923d7bb098a7799fbe620780e938385bc566ed28 100644 GIT binary patch delta 15694 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 MjPy6P;xHxsAF1msNdN!< delta 13939 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 oA#V893kL*$^x#qdR ``` -``` -## 电池 --------- ---------------- -| Icon | | 主Label | 分隔线 灰色状态值VALUE --------- ---------------- - -########################################----SLOT ----------------- -| 主Label | Slot-选择器Selector(值选择器, 时间选择器, 日期选择器) ----------------- -``` - ## (inline)内联及模拟 ### type="checkbox" @@ -291,6 +279,55 @@ ``` +### type="selector" 中与 select 标签的交互 + +结构: + +``` +┌───────────────┬──────────────────────────────────────────────────┐ +│ │ ┌─────────┐ ┌───────┬────────┐ ┌───────────────┐ │ +│ │ │ │ │ Label │ Square │ │ │ │ +│ │ │(App)Icon│ ├───────┴────────┤ │ Select │ │ +│type='selector'│ │ │ │ Description | │Single/Multiple│ │ +│ │ └─────────┘ └────────────────┘ └───────────────┘ │ +└───────────────┴──────────────────────────────────────────────────┘ +``` + +基本示例: + +```html + + + + + + + + +``` + +注意: + +1. selected 优先级: star-li > select, select 上的 selected 可被 star-li 覆写 +2. 点击 star-li 的选择器区域时,用户的实际点击事件将到达 select 标签(非 trusted 的 click 事件将无法打开用户代理的 select 窗) + +实际显示的弹窗内的 dom 结构: + +```html + + + + + + + + + + + + +``` + ## 完全插槽态 ### type='base' `star-card` 在 `star-li` 中 diff --git a/src/components/li/li.css.ts b/src/components/li/li.css.ts index d940ac2..95b6b9d 100644 --- a/src/components/li/li.css.ts +++ b/src/components/li/li.css.ts @@ -5,7 +5,7 @@ export default css` display: flex; padding-inline: var(--oc-li-padding-inline, var(--auto-48px)); width: calc(100% - var(--oc-li-padding-inline, var(--auto-48px)) * 2); - min-height: var(--auto-96px); + min-height: var(--oc-li-min-height, var(--auto-96px)); } li { width: 100%; @@ -105,7 +105,7 @@ export default css` span#label { font-weight: bold; color: var(--li-label); - font-weight: 400px; + font-weight: 400; font-size: var(--auto-26px); line-height: var(--auto-34px); } @@ -118,7 +118,7 @@ export default css` margin: auto var(--auto-16px) var(--auto-1px) var(--auto-16px); padding-inline: var(--auto-8px); line-height: var(--auto-23px); - font-weight: 500px; + font-weight: 500; font-size: var(--auto-20px); color: var(--li-square); } @@ -129,9 +129,9 @@ export default css` } span#description { color: var(--li-description); - font-weight: 400px; + font-weight: 400; font-size: var(--auto-20px); - line-height: var(--auto-18px); + line-height: var(--auto-20px); margin-top: var(--auto-12px); } span#value { @@ -148,6 +148,9 @@ export default css` :host([variant='primary']) span#value { color: var(--li-value-primary); } + :host([variant='warn']) span#value { + color: var(--li-value-warn); + } /* 边界折叠 */ span#label, @@ -180,7 +183,7 @@ export default css` box-sizing: border-box; vertical-align: top; text-align: right; - font-weight: 400px; + font-weight: 400; font-size: var(--auto-26px); line-height: var(--auto-26px); color: var(--li-input); @@ -202,6 +205,7 @@ export default css` content: ' '; width: var(--auto-32px); height: var(--auto-32px); + min-width: var(--auto-32px); margin: auto 0px auto auto; box-sizing: border-box; border: var(--auto-3px) solid rgba(38, 38, 38, 0.2); @@ -335,7 +339,7 @@ export default css` --oc-text-padding-inline: 0px; --oc-text-min-width: min-width; margin: auto 0px auto auto; - font-weight: 400px; + font-weight: 400; font-size: var(--auto-26px); line-height: var(--auto-34px); color: var(--font-auxiliary-black); @@ -343,6 +347,12 @@ export default css` ::slotted(star-slider[slot='slider']) { width: 100%; } + ::slotted(star-select[slot='select']) { + margin: auto 0px auto auto; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } /* 条目按压和释放时的变化 */ :host(.starpress:not([type='base'], [type='embed-switch'], [type='embed-checkbox'])) { diff --git a/src/components/li/li.ts b/src/components/li/li.ts index abf477c..eedd11f 100644 --- a/src/components/li/li.ts +++ b/src/components/li/li.ts @@ -26,7 +26,7 @@ export enum LiType { CHECKBOX = 'checkbox', INPUT = 'input', RADIO = 'radio', - /* todo */ SELECTOR = 'selector', + SELECTOR = 'selector', SWITCH = 'switch', /* 嵌入式 */ @@ -313,6 +313,10 @@ export class StarLi extends StarBaseElement { return html` ` + case LiType.SELECTOR: + return html` + + ` case LiType.SWITCH: return html` { + if (child instanceof StarRadio) { + return true + } else if (child.type === 'radio' && child.checked === true) { + /* 非 , 但是具有同样表现的标签扩展 */ + return true + } + return false + }) + } + + public get label(): string { + return [...this.activeRadios].reduce((sum, radio) => { + if (sum === '') return radio.label + else return (sum += ',' + radio.label) + }, '') + } + + public setSelected(str: string): boolean { + try { + const selected = str.split(',') + for (const radio of this.radios) { + if (selected.includes(radio.value)) { + radio.checked = true + } else { + radio.checked = false + } + } + this.emit('change') + return true + } catch (err) { + console.error(err) + return false + } + } + + public getSelected(): string { + return [...this.activeRadios].reduce((sum, radio) => { + if (sum === '') return radio.value + else return (sum += ',' + radio.value) + }, '') + } + render() { return html`
@@ -52,7 +104,7 @@ export class StarRadioGroup extends LitElement { ` } - handleEvent(evt: Event) { + _onSingleRadioClick(evt: Event) { const target = evt.target as StarRadio if (this.radios.indexOf(target) !== -1 && target.checked !== true) { target.checked = true @@ -61,18 +113,26 @@ export class StarRadioGroup extends LitElement { radio.checked = false } } - this.selected = target.value - this.dispatchEvent( - new Event('change', { - bubbles: true, - composed: false, - }) - ) + this.emit('change') } } - protected override firstUpdated(changes: PropertyValues): void { - super.firstUpdated(changes) + _onMultipleRadioClick(evt: Event) { + const target = evt.target as StarRadio + if (this.radios.indexOf(target) !== -1) { + target.checked = !target.checked + this.emit('change') + } + } + + handleEvent(evt: Event) { + ;(this.multiple + ? this._onMultipleRadioClick + : this._onSingleRadioClick + ).call(this, evt) + } + + protected override firstUpdated(): void { for (const radio of this.radios) { if (radio.value === this.selected) { radio.checked = true diff --git a/src/components/select/README.md b/src/components/select/README.md index 1ab43dc..c3190d2 100644 --- a/src/components/select/README.md +++ b/src/components/select/README.md @@ -1,8 +1,155 @@ # 选择 -如: iPhone-通用-键盘-听写语言 +star-select 的作用: 将原生 \ 标签包装为自定义的 UI 设计样式. ## 特点 - 支持多选 - 强制多选类型`的情况下,选择只有一个时,将其置灰 + +## 数据结构 + +### detail.type="SELECT" && detail.choices.multiple: false + +`isFocus``js +{ +bubbles: false +cancelBubble: false +cancelable: false +composed: false +composedTarget: ... +currentTarget: ... +defaultPrevented: false +defaultPreventedByChrome: false +defaultPreventedByContent: false +detail: { +activeEditable: Object { native: XPCWrappedNative_NoHelper } +​​​choices: XPCWrappedNative_NoHelper { +... +multiple: false, +choices: (5) [XPCWrappedNative_NoHelper, XPCWrappedNative_NoHelper, XPCWrappedNative_NoHelper, …], +... +} +imeGroup: null +inputMode: null +inputType: null +isFocus: true +lang: null +lastImeGroup: "" +max: null +maxLength: null +min: null +name: null +selectionEnd: 0 +selectionStart: 0 +type: "SELECT" +value: "" +voiceInputSupported: false +} +eventPhase: 2 +explicitOriginalTarget: ... +isReplyEventFromRemoteContent: false +isSynthesized: false +isTrusted: true +isWaitingReplyFromRemoteContent: false +multipleActionsPrevented: false +originalTarget: ... +returnValue: true +srcElement: ... +target: ... +timeStamp: 80988671.942999 +type: "\_inputmethod-contextchange" +} + +```` + +### detail.type="SELECT" && detail.choices.multiple: true + +```js +​choices: XPCWrappedNative_NoHelper { + ... + multiple: true, + choices: (5) [XPCWrappedNative_NoHelper, XPCWrappedNative_NoHelper, XPCWrappedNative_NoHelper, …], + ... +} +```` + +### detail.type="INPUT" + +#### detail.inputType="time" + +```js +detail: { + activeEditable: {…} + choices: XPCWrappedNative_NoHelper + imeGroup: null + inputMode: null + inputType: "time" + isFocus: true + lang: null + lastImeGroup: "" + max: null + maxLength: null + min: null + name: null + selectionEnd: 0 + selectionStart: 0 + type: "INPUT" + value: "" + voiceInputSupported: false +} +``` + +#### detail.inputType="time" && value="xxxx" + +```js +detail: { + ... + inputType: "time" + ... + type: "INPUT" + ... + value: "10:14" +} +``` + +#### detail.inputType="date" + +```js +detail: { + ... + inputType: "date" + ... + type: "INPUT" +} +``` + +#### detail.inputType="date" && value="xxxx" + +```js +detail { + ... + inputType: "date" + ... + type: "INPUT" + ... + value: "2016-05-20" +} +``` + +#### detail.type="datetime-local" + +``` +detail { + ...​ + inputType: "datetime-local" + ... + type: "INPUT" +} +``` + +系统 UI 框架, 将先跳出选择日期选择窗, 再跳出选择时间窗. + +#### [DEPRECATED]detail.type="datetime" + +`` 已被废弃. diff --git a/src/components/select/index.ts b/src/components/select/index.ts new file mode 100644 index 0000000..a238dc9 --- /dev/null +++ b/src/components/select/index.ts @@ -0,0 +1 @@ +export * from './select.js' diff --git a/src/components/select/package.json b/src/components/select/package.json new file mode 100644 index 0000000..27738a5 --- /dev/null +++ b/src/components/select/package.json @@ -0,0 +1,22 @@ +{ + "name": "@star-web-components/select", + "version": "0.0.1", + "description": "", + "type": "module", + "main": "./index.js", + "module": "./index.js", + "exports": { + ".": { + "default": "./index.js" + }, + "./index": { + "default": "./index.js" + }, + "./select.js": { + "default": "./select.js" + }, + "./package.json": "./package.json" + }, + "author": "", + "license": "ISC" +} diff --git a/src/components/select/select.css.ts b/src/components/select/select.css.ts new file mode 100644 index 0000000..04052bc --- /dev/null +++ b/src/components/select/select.css.ts @@ -0,0 +1,51 @@ +import {css} from 'lit' + +export default css` + :host { + display: block; + width: min-content; + } + + a { + position: relative; + display: flex; + align-items: center; + font-weight: 400; + font-size: var(--auto-26px); + line-height: var(--auto-26px); + color: var(--theme-blue); + } + + a::after { + content: 'selector'; + font-family: 'star-icons'; + font-size: var(--auto-32px); + min-width: var(--auto-32px); /* 应对可能的被挤压 */ + margin-left: var(--auto-14px); + color: var(--selector-icon-color); + } + + :host([variant='rightarrow']) a::after { + content: 'right-light'; + } + + span { + display: block; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin-left: auto; + } + + select { + opacity: 0; + position: absolute; + left: 0; + width: 100%; + height: 100%; + } + + ::slotted(*) { + display: none; + } +` diff --git a/src/components/select/select.ts b/src/components/select/select.ts new file mode 100644 index 0000000..10bbc25 --- /dev/null +++ b/src/components/select/select.ts @@ -0,0 +1,181 @@ +import { + customElement, + html, + property, + queryAssignedElements, + state, + CSSResult, + StarBaseElement, +} from '@star-web-components/base' +import selectStyles from './select.css.js' +import {StarSelectDialog} from '@star-web-components/dialog' +import {StarLi} from '@star-web-components/li' +import {StarRadio, StarRadioGroup} from '@star-web-components/radio' + +export type RadioVariant = 'circle' | 'checkmark' + +@customElement('star-select') +export class StarSelect extends StarBaseElement { + public static override get styles(): CSSResult { + return selectStyles + } + + @property({type: String, reflect: true}) selected!: string + + /* 提供给选择器窗的标题 */ + @property({type: String}) title!: string + + @property({type: Boolean, reflect: true}) multiple = false + + @property({type: Object}) data!: [ + [ + { + label?: string + value: string | number + } + ] + ] + + /* 用于指示自身风格,包括: selector(默认), rightarrow, */ + @property({type: String, reflect: true}) variant!: string + + @state() label!: string + + @queryAssignedElements() optionsSlot!: StarLi[] + + /** + * 兼容用户代理支持的分类选择器(带标签) + * + * 但预留分组. + */ + // a!: { + // size?: number, + // multiple?: string, + // options: [ + // { + // label: string + // value: string | number + // }, + // ] | [ + // { + // optgroup: + // } + // ] + + // } + + protected firstUpdated(): void { + // 1. 为 radio 标签,或是有实现类 radio 标签行为的标签 + // 如 + // 2. 不满足1的标签将被删除 + // 3. 如果option未指定label,将使用value值作为label值 + // 4. 将所有 option 的 checked 置为 false + // 5. 如果是多值选择,设置 star-li 的 variant 属性为 "checkmark" + if (this.optionsSlot.length === 0) throw new Error('No legal option') + + for (const option of this.optionsSlot) { + if ( + !( + option instanceof StarRadio || + (option.type === 'radio' && option.value !== undefined) + ) + ) { + option.parentElement?.removeChild(option) + continue + } + + if (option.label === undefined) { + option.label = option.value + } + if (this.multiple === true) { + option.variant = 'checkmark' + } + option.checked = false + } + + // 当使用端未指定 selected 时,将进行如下行为: + // 1. 使用 optionsSlot 中第一项的 value 和 label 作为默认值, + // 当使用端指定了 selected 时,将进行如下行为: + // 1. 遍历 optionsSlot 找到对等项 + // 当经历上述过程后,this.label 还为空,说明 this.selected 为错值 + // 那将继续使用 optionsSlot 中第一项的 value 和 label 作为默认值 + for (const [index, option] of this.optionsSlot.entries()) { + if (this.selected !== undefined) { + if (this.multiple === false) { + if (this.selected === option.value) { + this.label = option.label + option.checked = true + break + } + } else { + if (this.selected.split(',').includes(option.value)) { + if (this.label === undefined) this.label = option.label + else this.label += ',' + option.label + option.checked = true + continue + } + } + } + + if (index === this.optionsSlot.length - 1) { + if (this.selected === undefined || this.label === undefined) { + console.warn('Not find a legal option, we will use the first option') + + this.selected = this.optionsSlot[0].value + this.label = this.optionsSlot[0].label + this.optionsSlot[0].checked = true + } + } + } + } + + /** + * 事件来源:star-radio-group 上的change自定义事件 + */ + _onselect(e: Event) { + const target = e.target as StarRadioGroup + if (this.selected !== target.selected) { + this.selected = target.getSelected() + this.label = target.label + this.emit('change') + } + } + + /** + * 当自身被点击时,打开 ,将 options 租借给它。 + * 完成调用后, 再将 options 归还。 + */ + _onclick(_: Event) { + const starAlertDialog = new StarSelectDialog({ + title: this.title || '测试', + type: 'select', + multiple: this.multiple, + cancel: '取消', + confirm: '确定', + onselect: this._onselect.bind(this), + optionsSlot: this.optionsSlot, + }) + // TODO: 移到overlay层 + document.body?.appendChild(starAlertDialog) + } + + render() { + return html` + + ${this.label} + + + ` + } + + constructor() { + super() + this.addEventListener('click', this._onclick) + } +} + +declare global { + interface HTMLElementTagNameMap { + 'star-select': StarSelect + } +} diff --git a/src/test/panels/fonts/star-icons.ts b/src/test/panels/fonts/star-icons.ts index 311d5b5..ff99bf8 100644 --- a/src/test/panels/fonts/star-icons.ts +++ b/src/test/panels/fonts/star-icons.ts @@ -8,7 +8,6 @@ export class PanelFontsStarIcons extends LitElement { '2g', '3g', '4g', - 'ip', 'accessibility', 'achievement', 'add-contact', @@ -18,8 +17,6 @@ export class PanelFontsStarIcons extends LitElement { 'airplane', 'alarm-clock-stop', 'alarm-clock', - 'alarm-song-shock', - 'alarm-stop-shock', 'alarm-stop', 'alarm', 'album', @@ -48,6 +45,7 @@ export class PanelFontsStarIcons extends LitElement { 'battery-7', 'battery-8', 'battery-9', + 'battery-capacity', 'battery-charging', 'battery-gray-1', 'battery-gray', @@ -83,6 +81,7 @@ export class PanelFontsStarIcons extends LitElement { 'call', 'callback-emergency', 'camera-db', + 'camera-web', 'camera', 'change-wallpaper', 'clear-input-left', @@ -154,6 +153,7 @@ export class PanelFontsStarIcons extends LitElement { 'incoming-sms', 'info', 'invisible', + 'ip', 'keyboard-circle', 'keyboard', 'languages', @@ -170,6 +170,7 @@ export class PanelFontsStarIcons extends LitElement { 'media-mute', 'media-sound', 'media-storage', + 'memory', 'menu', 'message-voice', 'message', @@ -207,6 +208,7 @@ export class PanelFontsStarIcons extends LitElement { 'power', 'privacy', 'qq', + 'ratio', 'reboot', 'recent-calls', 'reduce-db', @@ -217,12 +219,16 @@ export class PanelFontsStarIcons extends LitElement { 'reply-all', 'right-light', 'right', + 'ring-mute-2', + 'ring-mute', + 'ring', 'rocket', 'rotate', 'safe', 'scene', 'screen-projection', 'screen-recording', + 'screen-size', 'screen', 'sd-card', 'search', @@ -230,6 +236,7 @@ export class PanelFontsStarIcons extends LitElement { 'seek-back', 'seek-forward', 'select', + 'selector', 'self-timer', 'send-left', 'send-right', @@ -411,6 +418,8 @@ export class PanelFontsStarIcons extends LitElement { 'update-balance', 'usb', 'user', + 'vibrate-and-ring-mute', + 'vibrate-and-ring', 'vibrate', 'video-mic', 'video-size', diff --git a/src/test/panels/li/li.ts b/src/test/panels/li/li.ts index bc5579f..7c70766 100644 --- a/src/test/panels/li/li.ts +++ b/src/test/panels/li/li.ts @@ -322,13 +322,7 @@ export class PanelLi extends LitElement { variant="circle" vertical > - + - +
+ diff --git a/src/test/panels/select/select.ts b/src/test/panels/select/select.ts new file mode 100644 index 0000000..99656c5 --- /dev/null +++ b/src/test/panels/select/select.ts @@ -0,0 +1,219 @@ +import { + css, + customElement, + html, + LitElement, + CSSResult, +} from '@star-web-components/base' +import '@star-web-components/select' +import {lockscreen, lightmode, darkmode, weibo} from '../li/static/icons' + +@customElement('panel-select') +export class PanelSelect extends LitElement { + render() { + return html` +

star-select

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + alert('selected is:' + e.target.selected)} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${lockscreen} + + + ${lockscreen} + + + ${lockscreen} + + + + + + + + + ${lightmode} + + + + + ${darkmode} + + + + + + + + + ${weibo} + + + ${weibo} + + + + ` + } + public static override get styles(): CSSResult { + return css`` + } +} +declare global { + interface HTMLElementTagNameMap { + 'panel-select': PanelSelect + } +}