TASK: #103598 - add container

This commit is contained in:
luojiahao 2022-08-26 16:47:53 +08:00
parent 9d6b72000c
commit 5d7ec91fa7
11 changed files with 3281 additions and 1 deletions

View File

@ -7,4 +7,5 @@
- add button
- add bubble
- add indicator-page-point
- add blur
- add blur
- add contaienr

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import './icon/icon'
import './general/general'
import './indicators/indicators'
import './blur/use-blur'
import './container/homescreen-container'
type SEID = String
@ -124,6 +125,14 @@ export class PanelRoot extends LitElement {
href="#blur"
></star-li>
<hr />
<star-li
type=${LiType.ICON_LABEL}
label="主屏容器"
icon="dismiss-keyboard"
iconcolor="#eee"
href="#container"
></star-li>
<hr />
<star-li
type=${LiType.ICON_LABEL}
label="插件"