TASK: #103598 - add container
This commit is contained in:
parent
9d6b72000c
commit
5d7ec91fa7
|
@ -8,3 +8,4 @@
|
||||||
- add bubble
|
- add bubble
|
||||||
- add indicator-page-point
|
- add indicator-page-point
|
||||||
- add blur
|
- add blur
|
||||||
|
- add contaienr
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,132 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,295 @@
|
||||||
|
import GaiaContainer from './container'
|
||||||
|
import {Coordinate} from './contianer-interface'
|
||||||
|
/**
|
||||||
|
* Grid 布局
|
||||||
|
* 组件属性:
|
||||||
|
* master: 占位元素,也用于定位静置时的组件实际位置
|
||||||
|
* container: 显示元素,用于放置组件内容——element,也用于拖动时显示组件拖动位置
|
||||||
|
* element: 组件实际内容,通过构造传参传入
|
||||||
|
*
|
||||||
|
* 考虑:
|
||||||
|
* 1. 当为小组件时,插入 container 后需要锚固
|
||||||
|
* 2. 锚固分 landscape 和 portrait 两种方向
|
||||||
|
* 3. TBD:注意,需要考虑旋转屏幕后组件位置冲突问题,当旋转到某一方向时需要越界判断,越界
|
||||||
|
* 时解除所有组件锚固状态重新排列并更新锚固位置
|
||||||
|
*/
|
||||||
|
const defaultCoordinate = {
|
||||||
|
landscape: null,
|
||||||
|
portrait: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
folderName: 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
|
||||||
|
// 状态计时器
|
||||||
|
removed: number | undefined = undefined
|
||||||
|
added: number | undefined = undefined
|
||||||
|
constructor(
|
||||||
|
element: HTMLElement | null,
|
||||||
|
row: number = 1,
|
||||||
|
column: number = 1,
|
||||||
|
anchorCoordinate: Coordinate | null,
|
||||||
|
manager: GaiaContainer
|
||||||
|
) {
|
||||||
|
this._element = element
|
||||||
|
this.isWidget = element?.tagName === 'GAIA-WIDGET'
|
||||||
|
this.row = row
|
||||||
|
this.column = 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 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 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 = 'recorder') {
|
||||||
|
const area = this.getArea(type)
|
||||||
|
if (!area) return
|
||||||
|
|
||||||
|
const [rowStart, columStart] = area
|
||||||
|
const rowEnd = rowStart + +this.row
|
||||||
|
const columEnd = columStart + +this.column
|
||||||
|
this.master.style.gridArea = `${rowStart} / ${columStart} / ${rowEnd} / ${columEnd}`
|
||||||
|
;(this._element as HTMLElement).dataset.static = type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元素所在 Grid 网格区域
|
||||||
|
* @param {String} type 以何种方式获取网格区域: recorder 从记录中,current 从目前位置
|
||||||
|
*/
|
||||||
|
getArea(type = '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: string = '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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if (this._lastMasterTop !== top || this._lastMasterLeft !== left) {
|
||||||
|
this._lastMasterTop = top
|
||||||
|
this._lastMasterLeft = left
|
||||||
|
!isActive &&
|
||||||
|
!this.container.classList.contains('dragging') &&
|
||||||
|
(container.style.transform = 'translate(' + left + 'px, ' + top + 'px)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,553 @@
|
||||||
|
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
|
||||||
|
constructor(manager: GaiaContainer, name?: string) {
|
||||||
|
super(null, 1, 1, null, manager)
|
||||||
|
this.name = this.checkAndGetFolderName(name)
|
||||||
|
this._id = `folder-${new Date().getTime()}`
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.addAnimationStyle()
|
||||||
|
this.container.addEventListener('touchstart', this)
|
||||||
|
this.container.addEventListener('touchmove', this)
|
||||||
|
this.container.addEventListener('touchend', this)
|
||||||
|
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'
|
||||||
|
// this.manager.injectGlobalCss(this.shadowStyle, this.manager.name, 'gaia-container-folder');
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const icon = element.querySelector(this.iconName)
|
||||||
|
icon &&
|
||||||
|
icon.attributes.hasOwnProperty(this.hideAttrName) &&
|
||||||
|
icon.attributes.removeNamedItem(this.hideAttrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideIconsSubtitle(element: HTMLElement) {
|
||||||
|
const icon = element.querySelector(this.iconName)
|
||||||
|
const attr = document.createAttribute(this.hideAttrName)
|
||||||
|
icon && icon.attributes.setNamedItem(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将子节点从容器组件的节点表中挪到该文件夹的节点表中
|
||||||
|
* @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(element)
|
||||||
|
} else {
|
||||||
|
this.showIconsSubtitle(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._status && shouldOpen) {
|
||||||
|
this.suspendElement.push(element)
|
||||||
|
this.open()
|
||||||
|
} else {
|
||||||
|
this.movein(element)
|
||||||
|
this.element.appendChild(element)
|
||||||
|
}
|
||||||
|
child!.folderName = this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
;(removeChild as GaiaContainerChild).folderName = ''
|
||||||
|
this.showIconsSubtitle((removeChild as GaiaContainerChild).container)
|
||||||
|
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
|
||||||
|
const self = this
|
||||||
|
this._status = 1
|
||||||
|
this.master.classList.add('openning')
|
||||||
|
this._children.forEach((child) => this.showIconsSubtitle(child.master))
|
||||||
|
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', function transitionend(evt) {
|
||||||
|
if (evt.target == self.element && evt.propertyName == 'height') {
|
||||||
|
self._children.forEach((child) => child.synchroniseContainer())
|
||||||
|
}
|
||||||
|
self.container.style.setProperty('--folder-element-left', '0px')
|
||||||
|
self.container.style.setProperty('--folder-element-top', '0px')
|
||||||
|
if (
|
||||||
|
self._children[self._children.length].master.compareDocumentPosition(
|
||||||
|
evt.target as HTMLElement
|
||||||
|
) & 16
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.openTimer = setTimeout(() => {
|
||||||
|
self.master.classList.remove('openning')
|
||||||
|
self.master.classList.add('opened')
|
||||||
|
let element = self.suspendElement.shift()
|
||||||
|
while (element) {
|
||||||
|
self.movein(element)
|
||||||
|
self.element.appendChild(element)
|
||||||
|
element = self.suspendElement.shift()
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
self.element.removeEventListener('transitionend', transitionend)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭文件夹,以父组件网格单元大小显示,同时作为文件夹销毁的入口,
|
||||||
|
* 当应用图标数量不足时,销毁该文件夹
|
||||||
|
*/
|
||||||
|
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.master)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.master.classList.add('closing')
|
||||||
|
this.master.classList.remove('opened')
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{once: true}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._children.length > 1) {
|
||||||
|
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
|
||||||
|
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(master)
|
||||||
|
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 !== 'GAIA-APP-ICON'
|
||||||
|
) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addAnimationStyle() {
|
||||||
|
const styleArr = document.head.querySelectorAll('style')
|
||||||
|
let styleNode
|
||||||
|
styleArr.forEach((item) => {
|
||||||
|
try {
|
||||||
|
if (item.dataset?.name === 'gaia') {
|
||||||
|
styleNode = item
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
})
|
||||||
|
if (!styleNode) {
|
||||||
|
styleNode = document.createElement('style')
|
||||||
|
styleNode.dataset.name = 'gaia'
|
||||||
|
document.head.appendChild(styleNode)
|
||||||
|
}
|
||||||
|
styleNode.innerHTML += `
|
||||||
|
@keyframes folder-fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
get shadowStyle() {
|
||||||
|
return `
|
||||||
|
::slotted(.gaia-container-child) {
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
::slotted(.gaia-container-folder) {
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s, height 0.2s, width 0.2s !important;
|
||||||
|
}
|
||||||
|
::slotted(.folder:not(.opened)) .gaia-container-container {
|
||||||
|
}
|
||||||
|
::slotted(.gaia-container-folder) {
|
||||||
|
display: grid;
|
||||||
|
position: unset;
|
||||||
|
grid-template-rows: repeat(3, 33.3%);
|
||||||
|
grid-template-columns: repeat(3, 33.3%);
|
||||||
|
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: 16.5% !important;
|
||||||
|
width: 16.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) {
|
||||||
|
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;
|
||||||
|
color: #fff;
|
||||||
|
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,
|
||||||
|
::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;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
class GaiaContainerPage {
|
||||||
|
_pages: HTMLElement[] = [] // 存储所有添加进 gaia-container 的页面
|
||||||
|
// 等待被移除的页面,仅在编辑、拖拽时出现,若结束前两种状态时仍然没有子节点,则删除
|
||||||
|
_suspending: HTMLElement | null = null
|
||||||
|
_manager
|
||||||
|
observerCallback: MutationCallback
|
||||||
|
constructor(manager: any) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,69 @@
|
||||||
|
class GestureManager {
|
||||||
|
element: HTMLElement
|
||||||
|
touches: Touch[] = []
|
||||||
|
swipeTimer: number | undefined = undefined
|
||||||
|
velocity: number = 0.3
|
||||||
|
duration: number = 10
|
||||||
|
recorder: number = 0
|
||||||
|
swipDirection: string = ''
|
||||||
|
|
||||||
|
constructor(element: HTMLElement) {
|
||||||
|
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,61 @@
|
||||||
|
import {html, css, LitElement, TemplateResult} from 'lit'
|
||||||
|
import {customElement, query, state} from 'lit/decorators.js'
|
||||||
|
import GaiaContainer from '../../../components/grid-container/container'
|
||||||
|
import './icon'
|
||||||
|
|
||||||
|
@customElement('panel-container')
|
||||||
|
export class PanelContainer extends LitElement {
|
||||||
|
container!: GaiaContainer
|
||||||
|
@query('.reset') resetBtn!: 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() {
|
||||||
|
;(window as any).panel = this
|
||||||
|
console.log(this.container)
|
||||||
|
console.log('add')
|
||||||
|
const icon = document.createElement('site-icon')
|
||||||
|
|
||||||
|
icon.setAttribute('color', this.createRandomColor())
|
||||||
|
this.container.appendContainerChild(icon)
|
||||||
|
}
|
||||||
|
reset() {
|
||||||
|
console.log('reset')
|
||||||
|
}
|
||||||
|
firstUpdated() {
|
||||||
|
this.container = new GaiaContainer()
|
||||||
|
this.container.setAttribute('dragAndDrop', 'true')
|
||||||
|
this.shadowRoot?.appendChild(this.container)
|
||||||
|
;(window as any).container = this.container
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<button class="add" @click=${this.addAppIcon}>添加</button>
|
||||||
|
<button class="reset" @click=${this.reset}>重置</button>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
star-container {
|
||||||
|
height: 90vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'panel-container': PanelContainer
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {css} from 'lit'
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
:host() {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
#display {
|
||||||
|
--background-color: #fff;
|
||||||
|
width: 20vw;
|
||||||
|
height: 12.5vh;
|
||||||
|
}
|
||||||
|
:host([hide-subtitle]) #subtitle {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host() #subtitle {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
position: relative;
|
||||||
|
transition: visibility 0.2s, opacity 0.2s;
|
||||||
|
border: 50%;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitle {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(1deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1,54 @@
|
||||||
|
import {html, css, LitElement} from 'lit'
|
||||||
|
import {customElement, property, query} from 'lit/decorators.js'
|
||||||
|
import style from './icon-style'
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
@customElement('site-icon')
|
||||||
|
export default class SiteIcon extends LitElement {
|
||||||
|
static defaultColor = '#fff'
|
||||||
|
_color = '#fff'
|
||||||
|
|
||||||
|
@query('#subtitle') subtitle!: HTMLElement
|
||||||
|
@query('#image-container') imgContainer!: HTMLElement
|
||||||
|
@query('#display') displayImg!: HTMLElement
|
||||||
|
|
||||||
|
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() {
|
||||||
|
console.log('firstUpdated', this.displayImg)
|
||||||
|
}
|
||||||
|
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">图标${++count}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'site-icon': SiteIcon
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import './icon/icon'
|
||||||
import './general/general'
|
import './general/general'
|
||||||
import './indicators/indicators'
|
import './indicators/indicators'
|
||||||
import './blur/use-blur'
|
import './blur/use-blur'
|
||||||
|
import './container/homescreen-container'
|
||||||
|
|
||||||
type SEID = String
|
type SEID = String
|
||||||
|
|
||||||
|
@ -124,6 +125,14 @@ export class PanelRoot extends LitElement {
|
||||||
href="#blur"
|
href="#blur"
|
||||||
></star-li>
|
></star-li>
|
||||||
<hr />
|
<hr />
|
||||||
|
<star-li
|
||||||
|
type=${LiType.ICON_LABEL}
|
||||||
|
label="主屏容器"
|
||||||
|
icon="dismiss-keyboard"
|
||||||
|
iconcolor="#eee"
|
||||||
|
href="#container"
|
||||||
|
></star-li>
|
||||||
|
<hr />
|
||||||
<star-li
|
<star-li
|
||||||
type=${LiType.ICON_LABEL}
|
type=${LiType.ICON_LABEL}
|
||||||
label="插件"
|
label="插件"
|
||||||
|
|
Loading…
Reference in New Issue