Merge branch 'master' of ssh://172.20.184.160:7999/yr/star-web-components into feature-component-prompt

This commit is contained in:
wangguoqing 2022-09-09 17:09:25 +08:00
commit 198e08e5fb
42 changed files with 5774 additions and 30 deletions

View File

@ -10,3 +10,10 @@
- add blur
- add radio
- 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

View File

@ -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 {

View File

@ -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>

View File

@ -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
用来填写链接卡片的跳转网址。

View File

@ -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;
}
`

134
src/components/card/card.ts Normal file
View File

@ -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
}
}

View File

@ -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. 图标节点被另一个图标节点挤占位置时,仅能再去挤占相邻网格的图标的位置(即不允许挤占上下相邻图标)

View File

@ -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

View File

@ -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
}

View File

@ -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. 56
* 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}
}
}

View File

@ -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

View File

@ -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)')
}
}
}

View File

@ -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;
}
}
`
}
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,9 @@
import {css} from 'lit'
export default css`
:host {
/* 图标大小 */
--icon-size: 108px;
--icon-size: 50%;
}
`

View File

@ -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>
`}

View File

@ -0,0 +1,29 @@
# starwebcomponents——overflowmenu
## 介绍
### overflowmenu溢出菜单。
## 新需求(主页面要求——罗 9.5
1. 外部颜色控制思路使用自定义css样式 --test-colorXXX 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值

View File

@ -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
}
}

View File

@ -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;
}
`

View File

@ -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;

View File

@ -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>
```

View File

@ -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;
}
`

View File

@ -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
}
}

View File

@ -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 开关。

View File

@ -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;
}
`

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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;
}
`

View File

@ -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);
}
}
`

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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="主屏容器"

View File

@ -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
}
}

View File

@ -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}

View File

@ -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()