Merge branch 'master' of ssh://172.20.184.160:7999/yr/star-web-components into feature-component-prompt
This commit is contained in:
commit
198e08e5fb
|
@ -9,4 +9,11 @@
|
|||
- add indicator-page-point
|
||||
- add blur
|
||||
- add radio
|
||||
- add toast
|
||||
- add toast
|
||||
- add card
|
||||
- add contaienr
|
||||
- add SlotStyleHandler
|
||||
- edit container folder animation
|
||||
- optimize strategy of container
|
||||
- add delay exchange feature
|
||||
- fix bugs of container exchange stradegy
|
||||
|
|
|
@ -10,7 +10,6 @@ export const sharedStyles: CSSResult = css`
|
|||
color: #606266;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
transition: 0.1s;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
@ -83,10 +82,6 @@ export const sharedStyles: CSSResult = css`
|
|||
.secondary:hover {
|
||||
background-color: #D5D5D5;
|
||||
color: #d42222;
|
||||
transition-property: color;
|
||||
transition-duration: 2s;
|
||||
transition-timing-function: linear;
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
.negative {
|
||||
|
|
|
@ -49,7 +49,6 @@ export class StarButton extends LitElement {
|
|||
return html`
|
||||
<button
|
||||
class="disabled ${this.variant} ${this.size} ${this.treatment}"
|
||||
size=${this.size}
|
||||
>
|
||||
${this.label}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
## star-card
|
||||
|
||||
星光 web 组件 --- Card 组件介绍:(2022 年 9 月 02 日)
|
||||
|
||||
star-card 的用途:
|
||||
用于显示图片、文本等简要信息,帮助用户简单直观地获取卡片所在环境的主要信息,卡片组件具备拆卸、跳转页面等功能。
|
||||
|
||||
star-card 类型:
|
||||
1、base
|
||||
具有图片、标题、内容以及页脚几个模块,同时删除base类型对应模块可以转变成其他类型:无图卡片、无页脚卡片等。
|
||||
|
||||
2、linkcard
|
||||
该类型相比base类型多出点击后跳转相应链接的功能。
|
||||
|
||||
3、tupianonly
|
||||
该类型只展现一张正方形图片,用于陈列图片组。
|
||||
|
||||
star-card 其他属性:
|
||||
1、image
|
||||
通过填写图片URL来讲图片展示在卡片上。
|
||||
|
||||
2、heading
|
||||
填写卡片标题以表明该卡片的用途。
|
||||
|
||||
3、subheading
|
||||
简短描述卡片对应的内容,让用户快速获取卡片重要信息。
|
||||
|
||||
4、footer
|
||||
卡片页脚,一般用来填写卡片内容的时间、作者等信息。
|
||||
|
||||
5、link
|
||||
用来填写链接卡片的跳转网址。
|
|
@ -0,0 +1,113 @@
|
|||
import {css, CSSResult} from 'lit'
|
||||
|
||||
export const sharedStyles: CSSResult = css`
|
||||
:host {
|
||||
--background-image-url: ''
|
||||
}
|
||||
|
||||
div {
|
||||
width:200px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border-color: #E6E6E6;
|
||||
border-radius: 4px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
box-sizing: border-box;
|
||||
color: #222222;
|
||||
flex-direction: column;
|
||||
height: atuo;
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
text-decoration-color: #222222;
|
||||
text-decoration-thickness: auto;
|
||||
unicode-bidi: isolate;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.base:hover {
|
||||
background: #E6E6E6;
|
||||
border-color: #B1B1B1;
|
||||
}
|
||||
|
||||
.cardhead {
|
||||
background-image: url(var(--background-image-url));
|
||||
align-items: center;
|
||||
border-bottom-width: 1px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.base-image {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 198px;
|
||||
}
|
||||
|
||||
.cardbody {
|
||||
color: #222222;
|
||||
display: block;
|
||||
height: 80px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
padding-right: 0px;
|
||||
padding-top: 1px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
align-items: baseline;
|
||||
color: #222222;
|
||||
display: flex;
|
||||
height: 18px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
color: #222222;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
margin-top: 6px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.cardfooter {
|
||||
border-color: #E6E6E6;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
display: block;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 1px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.imageonly {
|
||||
background: #B1B1B1;
|
||||
border-color: #000000;
|
||||
border: 10px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-image-outset: 0;
|
||||
}
|
||||
|
||||
.imageonly-image {
|
||||
height: 198px;
|
||||
border: 1px;
|
||||
border-radius: 3px;
|
||||
object-fit: cover;
|
||||
width: 198px;
|
||||
}
|
||||
|
||||
.imageonly:hover {
|
||||
border-color: #E6E6E6;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
html,
|
||||
LitElement,
|
||||
CSSResultArray,
|
||||
HTMLTemplateResult,
|
||||
nothing,
|
||||
} from "lit"
|
||||
import {customElement, property} from "lit/decorators.js"
|
||||
import {sharedStyles} from "./card-styles"
|
||||
|
||||
export enum CardType {
|
||||
BASE = "base",
|
||||
LINKCARD = "linkcard",
|
||||
IMAGEONLY = "imageonly",
|
||||
LABELONLY = "labelonly",
|
||||
FOOTERDELETED = 'footerdeleted',
|
||||
}
|
||||
|
||||
// export enum CardSize {
|
||||
// SMALL = "small",
|
||||
// MEDIUM = "medium",
|
||||
// }
|
||||
|
||||
@customElement("star-card")
|
||||
export class StarCard extends LitElement {
|
||||
public static override get styles(): CSSResultArray {
|
||||
return [sharedStyles]
|
||||
}
|
||||
|
||||
@property({type: String}) type = "base"
|
||||
// @property({type: String}) size = "medium"
|
||||
@property({type: String}) heading = ""
|
||||
@property({type: String}) subheading = ""
|
||||
@property({type: String}) footer = ""
|
||||
@property({type: String}) image = ""
|
||||
@property({type: String}) link = ""
|
||||
|
||||
getBaseCard(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div class="card base">
|
||||
<div class="cardhead">
|
||||
<img class="base-image" src="${this.image}">
|
||||
</div>
|
||||
<div class="cardbody">
|
||||
<h3 class="heading">${this.heading}</h3>
|
||||
<p class="subheading">${this.subheading}</p>
|
||||
</div>
|
||||
<div class="cardfooter">
|
||||
<p class="foooter">${this.footer}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
getLinkCard(): HTMLTemplateResult {
|
||||
return html`
|
||||
<a href=${this.link} target="_blank" style="text-decoration:none;">
|
||||
<div class="card base">
|
||||
<div class="cardhead">
|
||||
<img class="base-image" src="${this.image}">
|
||||
</div>
|
||||
<div class="cardbody">
|
||||
<h3 class="heading">${this.heading}</h3>
|
||||
<p class="subheading">${this.subheading}</p>
|
||||
</div>
|
||||
<div class="cardfooter">
|
||||
<p class="foooter">${this.footer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
}
|
||||
|
||||
getLabelOnlyCard(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div class="card base">
|
||||
<div class="cardbody">
|
||||
<h3 class="heading">${this.heading}</h3>
|
||||
<p class="subheading">${this.subheading}</p>
|
||||
</div>
|
||||
<div class="cardfooter">
|
||||
<p class="foooter">${this.footer}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
getImageOnlyCard(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div class="card imageonly">
|
||||
<div class="cardhead">
|
||||
<img class="imageonly-image" src="${this.image}">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
getFooterDeletedCard(): HTMLTemplateResult {
|
||||
return html `
|
||||
<div class="card base">
|
||||
<div class="cardhead">
|
||||
<img class="base-image" src="${this.image}">
|
||||
</div>
|
||||
<div class="cardbody">
|
||||
<h3 class="heading">${this.heading}</h3>
|
||||
<p class="subheading">${this.subheading}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
render() {
|
||||
switch (this.type) {
|
||||
case CardType.BASE:
|
||||
return this.getBaseCard()
|
||||
case CardType.LINKCARD:
|
||||
return this.getLinkCard()
|
||||
case CardType.IMAGEONLY:
|
||||
return this.getImageOnlyCard()
|
||||
case CardType.LABELONLY:
|
||||
return this.getLabelOnlyCard()
|
||||
case CardType.FOOTERDELETED:
|
||||
return this.getFooterDeletedCard()
|
||||
default:
|
||||
console.error("unhanled 【star-card】 type")
|
||||
return nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"star-card": StarCard
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
# star-container
|
||||
|
||||
容器组件提供了一个便捷易用的拖拽容器组件,可以根据传参不同,将在指定位置生成不同的子节点
|
||||
|
||||
## 组件结构
|
||||
|
||||
1. `container-page`: 分页组件,用于创建、管理、删除容器分页
|
||||
2. `container-child`: 子节点组件,通过传参可以指定节点大小、位置,也用于给图标、组件定位
|
||||
3. `container-folder`: 文件夹组件,用于创建、管理、删除文件夹
|
||||
4. `gesture-manager`: 手势管理,用于提供多指手势判断
|
||||
5. `element-exchange-strategy`: 子节点之间的交换规则
|
||||
|
||||
## 基本用法
|
||||
|
||||
在`HTML`文件下添加该`container`文件即可使用:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="grid-container/container.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<star-container drag-and-drop></star-container>
|
||||
</body>
|
||||
```
|
||||
|
||||
之后可以在`js`中添加子节点:
|
||||
|
||||
```js
|
||||
const container = document.querySelector('star-container')
|
||||
container.appendContainerChild(element)
|
||||
```
|
||||
|
||||
方法`appendContainerChild`可以接收第二个参数`options`,用以定制子节点的位置、大小、添加完成回调、文件名:
|
||||
|
||||
```ts
|
||||
interface options {
|
||||
row: number // 节点占的行数
|
||||
column: number // 节点占的列数
|
||||
anchorCoordinate: {
|
||||
portrait: [number, number] // 竖屏时,节点左上角占据的位置
|
||||
landscape: [number, number] // 横屏时,节点左上角占据的位置
|
||||
}
|
||||
callback: Function // 添加完成回调
|
||||
folderName: string // 该图标所在的文件夹,若文件夹不存在则该图标所在位置自动生成一个文件夹
|
||||
}
|
||||
```
|
||||
|
||||
## 拖拽换位规则
|
||||
|
||||
交换、放置、移动规则,情景重合时,以越下方的规则为准:
|
||||
|
||||
1. 优先级高的可以挤占优先级低的子节点位置(gridId)
|
||||
2. 图标节点、拖拽中的子节点可以挤占同优先级的子节点位置
|
||||
3. 因位置被挤占而需要寻位的元素依照优先级(priority)的降序,依次寻位
|
||||
4. 被挤占节点(A)的寻位时的方向,与挤占节点(B)的 GridID 对比获得:
|
||||
1. A.gridId < B.gridId: 向上
|
||||
2. A.gridId % container.column < B.gridId % container.column:
|
||||
向左
|
||||
3. 以上两条,若其一的条件相反,相对应的方向也取反
|
||||
5. 寻位时,优先尝试水平方向移动,后尝试垂直方向移动
|
||||
6. 寻位方向并非绝对方向,以移动网格数量最短为优先条件
|
||||
7. 为满足规则 5,规则 6 的寻位方向变化以顺时针方向转变,如:上左 → 上右 → 下右 → 下左
|
||||
8. 图标节点被另一个图标节点挤占位置时,仅能再去挤占相邻网格的图标的位置(即不允许挤占上下相邻图标)
|
|
@ -0,0 +1,70 @@
|
|||
import {css} from 'lit'
|
||||
|
||||
export default css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:host #container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
/* 设置三列,每列33.33%宽,也可以用repeat重复 */
|
||||
/* grid-template-columns: 33.33% 33.33% 33.33%; */
|
||||
/* grid-template-columns: repeat(3, 33.33%); */
|
||||
grid-template-rows: repeat(1, 100%);
|
||||
grid-template-columns: repeat(auto-fit, 100%);
|
||||
|
||||
/* 间距,格式为 row column,单值时表示行列间距一致 */
|
||||
gap: 0px;
|
||||
/* 排列格式,此处意为先列后行,不在意元素顺序,尽量塞满每列 */
|
||||
grid-auto-flow: column dense;
|
||||
|
||||
/* 隐式网格宽度,即 grid-template-rows 属性规定列数外的网格列宽 */
|
||||
grid-auto-columns: 100vw;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-page) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
grid-template-rows: repeat(6, 16.66%);
|
||||
grid-auto-flow: row dense;
|
||||
|
||||
grid-auto-rows: 33%;
|
||||
height: 100%;
|
||||
transform: opacity 0.1s;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-child) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-child.dragging) {
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
::slotted(.container-master) {
|
||||
opacity: 1;
|
||||
transform: opacity 0.15s;
|
||||
}
|
||||
::slotted(.gaia-container-page) > .container-master.merging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-child) {
|
||||
height: calc(var(--grid-height) * var(--rows, 1));
|
||||
width: calc(var(--grid-width) * var(--columns, 1));
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
::slotted(.folder) > .gaia-container-child {
|
||||
transition: transform 0.2s, height 0.2s, width 0.2s;
|
||||
}
|
||||
`
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,140 @@
|
|||
type Timer = number | undefined
|
||||
type IndeterHTMLElement = HTMLElement | null
|
||||
export interface ChildElementInfo {
|
||||
pagination?: number
|
||||
row: number
|
||||
column: number
|
||||
folderName?: string
|
||||
anchorCoordinate?: Coordinate
|
||||
callback?: Function
|
||||
}
|
||||
interface ClickInfo {
|
||||
pageX: number
|
||||
pageY: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
translateX?: number
|
||||
translateY?: number
|
||||
}
|
||||
|
||||
interface lastClickInfo extends ClickInfo {
|
||||
timeStamp: number
|
||||
offsetHeight: number
|
||||
offsetWidth: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
[key: string]: number[] | null
|
||||
landscape: number[] | null
|
||||
portrait: number[] | null
|
||||
landscapeGridId: number[] | null
|
||||
portraitGridId: number[] | null
|
||||
}
|
||||
|
||||
export enum STATUS {
|
||||
STATIC = 0, // 静置
|
||||
SWIPE = 1 << 1, // 划动
|
||||
DRAG = 1 << 2, // 拖动元素
|
||||
TURN = 1 << 3, // 翻页
|
||||
SORT = 1 << 4, // 整理
|
||||
OPEN_FORDER = 1 << 5, // 开启文件夹状态
|
||||
}
|
||||
|
||||
export interface DragAndDrop {
|
||||
// Whether drag-and-drop is enabled
|
||||
enabled: boolean
|
||||
|
||||
// The time, in ms, to wait before initiating a drag-and-drop from a
|
||||
// long-press
|
||||
delay: number
|
||||
|
||||
// Timeout used to initiate drag-and-drop actions
|
||||
timeout: Timer
|
||||
|
||||
// The child that was tapped/clicked
|
||||
child: any
|
||||
|
||||
// Whether a drag is active
|
||||
active: boolean
|
||||
|
||||
// The start point of the drag action
|
||||
start: ClickInfo
|
||||
|
||||
// The last point of the drag action
|
||||
last: lastClickInfo
|
||||
|
||||
// Timeout used to send drag-move events
|
||||
moveTimeout: Timer
|
||||
|
||||
// The last time a move event was fired
|
||||
lastMoveEventTime: number
|
||||
|
||||
// Whether to capture the next click event
|
||||
clickCapture: boolean
|
||||
|
||||
// 下一个兄弟节点,用于跨页移动失败后返回原位置时定位用
|
||||
nextSibling: IndeterHTMLElement
|
||||
previousSibling: IndeterHTMLElement
|
||||
|
||||
// 是否正在跨页
|
||||
isSpanning: boolean
|
||||
|
||||
// 正被悬浮覆盖的元素
|
||||
dropTarget: IndeterHTMLElement
|
||||
|
||||
// 最后一个悬浮经过的元素
|
||||
lastDropChild: any
|
||||
pagination: number
|
||||
top: number
|
||||
left: number
|
||||
|
||||
// 中心坐标
|
||||
gridPosition: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Container extends HTMLElement {
|
||||
name: string
|
||||
row: number
|
||||
column: number
|
||||
_frozen: boolean
|
||||
_pendingStateChanges: Function[]
|
||||
_children: any[]
|
||||
_dnd: DragAndDrop
|
||||
// 当前所显示的页面页码
|
||||
pagination: number
|
||||
// 组件高度,也是页面高度,用于子组件越界判断
|
||||
height: number
|
||||
width: number
|
||||
// 页面列表
|
||||
pages: any
|
||||
// 滑动偏移量
|
||||
_offsetX: number
|
||||
// 本次触摸滑动距离
|
||||
distance: number
|
||||
// 是否进入整理模式
|
||||
_sortMode: boolean
|
||||
// 状态
|
||||
_status: typeof STATUS[keyof typeof STATUS]
|
||||
// 交换元素位置时,无法进行交换的元素
|
||||
_staticElements: HTMLElement[]
|
||||
// 用于首尾页划动的橡皮绳效果
|
||||
ratio: number
|
||||
// 合并成文件夹计时器
|
||||
mergeTimer: number | undefined
|
||||
// 文件夹
|
||||
folders: {[folderName: string]: any}
|
||||
pageHeight: number
|
||||
pageWidth: number
|
||||
gridWidth: number
|
||||
gridHeight: number
|
||||
status: number
|
||||
openedFolder: any
|
||||
|
||||
reorderChild: Function
|
||||
getChildByElement: Function
|
||||
}
|
|
@ -0,0 +1,644 @@
|
|||
import GaiaContainer from './container'
|
||||
import GaiaContainerChild from './gaia-container-child'
|
||||
type PlacedRecorder = {
|
||||
[pagination: number]: {[placedGridId: number]: GaiaContainerChild}
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换、放置、移动规则,情景重合时,以越下方的规则为准:
|
||||
* 1. 优先级高的可以挤占优先级低的子节点位置(gridId)
|
||||
* 2. 图标节点、拖拽中的子节点可以挤占同优先级的子节点位置
|
||||
* 3. 因位置被挤占而需要寻位的元素依照优先级(priority)的降序,依次寻位
|
||||
* 4. 被挤占节点(A)的寻位时的方向,与挤占节点(B)的GridID对比获得:
|
||||
* 1) A.gridId < B.gridId: 向上
|
||||
* 2) A.gridId % container.column < B.gridId % container.column:
|
||||
* 向左
|
||||
* 3) 以上两条,若其一的条件相反,相对应的方向也取反
|
||||
* 5. 寻位时,优先尝试水平方向移动,后尝试垂直方向移动
|
||||
* 6. 寻位方向并非绝对方向,以移动网格数量最短为优先条件
|
||||
* 7. 为满足规则5,规则6的寻位方向变化以顺时针方向转变,如:上左 → 上右 → 下右 → 下左
|
||||
* 8. 图标节点被另一个图标节点挤占位置时,仅能再去挤占相邻网格的图标的位置(即不允许挤占上下相邻图标)
|
||||
*/
|
||||
|
||||
enum Directions {
|
||||
BACKWARD = 1,
|
||||
FORWARD = -1,
|
||||
UPWARD = -1,
|
||||
DOWN = 1,
|
||||
}
|
||||
export default class ExchangeStrategy {
|
||||
manager: GaiaContainer
|
||||
// 待加入网格的 container child
|
||||
suspendChild: Set<GaiaContainerChild>[] = []
|
||||
coordinateBak: GaiaContainer['childCoordinate'] | undefined
|
||||
// 已做过移动的子节点表
|
||||
placedChild: Set<GaiaContainerChild> = new Set()
|
||||
// 移动记录
|
||||
placedRecorder: PlacedRecorder = {}
|
||||
constructor(manager: GaiaContainer) {
|
||||
this.manager = manager
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.suspendChild = []
|
||||
this.coordinateBak = undefined
|
||||
this.placedChild = new Set()
|
||||
this.placedRecorder = {}
|
||||
}
|
||||
|
||||
// 网格记录表,记录每个网格上放置的子元素
|
||||
get coordinate() {
|
||||
if (!this.coordinateBak) {
|
||||
this.coordinateBak = []
|
||||
this.manager.childCoordinate.forEach(
|
||||
(recorder, pagination) =>
|
||||
(this.coordinateBak![pagination] = {...recorder})
|
||||
)
|
||||
}
|
||||
return this.coordinateBak
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网格记录表中移除指定网格单元的子节点的记录
|
||||
* @param gridId 网格号
|
||||
* @param recorder 指定页面的网格记录表
|
||||
*/
|
||||
pickChild(
|
||||
child: GaiaContainerChild,
|
||||
recorder: {
|
||||
[gridId: number]: GaiaContainerChild | undefined
|
||||
}
|
||||
) {
|
||||
const {row, column} = child
|
||||
const gridId = child.gridId
|
||||
if (recorder[gridId] !== child) return false
|
||||
|
||||
for (let i = 0; i < row; i++) {
|
||||
for (let j = 0; j < column; j++) {
|
||||
delete recorder[gridId + j + i * this.manager.column]
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一群子节点在网格记录表中的记录
|
||||
*/
|
||||
pickChildren(childrenArr: Set<GaiaContainerChild>[], pagination: number) {
|
||||
const recorder = this.coordinate[pagination]
|
||||
for (const children of childrenArr) {
|
||||
if (!children) continue
|
||||
for (const child of [...children]) {
|
||||
if (!this.pickChild(child, recorder)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 将子节点放置到指定网格中
|
||||
* @param child 子节点
|
||||
* @param gridId 网格号
|
||||
* @param pagination 页码
|
||||
*/
|
||||
placeChild(child: GaiaContainerChild, gridId: number, pagination: number) {
|
||||
const recorder = this.coordinate[pagination]
|
||||
const {row, column} = child
|
||||
for (let i = 0; i < row; i++) {
|
||||
for (let j = 0; j < column; j++) {
|
||||
recorder[gridId + j + i * this.manager.column] = child
|
||||
}
|
||||
}
|
||||
this.placedChild.add(child)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交换节点的移动方向
|
||||
* @param child 交换节点
|
||||
* @param dropChild 落点节点
|
||||
* @param directed 与拖拽节点直接接触节点的移动方向,暂时仅用于图标间的交换
|
||||
* @returns
|
||||
*/
|
||||
getExchangeDirection(
|
||||
child: GaiaContainerChild,
|
||||
dropChild: GaiaContainerChild | undefined,
|
||||
directed?: [boolean, boolean]
|
||||
): [Directions, Directions] {
|
||||
if (!dropChild) return [0, 0]
|
||||
const mColumn = this.manager.column
|
||||
let horizontalDir, verticalDir
|
||||
if (child.priority + dropChild.priority == 2) {
|
||||
// 两个图标在进行交换
|
||||
horizontalDir = directed
|
||||
? directed[0]
|
||||
? Directions.FORWARD
|
||||
: Directions.BACKWARD
|
||||
: child.gridId < dropChild.gridId
|
||||
? Directions.BACKWARD
|
||||
: Directions.FORWARD
|
||||
|
||||
// 当垂直方向为0时,比起上下移动,更优先横向、换行移动
|
||||
verticalDir = 0
|
||||
} else {
|
||||
// 小组件参与交换
|
||||
horizontalDir =
|
||||
child.gridId % mColumn <= dropChild.gridId % mColumn
|
||||
? Directions.BACKWARD
|
||||
: Directions.FORWARD
|
||||
verticalDir =
|
||||
child.gridId > dropChild.gridId ? Directions.UPWARD : Directions.DOWN
|
||||
}
|
||||
|
||||
return [horizontalDir, verticalDir]
|
||||
}
|
||||
|
||||
allowPlace(
|
||||
child: GaiaContainerChild,
|
||||
comparisonArr: Set<GaiaContainerChild>[] | undefined,
|
||||
mode: 'move' | 'place' = 'move'
|
||||
) {
|
||||
if (!comparisonArr || !comparisonArr.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (let priority = comparisonArr.length - 1; priority > 1; priority--) {
|
||||
if (comparisonArr[priority]?.size) {
|
||||
if (
|
||||
(priority == child.priority && mode == 'move') ||
|
||||
priority > child.priority
|
||||
) {
|
||||
return false
|
||||
} else if (priority < child.priority) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
concatSuspendingChild(
|
||||
targetArr: GaiaContainerChild[][] | Set<GaiaContainerChild>[]
|
||||
) {
|
||||
targetArr.forEach((children, priority) => {
|
||||
if (!this.suspendChild[priority]) {
|
||||
this.suspendChild[priority] = new Set(children)
|
||||
} else {
|
||||
children.forEach((child) => {
|
||||
this.suspendChild[priority].add(child)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return this.suspendChild
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断并处理子节点处于指定网格时的右、下边界越界情况
|
||||
*/
|
||||
handleCrossField = (
|
||||
gridId: number,
|
||||
child: GaiaContainerChild | undefined
|
||||
) => {
|
||||
const result = {
|
||||
right: false,
|
||||
bottom: false,
|
||||
recommended: gridId,
|
||||
}
|
||||
if (!child || child.priority == 1) return result
|
||||
const mColumn = this.manager.column
|
||||
|
||||
// 拖拽中的子节点右下角单元格所在的网格ID
|
||||
let edge_bottom_right =
|
||||
gridId + child.column - 1 + (child.row - 1) * mColumn
|
||||
if (edge_bottom_right % mColumn < gridId % mColumn) {
|
||||
// 右边界越界
|
||||
result.right = true
|
||||
gridId -= (edge_bottom_right % mColumn) + 1
|
||||
edge_bottom_right -= (edge_bottom_right % mColumn) + 1
|
||||
}
|
||||
|
||||
if (edge_bottom_right > 23) {
|
||||
// 下边界越界
|
||||
result.bottom = true
|
||||
gridId -= Math.ceil((edge_bottom_right - 23) / mColumn) * mColumn
|
||||
}
|
||||
result.recommended = gridId
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照指定方向移动被遮盖的元素
|
||||
* @param dropChild 要移动的元素
|
||||
* @param direction 移动的方向组
|
||||
* @param pagination 页码
|
||||
* @param hasBeenTried 已经尝试过的网格
|
||||
* @returns 是否移动成功
|
||||
*/
|
||||
moveChild(
|
||||
dropChild: GaiaContainerChild,
|
||||
direction: [Directions, Directions],
|
||||
pagination: number,
|
||||
hasBeenTried = new Set([dropChild.gridId])
|
||||
): boolean {
|
||||
// 待尝试的网格
|
||||
let willBeenTried: Set<number> = new Set()
|
||||
// 因节点的优先级不够而无法放置的网格
|
||||
let preserve: Set<number> = new Set()
|
||||
// 优先级足以放置的网格
|
||||
let access: Set<number> = new Set()
|
||||
const recorder = this.coordinate[pagination]
|
||||
const mColumn = this.manager.column
|
||||
this.pickChild(dropChild, recorder)
|
||||
// 以二维网格的方式遍历格子
|
||||
const BFS = (
|
||||
origin: number,
|
||||
[horizontalDir, verticalDir]: [Directions, Directions],
|
||||
callback: () => number[][] | undefined
|
||||
) => {
|
||||
const threshold = origin % mColumn
|
||||
// 象限旋转方法,是否为0来决定方向 direction 逆时针变换时采用的变换矩阵顺序
|
||||
const quadrantType = horizontalDir + verticalDir
|
||||
const horizontalConvert = [-1, 1]
|
||||
const verticalConvert = [1, -1]
|
||||
const convertOrder =
|
||||
quadrantType !== 0
|
||||
? [[1, 1], horizontalConvert, verticalConvert, horizontalConvert]
|
||||
: [[1, 1], verticalConvert, horizontalConvert, verticalConvert]
|
||||
let methods: number[][] | undefined
|
||||
// 以 origin 为中心、step 为步长、顺时针沿着象限、先行后列收集范围内的网格ID
|
||||
for (let step = 1; !methods?.length && step < 10; step++) {
|
||||
let dir = [...direction]
|
||||
// 顺时针
|
||||
for (let time = 0; time < 4; time++) {
|
||||
const convert = convertOrder[time]
|
||||
const hdir = dir[0] * convert[0]
|
||||
const vdir = dir[1] * convert[1]
|
||||
dir = [hdir, vdir]
|
||||
// 沿着行
|
||||
for (let i = 0; i <= step; i++) {
|
||||
// 沿着列
|
||||
for (let j = 0; j > -1; j++) {
|
||||
if (i + j > step) break
|
||||
if (i + j !== step) continue
|
||||
|
||||
const horizontalStep = (hdir > 0 ? 1 : -1) * i
|
||||
const verticalStep = (vdir > 0 ? 1 : -1) * j * mColumn
|
||||
const testId = origin + horizontalStep + verticalStep
|
||||
|
||||
if (hasBeenTried.has(testId) || willBeenTried.has(testId))
|
||||
continue
|
||||
|
||||
if (
|
||||
((hdir > 0 && testId % mColumn >= threshold) || // 向后移动,测试网格取余后比原处取余大
|
||||
(hdir < 0 && testId % mColumn <= threshold)) && // 向前移动,测试网格取余后比原处取余小
|
||||
testId >= 0 &&
|
||||
testId <= 23
|
||||
) {
|
||||
willBeenTried.add(testId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
methods = callback()
|
||||
}
|
||||
return methods?.length ? methods : undefined
|
||||
}
|
||||
// 以一维轴的方式遍历格子
|
||||
const BFSinAxios = (origin: number, direction: number) => {
|
||||
let gridId: number | undefined
|
||||
const ergodic = (direction: number) => {
|
||||
for (
|
||||
let i = 1, testId = origin + i * direction;
|
||||
testId < 24 && testId > -1;
|
||||
i++, testId = origin + i * direction
|
||||
) {
|
||||
// 获取测试网格之中的子节点
|
||||
const dropGrid = this.coordinate[pagination][testId]
|
||||
|
||||
if (
|
||||
!dropGrid || // 测试网格为空格子时
|
||||
(!this.placedChild.has(dropGrid) && dropGrid.priority < 2) // 子节点为图标节点,且未移动过
|
||||
) {
|
||||
gridId = testId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
ergodic(direction)
|
||||
if (gridId === undefined) {
|
||||
ergodic(-1 * direction)
|
||||
}
|
||||
return gridId !== undefined ? [[gridId]] : gridId
|
||||
}
|
||||
|
||||
const place = () => {
|
||||
let methods: number[][] = []
|
||||
for (const grid of [...willBeenTried]) {
|
||||
hasBeenTried.add(grid)
|
||||
|
||||
let gridMethods = this.getAllPlacedMethods(
|
||||
dropChild,
|
||||
grid,
|
||||
pagination,
|
||||
preserve,
|
||||
access
|
||||
)
|
||||
|
||||
if (gridMethods.length) {
|
||||
for (let grids of gridMethods) {
|
||||
const dropChildren = this.getChildrenByGridArea(
|
||||
grids[0],
|
||||
dropChild.row,
|
||||
dropChild.column,
|
||||
pagination,
|
||||
dropChild.priority
|
||||
)
|
||||
if (dropChildren.canPlace) {
|
||||
methods.push(...gridMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
willBeenTried = new Set()
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
const chooseMethod = (methods: number[][]) => {
|
||||
let gridId: number | undefined
|
||||
let step = 9
|
||||
|
||||
if (methods) {
|
||||
for (const method of methods) {
|
||||
const diff = Math.abs(method[0] - dropChild.gridId)
|
||||
const methodStep = (diff % mColumn) + Math.floor(diff / mColumn)
|
||||
// 保证取所有方法中移动步数最少的
|
||||
if (step >= methodStep) {
|
||||
step = methodStep
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
Math.floor(method[0] / mColumn) ==
|
||||
Math.floor(dropChild.gridId / mColumn) // 优先选择同行位移(gridId 左右移动)
|
||||
) {
|
||||
gridId = method[0]
|
||||
// break
|
||||
} else if (
|
||||
Math.abs(method[0] - dropChild.gridId) % mColumn ==
|
||||
0 // 其次选择同列位移(gridId 上下移动)
|
||||
) {
|
||||
gridId = method[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
gridId = gridId ?? methods[0][0]
|
||||
|
||||
return gridId
|
||||
}
|
||||
const methods = direction[1]
|
||||
? BFS(dropChild.gridId, direction, place)
|
||||
: BFSinAxios(dropChild.gridId, direction[0])
|
||||
const gridId = methods ? chooseMethod(methods) : undefined
|
||||
if (typeof gridId == 'number') {
|
||||
if (this.placedRecorder[pagination]) {
|
||||
this.placedRecorder[pagination][gridId] = dropChild
|
||||
} else {
|
||||
this.placedRecorder[pagination] = {[gridId]: dropChild}
|
||||
}
|
||||
const conflictChildren = this.getChildrenByGridArea(
|
||||
gridId,
|
||||
dropChild.row,
|
||||
dropChild.column,
|
||||
pagination,
|
||||
dropChild.priority
|
||||
)
|
||||
this.placedChild.add(dropChild)
|
||||
this.pickChildren(conflictChildren.dropChildren, pagination)
|
||||
this.concatSuspendingChild(conflictChildren.dropChildren)
|
||||
this.placeChild(dropChild, gridId, pagination)
|
||||
}
|
||||
|
||||
return gridId !== undefined ? true : false
|
||||
}
|
||||
|
||||
/**
|
||||
* 假设子节点在指定网格时,其占据的所有网格ID,无第二个参数时,即
|
||||
* 获取子节点实际占据的网格
|
||||
*/
|
||||
getGridIds(child: GaiaContainerChild, assumeId: number | undefined) {
|
||||
if (assumeId === undefined || child.priority == 1) {
|
||||
assumeId = assumeId ?? child.gridId
|
||||
} else if (assumeId < 0 || assumeId > 23) {
|
||||
throw new Error('assumeId invalid')
|
||||
} else {
|
||||
// 使用假设ID时需要进行越界检查
|
||||
const crossFildeResult = this.handleCrossField(assumeId, child)
|
||||
if (crossFildeResult.bottom || crossFildeResult.right) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
let gridIds = []
|
||||
for (let i = 0; i < child.row; i++) {
|
||||
for (let j = 0; j < child.column; j++) {
|
||||
gridIds.push(assumeId + j + i * this.manager.column)
|
||||
}
|
||||
}
|
||||
|
||||
return gridIds
|
||||
}
|
||||
|
||||
/**
|
||||
* 以指定网格为中心,子节点可放置的所有可行方案
|
||||
* 输出每种方案所占据的所有网格ID集合的数组
|
||||
* @param child
|
||||
* @param grid
|
||||
* @param preserve
|
||||
* @returns
|
||||
*/
|
||||
getAllPlacedMethods(
|
||||
child: GaiaContainerChild,
|
||||
grid: number,
|
||||
pagination: number,
|
||||
preserve: Set<number>,
|
||||
access: Set<number>
|
||||
): number[][] {
|
||||
let methods: number[][] = []
|
||||
const recorder = this.coordinate[pagination]
|
||||
|
||||
for (let j = child.column - 1; j > -1; j--) {
|
||||
for (let i = child.row - 1; i > -1; i--) {
|
||||
// 挨个尝试放置节点
|
||||
const testId = grid - j - i * this.manager.column
|
||||
if (testId < 0 || testId > 23) continue
|
||||
const method = this.getGridIds(child, testId)
|
||||
|
||||
if (method.length) {
|
||||
let flag = true
|
||||
for (const grid of method) {
|
||||
if (preserve.has(grid)) {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
if (!access.has(grid)) {
|
||||
const dropChild = recorder[grid]
|
||||
|
||||
if (
|
||||
!dropChild ||
|
||||
(!this.placedChild.has(dropChild) &&
|
||||
dropChild.priority < child.priority)
|
||||
) {
|
||||
access.add(grid)
|
||||
} else {
|
||||
preserve.add(grid)
|
||||
flag = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
flag && methods.push(method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试将子节点放置于指定网格上
|
||||
* @param child 将要安放的子节点
|
||||
* @param gridId 将要安放的网格ID
|
||||
* @param pagination 将要安放的页面
|
||||
* @returns 安放是否成功
|
||||
*/
|
||||
tryToPlace(
|
||||
child: GaiaContainerChild,
|
||||
gridId: number,
|
||||
pagination: number,
|
||||
mode: 'place' | 'move',
|
||||
direction: [boolean, boolean]
|
||||
): {
|
||||
canPlace: boolean
|
||||
placedRecorder?: PlacedRecorder
|
||||
childCoordinate?: GaiaContainer['childCoordinate']
|
||||
} {
|
||||
const {column: mColumn} = this.manager
|
||||
// 子节点右下角单元格所在的网格ID
|
||||
const edge_bottom_right =
|
||||
gridId + child.column - 1 + (child.row - 1) * mColumn
|
||||
this.reset()
|
||||
// 越界判断
|
||||
if (
|
||||
edge_bottom_right > 23 || // 下边界越界
|
||||
edge_bottom_right % mColumn < gridId % mColumn // 右边界越界
|
||||
) {
|
||||
return {canPlace: false}
|
||||
}
|
||||
|
||||
const recorder = this.coordinate[pagination]
|
||||
this.placedRecorder = {}
|
||||
this.placedChild.add(child)
|
||||
this.pickChild(child, recorder)
|
||||
const dropChild = recorder[gridId]
|
||||
// 获取被挤占位置的元素,按照优先级排序
|
||||
const pickChildren = this.getChildrenByGridArea(
|
||||
gridId,
|
||||
child.row,
|
||||
child.column,
|
||||
pagination,
|
||||
child.priority,
|
||||
'place'
|
||||
)!
|
||||
// 检查该子节点与被挤占位置的元素之间的优先级,若存在优先级高的元素,该次放置失败
|
||||
const canPlace = this.allowPlace(child, pickChildren.dropChildren, mode)
|
||||
if (!canPlace || !pickChildren.canPlace) {
|
||||
return {canPlace: false}
|
||||
}
|
||||
|
||||
// 被挤出原本位置的元素
|
||||
this.suspendChild = pickChildren.dropChildren ?? []
|
||||
this.pickChildren(this.suspendChild, pagination)
|
||||
|
||||
// 放置该子节点
|
||||
this.placeChild(child, gridId, pagination)
|
||||
|
||||
// 尝试移动并放置被挤出去的子节点
|
||||
try {
|
||||
for (let i = 0; i < this.suspendChild.length; i++) {
|
||||
let j = this.suspendChild.length - i
|
||||
let suspendArr = this.suspendChild[j]
|
||||
if (suspendArr?.size) {
|
||||
suspendArr.forEach((suspendChild) => {
|
||||
const exchangeDirection = this.getExchangeDirection(
|
||||
child,
|
||||
suspendChild,
|
||||
direction
|
||||
)
|
||||
if (!this.moveChild(suspendChild, exchangeDirection, pagination)) {
|
||||
// 移动失败
|
||||
throw suspendChild
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (placeFaild) {
|
||||
console.info('try to place failed!')
|
||||
return {canPlace: false}
|
||||
}
|
||||
|
||||
return {
|
||||
canPlace: true,
|
||||
placedRecorder: this.placedRecorder,
|
||||
childCoordinate: this.coordinate,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定网格区域内的自由子节点(此次放置操作中未移动过网格位置的节点)
|
||||
* 但如果将要被拿出的子节点是已经移动过位置的,则拿出失败
|
||||
* 若要被拿出的子节点优先级高于传参priority,也拿出失败
|
||||
*/
|
||||
getChildrenByGridArea(
|
||||
gridId: number,
|
||||
row: number,
|
||||
column: number,
|
||||
pagination: number,
|
||||
priority: number = 0,
|
||||
mode: 'move' | 'place' = 'move'
|
||||
) {
|
||||
const recorder = this.coordinate[pagination]
|
||||
let children: Set<GaiaContainerChild>[] = []
|
||||
let canPlace = true
|
||||
for (let i = 0; i < row; i++) {
|
||||
for (let j = 0; j < column && canPlace; j++) {
|
||||
const targetGrid = gridId + j + i * this.manager.column
|
||||
const child = recorder[targetGrid]
|
||||
|
||||
if (child) {
|
||||
if (
|
||||
this.placedChild.has(child) ||
|
||||
child.priority > priority ||
|
||||
(mode == 'move' &&
|
||||
child.priority == priority &&
|
||||
child.priority != 1)
|
||||
) {
|
||||
canPlace = false
|
||||
break
|
||||
}
|
||||
if (children[child.priority]) {
|
||||
children[child.priority].add(child)
|
||||
} else {
|
||||
children[child.priority] = new Set([child])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {dropChildren: children, canPlace}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
@startuml exchange
|
||||
|
||||
StarContainer -> tryToPlace: 尝试将拖拽节点放置到指定网格区域中
|
||||
tryToPlace -> getDropChildrenByGridID: 获取网格区域中的子节点集
|
||||
getDropChildrenByGridID --> tryToPlace: 返回一个按优先级顺序排序的二维数组
|
||||
tryToPlace -> allowPlace: 检查拖拽节点能否放置
|
||||
alt 将占据的网格中存在更高优先级的节点
|
||||
allowPlace --> tryToPlace: 无法放置
|
||||
tryToPlace --> StarContainer: 不提供放置换位策略
|
||||
else 可以放置
|
||||
allowPlace --> tryToPlace: 可以放置
|
||||
tryToPlace -> pickChildren: 从网格记录表中摘除被挤占位置的节点记录
|
||||
tryToPlace -> pickChild: 从网格记录表中摘除拖拽节点记录
|
||||
tryToPlace -> placeChild: 放置拖拽节点
|
||||
tryToPlace -> getExchangeDirection: 被挤占节点获取寻位方向
|
||||
getExchangeDirection --> tryToPlace: 返回一个表示方向的数组
|
||||
tryToPlace -> moveChild: 按优先级从大到小地遍历被挤占节点,并尝试位移
|
||||
note over moveChild
|
||||
为确保能保证移动格数最小,
|
||||
以步进、顺时针沿象限、先行后列地BFS遍历
|
||||
出被挤占节点可达网格
|
||||
endnote
|
||||
moveChild -> getAllPlacedMethods: 获取节点放置在可达网格上的方法
|
||||
alt 存在放置方法
|
||||
getAllPlacedMethods --> moveChild: 返回一个
|
||||
moveChild -> getDropChildrenByGridID: 获取方法提供区域中的所有子节点
|
||||
else 不存在放置方法
|
||||
moveChild --> tryToPlace: 位移失败
|
||||
tryToPlace --> StarContainer: 不提供换位策略
|
||||
end
|
||||
end
|
||||
|
||||
@enduml
|
|
@ -0,0 +1,349 @@
|
|||
import GaiaContainer from './container'
|
||||
import {Coordinate} from './contianer-interface'
|
||||
/**
|
||||
* Grid 布局
|
||||
* 组件属性:
|
||||
* master: 占位元素,也用于定位静置时的组件实际位置
|
||||
* container: 显示元素,用于放置组件内容——element,也用于拖动时显示组件拖动位置
|
||||
* element: 组件实际内容,通过构造传参传入
|
||||
*
|
||||
* 考虑:
|
||||
* 1. 当为小组件时,插入 container 后需要锚固
|
||||
* 2. 锚固分 landscape 和 portrait 两种方向
|
||||
* 3. TBD:注意,需要考虑旋转屏幕后组件位置冲突问题,当旋转到某一方向时需要越界判断,越界
|
||||
* 时解除所有组件锚固状态重新排列并更新锚固位置
|
||||
*/
|
||||
const defaultCoordinate: Coordinate = {
|
||||
landscape: null,
|
||||
portrait: null,
|
||||
landscapeGridId: null,
|
||||
portraitGridId: null,
|
||||
}
|
||||
|
||||
type anchorType = 'recorder' | 'current'
|
||||
|
||||
export default class GaiaContainerChild {
|
||||
_element: HTMLElement | null
|
||||
_container: HTMLElement | null = null
|
||||
_master: HTMLElement | null = null
|
||||
isWidget: boolean
|
||||
isFolder: boolean = false
|
||||
row: number
|
||||
column: number
|
||||
manager: GaiaContainer
|
||||
_isStatic: boolean | string
|
||||
anchorCoordinate: Coordinate
|
||||
// 静态位置
|
||||
_lastMasterTop: number | null = null
|
||||
_lastMasterLeft: number | null = null
|
||||
_lastElementOrder: string | null = null
|
||||
_lastElementDisplay: string | null = null
|
||||
_lastElementHeight: number | null = null
|
||||
_lastElementWidth: number | null = null
|
||||
|
||||
_lastElementTop: number | null = null
|
||||
_lastElementLeft: number | null = null
|
||||
// 状态计时器
|
||||
removed: number | undefined = undefined
|
||||
added: number | undefined = undefined
|
||||
// 优先级 = rows × columns
|
||||
priority: number = 1
|
||||
// 网格位置
|
||||
preGridId: number = 0
|
||||
curGridId: number = -1
|
||||
// 中心位置
|
||||
center: {x: number; y: number} = {x: 0, y: 0}
|
||||
|
||||
constructor(
|
||||
element: HTMLElement | null,
|
||||
row: number = 1,
|
||||
column: number = 1,
|
||||
anchorCoordinate: Coordinate | undefined,
|
||||
manager: GaiaContainer
|
||||
) {
|
||||
this._element = element
|
||||
this.isWidget = element?.tagName === 'GAIA-WIDGET'
|
||||
this.row = row
|
||||
this.column = column
|
||||
this.priority = row * column
|
||||
this.manager = manager
|
||||
this._isStatic = false
|
||||
this.anchorCoordinate = anchorCoordinate ?? defaultCoordinate // 两种屏幕方向的锚固坐标
|
||||
this.markDirty()
|
||||
}
|
||||
|
||||
rotate() {
|
||||
const orientation = screen.orientation.type.split('-')[0]
|
||||
|
||||
// 如果没有锚固坐标,则先解除锚固以自适应
|
||||
if (this.anchorCoordinate[orientation]) {
|
||||
this.isStatic = false
|
||||
|
||||
this.setArea()
|
||||
}
|
||||
// this.synchroniseMaster();
|
||||
this.isStatic = true
|
||||
}
|
||||
|
||||
get gridId() {
|
||||
return this.curGridId
|
||||
}
|
||||
|
||||
set gridId(value: number) {
|
||||
// if (value !== this.curGridId) {
|
||||
const orientation = screen.orientation.type.split('-')[0]
|
||||
const rowStart = Math.floor(value / this.manager.column) + 1
|
||||
const columnStart = (value % this.manager.column) + 1
|
||||
this.anchorCoordinate[orientation] = [rowStart, columnStart]
|
||||
this.anchor()
|
||||
// }
|
||||
}
|
||||
|
||||
get pagination() {
|
||||
for (let i = 0; i < this.manager.pages.length; i++) {
|
||||
if (this.manager.pages[i].compareDocumentPosition(this.master) & 16) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Can not find pagination`)
|
||||
}
|
||||
|
||||
get position() {
|
||||
if (!this.master.parentElement) return 'unsettled'
|
||||
|
||||
const inPage = this.master.parentElement.classList.contains(
|
||||
'gaia-container-page'
|
||||
)
|
||||
return inPage ? 'page' : 'folder'
|
||||
}
|
||||
|
||||
get folderName() {
|
||||
const name = this.master.parentElement?.dataset.name
|
||||
return name || ''
|
||||
}
|
||||
|
||||
get element() {
|
||||
return this._element
|
||||
}
|
||||
|
||||
get isStatic() {
|
||||
return this._isStatic
|
||||
}
|
||||
|
||||
set isStatic(value) {
|
||||
!!value ? this.anchor() : this.loosen()
|
||||
if (!this._isStatic && value) {
|
||||
;(this.element as HTMLElement).dispatchEvent(
|
||||
new CustomEvent('anchor', {
|
||||
detail: {
|
||||
anchorCoordinate: this.anchorCoordinate,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
this._isStatic = !!value
|
||||
}
|
||||
|
||||
// 是否是页面最后一个组件
|
||||
get isTail() {
|
||||
const page = this.manager.pages[this.pagination]
|
||||
return page.lastChild === this.master
|
||||
}
|
||||
|
||||
/**
|
||||
* 按记录的锚固坐标,将元素锚固到 Grid 网格中
|
||||
*/
|
||||
anchor(type: anchorType = 'recorder') {
|
||||
const area = this.getArea(type)
|
||||
if (!area) return
|
||||
|
||||
const [rowStart, columnStart] = area
|
||||
|
||||
const rowEnd = rowStart + this.row
|
||||
const columnEnd = columnStart + this.column
|
||||
this.center.x = columnStart + this.column / 2 - 1
|
||||
this.center.y = rowStart + this.row / 2 - 1
|
||||
|
||||
this.master.style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`
|
||||
;(this._element as HTMLElement).dataset.static = type
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素所在 Grid 网格区域
|
||||
* @param {String} type 以何种方式获取网格区域: recorder 从记录中,current 从目前位置
|
||||
*/
|
||||
getArea(type: anchorType = 'recorder') {
|
||||
const orientation = screen.orientation.type.split('-')[0]
|
||||
|
||||
if (type === 'recorder' && this.anchorCoordinate[orientation]) {
|
||||
return this.anchorCoordinate[orientation]
|
||||
}
|
||||
|
||||
const unitHeight = this.master.offsetHeight / this.row
|
||||
const unitWidth = this.master.offsetWidth / this.column
|
||||
const offsetTop = Math.abs(this.master.offsetTop)
|
||||
const offsetLeft = Math.abs(
|
||||
this.master.offsetLeft - this.pagination * this.manager.pageHeight
|
||||
)
|
||||
const rowStart = Math.floor(offsetTop / unitHeight) + 1
|
||||
const columnStart = Math.floor(offsetLeft / unitWidth) + 1
|
||||
|
||||
return [rowStart, columnStart]
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置组件在 Grid 布局中的位置
|
||||
* @param {String} type 以何种方式设置网格区域: recorder 从记录中,current 从目前位置
|
||||
*/
|
||||
setArea(type: anchorType = 'recorder') {
|
||||
const orientation = screen.orientation.type.split('-')[0]
|
||||
this.anchorCoordinate[orientation] = this.getArea(type)
|
||||
return this.anchorCoordinate[orientation]
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除元素的锚固
|
||||
*/
|
||||
loosen() {
|
||||
const orientation = screen.orientation.type.split('-')[0]
|
||||
this.anchorCoordinate[orientation] = null
|
||||
this.master.style.gridArea = 'unset'
|
||||
this.master.style.gridRowStart = `span ${this.row}`
|
||||
this.master.style.gridColumnStart = `span ${this.column}`
|
||||
;(this._element as HTMLElement).dataset.static = String(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* The element that will contain the child element and control its position.
|
||||
*/
|
||||
get container() {
|
||||
if (!this._container) {
|
||||
// Create container
|
||||
let container = document.createElement('div')
|
||||
container.classList.add('gaia-container-child')
|
||||
container.style.position = 'absolute'
|
||||
container.style.top = '0'
|
||||
container.style.left = '0'
|
||||
|
||||
// container.style.height = height + 'px';
|
||||
// container.style.width = width + 'px';
|
||||
container.style.setProperty('--columns', String(this.column))
|
||||
container.style.setProperty('--rows', String(this.row))
|
||||
|
||||
container.appendChild(this._element as HTMLElement) //this.element是div.icon-container
|
||||
|
||||
this._container = container
|
||||
}
|
||||
return this._container
|
||||
}
|
||||
|
||||
changeSize(container = this.container) {
|
||||
const {height, width} = this.master.getBoundingClientRect()
|
||||
container.style.height = height + 'px'
|
||||
container.style.width = width + 'px'
|
||||
this.synchroniseContainer()
|
||||
}
|
||||
|
||||
/**
|
||||
* The element that will be added to the container that will
|
||||
* control the element's transform.
|
||||
*/
|
||||
get master() {
|
||||
if (!this._master) {
|
||||
// Create master
|
||||
let master = document.createElement('div')
|
||||
master.style.gridRowStart = `span ${this.row}`
|
||||
master.style.gridColumnStart = `span ${this.column}`
|
||||
master.style.height = '100%'
|
||||
master.style.width = '100%'
|
||||
master.className = 'container-master'
|
||||
master.appendChild(this.container)
|
||||
this._master = master
|
||||
}
|
||||
return this._master
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any cached style properties. To be used if elements are
|
||||
* manipulated outside of the methods of this object.
|
||||
*/
|
||||
markDirty() {
|
||||
this._lastElementWidth = null
|
||||
this._lastElementHeight = null
|
||||
this._lastElementDisplay = null
|
||||
this._lastElementOrder = null
|
||||
this._lastMasterTop = null
|
||||
this._lastMasterLeft = null
|
||||
|
||||
this._lastElementTop = null
|
||||
this._lastElementLeft = null
|
||||
}
|
||||
|
||||
get pageOffsetX() {
|
||||
if (!this.master.parentElement) return 0
|
||||
return this.master.parentElement.offsetLeft
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise the size of the master with the managed child element.
|
||||
*/
|
||||
synchroniseMaster() {
|
||||
let master = this.master
|
||||
let element = this.element
|
||||
|
||||
let style = window.getComputedStyle(element as HTMLElement)
|
||||
let display = style.display
|
||||
let order = style.order
|
||||
let width = (element as HTMLElement).offsetWidth
|
||||
let height = (element as HTMLElement).offsetHeight
|
||||
|
||||
if (
|
||||
this._lastElementWidth !== width ||
|
||||
this._lastElementHeight !== height ||
|
||||
this._lastElementDisplay !== display ||
|
||||
this._lastElementOrder !== order
|
||||
) {
|
||||
this._lastElementWidth = width
|
||||
this._lastElementHeight = height
|
||||
this._lastElementDisplay = display
|
||||
this._lastElementOrder = order
|
||||
|
||||
// master.style.width = width + "px";
|
||||
// master.style.height = height + "px";
|
||||
master.style.display = display
|
||||
master.style.order = order
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise the container's transform with the position of the master.
|
||||
*/
|
||||
synchroniseContainer(isActive = false) {
|
||||
let master = this.master
|
||||
let container = this.container
|
||||
let top = master.offsetTop
|
||||
let left = master.offsetLeft
|
||||
const position = this.position
|
||||
// 左上角的 GridID
|
||||
const gridId =
|
||||
position == 'page'
|
||||
? this.manager.getGridIdByCoordinate(left, top, this.pagination)
|
||||
: this.manager.getFolderGridIdByCoordinate(left, top, this.pagination)
|
||||
if (gridId !== this.curGridId) {
|
||||
this.manager.recordCoordinate(this, this.pagination, gridId)
|
||||
this.preGridId = this.curGridId
|
||||
this.curGridId = gridId
|
||||
}
|
||||
|
||||
if (this._lastMasterTop !== top || this._lastMasterLeft !== left) {
|
||||
this._lastMasterTop = top
|
||||
this._lastMasterLeft = left
|
||||
this._lastElementLeft = this.element?.offsetLeft!
|
||||
this._lastElementHeight = this.element?.offsetHeight!
|
||||
!isActive &&
|
||||
!this.container.classList.contains('dragging') &&
|
||||
(container.style.transform = 'translate(' + left + 'px, ' + top + 'px)')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,559 @@
|
|||
import GaiaContainerChild from './gaia-container-child'
|
||||
import GaiaContainer from './container'
|
||||
|
||||
/**
|
||||
* 主屏文件夹,只允许 App 图标进入,且文件夹内图标数量大于2时才稳定存在
|
||||
*/
|
||||
|
||||
export default class GaiaContainerFolder extends GaiaContainerChild {
|
||||
// 文件夹名称
|
||||
name: string
|
||||
// 图标 TagName
|
||||
iconName: string = 'gaia-app-icon'
|
||||
// 图标隐藏标题属性
|
||||
hideAttrName = 'hide-subtitle'
|
||||
// 文件夹子节点
|
||||
_children: GaiaContainerChild[] = []
|
||||
isFolder: boolean = true
|
||||
// 文件夹开启状态
|
||||
_status: number = 0
|
||||
// 文件夹处于图标状态的大小
|
||||
folderIconWidth: number = 0
|
||||
// 待添加文件
|
||||
suspendElement: HTMLElement[] = []
|
||||
_id: string
|
||||
// 文件夹名元素
|
||||
_title: HTMLElement | null = null
|
||||
// 开启文件夹动画计时器
|
||||
openTimer: number | undefined = undefined
|
||||
// 子节点坐标
|
||||
childCoordinate: {[gridId: number]: GaiaContainerChild | undefined}[] = [{}]
|
||||
gridHeight: number = 0
|
||||
gridWidth: number = 0
|
||||
|
||||
constructor(manager: GaiaContainer, name?: string) {
|
||||
super(null, 1, 1, undefined, manager)
|
||||
this.name = this.checkAndGetFolderName(name)
|
||||
this._id = `folder-${new Date().getTime()}`
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.manager.editStyle(
|
||||
'GaiaContainerFolder',
|
||||
GaiaContainerFolder.shadowStyle()
|
||||
)
|
||||
this.container.addEventListener('touchstart', this)
|
||||
this.container.addEventListener('touchmove', this)
|
||||
this.container.addEventListener('touchend', this)
|
||||
this.element.dataset.name = this.name
|
||||
this.master.className = 'folder initializing'
|
||||
this.master.id = this._id
|
||||
this.master.addEventListener(
|
||||
'animationend',
|
||||
() => {
|
||||
this.master.classList.remove('initializing')
|
||||
// NOTE: 避免同步 synchroniseContainer 产生不必要的动画
|
||||
this.container.style.setProperty('transition', 'unset')
|
||||
this.synchroniseContainer()
|
||||
setTimeout(() => this.container.style.removeProperty('transition'))
|
||||
},
|
||||
{once: true}
|
||||
)
|
||||
this.container.appendChild(this.title)
|
||||
this.container.style.width = this.manager.gridWidth + 'px'
|
||||
}
|
||||
|
||||
resize() {
|
||||
let element = this.element
|
||||
this._lastElementTop = element?.offsetTop!
|
||||
this._lastElementLeft = element?.offsetLeft!
|
||||
|
||||
this.gridHeight = this._children[0]._lastElementHeight!
|
||||
this.gridWidth = this._children[0]._lastElementWidth!
|
||||
}
|
||||
|
||||
get element() {
|
||||
if (
|
||||
!this._element ||
|
||||
!this._element.classList.contains('gaia-container-folder')
|
||||
) {
|
||||
const element = document.createElement('div')
|
||||
|
||||
element.classList.add('gaia-container-folder')
|
||||
this.folderIconWidth = this.manager.gridWidth * 0.6
|
||||
element.style.width = this.folderIconWidth + 'px'
|
||||
element.style.height = this.folderIconWidth + 'px'
|
||||
|
||||
this._element = element
|
||||
}
|
||||
return this._element
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (!this._title || this._title.innerHTML !== this.name) {
|
||||
this._title?.remove()
|
||||
this._title = document.createElement('div')
|
||||
this._title.innerHTML = this.name
|
||||
this._title.classList.add('folder-title')
|
||||
this.container.appendChild(this._title)
|
||||
}
|
||||
return this._title
|
||||
}
|
||||
|
||||
get container() {
|
||||
if (!this._container) {
|
||||
// Create container
|
||||
let container = document.createElement('div')
|
||||
container.classList.add('gaia-container-child')
|
||||
container.style.position = 'absolute'
|
||||
container.style.top = '0'
|
||||
container.style.left = '0'
|
||||
|
||||
container.style.height = this.manager.gridHeight + 'px'
|
||||
container.style.width = this.manager.gridWidth + 'px'
|
||||
|
||||
container.appendChild(this.element) //this.element是div.icon-container
|
||||
|
||||
this._container = container
|
||||
this.master.appendChild(container)
|
||||
}
|
||||
return this._container
|
||||
}
|
||||
|
||||
get children() {
|
||||
return this._children.map((child) => child.element)
|
||||
}
|
||||
|
||||
showIconsSubtitle(element: HTMLElement) {
|
||||
element.removeAttribute(this.hideAttrName)
|
||||
}
|
||||
|
||||
hideIconsSubtitle(element: HTMLElement) {
|
||||
element.setAttribute(this.hideAttrName, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 将子节点从容器组件的节点表中挪到该文件夹的节点表中
|
||||
* @param {HTMLElement} element master
|
||||
*/
|
||||
movein(element: HTMLElement) {
|
||||
let target
|
||||
let targetIndex
|
||||
this.manager._children.forEach((child: GaiaContainerChild, i) => {
|
||||
if (child.master == element) {
|
||||
target = child
|
||||
targetIndex = i
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (!this.master.parentElement && target && targetIndex) {
|
||||
// 无父节点即处于文件夹创建阶段,此时文件夹要代替
|
||||
// element 在容器组件子节点表中的位置
|
||||
this.manager.reorderChild(
|
||||
this.element,
|
||||
(target as GaiaContainerChild).element!
|
||||
)
|
||||
targetIndex++
|
||||
}
|
||||
|
||||
typeof targetIndex == 'number' &&
|
||||
this.manager._children.splice(targetIndex, 1)
|
||||
}
|
||||
|
||||
addAppIcon(element: HTMLElement, shouldOpen = false) {
|
||||
const child = this.manager.getChildByElement(element)
|
||||
|
||||
this._children.push(child!)
|
||||
if (!this._status) {
|
||||
this.hideIconsSubtitle(child?.element!)
|
||||
} else {
|
||||
this.showIconsSubtitle(child?.element!)
|
||||
}
|
||||
|
||||
if (!this._status && shouldOpen) {
|
||||
this.suspendElement.push(element)
|
||||
this.open()
|
||||
} else {
|
||||
this.movein(element)
|
||||
this.element.appendChild(element)
|
||||
}
|
||||
}
|
||||
|
||||
removeAppIcon(node: GaiaContainerChild | HTMLElement) {
|
||||
let removeChild = node
|
||||
if (node instanceof HTMLElement) {
|
||||
this._children = this._children.filter((child) => {
|
||||
if (child.master == node && child.container == node) {
|
||||
removeChild = child
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if (!removeChild) return null
|
||||
this.showIconsSubtitle((removeChild as GaiaContainerChild).element!)
|
||||
this.manager._children.push(removeChild as GaiaContainerChild)
|
||||
return removeChild
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件夹名是否存在,存在则替换成 ‘新建文件夹n’ 样式的名字,
|
||||
* 未传入文件名,则生成一个不重复的文件夹名
|
||||
*/
|
||||
checkAndGetFolderName(folderName?: string, nth?: number): string {
|
||||
const folders = this.manager.folders
|
||||
if (!folderName) {
|
||||
nth = nth ? ++nth : 1
|
||||
folderName = '新建文件夹'
|
||||
}
|
||||
|
||||
const name = folderName + (nth ? nth : '')
|
||||
if (!folders[name]) {
|
||||
return name
|
||||
}
|
||||
|
||||
return this.checkAndGetFolderName(undefined, nth)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启文件夹,以父组件大小的形式展示,显示所有图标的 subtitle
|
||||
*/
|
||||
open() {
|
||||
if (this._status) return
|
||||
this._status = 1
|
||||
this.master.classList.add('openning')
|
||||
this._children.forEach((child) => this.showIconsSubtitle(child.element!))
|
||||
this.manager.status |= 16
|
||||
this.manager.openedFolder = this
|
||||
this.container.style.height = '100%'
|
||||
this.container.style.width = '100%'
|
||||
this.master.style.setProperty('z-index', String(10))
|
||||
this.container.style.removeProperty('--grid-height')
|
||||
this.container.style.removeProperty('--grid-width')
|
||||
|
||||
this.element.addEventListener('transitionend', this.openTransition)
|
||||
}
|
||||
|
||||
openTransition = (evt: TransitionEvent) => {
|
||||
if (evt.target == this.element && evt.propertyName == 'height') {
|
||||
this._children.forEach((child) => child.synchroniseContainer())
|
||||
let element = this.element
|
||||
this._lastElementTop = element?.offsetTop!
|
||||
this._lastElementLeft = element?.offsetLeft!
|
||||
}
|
||||
|
||||
if (
|
||||
this._children[this._children.length - 1].master.compareDocumentPosition(
|
||||
evt.target as HTMLElement
|
||||
) & 16
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.container.style.setProperty('--folder-element-left', '0px')
|
||||
this.container.style.setProperty('--folder-element-top', '0px')
|
||||
this.openTimer = setTimeout(() => {
|
||||
this.master.classList.remove('openning')
|
||||
this.master.classList.add('opened')
|
||||
let element = this.suspendElement.shift()
|
||||
while (element) {
|
||||
this.movein(element)
|
||||
this.element.appendChild(element)
|
||||
element = this.suspendElement.shift()
|
||||
}
|
||||
this.gridHeight = this._children[0]._lastElementHeight!
|
||||
this.gridWidth = this._children[0]._lastElementWidth!
|
||||
})
|
||||
|
||||
this.element.removeEventListener('transitionend', this.openTransition)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭文件夹,以父组件网格单元大小显示,同时作为文件夹销毁的入口,
|
||||
* 当应用图标数量不足时,销毁该文件夹
|
||||
*/
|
||||
close() {
|
||||
if (!this._status) return
|
||||
clearTimeout(this.openTimer)
|
||||
this._children = this._children.filter((child) => {
|
||||
if (child.container.classList.contains('dragging')) {
|
||||
this.removeAppIcon(child)
|
||||
return false
|
||||
} else {
|
||||
this.hideIconsSubtitle(child.element!)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
this.master.classList.remove('openning')
|
||||
this.master.classList.remove('opened')
|
||||
this.master.classList.add('closing')
|
||||
this.element.removeEventListener('transitionend', this.openTransition)
|
||||
|
||||
this.manager.status &= ~16
|
||||
this.manager.openedFolder = null
|
||||
this.container.style.height = this.manager.gridHeight + 'px'
|
||||
this.container.style.width = this.manager.gridWidth + 'px'
|
||||
this.element.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
this._status = 0
|
||||
this._children.forEach((child) => child.synchroniseContainer())
|
||||
this.container.style.setProperty(
|
||||
'--folder-element-left',
|
||||
this.element.offsetLeft + 'px'
|
||||
)
|
||||
this.container.style.setProperty(
|
||||
'--folder-element-top',
|
||||
this.element.offsetTop + 'px'
|
||||
)
|
||||
this.master.style.removeProperty('z-index')
|
||||
this.master.classList.remove('closing')
|
||||
if (this._children.length <= 1) {
|
||||
this.destroy()
|
||||
}
|
||||
this.gridHeight = this._children[0]._lastElementHeight!
|
||||
this.gridWidth = this._children[0]._lastElementWidth!
|
||||
},
|
||||
{once: true}
|
||||
)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._children.length > 0) {
|
||||
return
|
||||
}
|
||||
const {height: originHeight, width: originWidth} =
|
||||
this.element.getBoundingClientRect()
|
||||
const {height: targetHeight, width: targetWidth} =
|
||||
this.element.getBoundingClientRect()
|
||||
|
||||
const child = this._children[0]
|
||||
const master = this._children[0].master
|
||||
this.manager._children.push(child)
|
||||
const childContainer = master.querySelector(
|
||||
'.gaia-container-child'
|
||||
) as HTMLElement
|
||||
|
||||
this.element.style.height = originHeight + 'px'
|
||||
this.element.style.width = originWidth + 'px'
|
||||
this.element.classList.add('scaling') // 剩下的唯一一个图标放大至原来大小展示
|
||||
this.master.classList.add('destroying') // 文件夹背景缩小消失
|
||||
|
||||
// nextTick,用以配合 originXXX 形成动画
|
||||
setTimeout(() => {
|
||||
this.showIconsSubtitle(this._children[0].element!)
|
||||
this.element.style.height = targetHeight + 'px'
|
||||
this.element.style.width = targetWidth + 'px'
|
||||
})
|
||||
this.element.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
this.master.style.position = 'absolute'
|
||||
;(this.master.parentElement as HTMLElement).insertBefore(
|
||||
master,
|
||||
this.master
|
||||
)
|
||||
child.synchroniseContainer()
|
||||
childContainer.style.transition = 'unset'
|
||||
this.element.classList.remove('scaling')
|
||||
|
||||
this.manager.dispatchEvent(
|
||||
new CustomEvent('folder-destroy', {
|
||||
detail: this,
|
||||
composed: true,
|
||||
})
|
||||
)
|
||||
|
||||
setTimeout(() => childContainer.style.removeProperty('transition'))
|
||||
},
|
||||
{once: true}
|
||||
)
|
||||
}
|
||||
|
||||
handleEvent(evt: TouchEvent) {
|
||||
switch (evt.type) {
|
||||
case 'touchend':
|
||||
if (this._status && evt.target === this.container) {
|
||||
this.close()
|
||||
}
|
||||
case 'touchstart':
|
||||
case 'touchmove':
|
||||
if (
|
||||
this._status &&
|
||||
(evt.target as HTMLElement).tagName !== 'SITE-ICON'
|
||||
) {
|
||||
evt.preventDefault()
|
||||
evt.stopImmediatePropagation()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static shadowStyle() {
|
||||
return `
|
||||
::slotted(.gaia-container-child) {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
::slotted(.gaia-container-folder) {
|
||||
display: grid;
|
||||
position: unset;
|
||||
grid-template-rows: repeat(4, 25%);
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
grid-auto-flow: row dense;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 0 0px 3px rgba(255,255,255,0.5);
|
||||
border-radius: 5px;
|
||||
transition: transform 0.2s, box-shadow 0.2s, height 0.2s, width 0.2s !important;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-folder::before) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-folder) .gaia-container-child {
|
||||
height: 12.5% !important;
|
||||
width: 12.5% !important;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-folder) gaia-app-icon {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-folder::after) {
|
||||
content: '';
|
||||
z-index: 99;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
}
|
||||
::slotted(.folder.opened) .gaia-container-folder::after {
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
::slotted( ) .folder:not(.opened) .gaia-container-folder .gaia-container-child:not(.dragging) {
|
||||
position: unset !important;
|
||||
}
|
||||
::slotted(.folder.openning) .gaia-container-folder {
|
||||
position: raletive;
|
||||
}
|
||||
::slotted(.folder.initializing) .gaia-container-child {
|
||||
position: unset !important;
|
||||
transform: unset !important;
|
||||
transition: unset !important;
|
||||
}
|
||||
::slotted(.folder.initializing) .container-master {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::slotted(.folder.initializing) {
|
||||
animation: folder-fadein 0.3s cubic-bezier(.08,.82,.17,1);
|
||||
}
|
||||
|
||||
::slotted(.folder.opened) .gaia-container-folder {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
::slotted(.folder) .container-master {
|
||||
pointer-events: none;
|
||||
}
|
||||
::slotted(.folder.opened) .container-master {
|
||||
pointer-events: unset;
|
||||
}
|
||||
::slotted(.destroying) .gaia-container-folder {
|
||||
display: block;
|
||||
}
|
||||
::slotted(.destroying) .gaia-container-folder::before {
|
||||
transform-origin: bottom right;
|
||||
transform: scale(0);
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
::slotted(.destroying) .folder-title {
|
||||
opacity: 0;
|
||||
}
|
||||
::slotted(.scaling) .gaia-container-child {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
::slotted(.folder-title) {
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
margin-top: 3px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
::slotted(.folder.opened) .folder-title,
|
||||
::slotted(.folder.openning) .folder-title {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::slotted(.gaia-container-folder) .gaia-container-child {
|
||||
display: unset !important;
|
||||
}
|
||||
::slotted(.folder.openning) > .gaia-container-child,
|
||||
::slotted(.folder.opened) > .gaia-container-child {
|
||||
left: calc(var(--pagination) * var(--page-width)) !important;
|
||||
transform: translate(0) !important;
|
||||
}
|
||||
|
||||
::slotted(.folder.openning) > .gaia-container-child {
|
||||
transform: translate(0) !important;
|
||||
}
|
||||
::slotted(.folder.openning) .gaia-container-child {
|
||||
transition: left 0.2s, height 0.2s, width 0.2s !important;
|
||||
}
|
||||
|
||||
::slotted(.folder.openning) > .gaia-container-child,
|
||||
::slotted(.folder.closing) > .gaia-container-child {
|
||||
transition: transform 0.2s, left 0.2s, height 0.2s, width 0.2s !important;
|
||||
}
|
||||
|
||||
::slotted(.folder) > .gaia-container-child.dragging {
|
||||
transition: unset !important;
|
||||
}
|
||||
|
||||
::slotted(.folder.opened) .gaia-container-folder,
|
||||
::slotted(.folder.openning) .gaia-container-folder {
|
||||
height: 50% !important;
|
||||
max-height: 500px;
|
||||
width: 50% !important;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
::slotted(.scaling) {
|
||||
transform-origin: top left;
|
||||
position: unset !important;
|
||||
transition: width 0.5s !important;
|
||||
}
|
||||
|
||||
::slotted(.folder.merging) .folder-title {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes folder-fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import GaiaContainer from './container'
|
||||
|
||||
class GaiaContainerPage {
|
||||
_pages: HTMLElement[] = [] // 存储所有添加进 gaia-container 的页面
|
||||
// 等待被移除的页面,仅在编辑、拖拽时出现,若结束前两种状态时仍然没有子节点,则删除
|
||||
_suspending: HTMLElement | null = null
|
||||
_manager: GaiaContainer
|
||||
observerCallback: MutationCallback
|
||||
constructor(manager: GaiaContainer) {
|
||||
this._manager = manager
|
||||
|
||||
this._manager.addEventListener('statuschange', () => {
|
||||
// gaia-container 退出拖拽模式,且有待删除页面
|
||||
if (!(this._manager._status & 2) && this._suspending) {
|
||||
this.deletePage(this._suspending)
|
||||
}
|
||||
})
|
||||
|
||||
this.observerCallback = (mutations: MutationRecord[]) => {
|
||||
for (const mutation of mutations) {
|
||||
if (!(mutation.target as HTMLElement).children.length) {
|
||||
const page = mutation.target as HTMLElement
|
||||
const container = page.parentElement as any
|
||||
|
||||
page.classList.add('removed')
|
||||
const callback = () => {
|
||||
if (!container || !page.dataset.page) return
|
||||
if (
|
||||
container.pagination == page.dataset.page &&
|
||||
+page.dataset.page > 0
|
||||
) {
|
||||
container.smoothSlide(this._pages[--container.pagination])
|
||||
}
|
||||
|
||||
if (this.editMode) {
|
||||
this._suspending = page
|
||||
} else {
|
||||
this.deletePage(page)
|
||||
}
|
||||
}
|
||||
container && callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let proxy = new Proxy(this, {
|
||||
get: (obj, property) => {
|
||||
if (typeof property == 'string' && +property < obj._pages.length) {
|
||||
return obj._pages[+property]
|
||||
}
|
||||
return (obj as any)[property]
|
||||
},
|
||||
})
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
addPage = () => {
|
||||
const div = document.createElement('div')
|
||||
const pagination = `${this._pages.length}`
|
||||
div.className = `gaia-container-page`
|
||||
div.style.setProperty('--pagination', pagination)
|
||||
this._pages.push(div)
|
||||
div.dataset.page = `${this._pages.length - 1}`
|
||||
this.observe(div)
|
||||
this._manager.childCoordinate[this._pages.length - 1] = {}
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
get editMode() {
|
||||
return this._manager._status & 2 || this._manager._status & 8
|
||||
}
|
||||
|
||||
observe = (page: HTMLElement) => {
|
||||
let observe = new MutationObserver(this.observerCallback)
|
||||
observe.observe(page, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
}
|
||||
|
||||
deletePage = (page: HTMLElement) => {
|
||||
if (page.children.length) return
|
||||
let index = this._pages.indexOf(page)
|
||||
|
||||
if (this.editMode && index == this.length - 1) {
|
||||
// 处于拖拽状态时,尾页不被删除
|
||||
this._suspending = page
|
||||
return
|
||||
}
|
||||
delete this._pages[index]
|
||||
if (index > -1) {
|
||||
page?.remove?.()
|
||||
let flag = false
|
||||
let coordinates: GaiaContainer['childCoordinate'] = []
|
||||
this._pages = this._pages.filter((page, i) => {
|
||||
if (flag) {
|
||||
;(page.dataset.page as any) = --i
|
||||
page.style.setProperty('--pagination', String(i))
|
||||
}
|
||||
if (i == index) flag = true
|
||||
coordinates[i] = this._manager.childCoordinate[i - +flag]
|
||||
return
|
||||
})
|
||||
this._manager.childCoordinate = coordinates
|
||||
}
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._pages.length
|
||||
}
|
||||
|
||||
forEach = (callback: Function) => {
|
||||
const paginations = this._pages.length
|
||||
for (let i = 0; i < paginations; i++) {
|
||||
callback(this._pages[i], i, this._pages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GaiaContainerPage
|
|
@ -0,0 +1,71 @@
|
|||
import GaiaContainer from './container'
|
||||
|
||||
class GestureManager {
|
||||
element: GaiaContainer
|
||||
touches: Touch[] = []
|
||||
swipeTimer: number | undefined = undefined
|
||||
velocity: number = 0.3
|
||||
duration: number = 10
|
||||
recorder: number = 0
|
||||
swipDirection: string = ''
|
||||
|
||||
constructor(element: GaiaContainer) {
|
||||
this.element = element
|
||||
this.listeneTouch()
|
||||
}
|
||||
|
||||
listeneTouch() {
|
||||
this.element.addEventListener('touchstart', this)
|
||||
this.element.addEventListener('touchmove', this)
|
||||
this.element.addEventListener('touchend', this)
|
||||
}
|
||||
|
||||
handleEvent(evt: TouchEvent) {
|
||||
switch (evt.type) {
|
||||
case 'touchstart':
|
||||
Array.prototype.forEach.call(evt.changedTouches, (touch: Touch) => {
|
||||
;(touch as any).timeStamp = evt.timeStamp
|
||||
this.touches[touch.identifier] = touch
|
||||
})
|
||||
break
|
||||
case 'touchmove':
|
||||
this.detectHorizonSwipe(evt)
|
||||
break
|
||||
case 'touchend':
|
||||
Array.prototype.forEach.call(
|
||||
evt.changedTouches,
|
||||
(touch) => delete this.touches[touch.identifier]
|
||||
)
|
||||
this.dispatchGesture()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
detectHorizonSwipe(event: TouchEvent) {
|
||||
const {changedTouches, timeStamp: curTime} = event
|
||||
Array.prototype.forEach.call(changedTouches, (touch) => {
|
||||
const {identifier, pageX: curX} = touch
|
||||
const {pageX: preX, timeStamp: preTime} = this.touches[identifier] as any
|
||||
const velocity = (curX - preX) / (curTime - preTime)
|
||||
this.swipDirection = velocity < 0 ? 'swipeleft' : 'swiperight'
|
||||
this.recorder = velocity
|
||||
|
||||
// TBD:暂停翻页动画,再划动手指时会出现速度计算异常
|
||||
if (Math.abs(velocity) > this.velocity) {
|
||||
clearTimeout(this.swipeTimer)
|
||||
this.swipeTimer = setTimeout(
|
||||
() => (this.swipeTimer = undefined),
|
||||
this.duration
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
dispatchGesture() {
|
||||
if (this.swipeTimer) {
|
||||
this.element.dispatchEvent(new Event(this.swipDirection))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GestureManager
|
|
@ -0,0 +1,9 @@
|
|||
import {css} from 'lit'
|
||||
|
||||
export default css`
|
||||
:host {
|
||||
/* 图标大小 */
|
||||
--icon-size: 108px;
|
||||
--icon-size: 50%;
|
||||
}
|
||||
`
|
|
@ -27,6 +27,7 @@ export class StarLi extends LitElement {
|
|||
@property({type: String}) switchcolor = ''
|
||||
@property({type: Boolean}) disabled = false
|
||||
@property({type: Boolean}) checked = false
|
||||
@property({type: Boolean}) switchicon = false
|
||||
@property({type: String}) size = ''
|
||||
|
||||
getbase(): HTMLTemplateResult {
|
||||
|
@ -232,6 +233,7 @@ export class StarLi extends LitElement {
|
|||
?checked="${this.checked}"
|
||||
switchcolor=${this.switchcolor}
|
||||
size=${this.size}
|
||||
?switchicon="${this.switchicon}"
|
||||
></star-switch>
|
||||
</a>
|
||||
`
|
||||
|
@ -244,6 +246,7 @@ export class StarLi extends LitElement {
|
|||
?checked="${this.checked}"
|
||||
switchcolor=${this.switchcolor}
|
||||
size=${this.size}
|
||||
?switchicon="${this.switchicon}"
|
||||
></star-switch>
|
||||
</a>
|
||||
`}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# starwebcomponents——overflowmenu
|
||||
|
||||
## 介绍
|
||||
|
||||
### overflowmenu:溢出菜单。
|
||||
|
||||
## 新需求(主页面要求——罗 9.5)
|
||||
1. 外部颜色控制(思路:使用自定义css样式,如: --test-color:XXX p{color: --test-color},通过修改自定义css样式的值达到从外部修改组件颜色)
|
||||
2. 弹出菜单时的越界判断,包括主、副屏切换时的图标定位以及旋转屏幕时的定位
|
||||
思路:
|
||||
(1)首先获取button在屏幕显示的left、right、top和bottom值以及menu的width和height
|
||||
(2)对于右侧边界:right >= width ? true则menu的left = button的left : false则menu的right = button的right
|
||||
(3)对于下边界:bottom >= height ? true则menu的top = button的bottom : false则menu的bottom = button的top
|
||||
3. 外部控制接口,事件还是属性(暂定)
|
||||
4. 弹出的菜单绑定在父节点上以供调用,减少重复使用(思路:后续通过overlay组件实现)
|
||||
|
||||
|
||||
## 问题(9.6)
|
||||
1. 首次点击最右侧的按钮是获取到的菜单宽度和高度与实际不符:(该问题已消失,但不知道为何消失)
|
||||
2. 点击空白处无法关闭菜单栏(解决方法:将点击事件绑定在父容器中)
|
||||
|
||||
|
||||
## 新要求:(9.7)
|
||||
(1)将不需要修改的“var”变量声明变成“const”(已修改)
|
||||
(2)变量命名要直观且有解释(已修改变量命名规范并添加对应注释)
|
||||
(3)点击一个按钮后其余按钮应关闭(方法同(1))
|
||||
(4)可以将slot增加名称从而将div以删除
|
||||
(5)定位方式修改为相对定位,将将fixed改为relative,达到适应效果
|
||||
(6)控制菜单栏宽度,菜单栏中star-ul中的ul标签负责扩充大小,修改其width值
|
|
@ -0,0 +1,106 @@
|
|||
import {LitElement, html, HTMLTemplateResult, CSSResultArray} from 'lit'
|
||||
import {customElement, property, queryAssignedElements} from 'lit/decorators.js'
|
||||
import '../button/button'
|
||||
import {sharedStyles} from './overflowmenustyle'
|
||||
|
||||
@customElement('star-overflowmenu')
|
||||
export class StarOverflowMenu extends LitElement {
|
||||
public static override get styles(): CSSResultArray {
|
||||
return [sharedStyles]
|
||||
}
|
||||
|
||||
@property({type: String}) type = 'base'
|
||||
@property({type: String}) size = 'medium'
|
||||
@property({type: String}) icon = ''
|
||||
@property({type: String}) label = ''
|
||||
@property({type: Boolean}) disabled = false
|
||||
@property({type: Boolean, reflect: true}) open = false
|
||||
|
||||
//获取slot元素
|
||||
@queryAssignedElements({flatten: true})
|
||||
_evenEl: any
|
||||
|
||||
_getElement() {
|
||||
// 获取网页宽度用于判断菜单显示位置是否越界
|
||||
const bodywidth = document.documentElement.clientWidth
|
||||
const bodyheight = document.documentElement.clientHeight
|
||||
// 获取菜单所在div,用于控制menu显示或隐藏,ts默认使用Element,需转换为HTMLElement
|
||||
const mu = this.renderRoot.querySelector("#menuitem") as HTMLElement
|
||||
// 获取star-button相对屏幕的位置
|
||||
const buttonposition = this.renderRoot
|
||||
.querySelector('star-button')
|
||||
?.getBoundingClientRect()
|
||||
// star-button的top、bottom、left及right值
|
||||
const buttontop = Number(buttonposition?.top)
|
||||
const buttonbottom = Number(buttonposition?.bottom)
|
||||
const buttonleft = Number(buttonposition?.left)
|
||||
const buttonright = Number(buttonposition?.right)
|
||||
// 通过“open”判断是否显示menu
|
||||
if (this.open == true) {
|
||||
for (var i = 0; i < this._evenEl.length; i++) {
|
||||
const slotelement = this._evenEl[i]
|
||||
// 设置div显示display状态
|
||||
mu.style.display = "block"
|
||||
// 设置显示位置类型
|
||||
// this._evenEl[i].style.position = 'fixed'
|
||||
slotelement.style.position = 'relative'
|
||||
this.open = false
|
||||
// 获取溢出菜单width及height
|
||||
const menuwidth = slotelement.getBoundingClientRect().width
|
||||
const menuheight = slotelement.getBoundingClientRect().height
|
||||
// 弹出菜单边界,rightline和bottomline分别为是否超过右侧和下侧显示区域
|
||||
const rightline = (buttonright + menuwidth > bodywidth)?true:false
|
||||
const bottomline = (buttonbottom + menuheight > bodyheight)?true:false
|
||||
// 右下角边界
|
||||
if (rightline && bottomline) {
|
||||
slotelement.style.left = -(menuwidth - (buttonright - buttonleft)) + 'px'
|
||||
slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px'
|
||||
return
|
||||
} else if (rightline) {
|
||||
// 右侧边界
|
||||
slotelement.style.right = menuwidth - (buttonright - buttonleft) + 'px'
|
||||
return
|
||||
} else if (bottomline) {
|
||||
// 下侧边界
|
||||
slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px'
|
||||
return
|
||||
} else {
|
||||
// 正常情况
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < this._evenEl.length; i++) {
|
||||
mu.style.display = "none"
|
||||
this.open = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._getElement()
|
||||
}
|
||||
|
||||
getBaseMenu(): HTMLTemplateResult {
|
||||
return html`
|
||||
<star-button
|
||||
type="icononly"
|
||||
icon=${this.icon}
|
||||
@click=${this._getElement}
|
||||
></star-button>
|
||||
<div id="menuitem">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.getBaseMenu()
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'star-overflowmenu': StarOverflowMenu
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import {css, CSSResult} from 'lit'
|
||||
|
||||
export const sharedStyles: CSSResult = css`
|
||||
:host {
|
||||
width: auto;
|
||||
display: block;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
#menuitem {
|
||||
width: auto;
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
star-button {
|
||||
display: block;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
::slotted(star-ul) {
|
||||
margin: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
`
|
|
@ -33,7 +33,7 @@ export class StarAnimateSection extends LitElement {
|
|||
position: absolute;
|
||||
width: 100%; /* 100vw会有x轴溢出 */
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
/* height: calc(100vh + 1px); */ /* 手动制造溢出 */
|
||||
animation-duration: 0.3s;
|
||||
background-color: #f0f0f0;
|
||||
|
|
|
@ -1,12 +1,38 @@
|
|||
# 滑块-Slider
|
||||
|
||||
工作职责:
|
||||
|
||||
- 滑块空间
|
||||
|
||||
说明:
|
||||
- | 用途:分割位置
|
||||
## 类型包括:
|
||||
|
||||
1. 默认滑块
|
||||
|
||||
类型包括:
|
||||
```
|
||||
<star-slider></star-slider>
|
||||
```
|
||||
|
||||
- 左侧图标|滑块|右侧图标
|
||||
2. 滑块中小球左侧进行填充 --- `filled`
|
||||
|
||||
```
|
||||
<star-slider variant="filled"></star-slider>
|
||||
```
|
||||
|
||||
3. 禁用滑块 --- `disabled`
|
||||
|
||||
```
|
||||
<star-slider disabled></star-slider>
|
||||
```
|
||||
|
||||
4. 分格滑块 --- `Tick`
|
||||
|
||||
```
|
||||
<star-slider variant="tick" tick-step="20"></star-slider>
|
||||
<star-slider variant="tick" tick-step="20" disabled></star-slider>
|
||||
```
|
||||
|
||||
5. 左侧图标|滑块|右侧图标
|
||||
|
||||
```
|
||||
<star-slider ><slot></slot></star-slider>
|
||||
```
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import {css, CSSResult} from 'lit'
|
||||
export const sharedStyles: CSSResult = css`
|
||||
:host {
|
||||
--cover-width: 100px;
|
||||
--dot-move: 87px;
|
||||
}
|
||||
.content {
|
||||
margin: 5px 5px;
|
||||
position: relative;
|
||||
padding: 50px 50px;
|
||||
border: 1px solid skyblue;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.sliderBar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: calc(50% - 6px / 2);
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
width: var(--cover-width);
|
||||
/*width: 100px;*/
|
||||
height: 6px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: calc(50% - 6px / 2);
|
||||
background: #4d4d4d;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
left: var(--dot-move);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
top: calc(50% - 26px / 2);
|
||||
background: #544f4f;
|
||||
border-radius: 50%;
|
||||
}
|
||||
p {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 1px;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,94 @@
|
|||
import {html, LitElement, CSSResultArray} from 'lit'
|
||||
import {customElement, property, query} from 'lit/decorators.js'
|
||||
import {sharedStyles} from './slider-styles'
|
||||
|
||||
export const variants = ['filled', 'tick']
|
||||
|
||||
@customElement('star-slider')
|
||||
export class StarSlider extends LitElement {
|
||||
_coverWidth: string = ''
|
||||
public static override get styles(): CSSResultArray {
|
||||
return [sharedStyles]
|
||||
}
|
||||
|
||||
@query('.content') content!: HTMLDivElement
|
||||
@query('.sliderBar') sliderBar!: HTMLDivElement
|
||||
@query('.progress') progress!: HTMLDivElement
|
||||
@query('.dot') dot!: HTMLDivElement
|
||||
@query('p') p!: HTMLParagraphElement
|
||||
@property({type: Number}) startX = 0
|
||||
@property({type: Number}) touchX = 0
|
||||
@property({type: Number}) moveX = 0
|
||||
@property({type: Number}) newX = 0
|
||||
@property({type: Number}) barWidth = 0
|
||||
@property({type: Number}) dotL = 0
|
||||
@property({type: Number}) proportion = 0
|
||||
@property({type: String}) pValue = ''
|
||||
@property({type: Number}) sliderBarLeft = 0
|
||||
@property({type: Number}) sliderBarRight = 0
|
||||
@property({type: Number}) ball = 0
|
||||
@property({type: String}) sliderCoverWidth = ''
|
||||
@property({type: String}) ballMove = ''
|
||||
@property({type: String})
|
||||
get coverWidth() {
|
||||
return this._coverWidth
|
||||
}
|
||||
set coverWidth(value: string) {
|
||||
this.style.setProperty('--cover-width', value)
|
||||
this._coverWidth = value
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<p>${this.pValue}</p>
|
||||
<div class="sliderBar">
|
||||
<div class="progress" coverWidth="100px"></div>
|
||||
<div
|
||||
class="dot"
|
||||
@touchstart=${this.touchStart}
|
||||
@touchend=${this.touchEnd}
|
||||
@touchmove=${this.touchMove}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
private touchStart(evt: TouchEvent) {
|
||||
this.barWidth = this.sliderBar.offsetWidth - this.dot.offsetWidth //总长度减去小球覆盖的部分
|
||||
this.dotL = this.dot.offsetLeft //小球左侧相对于父元素的左边距
|
||||
this.startX = evt.touches[0].clientX //手指点下的 X 坐标
|
||||
}
|
||||
private touchMove(evt: TouchEvent) {
|
||||
//阻止默认行为
|
||||
evt.preventDefault()
|
||||
this.touchX = evt.touches[0].clientX //整个屏幕实时触摸的 X 坐标
|
||||
this.moveX = this.touchX - this.startX //手指移动的距离
|
||||
//判断最大值和最小值
|
||||
this.newX = this.dotL + this.moveX
|
||||
if (this.newX < 0) {
|
||||
this.newX = 0
|
||||
}
|
||||
if (this.newX >= this.barWidth) {
|
||||
this.newX = this.barWidth
|
||||
}
|
||||
//改变dot的left值
|
||||
this.style.setProperty('--dot-move', this.newX + 'px')
|
||||
//计算比例
|
||||
this.proportion = (this.newX / this.barWidth) * 100
|
||||
this.pValue = Math.ceil(this.proportion) + ''
|
||||
this.progress.style.setProperty(
|
||||
'width',
|
||||
(this.barWidth * Math.ceil(this.proportion)) / 100 + 'px'
|
||||
)
|
||||
}
|
||||
private touchEnd(evt: TouchEvent) {
|
||||
return console.log(this.pValue)
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'star-slider': StarSlider
|
||||
}
|
||||
}
|
|
@ -2,17 +2,28 @@
|
|||
|
||||
星光 Web 组件 --- Switch 组件(8 月 29 日)
|
||||
|
||||
`star-switch` 组件包含**四种**互相独立的属性,介绍如下:
|
||||
`star-switch` 组件包含**五种**互相独立的属性,介绍如下:
|
||||
|
||||
1. switchcolor : 控制 switch 的选中背景颜色,默认为蓝色`#0265dc`。
|
||||
|
||||
```
|
||||
<star-switch></star-switch>
|
||||
<hr />
|
||||
<star-switch switchcolor="#4cd964"></star-switch>
|
||||
<star-switch switchcolor="#4cd964">ios绿</star-switch>
|
||||
<star-switch switchcolor="#ff3b30">ios红</star-switch>
|
||||
```
|
||||
|
||||
2. checked : 用于选择对应 switch 首次加载时是否被选中,默认为`false`。
|
||||
2. switchicon : 用于控制内含文本,默认为`false`。
|
||||
|
||||
```
|
||||
<star-switch></star-switch>
|
||||
<hr />
|
||||
<star-switch switchicon></star-switch>
|
||||
<hr />
|
||||
<star-switch checked switchicon></star-switch>
|
||||
```
|
||||
|
||||
3. checked : 用于选择对应 switch 首次加载时是否被选中,默认为`false`。
|
||||
|
||||
```
|
||||
<star-switch></star-switch>
|
||||
|
@ -22,7 +33,7 @@
|
|||
<star-switch checked switchcolor="#4cd964"></star-switch>
|
||||
```
|
||||
|
||||
3. disabled : 控制 switch 是否**禁用**状态,默认为`false`。
|
||||
4. disabled : 控制 switch 是否**禁用**状态,默认为`false`。
|
||||
|
||||
```
|
||||
<star-switch></star-switch>
|
||||
|
@ -32,7 +43,7 @@
|
|||
<star-switch disabled checked></star-switch>
|
||||
```
|
||||
|
||||
4. size : 控制 switch 的大小,包括 small、medium、large 和 extralarge,默认为 `medium` 。
|
||||
5. size : 控制 switch 的大小,包括 small、medium、large 和 extralarge,默认为 `medium` 。
|
||||
|
||||
```
|
||||
<star-switch size="small" switchcolor="#4cd964"></star-switch>
|
||||
|
@ -45,3 +56,7 @@
|
|||
<hr />
|
||||
<star-switch size="extralarge" disabled checked></star-switch>
|
||||
```
|
||||
|
||||
该组件支持焦点选中,且可以在焦点选中后任意拖动。
|
||||
|
||||
拖动后可间接触发 checked 开关。
|
||||
|
|
|
@ -18,7 +18,7 @@ export const sharedStyles: CSSResult = css`
|
|||
display: inline-block;
|
||||
position: relative;
|
||||
width: 46px;
|
||||
height: 24px;
|
||||
height: 25px;
|
||||
border-radius: 30px;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
@ -30,8 +30,8 @@ export const sharedStyles: CSSResult = css`
|
|||
/*使用伪元素生成一个按钮*/
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
height: 23px;
|
||||
width: 23px;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
position: absolute;
|
||||
|
@ -46,7 +46,7 @@ export const sharedStyles: CSSResult = css`
|
|||
.base:checked + label::before {
|
||||
/*checkbox选中时按钮的样式*/
|
||||
transition: 0.25s cubic-bezier(0.16, 0.67, 0.18, 1.1);
|
||||
left: 22px;
|
||||
left: 21px;
|
||||
}
|
||||
|
||||
/*Disabled*/
|
||||
|
@ -75,6 +75,17 @@ export const sharedStyles: CSSResult = css`
|
|||
/*checkbox选中时按钮的样式*/
|
||||
left: 18px;
|
||||
}
|
||||
:host([size='small'][switchicon]) .iconFalse {
|
||||
left: 24px;
|
||||
top: 6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
:host([size='small'][switchicon]) .iconTrue {
|
||||
left: 11px;
|
||||
top: 6px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
/*Large*/
|
||||
:host([size='large']) label {
|
||||
|
@ -92,6 +103,18 @@ export const sharedStyles: CSSResult = css`
|
|||
left: 26px;
|
||||
}
|
||||
|
||||
:host([size='large'][switchicon]) .iconFalse {
|
||||
left: 34px;
|
||||
top: 8px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
:host([size='large'][switchicon]) .iconTrue {
|
||||
top: 9px;
|
||||
height: 9px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
/*ExtraLarge*/
|
||||
:host([size='extralarge']) label {
|
||||
width: 62px;
|
||||
|
@ -107,4 +130,33 @@ export const sharedStyles: CSSResult = css`
|
|||
/*checkbox选中时按钮的样式*/
|
||||
left: 30px;
|
||||
}
|
||||
:host([size='extralarge'][switchicon]) .iconFalse {
|
||||
left: 39px;
|
||||
top: 9px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
||||
:host([size='extralarge'][switchicon]) .iconTrue {
|
||||
top: 10px;
|
||||
height: 10px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
:host([switchicon]) .iconFalse {
|
||||
position: absolute;
|
||||
left: 29px;
|
||||
top: 7px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #e9e9e9;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #b1b1b1;
|
||||
}
|
||||
:host([switchicon]) .iconTrue {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 7px;
|
||||
height: 8px;
|
||||
border-left: 1px solid #fff;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {html, LitElement, CSSResultArray} from 'lit'
|
||||
import {customElement, property} from 'lit/decorators.js'
|
||||
import {customElement, property, query} from 'lit/decorators.js'
|
||||
import {sharedStyles} from './switch-styles'
|
||||
// import {classMap} from 'lit/directives/class-map.js'
|
||||
|
||||
@customElement('star-switch')
|
||||
export class StarSwitch extends LitElement {
|
||||
|
@ -9,34 +10,124 @@ export class StarSwitch extends LitElement {
|
|||
return [sharedStyles]
|
||||
}
|
||||
|
||||
// @property({type: String}) switchtype = ''
|
||||
@property({type: Number}) right = 0
|
||||
@property({type: Number}) left = 0
|
||||
@property({type: Number}) switchx = 0
|
||||
@property({type: Number}) startx = 0
|
||||
@property({type: Number}) _x = 0
|
||||
@property({type: Number}) rightx = 0
|
||||
@property({type: Number}) leftx = 10000
|
||||
@property({type: Number}) x = 0
|
||||
@property({type: Boolean}) disabled = false
|
||||
@property({type: Boolean}) checked = false
|
||||
@property({type: String})
|
||||
get switchColor() {
|
||||
get switchcolor() {
|
||||
return this._backgoundColor
|
||||
}
|
||||
|
||||
set switchColor(value: string) {
|
||||
set switchcolor(value: string) {
|
||||
this.style.setProperty('--background-color', value)
|
||||
this._backgoundColor = value
|
||||
}
|
||||
|
||||
@query('#switchBall') switchBall!: HTMLLabelElement
|
||||
@query('#base') base!: HTMLInputElement
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<input
|
||||
?disabled="${this.disabled}"
|
||||
?checked="${this.checked}"
|
||||
@change=${(evt: Event) =>
|
||||
(this.checked = (evt.target as HTMLInputElement).checked)}
|
||||
type="checkbox"
|
||||
class="base"
|
||||
id="base"
|
||||
switchcolor="#0265dc"
|
||||
/>
|
||||
<label for="base"></label>
|
||||
<label
|
||||
id="switchBall"
|
||||
for="base"
|
||||
@touchstart=${this.touchStart}
|
||||
@touchend=${this.touchEnd}
|
||||
@touchmove=${this.selectTouchMove}
|
||||
>
|
||||
<div class="${this.checked ? 'iconTrue' : 'iconFalse '}"></div>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
}
|
||||
private touchStart(evt: TouchEvent) {
|
||||
console.log('##')
|
||||
this.startx = evt.touches[0].clientX
|
||||
this._x = this.startx
|
||||
}
|
||||
private touchEnd(evt: TouchEvent) {
|
||||
this.rightx = 0
|
||||
this.leftx = 10000
|
||||
this.startx = 0
|
||||
}
|
||||
private selectTouchMove(evt: TouchEvent) {
|
||||
evt.preventDefault()
|
||||
// disabled不允许拖动
|
||||
if (!this.disabled) {
|
||||
let right = this.switchBall.getBoundingClientRect().right
|
||||
let left = this.switchBall.getBoundingClientRect().left
|
||||
let switchx = (right - left) / 2
|
||||
let x = evt.touches[0].clientX
|
||||
// 向左滑
|
||||
if (x < this._x) {
|
||||
// console.log('往左滑', x, this._x)
|
||||
this.leftx = this.leftx > this._x ? this._x : this.leftx
|
||||
//到最左端后向右走 switchx 变true
|
||||
if (x > this.leftx + this.switchx) {
|
||||
this.base.checked = true
|
||||
// 解决touchmove监测不到checked变化
|
||||
this.checked = true
|
||||
this.leftx = 10000
|
||||
}
|
||||
|
||||
//初始的开关阈值调节
|
||||
if (x > left && x < right) {
|
||||
if (this.startx - this.leftx > switchx) {
|
||||
this.base.checked = false
|
||||
this.checked = false
|
||||
}
|
||||
}
|
||||
//有rightx的情况下向左移动 switch 变false
|
||||
if (x < this.rightx - switchx) {
|
||||
this.base.checked = false
|
||||
this.checked = false
|
||||
}
|
||||
} else if (x > this._x) {
|
||||
//向右滑
|
||||
// console.log('往右滑', x, this._x)
|
||||
this.rightx = this.rightx > this._x ? this.rightx : this._x
|
||||
//到最右端后向左移动 switch 变false
|
||||
if (x < this.rightx - this.switchx) {
|
||||
this.base.checked = false
|
||||
this.checked = false
|
||||
this.rightx = 0
|
||||
}
|
||||
|
||||
//初始的开关阈值调节
|
||||
if (x > left && x < right) {
|
||||
if (this.rightx - this.startx > switchx) {
|
||||
this.base.checked = true
|
||||
this.checked = true
|
||||
}
|
||||
}
|
||||
|
||||
//到最左端开始向右走 switchx 变true
|
||||
if (x - this.leftx > switchx) {
|
||||
this.base.checked = true
|
||||
this.checked = true
|
||||
this.leftx = 10000
|
||||
}
|
||||
}
|
||||
//更新_x的值,保持_x在x后面
|
||||
this._x = x
|
||||
}
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'star-switch': StarSwitch
|
||||
|
|
|
@ -12,7 +12,9 @@ import './components/radio/radio-group'
|
|||
import './components/radio/radio'
|
||||
import './components/toast/toast'
|
||||
import './components/picker/picker'
|
||||
import './components/prompt/prompt'
|
||||
import './components/overflowmenu/overflowmenu'
|
||||
import './components/slider/slider'
|
||||
|
||||
@customElement('settings-app')
|
||||
export class SettingsApp extends LitElement {
|
||||
@query('star-animate-section#root') private rootSection!: StarAnimateSection
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import {html, LitElement, CSSResultArray} from "lit"
|
||||
import {customElement} from "lit/decorators.js"
|
||||
import {sharedStyles} from "../shared-styles"
|
||||
import "../../../components/card/card"
|
||||
|
||||
@customElement("panel-card")
|
||||
export class PanelCard extends LitElement {
|
||||
render() {
|
||||
return html `
|
||||
<div>
|
||||
<h4>基础卡片</h4>
|
||||
<star-card
|
||||
type="base"
|
||||
image="./src/test/panels/card/image/1.png"
|
||||
heading="基础卡片"
|
||||
subheading="详情叙述"
|
||||
footer="footer"
|
||||
></star-card>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<h4>链接卡片</h4>
|
||||
<star-card
|
||||
type="linkcard"
|
||||
link="https://www.kylinos.cn/"
|
||||
image="./src/test/panels/card/image/1.png"
|
||||
heading="连接卡片"
|
||||
subheading="详情叙述"
|
||||
footer="footer"
|
||||
></star-card>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<h4>无图卡片</h4>
|
||||
<star-card
|
||||
type="labelonly"
|
||||
heading="无图卡片"
|
||||
subheading="内容展示"
|
||||
footer="footer"
|
||||
></star-card>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<h4>无页脚卡片</h4>
|
||||
<star-card
|
||||
type="footerdeleted"
|
||||
image="./src/test/panels/card/image/2.jpg"
|
||||
heading="无页脚卡片"
|
||||
subheading="内容展示"
|
||||
></star-card>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<h4>只图卡片</h4>
|
||||
<star-card
|
||||
type="imageonly"
|
||||
image="./src/test/panels/card/image/2.jpg"
|
||||
></star-card>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
public static override get styles(): CSSResultArray {
|
||||
return [sharedStyles]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"panel-card": PanelCard
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,151 @@
|
|||
import {html, css, LitElement, TemplateResult} from 'lit'
|
||||
import {customElement, query, state} from 'lit/decorators.js'
|
||||
import GaiaContainer from '../../../components/grid-container/container'
|
||||
import GaiaContainerChild from '../../../components/grid-container/gaia-container-child'
|
||||
import homescreenStyle from './homescreen-style'
|
||||
|
||||
import './icon'
|
||||
|
||||
@customElement('panel-homescreen')
|
||||
export class PanelContainer extends LitElement {
|
||||
container!: GaiaContainer
|
||||
icons: {[prop: string]: GaiaContainerChild} = {}
|
||||
@query('.reset') resetBtn!: HTMLElement
|
||||
@query('#row') rowInput!: HTMLInputElement
|
||||
@query('#column') columnInput!: HTMLInputElement
|
||||
@query('#indicator') indicator!: HTMLElement
|
||||
createRandomColor() {
|
||||
function randomInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
return (
|
||||
'rgb(' +
|
||||
randomInt(100, 200) +
|
||||
',' +
|
||||
randomInt(100, 200) +
|
||||
',' +
|
||||
randomInt(100, 200) +
|
||||
')'
|
||||
)
|
||||
}
|
||||
addAppIcon(x?: number, y?: number) {
|
||||
x =
|
||||
typeof x == 'number' ? x : this.rowInput.value ? +this.rowInput.value : 1
|
||||
y = y ? y : this.columnInput.value ? +this.columnInput.value : 1
|
||||
const icon = document.createElement('site-icon')
|
||||
|
||||
icon.setAttribute('color', this.createRandomColor())
|
||||
this.container.appendContainerChild(icon, {
|
||||
row: x,
|
||||
column: y,
|
||||
})
|
||||
this.icons[icon.name] = this.container.getChildByElement(icon)!
|
||||
}
|
||||
reset() {}
|
||||
firstUpdated() {
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const index = document.createElement('div')
|
||||
index.classList.add('index')
|
||||
index.dataset.number = String(i)
|
||||
|
||||
this.indicator.appendChild(index)
|
||||
}
|
||||
this.container = new GaiaContainer()
|
||||
this.container.setAttribute('drag-and-drop', '')
|
||||
this.shadowRoot?.appendChild(this.container)
|
||||
this.container.sortMode = true
|
||||
;(window as any).container = this.container
|
||||
;(window as any).home = this
|
||||
this.addAppIcon(2, 2)
|
||||
// this.addAppIcon(2, 2)
|
||||
}
|
||||
@state() test!: string
|
||||
@state()
|
||||
protected styles: {[subModule: string]: string} = new Proxy(
|
||||
{},
|
||||
{
|
||||
set: (
|
||||
target: {[module: string]: string},
|
||||
prop: string,
|
||||
value
|
||||
): boolean => {
|
||||
if (!target[prop] || target[prop] !== value) {
|
||||
target[prop] = value
|
||||
this.requestUpdate()
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
render() {
|
||||
let style = ''
|
||||
for (const subModule in this.styles) {
|
||||
const str = this.styles[subModule]
|
||||
style += str
|
||||
style += '\n'
|
||||
}
|
||||
return html`
|
||||
<style>
|
||||
${style}
|
||||
</style>
|
||||
<div class="btns">
|
||||
<button class="add" @click=${this.addAppIcon}>添加</button>
|
||||
<button class="reset" @click=${() => this.addAppIcon(1, 1)}>1×1</button>
|
||||
<button class="reset" @click=${() => this.addAppIcon(2, 2)}>2×2</button>
|
||||
<input id="row" placeholder="row" value="3" />
|
||||
<input id="column" placeholder="column" value="2" />
|
||||
<div id="indicator"></div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
star-container {
|
||||
height: 90vh;
|
||||
width: 100vw;
|
||||
}
|
||||
.btns {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
#indicator {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(6, 16.66%);
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
height: 90vh;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
background-color: rgba(1, 1, 0, 0.05);
|
||||
}
|
||||
|
||||
.index::before {
|
||||
content: attr(data-number);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 4rem;
|
||||
color: rgba(1, 1, 1, 0.15);
|
||||
}
|
||||
.gaia-container-child {
|
||||
border: 1px solid black;
|
||||
}
|
||||
`,
|
||||
homescreenStyle,
|
||||
]
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'panel-homescreen': PanelContainer
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import {css} from 'lit'
|
||||
|
||||
export default css`
|
||||
/* 图标归位时的动画 */
|
||||
star-container:not(.loading)
|
||||
.gaia-container-child:not(.added):not(.dragging) {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,92 @@
|
|||
import {css} from 'lit'
|
||||
|
||||
export default css`
|
||||
:host([color]) {
|
||||
position: var(--width, absolute);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: var(--icon-size, 50%);
|
||||
height: var(--icon-size, 50%);
|
||||
max-width: var(--width);
|
||||
max-height: var(--width);
|
||||
border-radius: 50%;
|
||||
}
|
||||
#display {
|
||||
--background-color: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
:host([hide-subtitle]) #subtitle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host() #subtitle {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#image-container {
|
||||
position: relative;
|
||||
transition: visibility 0.2s, opacity 0.2s;
|
||||
border-radius: 50%;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
#image-container img {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#image-container.initial-load {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#image-container > #spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#image-container.downloading > #spinner {
|
||||
display: block;
|
||||
background-size: contain;
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
|
||||
/* #image-container,
|
||||
#image-container > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
} */
|
||||
|
||||
#subtitle {
|
||||
/* 小黑 文字 中 */
|
||||
margin-top: 6px;
|
||||
height: 26px;
|
||||
|
||||
/* 小正文-浅-居中 */
|
||||
font-family: 'OPPOSans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
/* identical to box height, or 130% */
|
||||
text-align: center;
|
||||
|
||||
/* 字体/ 高亮白 */
|
||||
color: #fafafa;
|
||||
|
||||
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,69 @@
|
|||
import {html, css, LitElement} from 'lit'
|
||||
import {customElement, property, query} from 'lit/decorators.js'
|
||||
import style from './icon-style'
|
||||
|
||||
let count = 0
|
||||
let imgBlob!: Blob
|
||||
const getImage = (): Promise<Blob> => {
|
||||
return new Promise((res, rej) => {
|
||||
if (imgBlob) return res(imgBlob)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 100
|
||||
canvas.height = 100
|
||||
canvas.toBlob((blob) => (blob ? res(blob) : rej()))
|
||||
})
|
||||
}
|
||||
|
||||
@customElement('site-icon')
|
||||
export default class SiteIcon extends LitElement {
|
||||
static defaultColor = '#fff'
|
||||
_color = '#fff'
|
||||
name: string = `图标${++count}`
|
||||
@query('#subtitle') subtitle!: HTMLElement
|
||||
@query('#image-container') imgContainer!: HTMLElement
|
||||
@query('#display') displayImg!: HTMLImageElement
|
||||
|
||||
set color(value: string) {
|
||||
this._color = value
|
||||
this.style.setProperty('--background-color', value)
|
||||
}
|
||||
@property({
|
||||
hasChanged: (newValue, oldValue) => {
|
||||
return newValue === oldValue
|
||||
},
|
||||
})
|
||||
get color() {
|
||||
return this._color ?? SiteIcon.defaultColor
|
||||
}
|
||||
|
||||
static styles = style
|
||||
changeColor(color: string) {
|
||||
this.style.setProperty('--background-color', color)
|
||||
}
|
||||
firstUpdated() {
|
||||
const {width} = this.getBoundingClientRect()
|
||||
this.style.setProperty('--width', width + 'px')
|
||||
getImage().then((blob: Blob) => {
|
||||
let url = URL.createObjectURL(blob)
|
||||
this.displayImg.src = url
|
||||
})
|
||||
}
|
||||
render() {
|
||||
this.changeColor(this.color)
|
||||
return html`
|
||||
<div id="image-container">
|
||||
<div id="spinner"></div>
|
||||
<img id="display" />
|
||||
</div>
|
||||
<div>
|
||||
<div dir="auto" id="subtitle">${this.name}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'site-icon': SiteIcon
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import {html, LitElement, css} from 'lit'
|
||||
import {customElement, property} from 'lit/decorators.js'
|
||||
import '../../../components/button/button'
|
||||
import '../../../components/ul/ul'
|
||||
import '../../../components/li//li'
|
||||
import {UlType} from '../../../components/ul/ul'
|
||||
import {LiType} from '../../../components/li//li'
|
||||
|
||||
@customElement('panel-overflowmenu')
|
||||
export class PanelOverflowMenu extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
// state用于记录展开的菜单数量,用以菜单展开状态互斥的判断
|
||||
@property({type: Number}) state = 0
|
||||
|
||||
// 关闭菜单
|
||||
closeoverflowmenu(e) {
|
||||
// 获取点击事件所在的标签名字
|
||||
const tagName = e.target.tagName.toLowerCase()
|
||||
// 判断是否点击的star-overflowmenu标签
|
||||
if (tagName == 'star-overflowmenu') {
|
||||
this.state++
|
||||
}
|
||||
// 如果点在空白处则关闭菜单
|
||||
if (tagName != 'star-overflowmenu') {
|
||||
// 获取所有的star-overflowmenu
|
||||
var menulist = this.shadowRoot!.querySelectorAll('star-overflowmenu')
|
||||
for (var i = 0; i < menulist.length; i++) {
|
||||
menulist[i].open = true
|
||||
var menu = menulist[i].renderRoot.querySelector(
|
||||
'#menuitem'
|
||||
) as HTMLElement
|
||||
menu.style.display = 'none'
|
||||
this.state = 0
|
||||
}
|
||||
}
|
||||
// 通过state判断是否已有展开的菜单,若已有则关闭菜单
|
||||
if (this.state > 1) {
|
||||
var menulist = this.shadowRoot!.querySelectorAll('star-overflowmenu')
|
||||
for (var i = 0; i < menulist.length; i++) {
|
||||
menulist[i].open = true
|
||||
var menu = menulist[i].renderRoot.querySelector(
|
||||
'#menuitem'
|
||||
) as HTMLElement
|
||||
menu.style.display = 'none'
|
||||
this.state = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback()
|
||||
// 添加click事件
|
||||
this.shadowRoot?.addEventListener('click', (e) => this.closeoverflowmenu(e))
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<star-overflowmenu icon="more">
|
||||
<star-ul type=${UlType.BASE}>
|
||||
<star-li
|
||||
type=${LiType.ONLY_EDIT}
|
||||
label="星光麒麟"
|
||||
default="星光麒麟"
|
||||
></star-li>
|
||||
</star-ul>
|
||||
</star-overflowmenu>
|
||||
|
||||
<star-overflowmenu
|
||||
icon="more"
|
||||
style="position: fixed; top: 50%; left: 50%;"
|
||||
>
|
||||
<star-ul type=${UlType.BASE}>
|
||||
<star-li
|
||||
type=${LiType.ONLY_EDIT}
|
||||
label="星光麒麟"
|
||||
default="星光麒麟"
|
||||
></star-li>
|
||||
</star-ul>
|
||||
</star-overflowmenu>
|
||||
|
||||
<star-overflowmenu
|
||||
icon="more"
|
||||
style="position: fixed; top: 0; right: 0;"
|
||||
>
|
||||
<star-ul type=${UlType.BASE}>
|
||||
<star-li
|
||||
type=${LiType.ONLY_EDIT}
|
||||
label="星光麒麟"
|
||||
default="星光麒麟"
|
||||
></star-li>
|
||||
<star-li
|
||||
type=${LiType.ONLY_EDIT}
|
||||
label="星光麒麟"
|
||||
default="星光麒麟"
|
||||
></star-li>
|
||||
</star-ul>
|
||||
</star-overflowmenu>
|
||||
|
||||
<star-overflowmenu
|
||||
icon="more"
|
||||
style="position: fixed; bottom: 0; left: 0;"
|
||||
>
|
||||
<star-ul
|
||||
type=${UlType.ONLY_HEADER}
|
||||
title="头部有文字"
|
||||
text="尾部有文字"
|
||||
>
|
||||
<star-li type=${LiType.ONLY_LABEL} label="素条目"></star-li>
|
||||
</star-ul>
|
||||
</star-overflowmenu>
|
||||
|
||||
<star-overflowmenu
|
||||
icon="more"
|
||||
style="position: fixed; bottom: 0; right: 0;"
|
||||
>
|
||||
<star-ul
|
||||
type=${UlType.ONLY_HEADER}
|
||||
title="头部有文字"
|
||||
text="尾部有文字"
|
||||
>
|
||||
<star-li type=${LiType.ONLY_LABEL} label="素条目"></star-li>
|
||||
</star-ul>
|
||||
</star-overflowmenu>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
div {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'panel-overflowmenu': PanelOverflowMenu
|
||||
}
|
||||
}
|
|
@ -9,12 +9,13 @@ import '../../components/li/li'
|
|||
import './about/about'
|
||||
import './icon/icon'
|
||||
import './general/general'
|
||||
import './card/card'
|
||||
import './indicators/indicators'
|
||||
import './blur/use-blur'
|
||||
import './button/button'
|
||||
import './container/container'
|
||||
import './radio/radio'
|
||||
import './switch/switch'
|
||||
|
||||
import './toast/toast'
|
||||
import './picker/picker'
|
||||
import './prompt/prompt'
|
||||
|
@ -114,6 +115,14 @@ export class PanelRoot extends LitElement {
|
|||
href="#switch"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="SliderTest"
|
||||
icon="scene"
|
||||
iconcolor="brown"
|
||||
href="#slider"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="查看所有按钮"
|
||||
|
@ -146,6 +155,14 @@ export class PanelRoot extends LitElement {
|
|||
href="#prompt"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="溢出菜单"
|
||||
icon="menu"
|
||||
iconcolor="blue"
|
||||
href="#overflowmenu"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="关于"
|
||||
|
@ -170,6 +187,22 @@ export class PanelRoot extends LitElement {
|
|||
href="#blur"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="主屏"
|
||||
icon="dismiss-keyboard"
|
||||
iconcolor="#eee"
|
||||
href="#homescreen"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="基础卡片组件"
|
||||
icon="play-circle"
|
||||
iconcolor="blue"
|
||||
href="#card"
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.ICON_LABEL}
|
||||
label="主屏容器"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import {html, LitElement, css} from 'lit'
|
||||
import {customElement} from 'lit/decorators.js'
|
||||
|
||||
@customElement('panel-slider')
|
||||
export class PanelSlider extends LitElement {
|
||||
static styles = css`
|
||||
div {
|
||||
position: relative;
|
||||
border: 1px solid;
|
||||
margin: 50px 50px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<h4>default</h4>
|
||||
<star-slider></star-slider>
|
||||
<h4>初始化覆盖长度</h4>
|
||||
<star-slider coverWidth="150px"></star-slider>
|
||||
<star-slider coverWidth="50%"></star-slider>
|
||||
<h4>disabled</h4>
|
||||
<star-slider></star-slider>
|
||||
<star-slider></star-slider>
|
||||
<star-slider></star-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'panel-slider': PanelSlider
|
||||
}
|
||||
}
|
|
@ -50,6 +50,46 @@ export class PanelSwitch extends LitElement {
|
|||
<hr />
|
||||
</star-ul>
|
||||
|
||||
<star-ul type=${UlType.ONLY_HEADER} title="SWITCH - ICON">
|
||||
<star-li
|
||||
type=${LiType.SWITCH_LABEL}
|
||||
label="BASE"
|
||||
icon="switch"
|
||||
iconcolor="blue"
|
||||
switchicon
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.SWITCH_LABEL}
|
||||
label="GREEN"
|
||||
icon="switch"
|
||||
iconcolor="green"
|
||||
switchcolor="#4cd964"
|
||||
checked
|
||||
switchicon
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.SWITCH_LABEL}
|
||||
label="BLACK"
|
||||
icon="switch"
|
||||
iconcolor="black"
|
||||
switchcolor="#222222"
|
||||
switchicon
|
||||
></star-li>
|
||||
<hr />
|
||||
<star-li
|
||||
type=${LiType.SWITCH_LABEL}
|
||||
label="RED"
|
||||
checked
|
||||
icon="switch"
|
||||
iconcolor="red"
|
||||
switchcolor="#ff3b30"
|
||||
switchicon
|
||||
></star-li>
|
||||
<hr />
|
||||
</star-ul>
|
||||
|
||||
<star-ul type=${UlType.ONLY_HEADER} title="SWITCH - CHECKED">
|
||||
<star-li
|
||||
type=${LiType.SWITCH_LABEL}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
class SlotStyleHandler {
|
||||
regex: {[regStr: string]: RegExp} = {
|
||||
slottedCss: /(?:\:\:slotted\(.*\))[^{]+\{[^}]*\}/g,
|
||||
'::slotted()': /\:\:slotted\(([^\(]+)\)/g,
|
||||
keyframes: /@keyframes[^{]+\{([^{]+\{[^}]*\})*\D*\}/g,
|
||||
}
|
||||
head!: HTMLElement
|
||||
headFirstElement!: ChildNode | null
|
||||
headStyle!: HTMLElement
|
||||
shouldRefresh: boolean = false
|
||||
styles: {[styleName: string]: string} = new Proxy(
|
||||
{},
|
||||
{
|
||||
set: (
|
||||
target: {[styleName: string]: string},
|
||||
prop: string,
|
||||
value: string
|
||||
) => {
|
||||
if (!target[prop] || target[prop] !== value) {
|
||||
target[prop] = value
|
||||
this.shouldRefresh = true
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
processCss(style: string, name: string) {
|
||||
let globalCss = ''
|
||||
style.replace(this.regex.keyframes, (match) => {
|
||||
globalCss += match
|
||||
return ''
|
||||
})
|
||||
style = style.replace(this.regex.slottedCss, (match) => {
|
||||
globalCss += match.replace(this.regex['::slotted()'], name + ' $1')
|
||||
return ''
|
||||
})
|
||||
|
||||
return globalCss
|
||||
}
|
||||
|
||||
/**
|
||||
* 将CSS注入头部<style></style>
|
||||
*
|
||||
* @param {String} style
|
||||
* @param {String} name
|
||||
* @returns
|
||||
*/
|
||||
injectGlobalCss(
|
||||
component: HTMLElement,
|
||||
style: string,
|
||||
name: string,
|
||||
subName = ''
|
||||
) {
|
||||
if (!style) return
|
||||
|
||||
const styleName = subName ? `${name}-${subName}` : name
|
||||
let css = this.processCss(style, name)
|
||||
this.styles[styleName] = css
|
||||
|
||||
if (this.shouldRefresh) {
|
||||
if (!this.head || !this.headFirstElement) {
|
||||
this.head = document.head
|
||||
this.headFirstElement = this.head.firstChild
|
||||
}
|
||||
|
||||
if (!this.headStyle) {
|
||||
this.headStyle = document.createElement('style')
|
||||
if (this.headFirstElement) {
|
||||
this.head.insertBefore(this.headStyle, this.headFirstElement)
|
||||
} else {
|
||||
this.head.appendChild(this.headStyle)
|
||||
}
|
||||
}
|
||||
// 当父节点也是 Web Component时,防止全局注册的 Style 被父节点的
|
||||
// ShadowRoot 隔离,需要再在父节点中插入一份样式
|
||||
if (
|
||||
component.parentNode &&
|
||||
(Object.prototype.toString
|
||||
.call(component.parentNode)
|
||||
.includes('DocumentFragment') ||
|
||||
Object.prototype.toString
|
||||
.call(component.parentNode)
|
||||
.includes('ShadowRoot'))
|
||||
) {
|
||||
let scoped = document.createElement('style')
|
||||
scoped.innerHTML = css.trim()
|
||||
component.parentNode.appendChild(scoped)
|
||||
}
|
||||
|
||||
let style = ''
|
||||
for (const styleName in this.styles) {
|
||||
const content = this.styles[styleName]
|
||||
style += content
|
||||
}
|
||||
|
||||
this.headStyle.innerHTML = style
|
||||
this.shouldRefresh = false
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new SlotStyleHandler()
|
Loading…
Reference in New Issue