From 5d7ec91fa7d5317fb2373b2e6d452c7815a32a4c Mon Sep 17 00:00:00 2001 From: luojiahao Date: Fri, 26 Aug 2022 16:47:53 +0800 Subject: [PATCH 01/18] TASK: #103598 - add container --- CHANGELOG.md | 3 +- src/components/grid-container/container.ts | 1915 +++++++++++++++++ .../grid-container/contianer-interface.ts | 132 ++ .../grid-container/gaia-container-child.ts | 295 +++ .../grid-container/gaia-container-folder.ts | 553 +++++ .../grid-container/gaia-container-page.ts | 116 + .../grid-container/gesture-manager.ts | 69 + .../panels/container/homescreen-container.ts | 61 + src/test/panels/container/icon-style.ts | 75 + src/test/panels/container/icon.ts | 54 + src/test/panels/root.ts | 9 + 11 files changed, 3281 insertions(+), 1 deletion(-) create mode 100644 src/components/grid-container/container.ts create mode 100644 src/components/grid-container/contianer-interface.ts create mode 100644 src/components/grid-container/gaia-container-child.ts create mode 100644 src/components/grid-container/gaia-container-folder.ts create mode 100644 src/components/grid-container/gaia-container-page.ts create mode 100644 src/components/grid-container/gesture-manager.ts create mode 100644 src/test/panels/container/homescreen-container.ts create mode 100644 src/test/panels/container/icon-style.ts create mode 100644 src/test/panels/container/icon.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ea08370..08be918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,5 @@ - add button - add bubble - add indicator-page-point -- add blur \ No newline at end of file +- add blur +- add contaienr diff --git a/src/components/grid-container/container.ts b/src/components/grid-container/container.ts new file mode 100644 index 0000000..996310b --- /dev/null +++ b/src/components/grid-container/container.ts @@ -0,0 +1,1915 @@ +import {html, css, LitElement} from 'lit' +import {customElement, property, state} from 'lit/decorators.js' +import GaiaContainerChild from './gaia-container-child' +import GestureManager from './gesture-manager' +import GaiaContainerPage from './gaia-container-page' +import GaiaContainerFolder from './gaia-container-folder' +import {DragAndDrop, STATUS, ChildElementInfo} from './contianer-interface' +/** + * 想法: + * 1. 用 grid 布局的特性排列应用图标(1×1)和小组件(n×m) + * 2. 用 row、column 属性控制布局 + * 3. 记录初始化后、布局变化后的高度属性,之后插入的图标和小组件进行越界判断,越界则尝试 + * 放入下一个页面中,直至不越界 + * 4. 翻页手势判断需要考量两个方面:触控结束时,划动距离/划动速度 + * 5. 跨页移动图标/组件要考虑页面容量,容量不够则跨页移动失败,需要将被移动元素放回原位 + * 6. 组件有 static dragging sorting turning swipping open_folder 状态, + * 分别对应静止、拖动、整理、翻页、划动、开启文件夹状态,状态发生变化时,将会发出 + * statuschange 事件 + * 7. 因为图标、组件换位需要消耗资源,采用如下策略: + * a. 拖拽元素越过可交换元素两侧时,可以尝试向前(/后)插入,并做越界判断 + * b. 拖拽元素尝试插入空间不足的页面时,记录此次拖拽期间无法向该页面插入拖拽元素 + * c. 拖拽元素越过不可交换元素两侧时,记录该元素的 element 为不可交换元素 + * d. 拖拽移动时,通过手指落点判断当前落点元素,在记录中查找是否为不可交换元素或不可插入页面, + * 是则跳过 【插入 -> 同步图标位置 -> 判断是否越界 -> 越界处理及记录不可交换元素】 这一 + * 系列步骤,否则执行步骤 + * 8. 合并文件夹,当有图标悬浮到另一个图标正上方一段时间时,处于下方的图标会合并为一个文件夹 + */ + +/** + * The time, in ms, to wait for an animation to start in response to a state + * change, before removing the associated style class. + */ +const STATE_CHANGE_TIMEOUT = 100 + +/** + * The time, in ms, to wait before initiating a drag-and-drop from a + * long-press. + */ +const DEFAULT_DND_TIMEOUT = 300 +/** + * The distance, in CSS pixels, in which a touch or mouse point can deviate + * before it being discarded for initiation of a drag-and-drop action. + */ +const DND_THRESHOLD = 5 + +/** + * 图标重叠时,确认要合并为或并入一个文件夹的时间 + */ +const MERGE_FORDER_TIME = 700 + +/** + * 与文件夹交互的计时器时长 + */ +const FORDER_OPERATE_TIME = 500 + +/** + * The minimum time between sending move events during drag-and-drop. + */ +const DND_MOVE_THROTTLE = 50 + +/** + * bezier + */ + +const cubic_bezier = ( + p0: number, + p1: number, + p2: number, + p3: number, + t: number +): number => { + return ( + p0 * Math.pow(1 - t, 3) + + 3 * p1 * t * Math.pow(1 - t, 2) + + 3 * p2 * t * t * (1 - t) + + p3 * Math.pow(t, 3) + ) +} + +@customElement('star-container') +class GaiaContainer extends LitElement { + name: string = 'star-container' + row: number = 6 + column: number = 4 + _frozen: boolean = false + _pendingStateChanges: Function[] = [] + _children: GaiaContainerChild[] = [] + _dnd: DragAndDrop = { + // Whether drag-and-drop is enabled + enabled: false, + + // The time, in ms, to wait before initiating a drag-and-drop from a + // long-press + delay: DEFAULT_DND_TIMEOUT, + + // Timeout used to initiate drag-and-drop actions + timeout: undefined, + + // The child that was tapped/clicked + child: GaiaContainerChild, + + // Whether a drag is active + active: false, + + // The start point of the drag action + start: { + pageX: 0, + pageY: 0, + clientX: 0, + clientY: 0, + translateX: 0, + translateY: 0, + }, + + // The last point of the drag action + last: { + pageX: 0, + pageY: 0, + clientX: 0, + clientY: 0, + timeStamp: 0, + offsetHeight: 0, + offsetWidth: 0, + offsetX: 0, + offsetY: 0, + }, + + // Timeout used to send drag-move events + moveTimeout: undefined, + + // The last time a move event was fired + lastMoveEventTime: 0, + + // Whether to capture the next click event + clickCapture: false, + + // 下一个兄弟节点,用于跨页移动失败后返回原位置时定位用 + nextSibling: null, + previousSibling: null, + + // 是否正在跨页 + isSpanning: false, + + // 正被悬浮覆盖的元素 + dropTarget: null, + + // 最后一个悬浮经过的元素 + lastDropChild: null, + + pagination: 0, + top: 0, + left: 0, + } + // 当前所显示的页面页码 + pagination: number = 0 + // 组件高度,也是页面高度,用于子组件越界判断 + height: number = 0 + width: number = 0 + // 页面列表 + pages = new GaiaContainerPage(this) as unknown as HTMLElement[] + // 滑动偏移量 + _offsetX: number = 0 + // 本次触摸滑动距离 + distance: number = 0 + // 是否进入整理模式 + _sortMode: boolean = false + // 状态 + _status: typeof STATUS[keyof typeof STATUS] = STATUS.STATIC + // 交换元素位置时,无法进行交换的元素 + _staticElements: HTMLElement[] = [] + // 用于首尾页划动的橡皮绳效果 + ratio: number = 1 + // 合并成文件夹计时器 + mergeTimer: number | undefined = undefined + // 文件夹 + folders: {[folderName: string]: GaiaContainerFolder} = {} + // 开启状态的文件夹 + openedFolder: GaiaContainerFolder | null = null + gesture: GestureManager | null = null + dndObserver: MutationObserver | null = null + pageHeight: number = 0 + pageWidth: number = 0 + gridHeight: number = 0 + gridWidth: number = 0 + istouching: boolean = false + + constructor(row = 6, column = 4) { + super() + this.row = row + this.column = column + // this.attachShadow({ mode: "open" }); + + // this.shadowRoot && (this.shadowRoot.innerHTML = this.template); + } + + firstUpdated() { + let dndObserverCallback = () => { + console.log(this._dnd.enabled, this.dragAndDrop) + if (this._dnd.enabled !== this.dragAndDrop) { + this._dnd.enabled = this.dragAndDrop + if (this._dnd.enabled) { + this.addEventListener('touchstart', this) + this.addEventListener('touchmove', this) + this.addEventListener('touchcancel', this) + this.addEventListener('touchend', this) + // 暂不考虑mouse事件,因其与touch事件的触发顺序会影响翻页动画: + // touchend 触发 resetView, mousedown 随后就打断 + // this.addEventListener("mousedown", this); + // this.addEventListener("mousemove", this); + // this.addEventListener("mouseup", this); + this.addEventListener('click', this, true) + this.addEventListener('contextmenu', this, true) + + this.addEventListener('swipeleft', this.swipe) + this.addEventListener('swiperight', this.swipe) + // this.gestureDetector.on('swipeleft', this.swipe); + // this.gestureDetector.on('swiperight', this.swipe); + // this.gestureDetector.on('doubleSwipe', this.swipe); + + this.addEventListener('folder-destroy', this.destroyFolder) + // 多指划屏 + this.gesture = new GestureManager(this) + } else { + this.cancelDrag() + this.removeEventListener('touchstart', this) + this.removeEventListener('touchmove', this) + this.removeEventListener('touchcancel', this) + this.removeEventListener('touchend', this) + // this.removeEventListener("mousedown", this); + // this.removeEventListener("mousemove", this); + // this.removeEventListener("mouseup", this); + this.removeEventListener('click', this, true) + this.removeEventListener('contextmenu', this, true) + + this.removeEventListener('swipeleft', this.swipe) + this.removeEventListener('swiperight', this.swipe) + // this.gestureDetector.off('swipeleft', this.swipe); + // this.gestureDetector.off('swiperight', this.swipe); + // this.gestureDetector.off('doubleSwipe', this.swipe); + this.removeEventListener('folder-destroy', this.destroyFolder) + } + } + } + this.dndObserver = new MutationObserver(dndObserverCallback) + this.dndObserver.observe(this, { + attributes: true, + attributeFilter: ['drag-and-drop'], + }) + + dndObserverCallback() + this.changeLayout() + } + + _getStatus(status = this._status) { + if (status === 0) return ['static'] + + return Object.keys(STATUS) + .filter((type) => { + return status & STATUS[type as keyof typeof STATUS] + }) + .map((type) => type.toLowerCase()) + } + + get status() { + return this._status + } + + set status(value) { + if (value !== this._status) { + this.dispatchEvent( + new CustomEvent('statuschange', { + detail: { + preStatus: this._getStatus(this._status), + curStatus: this._getStatus(value), + changeStatus: this._getStatus(this._status ^ value)[0], + }, + }) + ) + this._status = value + } + } + + swipe = (event: Event) => { + event.type === 'swipeleft' ? this.turnNext('swipe') : this.turnPre('swipe') + } + + turnPre(type: string) { + if (this.pagination <= 0) { + this.pagination = 0 + } else { + this.status |= STATUS.TURN + this.pagination-- + } + this.resetView(type) + } + + turnNext(type: string) { + if (this.pagination < this.pages.length - 1) { + this.pagination++ + this.status |= STATUS.TURN + } else { + this.pagination = this.pages.length - 1 + } + this.resetView(type) + } + + resetView(type: string) { + const target: HTMLElement = (this.pages as any)[this.pagination] + this.smoothSlide(target, type) + } + + // 同时触发划动时,不同触发原因的优先级顺序,越后越优先 + typeIndex = ['reset', 'swipe', 'mouseup', 'touchend'] + // 划动计时器 + timer: number | undefined = undefined + // 触发划动的原因类型 + slideType: string = '' + + smoothSlide(element: HTMLElement, type: string) { + if ( + !element || + this.typeIndex.indexOf(this.slideType) > this.typeIndex.indexOf(type) + ) + return + if (this.timer) { + clearInterval(this.timer) + this.slideType = '' + } + const animateTime = 450 + const target = -element.offsetLeft + const origin = this.offsetX + // 移动总距离 + const distance = target - origin + const startTime = new Date().getTime() + this.slideType = type + this.timer = setInterval(() => { + requestAnimationFrame(() => { + const cur = new Date().getTime() + const t = (cur - startTime) / animateTime + const ratio = cubic_bezier(0.2, 1, 0.95, 1, t) + this.offsetX = + origin + parseInt((distance * ratio) as unknown as string) + + // 在切换页面的时候,由以下代码控制拖拽元素的位置 + if (this._status & STATUS.DRAG) { + this.status |= STATUS.SWIPE + ;( + this._dnd.child as unknown as GaiaContainerChild + ).container.style.transform = + 'translate(' + + (this._dnd.left - this.offsetX) + + 'px, ' + + this._dnd.top + + 'px)' + // 控制与文件夹交互时的拖拽操作 + const child = this._dnd.child as unknown as GaiaContainerChild + if (child) { + child.container.style.setProperty( + '--offset-position-left', + this._dnd.left - this.offsetX + 'px' + ) + child.container.style.setProperty( + '--offset-position-top', + this._dnd.top + 'px' + ) + } + } + + if (ratio >= 1) { + clearInterval(this.timer) + this.timer = undefined + this.slideType = '' + this.offsetX = target + + // 矫正页码 + this.pagination = +(element.dataset.page || this.pagination) + // 清除翻页、划动状态 + this.status &= ~STATUS.TURN + this.status &= ~STATUS.SWIPE + } + }) + }, 10) + } + + destroyFolder = (evt: Event) => { + evt.stopImmediatePropagation() + evt.preventDefault() + + const folder: GaiaContainerFolder = (evt as CustomEvent).detail + this._children = this._children.filter((child) => child !== folder) + folder.master.remove() + delete this.folders[folder.name] + } + + get styleDom() { + return this.shadowRoot && this.shadowRoot.querySelector('style') + } + + get offsetX() { + return this._offsetX + } + + set offsetX(value) { + this.style.transform = `translateX(${value}px)` + this._offsetX = value + } + + get sortMode() { + return this._sortMode + } + + set sortMode(value) { + if (value) { + this._dnd.delay = 0 + this.status |= STATUS.SORT + } else { + this._dnd.delay = DEFAULT_DND_TIMEOUT + this.status &= ~STATUS.SORT + } + this._sortMode = value + } + + // TODO:用于适应旋转屏幕时,改变行数与列数,需要配合row、column属性 + changeLayout() { + // this.styleDom.innerHTML = this.shadowStyle; + const {height, width} = this.getBoundingClientRect() + this.height = height + this.width = width + this.pageWidth = this.pages.length ? this.pages[0].offsetWidth : 0 + this.pageHeight = this.pages.length ? this.pages[0].offsetHeight : 0 + this.gridHeight = this.pageHeight / this.row + this.gridWidth = this.pageWidth / this.column + + this.style.setProperty('--page-width', this.pageWidth + 'px') + this.style.setProperty('--page-height', this.pageHeight + 'px') + this.style.setProperty('--grid-height', this.gridHeight + 'px') + this.style.setProperty('--grid-width', this.gridWidth + 'px') + + this._children.forEach((child) => child.changeSize()) + } + + get containerChildren(): HTMLElement[] { + //div.icon-container + let target: HTMLElement[] = [] + this._children.forEach((child) => { + if (child instanceof GaiaContainerFolder) { + target.push(...(child.children as HTMLElement[])) + } else { + target.push(child.element as HTMLElement) + } + }) + return target + } + + getElementInfo(element: HTMLElement) { + let child = this.getChildByElement(element) + + if (child) { + return { + pagination: child.pagination, + row: child.row, + column: child.column, + anchorCoordinate: child.anchorCoordinate, + folderName: child.folderName, + } + } else { + return null + } + } + + getChildByElement(element: HTMLElement) { + let children = [...this._children] + for (const child of children) { + if ( + child._element === element || + child.master === element || + child.container === element + ) { + return child + } + + if (child instanceof GaiaContainerFolder) { + children.push(...child._children) + } + } + + return null + } + + get firstChild() { + return this._children.length ? this._children[0].element : null + } + + get lastChild() { + const length = this._children.length + return length ? this._children[length - 1].element : null + } + + @property({type: Boolean}) dragAndDrop!: boolean + + get dragAndDropTimeout() { + return this._dnd.delay + } + + set dragAndDropTimeout(timeout) { + if (timeout >= 0) { + this._dnd.delay = timeout + } else { + this._dnd.delay = DEFAULT_DND_TIMEOUT + } + } + + /** + * 在指定页面尾部添加元素,如果页面容量不够,则向后一页尾部添加 + * @param {Number} pagination 添加到的页码 + * @param {...any} args 被添加的元素 + * @returns 被添加的元素 + */ + realAppendChild(pagination = 0, ...args: HTMLElement[]) { + let page = this.pages[pagination] + if (!page) { + page = this.addPage() + } + let proto = HTMLElement.prototype.appendChild + let func + Array.prototype.forEach.call(args, (element) => { + func = proto.call(page, element) + + // 判断插入节点后是否会越界 + if (page.offsetHeight < page.scrollHeight) { + // 越界后判断是否需要重新拿出来放入下一个页面 + if (this._status & STATUS.DRAG) { + // 如果是拖拽中的元素,则返回原位,同时记录本页面无法插入 + if (!this._staticElements.includes(page)) { + this._staticElements.push(page) + } + if (this._dnd.nextSibling) { + this.realInsertBefore(args[0], this._dnd.nextSibling) + } else if (this._dnd.previousSibling) { + this.realInsertAfter(args[0], this._dnd.previousSibling) + } + } else { + // 不是拖拽中的元素,放入下一页 + this.realRemoveChild(element) + this.realAppendChild(++pagination, element) + } + } + }) + return func + } + + /** + * 移除组件内部的指定元素 + * @returns + */ + realRemoveChild(...args: HTMLElement[]) { + let func + Array.from(args).forEach((element) => { + const target = element.parentNode + + let proto = HTMLElement.prototype.removeChild + func = proto.call(target, element) + }) + return func + } + + /** + * 向指定元素前插入节点,若页面容量不够,还原节点位置或者向后一页尾部添加 + * @returns + */ + realInsertBefore(...args: HTMLElement[]) { + const targetParent = arguments[1].parentElement + const nodes = args as unknown as [HTMLElement, HTMLElement] + let proto = HTMLElement.prototype.insertBefore + let func = proto.apply(targetParent, nodes) + // 判断插入节点后是否会越界 + if (targetParent.offsetHeight < targetParent.scrollHeight) { + this.insertCrossBorder(...nodes) + } + return func + } + + /** + * 向指定元素后插入节点 + * @returns + */ + realInsertAfter(...args: [HTMLElement, HTMLElement]) { + const insert = HTMLElement.prototype.insertBefore + const append = HTMLElement.prototype.appendChild + const targetParent = args[1].parentElement + if (!targetParent) throw new Error('parentElement does not exist!') + let func + if (targetParent.lastChild == args[1]) { + func = append.call(targetParent, arguments[0]) + } else { + func = insert.call(targetParent, arguments[0], arguments[1].nextSibling) + } + + // 判断插入节点后是否会越界 + if (targetParent.offsetHeight < targetParent.scrollHeight) { + this.insertCrossBorder(...arguments) + } + + return func + } + + insertCrossBorder(...args: HTMLElement[]) { + if (!args[1] || !args[1].parentElement) { + throw new Error('gaia-container-page does not exist!') + } + let pagination = args[1].parentElement.dataset.page + // 越界后重新拿出来放入下一个页面 + if (!this._dnd.child && pagination) { + // 非拖拽元素导致的越界,后插元素直接加入下一个页面 + return this.realAppendChild(+pagination + 1, arguments[0]) + } + + // 同时记录被后插元素为无法交换元素 + // this._staticElements.push(args[1]); + // 同时记录当前页为无法插入页 + if (!this._staticElements.includes(args[1].parentElement)) { + this._staticElements.push(args[1].parentElement) + } + if (this._dnd?.child?.master === args[0] && this._dnd.isSpanning) { + if (this._dnd.nextSibling) { + // 当前图标/小组件跨页移动失败,根据后一个兄弟节点返回原位置 + return this.realInsertBefore(args[0], this._dnd.nextSibling) + } else if (this._dnd.previousSibling) { + // 当前图标/小组件跨页移动失败,根据前一个兄弟节点返回原位置 + return this.realInsertAfter(args[0], this._dnd.previousSibling) + } else if (this._dnd.pagination) { + // 无前后兄弟节点,退回源页面 + return this.realAppendChild(this._dnd.pagination, args[0]) + } + } + + return this.realAppendChild(this.pagination + 1, args[0]) + } + + realReplaceChild(...args: [HTMLElement, HTMLElement]) { + let proto = HTMLElement.prototype.replaceChild + let func = proto.apply(this, args) + return func + } + + /** + * + * @param {HTMLElement} element 向该组件添加的图标/小组件 + * @param {Object} options 所添加元素的尺寸信息和添加回调 + */ + appendContainerChild(element: HTMLElement, options?: ChildElementInfo) { + if (element.tagName === 'GAIA-WIDGET') { + let obj: ChildElementInfo = options + ? {...options} + : ({} as ChildElementInfo) + obj.row = (element as any).size[0] + obj.column = (element as any).size[1] + this.insertContainerBefore(element, null, obj) + } else { + this.insertContainerBefore(element, null, options) + } + } + + removeContainerChild(element: HTMLElement, callback: Function) { + let children = this._children + let childToRemove: GaiaContainerChild | null = null + + for (let child of children) { + if (child.element === element) { + childToRemove = child + break + } + } + + if (childToRemove === null) { + throw 'removeChild called on unknown child' + } + let that = this + this.changeState(childToRemove, 'removed', () => { + // that.realRemoveChild(childToRemove.container); + + childToRemove && that.realRemoveChild(childToRemove.master) + + // Find the child again. We need to do this in case the container was + // manipulated between removing the child and this callback being reached. + for (let i = 0, iLen = children.length; i < iLen; i++) { + if (children[i] === childToRemove) { + children.splice(i, 1) + break + } + } + + if (callback) { + callback() + } + + this.synchronise() + }) + } + + replaceContainerChild( + newElement: HTMLElement, + oldElement: HTMLElement, + callback: Function + ) { + if (!newElement || !oldElement) { + throw 'replaceChild called with null arguments' + } + + const info = this.getElementInfo(oldElement) + if (!info) throw Error('Can not get oldElement info!') + // Unparent the newElement if necessary (with support for gaia-container) + if (newElement.parentNode == this) { + this.removeContainerChild(newElement, () => { + this.replaceContainerChild(newElement, oldElement, callback) + }) + if (newElement.parentNode) { + return + } + } + + // Remove the old child and add the new one, but don't run the removed/ + // added state changes. + let children = this._children + + for (let i = 0, iLen = children.length; i < iLen; i++) { + let oldChild = children[i] + if (oldChild.element === oldElement) { + const {row, column, anchorCoordinate} = info + let newChild = new GaiaContainerChild( + newElement, + row, + column, + anchorCoordinate, + this + ) + // this.realInsertBefore(newChild.container, oldChild.container); + this.realInsertBefore(newChild.master, oldChild.master) + // this.realRemoveChild(oldChild.container); + this.realRemoveChild(oldChild.master) + this._children.splice(i, 1, newChild) + this.synchronise() + + if (callback) { + callback() + } + return + } + } + + throw 'removeChild called on unknown child' + } + + addPage() { + const page = (this.pages as any).addPage() + HTMLElement.prototype.appendChild.call(this, page) + + if (!this.pageHeight || !this.pageWidth) { + this.pageWidth = page.offsetWidth + this.pageHeight = page.offsetHeight + this.gridHeight = this.pageHeight / this.row + this.gridWidth = this.pageWidth / this.column + + this.style.setProperty('--page-width', this.pageWidth + 'px') + this.style.setProperty('--page-height', this.pageHeight + 'px') + this.style.setProperty('--grid-height', this.gridHeight + 'px') + this.style.setProperty('--grid-width', this.gridWidth + 'px') + } + return page + } + + /** + * Reorders the given element to appear before referenceElement. + */ + reorderChild( + element: HTMLElement, + referenceElement: HTMLElement, + callback?: Function + ) { + if (!element) { + throw 'reorderChild called with null element' + } + + let children = this._children + let child: GaiaContainerChild | null = null + let childIndex: number | null = null + let referenceChild: GaiaContainerChild | null = null + let referenceChildIndex = null + + for (let i = 0, iLen = children.length; i < iLen; i++) { + if (children[i].element === element) { + child = children[i] + childIndex = i + } else if (children[i].element === referenceElement) { + referenceChild = children[i] + referenceChildIndex = i + } + + if (child && (referenceChild || !referenceElement)) { + children.splice(childIndex as number, 1) + + if (referenceChild) { + this.realInsertBefore(child.master, referenceChild.master) + } else { + this.realAppendChild(child.pagination, child.master) + } + + referenceChild + ? children.splice( + (referenceChildIndex as number) - + ((childIndex as number) < (referenceChildIndex as number) + ? 1 + : 0), + 0, + child + ) + : children.splice(children.length, 0, child) + + this.synchronise() + + if (callback) { + callback() + } + return + } + } + + throw child + ? 'reorderChild called on unknown reference element' + : 'reorderChild called on unknown child' + } + + insertContainerBefore( + element: HTMLElement, + reference: HTMLElement | null, + { + callback, + row, + column, + pagination, + anchorCoordinate, + folderName, + }: ChildElementInfo = {} as ChildElementInfo + ) { + let children = this._children + let childToInsert = new GaiaContainerChild( + element, + row, + column, + anchorCoordinate, + this + ) + let referenceIndex = -1 + + if (reference !== null) { + for ( + let i = 0, child = children[i], iLen = children.length; + i < iLen; + i++ + ) { + if (child.element === reference) { + referenceIndex = i + break + } + if (child instanceof GaiaContainerFolder) { + for (const element of child.children) { + if (element === reference) { + // reference 处于文件夹中,referenceIndex 指向该文件夹 + referenceIndex = i + break + } + } + } + } + if (referenceIndex === -1) { + throw 'insertBefore called on unknown child' + } + } + + if (folderName && this.folders[folderName]) { + // 属于文件夹内的图标 + children.push(childToInsert) + this.folders[folderName].addAppIcon(childToInsert.master) + } else if (referenceIndex === -1) { + this.realAppendChild(pagination ?? 0, childToInsert.master) + children.push(childToInsert) + } else { + const referenceNode = children[referenceIndex].folderName + ? this.folders[children[referenceIndex].folderName].master + : children[referenceIndex].master + this.realInsertBefore(childToInsert.master, referenceNode) + children.splice(referenceIndex, 0, childToInsert) + } + + if (folderName && !this.folders[folderName]) { + // 有文件夹名但不存在该文件夹,则将该图标转化为文件夹 + this.appToFolder(childToInsert.master) + } + + childToInsert.isWidget && (childToInsert.isStatic = true) + + this.changeState(childToInsert, 'added', callback) + this.synchronise() + } + + /** + * Used to execute a state-change of a child that may possibly be animated. + * @state will be added to the child's class-list. If an animation starts that + * has the same name, that animation will complete and @callback will be + * called. Otherwise, the class will be removed and @callback called on the + * next frame. + */ + changeState( + child: GaiaContainerChild, + state: 'added' | 'removed', + callback: Function | undefined + ) { + // Check that the child is still attached to this parent (can happen if + // the child is removed while frozen). + if ((child.master.parentElement as HTMLElement).parentNode !== this) { + return + } + + // Check for a redundant state change. + if (child.container.classList.contains(state)) { + return + } + + // Queue up state change if we're frozen. + if (this._frozen) { + this._pendingStateChanges.push( + this.changeState.bind(this, child, state, callback) + ) + return + } + + let animStart = (e: AnimationEvent) => { + if (!e.animationName.endsWith(state)) { + return + } + + child.container.removeEventListener('animationstart', animStart) + + window.clearTimeout(child[state]) + delete child[state] + + // let self = this; + child.container.addEventListener('animationend', function animEnd() { + child.container.removeEventListener('animationend', animEnd) + child.container.classList.remove(state) + if (callback) { + callback() + } + }) + } + + child.container.addEventListener('animationstart', animStart) + child.container.classList.add(state) + + child[state] = window.setTimeout(() => { + delete child[state] + child.container.removeEventListener('animationstart', animStart) + child.container.classList.remove(state) + if (callback) { + callback() + } + }, STATE_CHANGE_TIMEOUT) + } + + getChildOffsetRect(element: HTMLElement) { + let children = this._children + for (let i = 0, iLen = children.length; i < iLen; i++) { + let child = children[i] + if (child.element === element) { + let top = child._lastMasterTop as number + let left = child._lastMasterLeft as number + let width = child._lastElementWidth as number + let height = child._lastElementHeight as number + + return { + top: top, + left: left, + width: width, + height: height, + right: left + width, + bottom: top + height, + } + } + } + + throw 'getChildOffsetRect called on unknown child' + } + + getChildFromPoint(x: number, y: number) { + // 是否与主屏文件夹进行交互中 + const interactWithFolder = !!this.openedFolder + const {offsetHeight, offsetWidth, offsetX, offsetY} = this._dnd.last + let children + let folderPosition = {left: 0, top: 0, height: 0, width: 0} + let page = this.pages[this.pagination] + + if (interactWithFolder && this.openedFolder) { + children = this.openedFolder?.children + folderPosition.left = this.openedFolder.element.offsetLeft + folderPosition.top = this.openedFolder.element.offsetTop + folderPosition.height = this.openedFolder.element.offsetHeight + folderPosition.width = this.openedFolder.element.offsetWidth + } else { + children = this._children + } + x += -page.scrollLeft + page.offsetLeft + y += -page.scrollTop + page.offsetTop + + const childX = x - folderPosition.left + const childY = y - folderPosition.top + + if ( + childX >= offsetX && + childY >= offsetY && + childX < offsetX + offsetWidth && + childY < offsetY + offsetHeight + ) { + // 悬浮在自身容器之上 + this._staticElements = [this._dnd.child.element] + return {dropTarget: this._dnd.child.element, isPage: false} + } + + for (let i = 0, iLen = children.length; i < iLen; i++) { + let child = children[i] as GaiaContainerChild + if ( + childX >= (child._lastMasterLeft as number) && + childY >= (child._lastMasterTop as number) && + childX < + (child._lastMasterLeft as number) + + (child._lastElementWidth as number) && + childY < + (child._lastMasterTop as number) + + (child._lastElementHeight as number) + ) { + if (child.pagination !== this._dnd.child.pagination) { + // 当被选中元素与被移动元素页码不一致时,该次移动属于跨页移动 + this._dnd.isSpanning = true + } + return { + dropTarget: child.element, + isPage: false, + isFolder: child.isFolder, + } + } + } + + if ( + this.openedFolder && + x > folderPosition.left && + x < folderPosition.left + folderPosition.width && + y > folderPosition.top && + y < folderPosition.top + folderPosition.height + ) { + return { + dropTarget: this.openedFolder.element, + isPage: false, + isFolder: true, + } + } else if (this.openedFolder) { + return { + dropTarget: this.openedFolder.container, + isPage: false, + isFolder: true, + } + } + + let targetPage = null + let i = 0 + for (; i < this.pages.length; i++) { + const page = this.pages[i] + if (x > page.offsetLeft) { + targetPage = page + + if (page.dataset.page !== this._dnd.child.pagination) { + // 当目标页与被移动元素页码不一致时,该次移动属于跨页移动 + this._dnd.isSpanning = true + } + } else { + break + } + } + + return { + dropTarget: targetPage, + isPage: true, + pagination: --i, + } + } + + cancelDrag() { + if (this._dnd.timeout !== null) { + clearTimeout(this._dnd.timeout) + this._dnd.timeout = undefined + } + + if (this._dnd.moveTimeout !== null) { + clearTimeout(this._dnd.moveTimeout) + this._dnd.moveTimeout = undefined + } + + if (this._dnd.active) { + this._dnd.child.container.classList.remove('dragging') + this._dnd.child.container.style.position = 'absolute' + this._dnd.child.container.style.top = '0' + this._dnd.child.container.style.left = '0' + this._dnd.child.markDirty() + this._dnd.active = false + this._dnd.clickCapture = true + this.dispatchEvent(new CustomEvent('drag-finish')) + } + this.synchronise() + + if (this._dnd.lastDropChild) { + this._dnd.lastDropChild.master.classList.remove('merging') + this._dnd.lastDropChild = null + } + + this._dnd.child?.container.style.setProperty( + '--offset-position-left', + '0px' + ) + this._dnd.child?.container.style.setProperty('--offset-position-top', '0px') + this._dnd.child = null + this._dnd.isSpanning = false + this.status &= ~STATUS.DRAG + } + + startDrag() { + this.status |= STATUS.DRAG + this._staticElements = [this._dnd.child.element] + this._dnd.start.translateX = this._dnd.child.master.offsetLeft + this._dnd.start.translateY = this._dnd.child.master.offsetTop + + this._dnd.last.offsetHeight = this._dnd.child.master.offsetHeight + this._dnd.last.offsetWidth = this._dnd.child.master.offsetWidth + this._dnd.last.offsetX = this._dnd.child._lastMasterLeft + this._dnd.last.offsetY = this._dnd.child._lastMasterTop + if ( + !this.dispatchEvent( + new CustomEvent('drag-start', { + cancelable: true, + detail: { + target: this._dnd.child.element, + pageX: this._dnd.start.pageX, + pageY: this._dnd.start.pageY, + clientX: this._dnd.start.clientX, + clientY: this._dnd.start.clientY, + }, + }) + ) + ) { + return + } + + this._dnd.active = true + this._dnd.child.container.classList.add('dragging') + this._dnd.child.isStatic = false + // this._dnd.child.container.style.position = "fixed"; + let rect = this.getBoundingClientRect() + this._dnd.child.container.style.top = rect.top + 'px' + this._dnd.child.container.style.left = rect.left - this.offsetX + 'px' + this._dnd.pagination = this.pagination + } + + continueDrag() { + if (!this._dnd.active) { + return + } + + let left = + (this._dnd.start.translateX as number) + + (this._dnd.last.pageX - this._dnd.start.pageX) - + this.pages[this._dnd.pagination].offsetLeft + let top = + (this._dnd.start.translateY as number) + + (this._dnd.last.pageY - this._dnd.start.pageY) + this._dnd.left = left + this._dnd.top = top + + // 在切换页面的时候,不由以下代码控制拖拽元素的位置 + if (!this.timer) { + this._dnd.child.container.style.transform = + 'translate(' + (this._dnd.left - this.offsetX) + 'px, ' + top + 'px)' + // 控制与文件夹交互时的拖拽操作 + this._dnd.child?.container.style.setProperty( + '--offset-position-left', + this._dnd.last.pageX - this._dnd.start.pageX + 'px' + ) + this._dnd.child?.container.style.setProperty( + '--offset-position-top', + this._dnd.last.pageY - this._dnd.start.pageY + 'px' + ) + } + + if (this._dnd.moveTimeout === undefined) { + let delay = Math.max( + 0, + DND_MOVE_THROTTLE - + (this._dnd.last.timeStamp - this._dnd.lastMoveEventTime) + ) + this._dnd.moveTimeout = setTimeout(() => { + this._dnd.moveTimeout = undefined + this._dnd.lastMoveEventTime = this._dnd.last.timeStamp + this.dispatchEvent( + new CustomEvent('drag-move', { + detail: { + target: this._dnd.child.element, + pageX: this._dnd.last.pageX, + pageY: this._dnd.last.pageY, + clientX: this._dnd.last.clientX, + clientY: this._dnd.last.clientY, + }, + }) + ) + }, delay) + } + + this.dropElement() + } + + clearMergeTimer() { + clearTimeout(this.mergeTimer) + this.mergeTimer = undefined + } + + /** + * 交换被拖拽元素位置 + */ + dropElement() { + let {dropTarget, isPage, pagination} = this.getChildFromPoint( + this._dnd.last.clientX, + this._dnd.last.clientY + ) + this._dnd.dropTarget = dropTarget + // 拖拽元素悬浮页面默认为当前页面 + const suspendingPage = isPage ? dropTarget : this.pages[this.pagination] + if ( + dropTarget === this._dnd.child.element || // 悬浮在自身容器之上 + (this._dnd.child.isTail && isPage) + ) { + // 拖拽元素为页面尾部元素,且悬浮在页面空白处 + if (this._dnd.lastDropChild) { + this._dnd.lastDropChild.master.classList.remove('merging') + this._dnd.lastDropChild = null + } + + this.clearMergeTimer() + return + } + if (dropTarget?.dataset?.static) { + // 在尝试与锚固元素交换 + if (dropTarget.tagName === 'GAIA-WIDGET') { + // 锚固元素为主屏小组件 + if (this._dnd.child.isWidget) { + const widget = this.getChildByElement( + dropTarget + ) as GaiaContainerChild + // 交换元素也为小组件时,解除锚固并进行交换 + widget.isStatic = false + } else { + // 交换元素为普通的图标,无法交换 + return this._staticElements.push(dropTarget) + } + } + } + + let children = this._children + let folderPosition = {left: 0, top: 0} + if (this.openedFolder) { + children = this.openedFolder._children + folderPosition.left = this.openedFolder.element.offsetLeft + folderPosition.top = this.openedFolder.element.offsetTop + } + if (isPage) { + this._dnd.child.isWidget && (this._dnd.child.isStatic = false) + this.realAppendChild(pagination, this._dnd.child.master) + this._dnd.child.isWidget && (this._dnd.child.isStatic = 'current') + return this.synchronise() + } + if (dropTarget) { + let dropChild: GaiaContainerChild | null = null + let dropIndex = -1 + let childIndex = -1 + let operator = '' + const lastX = + this._dnd.last.pageX + this.pages[this.pagination].offsetLeft + const lastY = this._dnd.last.pageY + for (let i = 0, iLen = children.length; i < iLen; i++) { + if (children[i] === this._dnd.child) { + childIndex = i + } + + if (children[i].element === dropTarget) { + dropChild = children[i] + dropIndex = i + + if (this._dnd.lastDropChild !== dropChild) { + this._dnd.lastDropChild?.master.classList.remove('merging') + this.clearMergeTimer() + this._dnd.lastDropChild = dropChild + } + + if (this.openedFolder) { + if (dropTarget === this.openedFolder.element) { + // 添加进文件夹 + operator = 'addInFolder' + } else if (dropTarget === this.openedFolder.container) { + // 将要移出文件夹 + setTimeout(() => { + ;(this.openedFolder as GaiaContainerFolder).close() + operator = 'removeFromFolder' + }, FORDER_OPERATE_TIME) + } else { + // 与文件夹内其他图标交换 + } + } + + const containerX = + lastX - children[i].master.offsetLeft - folderPosition.left + const containerY = + lastY - children[i].master.offsetTop - folderPosition.top + const ratioX = containerX / children[i].master.offsetWidth + const ratioY = containerY / children[i].master.offsetHeight + + if (ratioX < 0.3 || (ratioX < 0.5 && ratioY < 0.15)) { + operator = 'insertBefore' + dropChild.master.classList.remove('merging') + this.clearMergeTimer() + } else if (ratioX > 0.7 || (ratioX >= 0.5 && ratioY < 0.15)) { + operator = 'insertAfter' + dropChild.master.classList.remove('merging') + this.clearMergeTimer() + } else if (ratioY >= 0.85) { + dropChild.master.classList.remove('merging') + this.clearMergeTimer() + } else { + operator = 'merge' + if ( + !this.mergeTimer && + dropChild.position === 'page' && + !dropChild.isFolder && + !this._dnd.child.isFolder && + !dropChild.isWidget && + !this._dnd.child.isWidget + ) { + // 图标悬浮于另一个图标正上方 + dropChild.master.classList.add('merging') + this.mergeTimer = setTimeout(() => { + this.mergeFolder( + (dropChild as GaiaContainerChild).master, + this._dnd.child.master + ) + this.clearMergeTimer() + }, MERGE_FORDER_TIME) + } else if ( + dropChild.position === 'page' && + dropChild.isFolder && + !this._dnd.child.isFolder && + !dropChild.isWidget && + !this._dnd.child.isWidget + ) { + // 图标悬浮于文件夹正上方 + this.clearMergeTimer() + dropChild.master.classList.add('merging') + this.mergeTimer = setTimeout(() => { + ;(dropChild as GaiaContainerFolder).open() + this.mergeFolder( + (dropChild as GaiaContainerFolder).master, + this._dnd.child.master + ) + this.clearMergeTimer() + }, MERGE_FORDER_TIME) + } + } + } + + if (dropIndex >= 0 && childIndex >= 0) { + if ( + (operator !== 'merge' && + this._staticElements.includes(dropTarget)) || + this._staticElements.includes(suspendingPage) + ) { + break + } + this.dropOperate(dropIndex, dropChild, childIndex, operator) + break + } + } + } + } + + /** + * 将 App 合并入落点所在的文件夹,若落点无文件夹,则由落点元素转化为文件夹 + * @param {HTMLElement} dropItem 落点元素 + * @param {HTMLElement} appMaster 悬浮应用图标 + */ + mergeFolder(dropItem: HTMLElement, appMaster: HTMLElement) { + if (!dropItem.classList.contains('folder')) { + this.appToFolder(dropItem) + } else { + const folder = this.getChildByElement(dropItem) as GaiaContainerFolder + folder.addAppIcon(appMaster, true) + } + } + + /** + * 将应用图标转化为文件夹 + * @param {HTMLElement} appMaster 将要被转化为文件夹的应用图标 + * @param {String | undefined} folderName 文件名 + */ + appToFolder(appMaster: GaiaContainerChild['master']) { + const folder = new GaiaContainerFolder(this) + const name = folder.name + this.folders[name] = folder + this._children.push(folder) + let referenceNode + const page = appMaster.parentElement + let appendFolder = (referenceNode: HTMLElement) => + HTMLElement.prototype.insertBefore.call( + page, + folder.master, + referenceNode + ) + if (appMaster.nextElementSibling) { + referenceNode = appMaster.nextElementSibling + } else { + if (appMaster.nextElementSibling) { + referenceNode = appMaster.nextElementSibling + } else { + appendFolder = () => + HTMLElement.prototype.appendChild.call(page, folder.master) + } + } + + folder.addAppIcon(appMaster) + appendFolder(referenceNode as HTMLElement) + + this._staticElements = this._staticElements.filter( + (el) => el !== folder.element + ) + // this.getChildByElement(folder.master).synchroniseContainer(); + // this.getChildByElement(appMaster).synchroniseContainer(); + // folder.open(); + return folder + } + + /** + * 拖拽操作,分为向前插入、向后插入、融合为文件夹 + * @param {Number} dropIndex 拖拽落点元素序号 + * @param {HTMLElement} dropChild 拖拽落点元素 + * @param {Number} childIndex 拖拽元素序号 + * @param {String} operator 拖拽操作 + */ + dropOperate( + dropIndex: number, + dropChild: GaiaContainerChild | null, + childIndex: number, + operator: string + ) { + // 被拖拽元素与放置位置元素为同一元素,取消接下来的插入操作 + // if (dropIndex === childIndex) return this.cancelDrag(); + const children = this._children + this._dnd.child.isWidget && (this._dnd.child.isStatic = false) + // Default action, rearrange the dragged child to before or after + // the child underneath the touch/mouse point. + // this.realRemoveChild(this._dnd.child.container); + // this.realRemoveChild(this._dnd.child.master); + // 此次拖拽操作不属于向前插入动作 + // if (!insertBefore && !dropChild.master.nextSibling) { + if (operator === 'insertAfter' && dropChild) { + this.realInsertAfter(this._dnd.child.master, dropChild.master) + this._staticElements.push(dropChild.element as HTMLElement) + } else if (operator === 'insertBefore' && dropChild) { + this.realInsertBefore(this._dnd.child.master, dropChild.master) + this._staticElements.push(dropChild.element as HTMLElement) + } else { + // TODO: 此处为合并为文件夹方法 + // this.mergeForder(); + } + children.splice(dropIndex, 0, children.splice(childIndex, 1)[0]) + this.dispatchEvent(new CustomEvent('drag-rearrange')) + this.synchronise() + } + + endDrag(event: Event) { + if (this._dnd.active) { + this.dropElement() + this.dispatchEvent( + new CustomEvent('drag-end', { + cancelable: true, + detail: { + target: this._dnd.child.element, + dropTarget: this._dnd.dropTarget, + pageX: this._dnd.last.pageX, + pageY: this._dnd.last.pageY, + clientX: this._dnd.last.clientX, + clientY: this._dnd.last.clientY, + }, + }) + ) + } else if (this._dnd.timeout !== null && this._dnd.child?.isFolder) { + this._dnd.child.open() + } else if (this._dnd.timeout !== null) { + let handled = !this.dispatchEvent( + new CustomEvent('activate', { + cancelable: true, + detail: { + target: this._dnd.child.element, + }, + }) + ) + if (handled) { + event.stopImmediatePropagation() + event.preventDefault() + } + } + + this.cancelDrag() + } + + handleTransformRatio(x: number) { + if (this._status & STATUS.DRAG) return x + + const percentage = Math.abs(x) / this.pageHeight + this.ratio = 1 + + if ( + (!+this.pagination && x >= 0) || + (this.pagination == this.pages.length - 1 && x < 0) + ) { + this.ratio = 1 / (4 * percentage + 1) + } + + return this.ratio > 0 ? this.ratio : 0 + } + + handleEvent(event: Event) { + switch (event.type) { + case 'touchstart': + case 'mousedown': + this.istouching = true + if (this.timer) { + clearInterval(this.timer) + this.timer = undefined + this.slideType = '' + } + if (this._dnd.active || this._dnd.timeout) { + // this.cancelDrag(); + + break + } + + if (event instanceof MouseEvent) { + this._dnd.start.pageX = event.pageX + this._dnd.start.pageY = event.pageY + this._dnd.start.clientX = event.clientX + this._dnd.start.clientY = event.clientY + } else if (event instanceof TouchEvent) { + this._dnd.start.pageX = event.touches[0].pageX + this._dnd.start.pageY = event.touches[0].pageY + this._dnd.start.clientX = event.touches[0].clientX + this._dnd.start.clientY = event.touches[0].clientY + } + + this._dnd.last.pageX = this._dnd.start.pageX + this._dnd.last.pageY = this._dnd.start.pageY + this._dnd.last.clientX = this._dnd.start.clientX + this._dnd.last.clientY = this._dnd.start.clientY + this._dnd.last.timeStamp = event.timeStamp + + let target = event.target as HTMLElement //gaia-app-icon + + // Find the child + let children = [...this._children] + for (let child of children) { + if (child.isFolder) { + children.push(...(child as GaiaContainerFolder)._children) + } + if ( + child.element === target || + child.master === target || + (!child.isFolder && + (child.element as HTMLElement).compareDocumentPosition(target) & + 16) + ) { + this._dnd.child = child + this._dnd.nextSibling = child.master.nextSibling as HTMLElement + this._dnd.previousSibling = child.master + .previousSibling as HTMLElement + break + } + } + + if (!this._dnd.child) { + return + } + + if (this._dnd.delay > 0) { + this._dnd.timeout = setTimeout(() => { + this._dnd.timeout = undefined + this.startDrag() + }, this._dnd.delay) + } else { + this.startDrag() + } + break + + case 'touchmove': + case 'mousemove': + let pageX, pageY, clientX, clientY + if (event instanceof MouseEvent) { + pageX = event.pageX + pageY = event.pageY + clientX = event.clientX + clientY = event.clientY + } else { + pageX = (event as TouchEvent).touches[0].pageX + pageY = (event as TouchEvent).touches[0].pageY + clientX = (event as TouchEvent).touches[0].clientX + clientY = (event as TouchEvent).touches[0].clientY + } + this.distance = pageX - this._dnd.last.pageX + + if (!(this._status & STATUS.DRAG)) { + const ratio = this.handleTransformRatio(this.distance) + this.istouching && + (this.status |= STATUS.SWIPE) && + (this.style.transform = `translateX(${ + parseInt(String(this.distance * ratio)) + this.offsetX + }px`) + } + + if (this._dnd.timeout) { + if ( + Math.abs(pageX - this._dnd.start.pageX) > DND_THRESHOLD || + Math.abs(pageY - this._dnd.start.pageY) > DND_THRESHOLD + ) { + clearTimeout(this._dnd.timeout) + this._dnd.timeout = undefined + } + } else if (this._dnd.active) { + event.preventDefault() + this._dnd.last.pageX = pageX + this._dnd.last.pageY = pageY + this._dnd.last.clientX = clientX + this._dnd.last.clientY = clientY + this._dnd.last.timeStamp = event.timeStamp + + this.continueDrag() + } + break + + case 'touchcancel': + this.istouching = false + this.cancelDrag() + break + + case 'touchend': + case 'mouseup': + this.istouching = false + if (!(this._status & STATUS.DRAG)) { + this.offsetX += +this.distance * this.ratio + this.ratio = 1 + if (Math.abs(this.distance) < this.width / 2) { + } else if (this.distance > 0) { + this.turnPre(event.type) + } else { + this.turnNext(event.type) + } + this.distance = 0 + } + this.resetView('') + + if (this._dnd.active) { + event.preventDefault() + // event.stopImmediatePropagation(); + } + + !(event as TouchEvent)?.touches?.length && this.endDrag(event) + break + + case 'click': + if (this._dnd.clickCapture) { + this._dnd.clickCapture = false + event.preventDefault() + event.stopImmediatePropagation() + } + + if (this._dnd?.child?.isFolder) { + event.preventDefault() + event.stopImmediatePropagation() + this._dnd.child.open() + this.cancelDrag() + } + break + + case 'contextmenu': + if (this._dnd.active || this._dnd.timeout) { + event.stopImmediatePropagation() + // event.preventDefault(); + } + break + } + } + + /** + * Temporarily disables element position synchronisation. Useful when adding + * multiple elements to the container at the same time, or in quick + * succession. + */ + freeze() { + this._frozen = true + } + + /** + * Enables element position synchronisation after freeze() has been called. + */ + thaw() { + if (this._frozen) { + this._frozen = false + for (let callback of this._pendingStateChanges) { + callback() + } + this._pendingStateChanges = [] + this.synchronise() + } + } + + /** + * Synchronise positions between the managed container and all children. + * This is called automatically when adding/inserting or removing children, + * but must be called manually if the managed container is manipulated + * outside of these methods (for example, if style properties change, or + * if it's resized). + */ + synchronise() { + if (this._frozen) { + return + } + + for (let child of this._children) { + if (child.isWidget) { + if (this._dnd.child === child) { + child.isStatic = 'current' + continue + } + child.isStatic = true + } + } + + let child + let children = [...this._children] + for (child of children) { + // if (!this._dnd.active || child !== this._dnd.child) { + child.synchroniseMaster() + // } + } + + for (let i = 0; i < children.length; i++) { + child = children[i] + if (child.isFolder) + children.splice(i + 1, 0, ...(child as GaiaContainerFolder)._children) + let isActive = this._dnd.active && child === this._dnd.child + child.synchroniseContainer(isActive) + + // 越界 + if (!isActive) { + if ( + child.position === 'page' && + this.pageHeight < + (child._lastMasterTop as number) + + (child._lastElementHeight as number) + ) { + // 页内越界 + this.realAppendChild(child.pagination + 1, child.master) + } else if ( + child.position === 'folder' && + this.folders[child.folderName].element.offsetHeight < + (child._lastMasterTop as number) + + (child._lastElementHeight as number) + ) { + // 文件夹内越界 + this.folders[child.folderName].element.appendChild(child.master) + } + } + + if (isActive) { + // 被拖拽元素更新容器位置后,还需要更新 _dnd.last + this._dnd.last.offsetHeight = child.master.offsetHeight + this._dnd.last.offsetWidth = child.master.offsetWidth + this._dnd.last.offsetX = child._lastMasterLeft as number + this._dnd.last.offsetY = child._lastMasterTop as number + + // 更新不可交换元素数组 + // this._staticElements = [child]; + } + } + } + + static styles = 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.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; + } + ` + + @state() + shadowStyles: {[subModule: string]: string} = new Proxy( + {}, + { + set: ( + target: {[module: string]: string}, + prop: string, + value + ): boolean => { + console.log('prop', prop) + console.log('value', value) + console.log('target[prop]', target[prop]) + + if (!target[prop] || target[prop] !== value) { + target[prop] = value + this.requestUpdate() + } + + return true + }, + } + ) + render() { + let styles = '' + for (const subModule in this.shadowStyles) { + const style = this.shadowStyles[subModule] + styles += style + } + return html` + +
+ +
+ ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'star-container': GaiaContainer + } +} + +export default GaiaContainer diff --git a/src/components/grid-container/contianer-interface.ts b/src/components/grid-container/contianer-interface.ts new file mode 100644 index 0000000..5c00768 --- /dev/null +++ b/src/components/grid-container/contianer-interface.ts @@ -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 +} diff --git a/src/components/grid-container/gaia-container-child.ts b/src/components/grid-container/gaia-container-child.ts new file mode 100644 index 0000000..01caca1 --- /dev/null +++ b/src/components/grid-container/gaia-container-child.ts @@ -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)') + } + } +} diff --git a/src/components/grid-container/gaia-container-folder.ts b/src/components/grid-container/gaia-container-folder.ts new file mode 100644 index 0000000..183d6b1 --- /dev/null +++ b/src/components/grid-container/gaia-container-folder.ts @@ -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; + } + ` + } +} diff --git a/src/components/grid-container/gaia-container-page.ts b/src/components/grid-container/gaia-container-page.ts new file mode 100644 index 0000000..28dbed4 --- /dev/null +++ b/src/components/grid-container/gaia-container-page.ts @@ -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 diff --git a/src/components/grid-container/gesture-manager.ts b/src/components/grid-container/gesture-manager.ts new file mode 100644 index 0000000..de75226 --- /dev/null +++ b/src/components/grid-container/gesture-manager.ts @@ -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 diff --git a/src/test/panels/container/homescreen-container.ts b/src/test/panels/container/homescreen-container.ts new file mode 100644 index 0000000..155bc21 --- /dev/null +++ b/src/test/panels/container/homescreen-container.ts @@ -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` + + + ` + } + + static styles = css` + star-container { + height: 90vh; + width: 100vw; + } + ` +} + +declare global { + interface HTMLElementTagNameMap { + 'panel-container': PanelContainer + } +} diff --git a/src/test/panels/container/icon-style.ts b/src/test/panels/container/icon-style.ts new file mode 100644 index 0000000..a1b49f8 --- /dev/null +++ b/src/test/panels/container/icon-style.ts @@ -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); + } + } +` diff --git a/src/test/panels/container/icon.ts b/src/test/panels/container/icon.ts new file mode 100644 index 0000000..1971f53 --- /dev/null +++ b/src/test/panels/container/icon.ts @@ -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` +
+
+ +
+
+
图标${++count}
+
+ ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'site-icon': SiteIcon + } +} diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts index 00a9607..26c1bf8 100644 --- a/src/test/panels/root.ts +++ b/src/test/panels/root.ts @@ -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" >
+ +
Date: Tue, 30 Aug 2022 10:10:42 +0800 Subject: [PATCH 02/18] TASK: #103599 - add SlotStyleHandler --- CHANGELOG.md | 1 + src/utils/SlotStyleHandler.ts | 103 ++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/utils/SlotStyleHandler.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 08be918..0c11db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,4 @@ - add indicator-page-point - add blur - add contaienr +- add SlotStyleHandler diff --git a/src/utils/SlotStyleHandler.ts b/src/utils/SlotStyleHandler.ts new file mode 100644 index 0000000..4349134 --- /dev/null +++ b/src/utils/SlotStyleHandler.ts @@ -0,0 +1,103 @@ +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) => { + console.log('=====', match) + globalCss += match + return '' + }) + style = style.replace(this.regex.slottedCss, (match) => { + globalCss += match.replace(this.regex['::slotted()'], name + ' $1') + return '' + }) + + return globalCss + } + + /** + * 将CSS注入头部 + * + * @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() From eab57a3d92c5678260c97a313a01ddb510781017 Mon Sep 17 00:00:00 2001 From: luojiahao Date: Tue, 30 Aug 2022 13:51:10 +0800 Subject: [PATCH 03/18] TASK: #103599 - edit container folder animation --- CHANGELOG.md | 1 + src/components/grid-container/container.ts | 52 +++--- .../grid-container/gaia-container-child.ts | 6 +- .../grid-container/gaia-container-folder.ts | 155 ++++++++---------- src/test/panels/container/asserts/icon.png | Bin 0 -> 2353 bytes .../panels/container/homescreen-container.ts | 68 ++++++-- src/test/panels/container/homescreen-style.ts | 9 + src/test/panels/container/icon-style.ts | 21 ++- src/test/panels/container/icon.ts | 25 ++- src/utils/SlotStyleHandler.ts | 1 - 10 files changed, 206 insertions(+), 132 deletions(-) create mode 100644 src/test/panels/container/asserts/icon.png create mode 100644 src/test/panels/container/homescreen-style.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c11db4..2174ba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ - add blur - add contaienr - add SlotStyleHandler +- edit container folder animation diff --git a/src/components/grid-container/container.ts b/src/components/grid-container/container.ts index 996310b..e36ebdf 100644 --- a/src/components/grid-container/container.ts +++ b/src/components/grid-container/container.ts @@ -5,6 +5,7 @@ import GestureManager from './gesture-manager' import GaiaContainerPage from './gaia-container-page' import GaiaContainerFolder from './gaia-container-folder' import {DragAndDrop, STATUS, ChildElementInfo} from './contianer-interface' +import slotStyleHandler from '../../utils/SlotStyleHandler' /** * 想法: * 1. 用 grid 布局的特性排列应用图标(1×1)和小组件(n×m) @@ -188,14 +189,19 @@ class GaiaContainer extends LitElement { super() this.row = row this.column = column + ;(window as any).con = this // this.attachShadow({ mode: "open" }); // this.shadowRoot && (this.shadowRoot.innerHTML = this.template); } firstUpdated() { + slotStyleHandler.injectGlobalCss( + this, + GaiaContainer.styles.cssText, + this.name + ) let dndObserverCallback = () => { - console.log(this._dnd.enabled, this.dragAndDrop) if (this._dnd.enabled !== this.dragAndDrop) { this._dnd.enabled = this.dragAndDrop if (this._dnd.enabled) { @@ -517,6 +523,7 @@ class GaiaContainer extends LitElement { * @returns 被添加的元素 */ realAppendChild(pagination = 0, ...args: HTMLElement[]) { + if (pagination < 0) return let page = this.pages[pagination] if (!page) { page = this.addPage() @@ -1028,14 +1035,10 @@ class GaiaContainer extends LitElement { for (let i = 0, iLen = children.length; i < iLen; i++) { let child = children[i] as GaiaContainerChild if ( - childX >= (child._lastMasterLeft as number) && - childY >= (child._lastMasterTop as number) && - childX < - (child._lastMasterLeft as number) + - (child._lastElementWidth as number) && - childY < - (child._lastMasterTop as number) + - (child._lastElementHeight as number) + childX >= child._lastMasterLeft! && + childY >= child._lastMasterTop! && + childX < child._lastMasterLeft! + this.gridWidth * child.column && + childY < child._lastMasterTop! + this.gridHeight * child.row ) { if (child.pagination !== this._dnd.child.pagination) { // 当被选中元素与被移动元素页码不一致时,该次移动属于跨页移动 @@ -1120,11 +1123,14 @@ class GaiaContainer extends LitElement { this._dnd.lastDropChild = null } - this._dnd.child?.container.style.setProperty( + this._dnd.child?.container?.style.setProperty( '--offset-position-left', '0px' ) - this._dnd.child?.container.style.setProperty('--offset-position-top', '0px') + this._dnd.child?.container?.style.setProperty( + '--offset-position-top', + '0px' + ) this._dnd.child = null this._dnd.isSpanning = false this.status &= ~STATUS.DRAG @@ -1351,6 +1357,7 @@ class GaiaContainer extends LitElement { // 图标悬浮于另一个图标正上方 dropChild.master.classList.add('merging') this.mergeTimer = setTimeout(() => { + if (!this._dnd.child) return this.mergeFolder( (dropChild as GaiaContainerChild).master, this._dnd.child.master @@ -1364,10 +1371,10 @@ class GaiaContainer extends LitElement { !dropChild.isWidget && !this._dnd.child.isWidget ) { - // 图标悬浮于文件夹正上方 this.clearMergeTimer() dropChild.master.classList.add('merging') this.mergeTimer = setTimeout(() => { + if (!this._dnd.child) return ;(dropChild as GaiaContainerFolder).open() this.mergeFolder( (dropChild as GaiaContainerFolder).master, @@ -1437,8 +1444,8 @@ class GaiaContainer extends LitElement { } } - folder.addAppIcon(appMaster) appendFolder(referenceNode as HTMLElement) + folder.addAppIcon(appMaster) this._staticElements = this._staticElements.filter( (el) => el !== folder.element @@ -1505,7 +1512,7 @@ class GaiaContainer extends LitElement { ) } else if (this._dnd.timeout !== null && this._dnd.child?.isFolder) { this._dnd.child.open() - } else if (this._dnd.timeout !== null) { + } else if (this._dnd.timeout !== null && this._dnd.child?.element) { let handled = !this.dispatchEvent( new CustomEvent('activate', { cancelable: true, @@ -1760,6 +1767,9 @@ class GaiaContainer extends LitElement { child.synchroniseMaster() // } } + if (Object.keys(this.folders).length) { + // debugger + } for (let i = 0; i < children.length; i++) { child = children[i] @@ -1840,6 +1850,10 @@ class GaiaContainer extends LitElement { transform: opacity 0.1s; } + ::slotted(.gaia-container-child) { + width: 100%; + } + ::slotted(.gaia-container-child.dragging) { z-index: 1; will-change: transform; @@ -1867,6 +1881,9 @@ class GaiaContainer extends LitElement { } ` + editStyle(moduleName: string, style: string) { + this.shadowStyles[moduleName] = style + } @state() shadowStyles: {[subModule: string]: string} = new Proxy( {}, @@ -1874,14 +1891,11 @@ class GaiaContainer extends LitElement { set: ( target: {[module: string]: string}, prop: string, - value + value: string ): boolean => { - console.log('prop', prop) - console.log('value', value) - console.log('target[prop]', target[prop]) - if (!target[prop] || target[prop] !== value) { target[prop] = value + slotStyleHandler.injectGlobalCss(this, value, this.name, prop) this.requestUpdate() } diff --git a/src/components/grid-container/gaia-container-child.ts b/src/components/grid-container/gaia-container-child.ts index 01caca1..02b3a6e 100644 --- a/src/components/grid-container/gaia-container-child.ts +++ b/src/components/grid-container/gaia-container-child.ts @@ -28,7 +28,6 @@ export default class GaiaContainerChild { column: number manager: GaiaContainer _isStatic: boolean | string - folderName: string = '' anchorCoordinate: Coordinate // 静态位置 _lastMasterTop: number | null = null @@ -89,6 +88,11 @@ export default class GaiaContainerChild { return inPage ? 'page' : 'folder' } + get folderName() { + const name = this.master.parentElement?.dataset.name + return name || '' + } + get element() { return this._element } diff --git a/src/components/grid-container/gaia-container-folder.ts b/src/components/grid-container/gaia-container-folder.ts index 183d6b1..5bada67 100644 --- a/src/components/grid-container/gaia-container-folder.ts +++ b/src/components/grid-container/gaia-container-folder.ts @@ -34,10 +34,14 @@ export default class GaiaContainerFolder extends GaiaContainerChild { } init() { - this.addAnimationStyle() + 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( @@ -53,7 +57,6 @@ export default class GaiaContainerFolder extends GaiaContainerChild { ) 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() { @@ -109,16 +112,11 @@ export default class GaiaContainerFolder extends GaiaContainerChild { } showIconsSubtitle(element: HTMLElement) { - const icon = element.querySelector(this.iconName) - icon && - icon.attributes.hasOwnProperty(this.hideAttrName) && - icon.attributes.removeNamedItem(this.hideAttrName) + element.removeAttribute(this.hideAttrName) } hideIconsSubtitle(element: HTMLElement) { - const icon = element.querySelector(this.iconName) - const attr = document.createAttribute(this.hideAttrName) - icon && icon.attributes.setNamedItem(attr) + element.setAttribute(this.hideAttrName, '') } /** @@ -156,9 +154,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { this._children.push(child!) if (!this._status) { - this.hideIconsSubtitle(element) + this.hideIconsSubtitle(child?.element!) } else { - this.showIconsSubtitle(element) + this.showIconsSubtitle(child?.element!) } if (!this._status && shouldOpen) { @@ -168,7 +166,6 @@ export default class GaiaContainerFolder extends GaiaContainerChild { this.movein(element) this.element.appendChild(element) } - child!.folderName = this.name } removeAppIcon(node: GaiaContainerChild | HTMLElement) { @@ -183,8 +180,7 @@ export default class GaiaContainerFolder extends GaiaContainerChild { }) } if (!removeChild) return null - ;(removeChild as GaiaContainerChild).folderName = '' - this.showIconsSubtitle((removeChild as GaiaContainerChild).container) + this.showIconsSubtitle((removeChild as GaiaContainerChild).element!) this.manager._children.push(removeChild as GaiaContainerChild) return removeChild } @@ -213,10 +209,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { */ 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._children.forEach((child) => this.showIconsSubtitle(child.element!)) this.manager.status |= 16 this.manager.openedFolder = this this.container.style.height = '100%' @@ -225,30 +220,35 @@ export default class GaiaContainerFolder extends GaiaContainerChild { 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()) + this.element.addEventListener('transitionend', this.openTransition) + } + + openTransition = (evt: TransitionEvent) => { + if (evt.target == this.element && evt.propertyName == 'height') { + this._children.forEach((child) => child.synchroniseContainer()) + } + + 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() } - 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) }) + + this.element.removeEventListener('transitionend', this.openTransition) } /** @@ -263,12 +263,16 @@ export default class GaiaContainerFolder extends GaiaContainerChild { this.removeAppIcon(child) return false } else { - this.hideIconsSubtitle(child.master) + this.hideIconsSubtitle(child.element!) return true } }) - this.master.classList.add('closing') + + 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' @@ -297,10 +301,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { } destroy() { - if (this._children.length > 1) { + if (this._children.length > 0) { return } - const {height: originHeight, width: originWidth} = this.element.getBoundingClientRect() const {height: targetHeight, width: targetWidth} = @@ -308,6 +311,7 @@ export default class GaiaContainerFolder extends GaiaContainerChild { 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 @@ -319,7 +323,7 @@ export default class GaiaContainerFolder extends GaiaContainerChild { // nextTick,用以配合 originXXX 形成动画 setTimeout(() => { - this.showIconsSubtitle(master) + this.showIconsSubtitle(this._children[0].element!) this.element.style.height = targetHeight + 'px' this.element.style.width = targetWidth + 'px' }) @@ -360,57 +364,23 @@ export default class GaiaContainerFolder extends GaiaContainerChild { this._status && (evt.target as HTMLElement).tagName !== 'GAIA-APP-ICON' ) { - evt.preventDefault() - evt.stopImmediatePropagation() + // 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() { + static 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-template-rows: repeat(4, 25%); + grid-template-columns: repeat(4, 25%); grid-auto-flow: row dense; height: 100%; width: 100%; @@ -432,8 +402,8 @@ export default class GaiaContainerFolder extends GaiaContainerChild { } ::slotted(.gaia-container-folder) .gaia-container-child { - height: 16.5% !important; - width: 16.5% !important; + height: 12.5% !important; + width: 12.5% !important; } ::slotted(.gaia-container-folder) gaia-app-icon { @@ -465,6 +435,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { 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); @@ -497,7 +470,6 @@ export default class GaiaContainerFolder extends GaiaContainerChild { ::slotted(.folder-title) { font-size: 1rem; - color: #fff; line-height: 1rem; margin-top: 3px; opacity: 1; @@ -521,6 +493,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { ::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 { @@ -548,6 +523,16 @@ export default class GaiaContainerFolder extends GaiaContainerChild { ::slotted(.folder.merging) .folder-title { opacity: 0; } + + @keyframes folder-fadein { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } ` } } diff --git a/src/test/panels/container/asserts/icon.png b/src/test/panels/container/asserts/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..00b88f79fe6e144c14fbf075b46f7f3732f40b7b GIT binary patch literal 2353 zcmV-13C{M3P) z>swOk8i)CNt8q_V9j2+NraeKKI3XkI!o$A~ZX(gHvp~qgeU8i@8A`CfZyWX(~}nO?wDFp+ZZdipUy%M^v&13GB50Ow~{G&949A3*l z2JbHlPp+DbK6PS!@j!UrHmn?LwgdFvZ7wOkr{|^(_Q?RGq%6F@wJ(Q4@GXS#s!lTv ztJHa#|6e11-uC@I>+Ifxe{pp8h)v@Xl-FbE644;)h90K3^4{7%lPU;SvtE zkM%1@I;Fre;zzs2r=y~Ham5Vl*p{$QjW9XBQSX1F63WCh#Zz-zwi!k zL~P?*%_~PDCgI&S)#G6BorH=vO9omf8 zKL#3C4n-k+ez`8Z6PuaOFV{ge+R&?+9I~WfqRl0|^Yp-!(F0jlf4W-}IZlry4E??= zywmi=M4Jm~N!|S!)#ZqVPn&+TOsfq4#{ub7Jwa5JBN0pCv8T#ukh4#xunk_+=64RQUtcisVSA6b$^r zYZ;Bir_S;Rs1?aE)xqP8M#N(-Z~RfV9NzhT;f-V}!`NcRaU@k6Ig*JqI$Yb1L46j;)#Wm*>Zg8<3cNp36AMtwjBL;___gO!g!nO zpK`x%F| z80}#oBD{IHBs>sCdl-m_OE@t(F%S{@ZZ8NAgwY-bBI3(SOim0$gwCs6%8ALwKty

;m-<_7X#mmW%9x6986veM1;4$@nd~VHU=WX=zpEU17Wm>fr#+_ zZ@2J380}#oB7Exd3J-+Q9tI-9!o;@lKp5>|AR?^u9a$y^1|mXu=U8|kjP@{O%1X+@ z17Wns|1UN1C2Hm(FVF6+5D_N$;b7(hQyrdjIjmz_!UJNogJULmE=Ry~Bs?HSJM8=8 z=W@uB!iRnsJilauvBd&eR{mMpXY?uWu`ni#w60{AeX{XNDRapL`^TMZIf83E?qJSe zP{=A$+wr4CcyNq32JhMPS1_LGH@#xm^BF9%O)^-X9aR`^K`K5(p0dVjJNe{aPy z>=zy=Q-5p|+r{OG>~f7T-ErB3lcQ8|Irv1n{+Kd)N`C;5ALY>(x!$$UD~BvA==+)L zcIbiryNfv;DEK0^b9PsF;7slDnD@$;LzWbD-{8V7%}oED1t^ugWluz~e+&q308=~G zZ3lUCUsMh$DWl^m@3%A~uYa`{%zaTgcF)I z@CGsJU~I7^xT_2T|r-IAaT6 zf@%c%eqMldD%VdAB7$!rEWF`NVcM^l5#BkeD8E(7K}47x@Cok#CO^*E^2Qme%Av># zK0M=Ugj!^7z*kjntCoW(Ge8}F!prMgVye?q@_I&1n;b-$BZk9I__ga=GSjnG^{hmQ97LIE?!;eQ z#78}>HiHP2byN>J>A7iz zZ=vwrlBSj8jG*_3pD+Cm)7wo*Np%;s8d46T%#_zUwx#bG&E?Fx#c?DhJRh2p7D6aLquc$6b&cGMMAz9zVxj(Y)Odj>sEYwvw=jcK8skolH&|nQn0ynh{+eLnjgRp z-m}9p>_=!LUb77{mLm^RQik6V!PnOt7`!L`3`x&T8$LX9V`(a&xdEo` zj{dug=)Gk{*R^>VTQXyr_7*d`uUql*whe>#7xDJDWsE&_Vs5|(=WGZ8?@?B@-)7{0 X!QNx3#GLz^00000NkvXXu0mjflQVxd literal 0 HcmV?d00001 diff --git a/src/test/panels/container/homescreen-container.ts b/src/test/panels/container/homescreen-container.ts index 155bc21..c817464 100644 --- a/src/test/panels/container/homescreen-container.ts +++ b/src/test/panels/container/homescreen-container.ts @@ -1,11 +1,15 @@ 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-container') export class PanelContainer extends LitElement { container!: GaiaContainer + icons: {[prop: string]: GaiaContainerChild} = {} @query('.reset') resetBtn!: HTMLElement createRandomColor() { function randomInt(min: number, max: number) { @@ -23,35 +27,73 @@ export class PanelContainer extends LitElement { } 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) + this.icons[icon.name] = this.container.getChildByElement(icon)! } - reset() { - console.log('reset') - } + reset() {} firstUpdated() { this.container = new GaiaContainer() this.container.setAttribute('dragAndDrop', 'true') this.shadowRoot?.appendChild(this.container) ;(window as any).container = this.container + ;(window as any).home = this + this.addAppIcon() } + @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` - - + +

+ + +
` } - static styles = css` - star-container { - height: 90vh; - width: 100vw; - } - ` + static styles = [ + css` + star-container { + height: 90vh; + width: 100vw; + } + .btns { + position: fixed; + top: 0; + left: 0; + z-index: 1; + } + `, + homescreenStyle, + ] } declare global { diff --git a/src/test/panels/container/homescreen-style.ts b/src/test/panels/container/homescreen-style.ts new file mode 100644 index 0000000..81ec1c3 --- /dev/null +++ b/src/test/panels/container/homescreen-style.ts @@ -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; + } +` diff --git a/src/test/panels/container/icon-style.ts b/src/test/panels/container/icon-style.ts index a1b49f8..71dd248 100644 --- a/src/test/panels/container/icon-style.ts +++ b/src/test/panels/container/icon-style.ts @@ -1,16 +1,19 @@ import {css} from 'lit' export default css` - :host() { - position: absolute; + :host([color]) { + position: var(--width, absolute); top: 50%; left: 50%; - transform: translate(-50%, -50%); + width: 50%; + height: 50%; + max-width: var(--width); + max-height: var(--width); + border-radius: 50%; } #display { --background-color: #fff; - width: 20vw; - height: 12.5vh; + width: 100%; } :host([hide-subtitle]) #subtitle { opacity: 0; @@ -23,12 +26,13 @@ export default css` #image-container { position: relative; transition: visibility 0.2s, opacity 0.2s; - border: 50%; + border-radius: 50%; background-color: var(--background-color); } #image-container img { display: block; + opacity: 0; } #image-container.initial-load { @@ -51,10 +55,11 @@ export default css` animation: rotate 2s infinite linear; } - #image-container, + /* #image-container, #image-container > div { width: 100%; - } + height: 100%; + } */ #subtitle { white-space: nowrap; diff --git a/src/test/panels/container/icon.ts b/src/test/panels/container/icon.ts index 1971f53..173fe62 100644 --- a/src/test/panels/container/icon.ts +++ b/src/test/panels/container/icon.ts @@ -3,15 +3,25 @@ import {customElement, property, query} from 'lit/decorators.js' import style from './icon-style' let count = 0 +let imgBlob!: Blob +const getImage = (): Promise => { + 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!: HTMLElement + @query('#display') displayImg!: HTMLImageElement set color(value: string) { this._color = value @@ -31,17 +41,22 @@ export default class SiteIcon extends LitElement { this.style.setProperty('--background-color', color) } firstUpdated() { - console.log('firstUpdated', this.displayImg) + 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`
- +
-
图标${++count}
+
${this.name}
` } diff --git a/src/utils/SlotStyleHandler.ts b/src/utils/SlotStyleHandler.ts index 4349134..7f5ab04 100644 --- a/src/utils/SlotStyleHandler.ts +++ b/src/utils/SlotStyleHandler.ts @@ -28,7 +28,6 @@ class SlotStyleHandler { processCss(style: string, name: string) { let globalCss = '' style.replace(this.regex.keyframes, (match) => { - console.log('=====', match) globalCss += match return '' }) From 35dcb012a219b1905b13846f5c080b45de81bbbc Mon Sep 17 00:00:00 2001 From: luojiahao Date: Wed, 31 Aug 2022 11:14:29 +0800 Subject: [PATCH 04/18] TASK: #103599 - optimize strategy of container --- CHANGELOG.md | 1 + .../grid-container/container-style.ts | 70 ++++++ src/components/grid-container/container.ts | 202 ++++++++++-------- .../grid-container/gaia-container-child.ts | 21 ++ .../grid-container/gaia-container-folder.ts | 31 ++- .../grid-container/gaia-container-page.ts | 10 +- .../grid-container/style-variables.ts | 8 + src/test/panels/container/icon-style.ts | 24 ++- 8 files changed, 269 insertions(+), 98 deletions(-) create mode 100644 src/components/grid-container/container-style.ts create mode 100644 src/components/grid-container/style-variables.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2174ba5..04fccf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,4 @@ - add contaienr - add SlotStyleHandler - edit container folder animation +- optimize strategy of container diff --git a/src/components/grid-container/container-style.ts b/src/components/grid-container/container-style.ts new file mode 100644 index 0000000..e8c8803 --- /dev/null +++ b/src/components/grid-container/container-style.ts @@ -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; + } +` diff --git a/src/components/grid-container/container.ts b/src/components/grid-container/container.ts index e36ebdf..fd19434 100644 --- a/src/components/grid-container/container.ts +++ b/src/components/grid-container/container.ts @@ -6,6 +6,8 @@ import GaiaContainerPage from './gaia-container-page' import GaiaContainerFolder from './gaia-container-folder' import {DragAndDrop, STATUS, ChildElementInfo} from './contianer-interface' import slotStyleHandler from '../../utils/SlotStyleHandler' +import varStyle from './style-variables' +import containerStyle from './container-style' /** * 想法: * 1. 用 grid 布局的特性排列应用图标(1×1)和小组件(n×m) @@ -179,11 +181,13 @@ class GaiaContainer extends LitElement { openedFolder: GaiaContainerFolder | null = null gesture: GestureManager | null = null dndObserver: MutationObserver | null = null - pageHeight: number = 0 - pageWidth: number = 0 - gridHeight: number = 0 - gridWidth: number = 0 + pageHeight: number = 1 + pageWidth: number = 1 + gridHeight: number = 1 + gridWidth: number = 1 istouching: boolean = false + // 组件坐标 + childCoordinate: {[gridId: number]: GaiaContainerChild}[] = [] constructor(row = 6, column = 4) { super() @@ -196,11 +200,7 @@ class GaiaContainer extends LitElement { } firstUpdated() { - slotStyleHandler.injectGlobalCss( - this, - GaiaContainer.styles.cssText, - this.name - ) + slotStyleHandler.injectGlobalCss(this, containerStyle.cssText, this.name) let dndObserverCallback = () => { if (this._dnd.enabled !== this.dragAndDrop) { this._dnd.enabled = this.dragAndDrop @@ -434,8 +434,8 @@ class GaiaContainer extends LitElement { this.width = width this.pageWidth = this.pages.length ? this.pages[0].offsetWidth : 0 this.pageHeight = this.pages.length ? this.pages[0].offsetHeight : 0 - this.gridHeight = this.pageHeight / this.row - this.gridWidth = this.pageWidth / this.column + this.gridHeight = Math.floor(this.pageHeight / this.row) + this.gridWidth = Math.floor(this.pageWidth / this.column) this.style.setProperty('--page-width', this.pageWidth + 'px') this.style.setProperty('--page-height', this.pageHeight + 'px') @@ -493,6 +493,67 @@ class GaiaContainer extends LitElement { return null } + /** + * 根据 x y 坐标获取指定页的网格 ID + * |---- Page 0 (landscape) -----| + * |----+----+----+----+----+----| + * | 0 | 1 | 2 | 3 | 4 | 5 | + * |----+----+----+----+----+----| + * | 6 | 7 | 8 | 9 | 10 | 11 | + * |----+----+----+----+----+----| ...... + * | 12 | 13 | 14 | 15 | 16 | 17 | + * |----+----+----+----+----+----| + * | 18 | 19 | 20 | 21 | 22 | 23 | + * |----+----+----+----+----+----| + * + * | Page 0 (portrait) | + * |----+----+----+----| + * | 0 | 1 | 2 | 3 | + * |----+----+----+----| + * | 4 | 5 | 6 | 7 | + * |----+----+----+----| ...... + * | 8 | 9 | 10 | 11 | + * |----+----+----+----| + * | 12 | 13 | 14 | 15 | + * |----+----+----+----| + * | 16 | 17 | 18 | 19 | + * |----+----+----+----| + * | 20 | 21 | 22 | 23 | + * |----+----+----+----| + * @param x + * @param y + * @returns + */ + getGridIdByCoordinate( + x: number, + y: number, + pagination: number = this.pagination + ) { + let page = this.pages[pagination] + + x += page.scrollLeft - page.offsetLeft + y += page.scrollTop - page.offsetTop + + const coordinateX = parseInt(String(x / this.gridWidth)) + const coordinateY = parseInt(String(y / this.gridHeight)) + const gridId = coordinateX + coordinateY * this.column + + return gridId + } + + getFolderGridIdByCoordinate(x: number, y: number, pagination: number = 0) { + if (!this.openedFolder || !this.openedFolder._status) return -1 + let page = this.pages[this.openedFolder.pagination] + + x += page.scrollLeft - page.offsetLeft - this.openedFolder._lastMasterLeft! + y += page.scrollTop - page.offsetTop - this.openedFolder._lastMasterTop! + const coordinateX = parseInt(String(x / this.openedFolder.gridWidth)) + const coordinateY = parseInt(String(y / this.openedFolder.gridHeight)) + const gridId = coordinateX + coordinateY * 4 + + return gridId + } + get firstChild() { return this._children.length ? this._children[0].element : null } @@ -765,8 +826,8 @@ class GaiaContainer extends LitElement { if (!this.pageHeight || !this.pageWidth) { this.pageWidth = page.offsetWidth this.pageHeight = page.offsetHeight - this.gridHeight = this.pageHeight / this.row - this.gridWidth = this.pageWidth / this.column + this.gridHeight = Math.floor(this.pageHeight / this.row) + this.gridWidth = Math.floor(this.pageWidth / this.column) this.style.setProperty('--page-width', this.pageWidth + 'px') this.style.setProperty('--page-height', this.pageHeight + 'px') @@ -998,7 +1059,7 @@ class GaiaContainer extends LitElement { throw 'getChildOffsetRect called on unknown child' } - getChildFromPoint(x: number, y: number) { + getChildFromPoint_bak(x: number, y: number) { // 是否与主屏文件夹进行交互中 const interactWithFolder = !!this.openedFolder const {offsetHeight, offsetWidth, offsetX, offsetY} = this._dnd.last @@ -1095,6 +1156,40 @@ class GaiaContainer extends LitElement { } } + getChildFromPoint(x: number, y: number) { + // 是否与主屏文件夹进行交互中 + const interactWithFolder = !!this.openedFolder + let children, child: GaiaContainerChild, childIndex, element + if (interactWithFolder && this.openedFolder) { + childIndex = this.getFolderGridIdByCoordinate(x, y, this.pagination) + children = this.openedFolder?.childCoordinate[this.pagination] + child = children[childIndex] + element = child?.element ?? this.openedFolder.container + } else { + childIndex = this.getGridIdByCoordinate(x, y, this.pagination) + children = this.childCoordinate[this.pagination] + child = children[childIndex] + element = child?.element ?? this.pages[this.pagination] + } + // TODO:还需要考虑跨文件夹移动 + if ( + typeof child?.pagination == 'number' && + child.pagination !== this._dnd.child.pagination + ) { + // 当被选中元素与被移动元素页码不一致时,+ + this._dnd.isSpanning = true + } else if (element.dataset.page != this._dnd.child.pagination) { + // 放置页面与原应用图标页面不一致,属于跨页移动 + this._dnd.isSpanning = true + } + + return { + dropTarget: element, + isPage: !interactWithFolder && !child, + pagination: this.pagination, + } + } + cancelDrag() { if (this._dnd.timeout !== null) { clearTimeout(this._dnd.timeout) @@ -1116,7 +1211,6 @@ class GaiaContainer extends LitElement { this._dnd.clickCapture = true this.dispatchEvent(new CustomEvent('drag-finish')) } - this.synchronise() if (this._dnd.lastDropChild) { this._dnd.lastDropChild.master.classList.remove('merging') @@ -1134,6 +1228,7 @@ class GaiaContainer extends LitElement { this._dnd.child = null this._dnd.isSpanning = false this.status &= ~STATUS.DRAG + this.synchronise() } startDrag() { @@ -1244,7 +1339,7 @@ class GaiaContainer extends LitElement { ) this._dnd.dropTarget = dropTarget // 拖拽元素悬浮页面默认为当前页面 - const suspendingPage = isPage ? dropTarget : this.pages[this.pagination] + const suspendingPage = isPage ? dropTarget! : this.pages[this.pagination] if ( dropTarget === this._dnd.child.element || // 悬浮在自身容器之上 (this._dnd.child.isTail && isPage) @@ -1481,10 +1576,10 @@ class GaiaContainer extends LitElement { // if (!insertBefore && !dropChild.master.nextSibling) { if (operator === 'insertAfter' && dropChild) { this.realInsertAfter(this._dnd.child.master, dropChild.master) - this._staticElements.push(dropChild.element as HTMLElement) + // this._staticElements.push(dropChild.element as HTMLElement) } else if (operator === 'insertBefore' && dropChild) { this.realInsertBefore(this._dnd.child.master, dropChild.master) - this._staticElements.push(dropChild.element as HTMLElement) + // this._staticElements.push(dropChild.element as HTMLElement) } else { // TODO: 此处为合并为文件夹方法 // this.mergeForder(); @@ -1531,7 +1626,7 @@ class GaiaContainer extends LitElement { } handleTransformRatio(x: number) { - if (this._status & STATUS.DRAG) return x + if (this._status & STATUS.DRAG) return 1 const percentage = Math.abs(x) / this.pageHeight this.ratio = 1 @@ -1812,74 +1907,7 @@ class GaiaContainer extends LitElement { } } - static styles = 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; - } - ` + static styles = [containerStyle, varStyle] editStyle(moduleName: string, style: string) { this.shadowStyles[moduleName] = style diff --git a/src/components/grid-container/gaia-container-child.ts b/src/components/grid-container/gaia-container-child.ts index 02b3a6e..9c9cedd 100644 --- a/src/components/grid-container/gaia-container-child.ts +++ b/src/components/grid-container/gaia-container-child.ts @@ -129,6 +129,7 @@ export default class GaiaContainerChild { 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}` @@ -287,6 +288,26 @@ export default class GaiaContainerChild { 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) + + for (let i = 0; i < this.row; i++) { + for (let j = 0; j < this.column; j++) { + if (position == 'page') { + this.manager.childCoordinate[this.pagination][gridId + i + j] = this + } else { + // TODO:文件夹分页 + this.manager.folders[this.folderName].childCoordinate[0][ + gridId + i + j + ] = this + } + } + } if (this._lastMasterTop !== top || this._lastMasterLeft !== left) { this._lastMasterTop = top diff --git a/src/components/grid-container/gaia-container-folder.ts b/src/components/grid-container/gaia-container-folder.ts index 5bada67..d571461 100644 --- a/src/components/grid-container/gaia-container-folder.ts +++ b/src/components/grid-container/gaia-container-folder.ts @@ -26,6 +26,13 @@ export default class GaiaContainerFolder extends GaiaContainerChild { _title: HTMLElement | null = null // 开启文件夹动画计时器 openTimer: number | undefined = undefined + // 子节点坐标 + childCoordinate: {[gridId: number]: GaiaContainerChild}[] = [{}] + gridHeight: number = 0 + gridWidth: number = 0 + + _lastElementTop!: number + _lastElementLeft!: number constructor(manager: GaiaContainer, name?: string) { super(null, 1, 1, null, manager) this.name = this.checkAndGetFolderName(name) @@ -59,6 +66,15 @@ export default class GaiaContainerFolder extends GaiaContainerChild { 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 || @@ -226,6 +242,11 @@ export default class GaiaContainerFolder extends GaiaContainerChild { openTransition = (evt: TransitionEvent) => { if (evt.target == this.element && evt.propertyName == 'height') { this._children.forEach((child) => child.synchroniseContainer()) + if (!this._lastElementLeft || !this._lastElementTop) { + let element = this.element + this._lastElementTop = element?.offsetTop! + this._lastElementLeft = element?.offsetLeft! + } } if ( @@ -246,6 +267,8 @@ export default class GaiaContainerFolder extends GaiaContainerChild { 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) @@ -295,6 +318,8 @@ export default class GaiaContainerFolder extends GaiaContainerChild { if (this._children.length <= 1) { this.destroy() } + this.gridHeight = this._children[0]._lastElementHeight! + this.gridWidth = this._children[0]._lastElementWidth! }, {once: true} ) @@ -362,10 +387,10 @@ export default class GaiaContainerFolder extends GaiaContainerChild { case 'touchmove': if ( this._status && - (evt.target as HTMLElement).tagName !== 'GAIA-APP-ICON' + (evt.target as HTMLElement).tagName !== 'SITE-ICON' ) { - // evt.preventDefault() - // evt.stopImmediatePropagation() + evt.preventDefault() + evt.stopImmediatePropagation() } break } diff --git a/src/components/grid-container/gaia-container-page.ts b/src/components/grid-container/gaia-container-page.ts index 28dbed4..9a5bd66 100644 --- a/src/components/grid-container/gaia-container-page.ts +++ b/src/components/grid-container/gaia-container-page.ts @@ -1,10 +1,12 @@ +import GaiaContainer from './container' + class GaiaContainerPage { _pages: HTMLElement[] = [] // 存储所有添加进 gaia-container 的页面 // 等待被移除的页面,仅在编辑、拖拽时出现,若结束前两种状态时仍然没有子节点,则删除 _suspending: HTMLElement | null = null - _manager + _manager: GaiaContainer observerCallback: MutationCallback - constructor(manager: any) { + constructor(manager: GaiaContainer) { this._manager = manager this._manager.addEventListener('statuschange', () => { @@ -61,6 +63,7 @@ class GaiaContainerPage { this._pages.push(div) div.dataset.page = `${this._pages.length - 1}` this.observe(div) + this._manager.childCoordinate[this._pages.length - 1] = {} return div } @@ -90,14 +93,17 @@ class GaiaContainerPage { 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 } } diff --git a/src/components/grid-container/style-variables.ts b/src/components/grid-container/style-variables.ts new file mode 100644 index 0000000..70abd19 --- /dev/null +++ b/src/components/grid-container/style-variables.ts @@ -0,0 +1,8 @@ +import {css} from 'lit' + +export default css` + :host { + /* 图标大小 */ + --icon-size: 108px; + } +` diff --git a/src/test/panels/container/icon-style.ts b/src/test/panels/container/icon-style.ts index 71dd248..fe1ec83 100644 --- a/src/test/panels/container/icon-style.ts +++ b/src/test/panels/container/icon-style.ts @@ -5,8 +5,8 @@ export default css` position: var(--width, absolute); top: 50%; left: 50%; - width: 50%; - height: 50%; + width: var(--icon-size, 50%); + height: var(--icon-size, 50%); max-width: var(--width); max-height: var(--width); border-radius: 50%; @@ -62,11 +62,23 @@ export default css` } */ #subtitle { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + /* 小黑 文字 中 */ + 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; - line-height: 1rem; + + /* 字体/ 高亮白 */ + color: #fafafa; + + text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.28); } @keyframes rotate { From 823fa638be82e0c273071d731abca18323e2f8cc Mon Sep 17 00:00:00 2001 From: luojiahao Date: Wed, 31 Aug 2022 13:42:37 +0800 Subject: [PATCH 05/18] TASK: #103599 - edit function of exchanging icons --- src/components/grid-container/container.ts | 48 ++++++++----------- .../grid-container/contianer-interface.ts | 6 +++ .../grid-container/gaia-container-child.ts | 8 ++++ .../grid-container/gaia-container-folder.ts | 10 ++-- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/src/components/grid-container/container.ts b/src/components/grid-container/container.ts index fd19434..0a14631 100644 --- a/src/components/grid-container/container.ts +++ b/src/components/grid-container/container.ts @@ -128,6 +128,11 @@ class GaiaContainer extends LitElement { offsetY: 0, }, + center: { + x: 0, + y: 0, + }, + // Timeout used to send drag-move events moveTimeout: undefined, @@ -434,8 +439,8 @@ class GaiaContainer extends LitElement { this.width = width this.pageWidth = this.pages.length ? this.pages[0].offsetWidth : 0 this.pageHeight = this.pages.length ? this.pages[0].offsetHeight : 0 - this.gridHeight = Math.floor(this.pageHeight / this.row) - this.gridWidth = Math.floor(this.pageWidth / this.column) + this.gridHeight = this.pageHeight / this.row + this.gridWidth = this.pageWidth / this.column this.style.setProperty('--page-width', this.pageWidth + 'px') this.style.setProperty('--page-height', this.pageHeight + 'px') @@ -534,8 +539,8 @@ class GaiaContainer extends LitElement { x += page.scrollLeft - page.offsetLeft y += page.scrollTop - page.offsetTop - const coordinateX = parseInt(String(x / this.gridWidth)) - const coordinateY = parseInt(String(y / this.gridHeight)) + const coordinateX = parseInt(String(x / Math.floor(this.gridWidth))) + const coordinateY = parseInt(String(y / Math.floor(this.gridHeight))) const gridId = coordinateX + coordinateY * this.column return gridId @@ -826,8 +831,8 @@ class GaiaContainer extends LitElement { if (!this.pageHeight || !this.pageWidth) { this.pageWidth = page.offsetWidth this.pageHeight = page.offsetHeight - this.gridHeight = Math.floor(this.pageHeight / this.row) - this.gridWidth = Math.floor(this.pageWidth / this.column) + this.gridHeight = this.pageHeight / this.row + this.gridWidth = this.pageWidth / this.column this.style.setProperty('--page-width', this.pageWidth + 'px') this.style.setProperty('--page-height', this.pageHeight + 'px') @@ -1233,20 +1238,18 @@ class GaiaContainer extends LitElement { startDrag() { this.status |= STATUS.DRAG - this._staticElements = [this._dnd.child.element] - this._dnd.start.translateX = this._dnd.child.master.offsetLeft - this._dnd.start.translateY = this._dnd.child.master.offsetTop - - this._dnd.last.offsetHeight = this._dnd.child.master.offsetHeight - this._dnd.last.offsetWidth = this._dnd.child.master.offsetWidth - this._dnd.last.offsetX = this._dnd.child._lastMasterLeft - this._dnd.last.offsetY = this._dnd.child._lastMasterTop + const child = this._dnd.child + this._staticElements = [child.element] + this._dnd.start.translateX = child.master.offsetLeft + this._dnd.start.translateY = child.master.offsetTop + this._dnd.center.x = child._lastElementLeft + child._lastElementWidth / 2 + this._dnd.center.y = child._lastElementTop + child._lastElementHeight / 2 if ( !this.dispatchEvent( new CustomEvent('drag-start', { cancelable: true, detail: { - target: this._dnd.child.element, + target: child.element, pageX: this._dnd.start.pageX, pageY: this._dnd.start.pageY, clientX: this._dnd.start.clientX, @@ -1334,8 +1337,8 @@ class GaiaContainer extends LitElement { */ dropElement() { let {dropTarget, isPage, pagination} = this.getChildFromPoint( - this._dnd.last.clientX, - this._dnd.last.clientY + this._dnd.center.x + (this._dnd.last.pageX - this._dnd.start.pageX), + this._dnd.center.y + (this._dnd.last.pageY - this._dnd.start.pageY) ) this._dnd.dropTarget = dropTarget // 拖拽元素悬浮页面默认为当前页面 @@ -1893,17 +1896,6 @@ class GaiaContainer extends LitElement { this.folders[child.folderName].element.appendChild(child.master) } } - - if (isActive) { - // 被拖拽元素更新容器位置后,还需要更新 _dnd.last - this._dnd.last.offsetHeight = child.master.offsetHeight - this._dnd.last.offsetWidth = child.master.offsetWidth - this._dnd.last.offsetX = child._lastMasterLeft as number - this._dnd.last.offsetY = child._lastMasterTop as number - - // 更新不可交换元素数组 - // this._staticElements = [child]; - } } } diff --git a/src/components/grid-container/contianer-interface.ts b/src/components/grid-container/contianer-interface.ts index 5c00768..e8316b1 100644 --- a/src/components/grid-container/contianer-interface.ts +++ b/src/components/grid-container/contianer-interface.ts @@ -87,6 +87,12 @@ export interface DragAndDrop { pagination: number top: number left: number + + // 中心坐标 + center: { + x: number + y: number + } } export interface Container extends HTMLElement { diff --git a/src/components/grid-container/gaia-container-child.ts b/src/components/grid-container/gaia-container-child.ts index 9c9cedd..41eb190 100644 --- a/src/components/grid-container/gaia-container-child.ts +++ b/src/components/grid-container/gaia-container-child.ts @@ -36,6 +36,9 @@ export default class GaiaContainerChild { _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 @@ -242,6 +245,9 @@ export default class GaiaContainerChild { this._lastElementOrder = null this._lastMasterTop = null this._lastMasterLeft = null + + this._lastElementTop = null + this._lastElementLeft = null } get pageOffsetX() { @@ -312,6 +318,8 @@ export default class GaiaContainerChild { 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)') diff --git a/src/components/grid-container/gaia-container-folder.ts b/src/components/grid-container/gaia-container-folder.ts index d571461..fb4f34c 100644 --- a/src/components/grid-container/gaia-container-folder.ts +++ b/src/components/grid-container/gaia-container-folder.ts @@ -31,8 +31,6 @@ export default class GaiaContainerFolder extends GaiaContainerChild { gridHeight: number = 0 gridWidth: number = 0 - _lastElementTop!: number - _lastElementLeft!: number constructor(manager: GaiaContainer, name?: string) { super(null, 1, 1, null, manager) this.name = this.checkAndGetFolderName(name) @@ -242,11 +240,9 @@ export default class GaiaContainerFolder extends GaiaContainerChild { openTransition = (evt: TransitionEvent) => { if (evt.target == this.element && evt.propertyName == 'height') { this._children.forEach((child) => child.synchroniseContainer()) - if (!this._lastElementLeft || !this._lastElementTop) { - let element = this.element - this._lastElementTop = element?.offsetTop! - this._lastElementLeft = element?.offsetLeft! - } + let element = this.element + this._lastElementTop = element?.offsetTop! + this._lastElementLeft = element?.offsetLeft! } if ( From 76d3800c557608d0bf7e38d5b67618f2889fed04 Mon Sep 17 00:00:00 2001 From: duanzhijiang Date: Thu, 1 Sep 2022 20:50:16 +0800 Subject: [PATCH 06/18] =?UTF-8?q?TASK:=20#105399=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=89=8B=E6=8C=87=E6=BF=80=E6=B4=BBswitch=E5=85=A8=E5=B1=8F?= =?UTF-8?q?=E8=BF=9E=E7=BB=AD=E6=BB=91=E5=8A=A8=E8=A7=A6=E5=8F=91=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/switch/switch-styles.ts | 52 ++++++++++++++++++++++++++ src/components/switch/switch.ts | 47 ++++++++++++++++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/components/switch/switch-styles.ts b/src/components/switch/switch-styles.ts index f5a4390..8baa25c 100644 --- a/src/components/switch/switch-styles.ts +++ b/src/components/switch/switch-styles.ts @@ -75,6 +75,17 @@ export const sharedStyles: CSSResult = css` /*checkbox选中时按钮的样式*/ left: 18px; } + :host([size='small']) .iconFalse { + left: 24px; + top: 6px; + width: 6px; + height: 6px; + } + :host([size='small']) .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']) .iconFalse { + left: 34px; + top: 8px; + width: 9px; + height: 9px; + } + :host([size='large']) .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']) .iconFalse { + left: 39px; + top: 9px; + width: 11px; + height: 11px; + } + :host([size='extralarge']) .iconTrue { + top: 10px; + height: 10px; + left: 16px; + } + + .iconFalse { + position: absolute; + left: 29px; + top: 7px; + width: 8px; + height: 8px; + background-color: #e9e9e9; + border-radius: 50%; + border: 1px solid #b1b1b1; + } + .iconTrue { + position: absolute; + left: 13px; + top: 7px; + height: 8px; + border-left: 1px solid #fff; + } ` diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts index 58e6ded..d4881e4 100755 --- a/src/components/switch/switch.ts +++ b/src/components/switch/switch.ts @@ -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,32 +10,66 @@ 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}) x = 0 @property({type: Boolean}) disabled = false @property({type: Boolean}) checked = false + @property({type: String}) switchicon = '' @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` + (this.checked = (evt.target as HTMLInputElement).checked)} type="checkbox" class="base" id="base" switchcolor="#0265dc" /> - + ` } + selectTouchMove(evt: TouchEvent) { + if (!this.disabled) { + let right = this.switchBall.getBoundingClientRect().right + let left = this.switchBall.getBoundingClientRect().left + let switchx = (right - left) / 2 + left + let x = evt.touches[0].clientX + if (x >= switchx) { + this.base.setAttribute('checked', '') + } else { + this.base.removeAttribute('checked') + } + } + } + // private touchMove(evt: TouchEvent) { + // let right = this.switchBall.getBoundingClientRect().right + // let left = this.switchBall.getBoundingClientRect().left + // let switchx = (right - left) / 2 + left + // let x = evt.touches[0].clientX + // if (x >= switchx) { + // this.base.setAttribute('checked', '') + // } else { + // this.base.removeAttribute('checked') + // } + // } } declare global { From a7aa6e3d8b48ba86613b258edd9b39b941ef0bd5 Mon Sep 17 00:00:00 2001 From: duanzhijiang Date: Fri, 2 Sep 2022 14:08:02 +0800 Subject: [PATCH 07/18] =?UTF-8?q?TASK:=20#105399=20=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E4=BA=86touchmove=E4=B8=8Eicon=E6=98=BE=E7=A4=BA=E4=B8=8D?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/switch/switch.ts | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts index d4881e4..c70a06f 100755 --- a/src/components/switch/switch.ts +++ b/src/components/switch/switch.ts @@ -34,42 +34,39 @@ export class StarSwitch extends LitElement { - (this.checked = (evt.target as HTMLInputElement).checked)} + @change=${ + (evt: Event) => + (this.checked = (evt.target as HTMLInputElement).checked) + // ,console.log( this.base.checked) + } type="checkbox" class="base" id="base" switchcolor="#0265dc" /> ` } - selectTouchMove(evt: TouchEvent) { + private selectTouchMove(evt: TouchEvent) { + // disabled不允许拖动 if (!this.disabled) { let right = this.switchBall.getBoundingClientRect().right let left = this.switchBall.getBoundingClientRect().left let switchx = (right - left) / 2 + left let x = evt.touches[0].clientX if (x >= switchx) { - this.base.setAttribute('checked', '') + this.base.checked = true + // 解决touchmove监测不到checked变化 + this.checked = true } else { - this.base.removeAttribute('checked') + this.base.checked = false + // 解决touchmove监测不到checked变化 + this.checked = false } } } - // private touchMove(evt: TouchEvent) { - // let right = this.switchBall.getBoundingClientRect().right - // let left = this.switchBall.getBoundingClientRect().left - // let switchx = (right - left) / 2 + left - // let x = evt.touches[0].clientX - // if (x >= switchx) { - // this.base.setAttribute('checked', '') - // } else { - // this.base.removeAttribute('checked') - // } - // } } declare global { From 12d975cb000d88bcfcd15c32290fd59d8dd1930e Mon Sep 17 00:00:00 2001 From: duanzhijiang Date: Fri, 2 Sep 2022 17:42:44 +0800 Subject: [PATCH 08/18] =?UTF-8?q?TASK:=20#105399=20icon=E5=81=9A=E6=88=90?= =?UTF-8?q?=E4=BA=86=E5=8F=AF=E9=80=89=E7=9A=84=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/li/li.ts | 3 ++ src/components/switch/switch-styles.ts | 24 ++++++++-------- src/components/switch/switch.ts | 9 ++---- src/test/panels/switch/switch.ts | 40 ++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/components/li/li.ts b/src/components/li/li.ts index 7ad98ae..6087eb1 100644 --- a/src/components/li/li.ts +++ b/src/components/li/li.ts @@ -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}" > ` @@ -244,6 +246,7 @@ export class StarLi extends LitElement { ?checked="${this.checked}" switchcolor=${this.switchcolor} size=${this.size} + ?switchicon="${this.switchicon}" > `} diff --git a/src/components/switch/switch-styles.ts b/src/components/switch/switch-styles.ts index 8baa25c..8a8687c 100644 --- a/src/components/switch/switch-styles.ts +++ b/src/components/switch/switch-styles.ts @@ -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,13 +75,13 @@ export const sharedStyles: CSSResult = css` /*checkbox选中时按钮的样式*/ left: 18px; } - :host([size='small']) .iconFalse { + :host([size='small'][switchicon]) .iconFalse { left: 24px; top: 6px; width: 6px; height: 6px; } - :host([size='small']) .iconTrue { + :host([size='small'][switchicon]) .iconTrue { left: 11px; top: 6px; height: 7px; @@ -103,13 +103,13 @@ export const sharedStyles: CSSResult = css` left: 26px; } - :host([size='large']) .iconFalse { + :host([size='large'][switchicon]) .iconFalse { left: 34px; top: 8px; width: 9px; height: 9px; } - :host([size='large']) .iconTrue { + :host([size='large'][switchicon]) .iconTrue { top: 9px; height: 9px; left: 14px; @@ -130,19 +130,19 @@ export const sharedStyles: CSSResult = css` /*checkbox选中时按钮的样式*/ left: 30px; } - :host([size='extralarge']) .iconFalse { + :host([size='extralarge'][switchicon]) .iconFalse { left: 39px; top: 9px; width: 11px; height: 11px; } - :host([size='extralarge']) .iconTrue { + :host([size='extralarge'][switchicon]) .iconTrue { top: 10px; height: 10px; left: 16px; } - .iconFalse { + :host([switchicon]) .iconFalse { position: absolute; left: 29px; top: 7px; @@ -152,7 +152,7 @@ export const sharedStyles: CSSResult = css` border-radius: 50%; border: 1px solid #b1b1b1; } - .iconTrue { + :host([switchicon]) .iconTrue { position: absolute; left: 13px; top: 7px; diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts index c70a06f..07f1bce 100755 --- a/src/components/switch/switch.ts +++ b/src/components/switch/switch.ts @@ -16,7 +16,6 @@ export class StarSwitch extends LitElement { @property({type: Number}) x = 0 @property({type: Boolean}) disabled = false @property({type: Boolean}) checked = false - @property({type: String}) switchicon = '' @property({type: String}) get switchcolor() { return this._backgoundColor @@ -34,11 +33,8 @@ export class StarSwitch extends LitElement { - (this.checked = (evt.target as HTMLInputElement).checked) - // ,console.log( this.base.checked) - } + @change=${(evt: Event) => + (this.checked = (evt.target as HTMLInputElement).checked)} type="checkbox" class="base" id="base" @@ -68,7 +64,6 @@ export class StarSwitch extends LitElement { } } } - declare global { interface HTMLElementTagNameMap { 'star-switch': StarSwitch diff --git a/src/test/panels/switch/switch.ts b/src/test/panels/switch/switch.ts index 37af8d2..2fb1369 100644 --- a/src/test/panels/switch/switch.ts +++ b/src/test/panels/switch/switch.ts @@ -50,6 +50,46 @@ export class PanelSwitch extends LitElement {
+ + +
+ +
+ +
+ +
+
+ Date: Sat, 3 Sep 2022 16:30:04 +0800 Subject: [PATCH 09/18] =?UTF-8?q?TASK:=20#105400=20StarWebComponents?= =?UTF-8?q?=E5=BC=80=E5=8F=91-card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- src/components/card/README.md | 32 +++++++ src/components/card/card-styles.ts | 113 ++++++++++++++++++++++++ src/components/card/card.ts | 134 +++++++++++++++++++++++++++++ src/test/panels/card/card.ts | 85 ++++++++++++++++++ src/test/panels/card/image/1.png | Bin 0 -> 62648 bytes src/test/panels/card/image/2.jpg | Bin 0 -> 14654 bytes src/test/panels/root.ts | 9 ++ 8 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 src/components/card/README.md create mode 100644 src/components/card/card-styles.ts create mode 100644 src/components/card/card.ts create mode 100644 src/test/panels/card/card.ts create mode 100644 src/test/panels/card/image/1.png create mode 100644 src/test/panels/card/image/2.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 957f0d9..4577419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,5 @@ - add indicator-page-point - add blur - add radio -- add toast \ No newline at end of file +- add toast +- add card \ No newline at end of file diff --git a/src/components/card/README.md b/src/components/card/README.md new file mode 100644 index 0000000..b5ecc1a --- /dev/null +++ b/src/components/card/README.md @@ -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 +用来填写链接卡片的跳转网址。 \ No newline at end of file diff --git a/src/components/card/card-styles.ts b/src/components/card/card-styles.ts new file mode 100644 index 0000000..0ee73d9 --- /dev/null +++ b/src/components/card/card-styles.ts @@ -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; + } + + .tupianonly { + background: #B1B1B1; + border-color: #000000; + border: 10px; + border-style: solid; + border-width: 1px; + border-image-outset: 0; + } + + .tupianonly-image { + height: 198px; + border: 1px; + border-radius: 3px; + object-fit: cover; + width: 198px; + } + + .tupianonly:hover { + border-color: #E6E6E6; + } +` \ No newline at end of file diff --git a/src/components/card/card.ts b/src/components/card/card.ts new file mode 100644 index 0000000..f3cde4a --- /dev/null +++ b/src/components/card/card.ts @@ -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", + TUPIANONLY = "tupianonly", + LABELONLY = "labelonly", + WUFOOTER = 'wufooter', + } + + // 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` +
+
+ +
+
+

${this.heading}

+

${this.subheading}

+
+
+

${this.footer}

+
+
+ ` + } + + getLinkCard(): HTMLTemplateResult { + return html` + +
+
+ +
+
+

${this.heading}

+

${this.subheading}

+
+
+

${this.footer}

+
+
+
+ ` + } + + getLabelOnlyCard(): HTMLTemplateResult { + return html` +
+
+

${this.heading}

+

${this.subheading}

+
+
+

${this.footer}

+
+
+ ` + } + + getTupianOnlyCard(): HTMLTemplateResult { + return html` +
+
+ +
+
+ ` + } + getWuFooterCard(): HTMLTemplateResult { + return html ` +
+
+ +
+
+

${this.heading}

+

${this.subheading}

+
+
+ ` + } + + render() { + switch (this.type) { + case CardType.BASE: + return this.getBaseCard() + case CardType.LINKCARD: + return this.getLinkCard() + case CardType.TUPIANONLY: + return this.getTupianOnlyCard() + case CardType.LABELONLY: + return this.getLabelOnlyCard() + case CardType.WUFOOTER: + return this.getWuFooterCard() + default: + console.error("unhanled 【star-card】 type") + return nothing + } + } + } + + declare global { + interface HTMLElementTagNameMap { + "star-card": StarCard + } + } \ No newline at end of file diff --git a/src/test/panels/card/card.ts b/src/test/panels/card/card.ts new file mode 100644 index 0000000..5921273 --- /dev/null +++ b/src/test/panels/card/card.ts @@ -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 ` +
+

基础卡片

+ +
+
+
+
+
+
+

链接卡片

+ +
+
+
+
+
+
+

无图卡片

+ +
+
+
+
+
+
+

无页脚卡片

+ +
+
+
+
+
+
+

只图卡片

+ +
+ ` + } + + public static override get styles(): CSSResultArray { + return [sharedStyles] + } + +} + +declare global { + interface HTMLElementTagNameMap { + "panel-card": PanelCard + } + } \ No newline at end of file diff --git a/src/test/panels/card/image/1.png b/src/test/panels/card/image/1.png new file mode 100644 index 0000000000000000000000000000000000000000..9a3616b5105b2ca33b1d38512b8dfe505136cb82 GIT binary patch literal 62648 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

$+s+6hv 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ℑ20&i!PPUpiAn4 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 Rg5?|Nkr0s;t`^ku{~v)X-8TRL literal 0 HcmV?d00001 diff --git a/src/test/panels/card/image/2.jpg b/src/test/panels/card/image/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9eb14d572cbae55961f59c935198071aae46cd5a GIT binary patch literal 14654 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 literal 0 HcmV?d00001 diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts index 80b5c0f..36977fe 100644 --- a/src/test/panels/root.ts +++ b/src/test/panels/root.ts @@ -9,6 +9,7 @@ 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' @@ -153,6 +154,14 @@ export class PanelRoot extends LitElement { href="#blur" >


+ +
Date: Sat, 3 Sep 2022 16:54:29 +0800 Subject: [PATCH 10/18] =?UTF-8?q?TASK:#-105400-StarWebComponents=E5=BC=80?= =?UTF-8?q?=E5=8F=91-Card=E4=BB=A3=E7=A0=81=E5=8F=98=E9=87=8F=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/card/card-styles.ts | 6 +++--- src/components/card/card.ts | 20 ++++++++++---------- src/test/panels/card/card.ts | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/card/card-styles.ts b/src/components/card/card-styles.ts index 0ee73d9..f81886b 100644 --- a/src/components/card/card-styles.ts +++ b/src/components/card/card-styles.ts @@ -90,7 +90,7 @@ export const sharedStyles: CSSResult = css` width: 150px; } - .tupianonly { + .imageonly { background: #B1B1B1; border-color: #000000; border: 10px; @@ -99,7 +99,7 @@ export const sharedStyles: CSSResult = css` border-image-outset: 0; } - .tupianonly-image { + .imageonly-image { height: 198px; border: 1px; border-radius: 3px; @@ -107,7 +107,7 @@ export const sharedStyles: CSSResult = css` width: 198px; } - .tupianonly:hover { + .imageonly:hover { border-color: #E6E6E6; } ` \ No newline at end of file diff --git a/src/components/card/card.ts b/src/components/card/card.ts index f3cde4a..4a55053 100644 --- a/src/components/card/card.ts +++ b/src/components/card/card.ts @@ -11,9 +11,9 @@ import { export enum CardType { BASE = "base", LINKCARD = "linkcard", - TUPIANONLY = "tupianonly", + IMAGEONLY = "imageonly", LABELONLY = "labelonly", - WUFOOTER = 'wufooter', + FOOTERDELETED = 'footerdeleted', } // export enum CardSize { @@ -85,16 +85,16 @@ import { ` } - getTupianOnlyCard(): HTMLTemplateResult { + getImageOnlyCard(): HTMLTemplateResult { return html` -
+
- +
` } - getWuFooterCard(): HTMLTemplateResult { + getFooterDeletedCard(): HTMLTemplateResult { return html `
@@ -114,12 +114,12 @@ import { return this.getBaseCard() case CardType.LINKCARD: return this.getLinkCard() - case CardType.TUPIANONLY: - return this.getTupianOnlyCard() + case CardType.IMAGEONLY: + return this.getImageOnlyCard() case CardType.LABELONLY: return this.getLabelOnlyCard() - case CardType.WUFOOTER: - return this.getWuFooterCard() + case CardType.FOOTERDELETED: + return this.getFooterDeletedCard() default: console.error("unhanled 【star-card】 type") return nothing diff --git a/src/test/panels/card/card.ts b/src/test/panels/card/card.ts index 5921273..30c7b63 100644 --- a/src/test/panels/card/card.ts +++ b/src/test/panels/card/card.ts @@ -52,7 +52,7 @@ export class PanelCard extends LitElement {

无页脚卡片

只图卡片

From 409142a38aa31505bfb4158a4f618e59874db207 Mon Sep 17 00:00:00 2001 From: duanzhijiang Date: Mon, 5 Sep 2022 14:05:19 +0800 Subject: [PATCH 11/18] =?UTF-8?q?TASK:=20#105399=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96switch=E6=8B=96=E5=8A=A8=E9=97=B4?= =?UTF-8?q?=E6=8E=A5=E8=A7=A6=E5=8F=91checked=E6=95=88=E6=9E=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/switch/README.md | 25 ++++++++-- src/components/switch/switch.ts | 84 +++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/components/switch/README.md b/src/components/switch/README.md index 8f18a17..a9a6b1c 100644 --- a/src/components/switch/README.md +++ b/src/components/switch/README.md @@ -2,17 +2,28 @@ 星光 Web 组件 --- Switch 组件(8 月 29 日) -`star-switch` 组件包含**四种**互相独立的属性,介绍如下: +`star-switch` 组件包含**五种**互相独立的属性,介绍如下: 1. switchcolor : 控制 switch 的选中背景颜色,默认为蓝色`#0265dc`。 ```
- + ios绿 + ios红 ``` -2. checked : 用于选择对应 switch 首次加载时是否被选中,默认为`false`。 +2. switchicon : 用于控制内含文本,默认为`false`。 + +``` + +
+ +
+ +``` + +3. checked : 用于选择对应 switch 首次加载时是否被选中,默认为`false`。 ``` @@ -22,7 +33,7 @@ ``` -3. disabled : 控制 switch 是否**禁用**状态,默认为`false`。 +4. disabled : 控制 switch 是否**禁用**状态,默认为`false`。 ``` @@ -32,7 +43,7 @@ ``` -4. size : 控制 switch 的大小,包括 small、medium、large 和 extralarge,默认为 `medium` 。 +5. size : 控制 switch 的大小,包括 small、medium、large 和 extralarge,默认为 `medium` 。 ``` @@ -45,3 +56,7 @@
``` + +该组件支持焦点选中,且可以在焦点选中后任意拖动。 + +拖动后可间接触发 checked 开关。 diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts index 07f1bce..b414a09 100755 --- a/src/components/switch/switch.ts +++ b/src/components/switch/switch.ts @@ -13,6 +13,10 @@ export class StarSwitch extends LitElement { @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 @@ -40,27 +44,87 @@ export class StarSwitch extends LitElement { id="base" switchcolor="#0265dc" /> -
diff --git a/src/test/panels/container/icon.ts b/src/test/panels/container/icon.ts index 173fe62..6708625 100644 --- a/src/test/panels/container/icon.ts +++ b/src/test/panels/container/icon.ts @@ -53,7 +53,7 @@ export default class SiteIcon extends LitElement { return html`
- +
${this.name}
From 83b05b1697258457a6ef2b14563a283564625cc3 Mon Sep 17 00:00:00 2001 From: luojiahao Date: Wed, 7 Sep 2022 15:39:33 +0800 Subject: [PATCH 14/18] TASK: #103600 - fix bugs of container exchange stradegy --- CHANGELOG.md | 1 + src/components/grid-container/container.ts | 73 ++++++++++--------- .../element-exchange-strategy.ts | 12 +-- .../grid-container/gesture-manager.ts | 6 +- src/components/section/section.ts | 2 +- 5 files changed, 49 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af7f82..3967cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,3 +16,4 @@ - edit container folder animation - optimize strategy of container - add delay exchange feature +- fix bugs of container exchange stradegy diff --git a/src/components/grid-container/container.ts b/src/components/grid-container/container.ts index 1e1c0ad..1490eb6 100644 --- a/src/components/grid-container/container.ts +++ b/src/components/grid-container/container.ts @@ -106,7 +106,7 @@ class GaiaContainer extends LitElement { timeout: undefined, // The child that was tapped/clicked - child: GaiaContainerChild, + child: undefined, // Whether a drag is active active: false, @@ -377,16 +377,16 @@ class GaiaContainer extends LitElement { 'px)' // 控制与文件夹交互时的拖拽操作 const child = this._dnd.child as unknown as GaiaContainerChild - if (child) { - child.container.style.setProperty( - '--offset-position-left', - this._dnd.left - this.offsetX + 'px' - ) - child.container.style.setProperty( - '--offset-position-top', - this._dnd.top + 'px' - ) - } + // if (child) { + // child.container.style.setProperty( + // '--offset-position-left', + // this._dnd.left - this.offsetX + 'px' + // ) + // child.container.style.setProperty( + // '--offset-position-top', + // this._dnd.top + 'px' + // ) + // } } if (ratio >= 1) { @@ -1153,6 +1153,7 @@ class GaiaContainer extends LitElement { this._dnd.active = false this._dnd.clickCapture = true this.dispatchEvent(new CustomEvent('drag-finish')) + this.resetView('touchend') } if (this._dnd.lastDropChild) { @@ -1160,14 +1161,6 @@ class GaiaContainer extends LitElement { this._dnd.lastDropChild = null } - this._dnd.child?.container?.style.setProperty( - '--offset-position-left', - '0px' - ) - this._dnd.child?.container?.style.setProperty( - '--offset-position-top', - '0px' - ) this._dnd.child = null this._dnd.isSpanning = false this.status &= ~STATUS.DRAG @@ -1231,14 +1224,14 @@ class GaiaContainer extends LitElement { this._dnd.child.container.style.transform = 'translate(' + (this._dnd.left - this.offsetX) + 'px, ' + top + 'px)' // 控制与文件夹交互时的拖拽操作 - this._dnd.child?.container.style.setProperty( - '--offset-position-left', - this._dnd.last.pageX - this._dnd.start.pageX + 'px' - ) - this._dnd.child?.container.style.setProperty( - '--offset-position-top', - this._dnd.last.pageY - this._dnd.start.pageY + 'px' - ) + // this._dnd.child?.container.style.setProperty( + // '--offset-position-left', + // this._dnd.last.pageX - this._dnd.start.pageX + 'px' + // ) + // this._dnd.child?.container.style.setProperty( + // '--offset-position-top', + // this._dnd.last.pageY - this._dnd.start.pageY + 'px' + // ) } if (this._dnd.moveTimeout === undefined) { @@ -1460,7 +1453,7 @@ class GaiaContainer extends LitElement { const child = this._dnd.child if ( this._staticElements.includes(dropTarget) && - (child !== dropChild || gridId == dropChild.gridId) + (child !== dropChild || (gridId == dropChild.gridId && mode == 'delay')) ) { return } @@ -1480,7 +1473,8 @@ class GaiaContainer extends LitElement { return this._staticElements.push(dropTarget) } - const exchange = (gridId: number) => { + const exchange = (gridId: number | undefined) => { + if (gridId == undefined) return // TODO: 图标与图标之间不应该立即进行尝试交换,而是给予一定时间,一定时间内没有进入 // 合并文件夹状态才进行尝试,同时判断图标间的挤占方向 let {canPlace, placedRecorder, childCoordinate} = @@ -1514,8 +1508,8 @@ class GaiaContainer extends LitElement { if (mode !== 'immediately' && gridId == this.exchangeGridId) { return } - const change = () => { - exchange(gridId) + var change = () => { + exchange(this.exchangeGridId!) this.exchangeTimer = undefined this.exchangeGridId = undefined } @@ -1533,12 +1527,14 @@ class GaiaContainer extends LitElement { x: ((gridId % this.column) + 0.5) * this.gridWidth, y: (Math.floor(gridId / this.column) + 0.5) * this.gridHeight, } + const pageX = this.pages[this.pagination].offsetLeft if ( - Math.abs(distanceX - gridCenter.x) < 0.5 * this.gridWidth && - Math.abs(distanceY - gridCenter.y) < 0.5 * this.gridHeight + (Math.abs(distanceX - pageX - gridCenter.x) < 0.5 * this.gridWidth && + Math.abs(distanceY - gridCenter.y) < 0.5 * this.gridHeight) || + mode == 'immediately' ) { - this.pushAhead = distanceX > gridCenter.x + this.pushAhead = distanceX - pageX > gridCenter.x this.pushUp = distanceY > gridCenter.y openChangeTimer(gridId) } @@ -1961,6 +1957,15 @@ class GaiaContainer extends LitElement { this.turnNext(event.type) } this.distance = 0 + } else { + // this._dnd.child?.container?.style.setProperty( + // '--offset-position-left', + // '0px' + // ) + // this._dnd.child?.container?.style.setProperty( + // '--offset-position-top', + // '0px' + // ) } this.resetView('') diff --git a/src/components/grid-container/element-exchange-strategy.ts b/src/components/grid-container/element-exchange-strategy.ts index 3a2309f..34cf8df 100644 --- a/src/components/grid-container/element-exchange-strategy.ts +++ b/src/components/grid-container/element-exchange-strategy.ts @@ -285,20 +285,16 @@ export default class ExchangeStrategy { if (i + j > step) break if (i + j !== step) continue - const horizontalStep = (hdir > 0 ? 1 : -1) * i * mColumn - const verticalStep = (vdir > 0 ? 1 : -1) * j + 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 > dropChild.gridId && - testId % mColumn >= threshold) || // 向后移动,测试网格应大于原网格,且取余后也比原处取余大 - (hdir < 0 && - testId < dropChild.gridId && - testId % mColumn <= threshold)) && // 向前移动,测试网格应小于原网格,且取余后也比原处取余小 + ((hdir > 0 && testId % mColumn >= threshold) || // 向后移动,测试网格取余后比原处取余大 + (hdir < 0 && testId % mColumn <= threshold)) && // 向前移动,测试网格取余后比原处取余小 testId >= 0 && testId <= 23 ) { diff --git a/src/components/grid-container/gesture-manager.ts b/src/components/grid-container/gesture-manager.ts index de75226..dee5a48 100644 --- a/src/components/grid-container/gesture-manager.ts +++ b/src/components/grid-container/gesture-manager.ts @@ -1,5 +1,7 @@ +import GaiaContainer from './container' + class GestureManager { - element: HTMLElement + element: GaiaContainer touches: Touch[] = [] swipeTimer: number | undefined = undefined velocity: number = 0.3 @@ -7,7 +9,7 @@ class GestureManager { recorder: number = 0 swipDirection: string = '' - constructor(element: HTMLElement) { + constructor(element: GaiaContainer) { this.element = element this.listeneTouch() } diff --git a/src/components/section/section.ts b/src/components/section/section.ts index eeff28f..e878d9b 100644 --- a/src/components/section/section.ts +++ b/src/components/section/section.ts @@ -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; From b01a32d85e4ab7816dba56dac04e28f2b021fcd6 Mon Sep 17 00:00:00 2001 From: duanzhijiang Date: Wed, 7 Sep 2022 19:30:03 +0800 Subject: [PATCH 15/18] =?UTF-8?q?TASK:=20#109540=20slider=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=A1=B5=E9=9D=A2=E3=80=81slider=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=92=8C=E6=A0=B7=E5=BC=8F=E4=BB=A5=E5=8F=8A=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/slider/README.md | 34 ++++++++-- src/components/slider/slider-styles.ts | 49 ++++++++++++++ src/components/slider/slider.ts | 94 ++++++++++++++++++++++++++ src/index.ts | 1 + src/test/panels/root.ts | 11 ++- src/test/panels/slider/slider.ts | 35 ++++++++++ 6 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 src/components/slider/slider-styles.ts create mode 100644 src/components/slider/slider.ts create mode 100644 src/test/panels/slider/slider.ts diff --git a/src/components/slider/README.md b/src/components/slider/README.md index a7db29b..93e314e 100644 --- a/src/components/slider/README.md +++ b/src/components/slider/README.md @@ -1,12 +1,38 @@ # 滑块-Slider 工作职责: + - 滑块空间 -说明: -- | 用途:分割位置 +## 类型包括: +1. 默认滑块 -类型包括: +``` + +``` -- 左侧图标|滑块|右侧图标 \ No newline at end of file +2. 滑块中小球左侧进行填充 --- `filled` + +``` + +``` + +3. 禁用滑块 --- `disabled` + +``` + +``` + +4. 分格滑块 --- `Tick` + +``` + + +``` + +5. 左侧图标|滑块|右侧图标 + +``` + +``` diff --git a/src/components/slider/slider-styles.ts b/src/components/slider/slider-styles.ts new file mode 100644 index 0000000..237be0a --- /dev/null +++ b/src/components/slider/slider-styles.ts @@ -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; + } +` diff --git a/src/components/slider/slider.ts b/src/components/slider/slider.ts new file mode 100644 index 0000000..fa55319 --- /dev/null +++ b/src/components/slider/slider.ts @@ -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` +
+

${this.pValue}

+
+
+
+
+
+ ` + } + 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 + } +} diff --git a/src/index.ts b/src/index.ts index 9de3d75..2a6f4d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import './components/radio/radio-group' import './components/radio/radio' import './components/toast/toast' import './components/picker/picker' +import './components/slider/slider' @customElement('settings-app') export class SettingsApp extends LitElement { @query('star-animate-section#root') private rootSection!: StarAnimateSection diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts index 36977fe..7a71651 100644 --- a/src/test/panels/root.ts +++ b/src/test/panels/root.ts @@ -15,7 +15,8 @@ import './blur/use-blur' import './button/button' import './container/container' import './radio/radio' - +import './switch/switch' +import './slider/slider' import './toast/toast' import './picker/picker' type SEID = string @@ -114,6 +115,14 @@ export class PanelRoot extends LitElement { href="#switch" >
+ +
+

default

+ +

初始化覆盖长度

+ + +

disabled

+ + + +
+ ` + } +} +declare global { + interface HTMLElementTagNameMap { + 'panel-slider': PanelSlider + } +} From 467ac5f3a143bf753de2aa571ba45ebe1f515560 Mon Sep 17 00:00:00 2001 From: yajun Date: Fri, 2 Sep 2022 14:42:07 +0800 Subject: [PATCH 16/18] =?UTF-8?q?TASK=20#107657=20OverflowMenu=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/button/button-styles.ts | 5 - src/components/button/button.ts | 1 - src/components/overflowmenu/README.md | 35 ++++ src/components/overflowmenu/overflowmenu.ts | 109 +++++++++++ .../overflowmenu/overflowmenustyle.ts | 34 ++++ src/index.ts | 2 + src/test/panels/overflowmenu/overflowmenu.ts | 173 ++++++++++++++++++ src/test/panels/root.ts | 10 +- 8 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 src/components/overflowmenu/README.md create mode 100644 src/components/overflowmenu/overflowmenu.ts create mode 100644 src/components/overflowmenu/overflowmenustyle.ts create mode 100644 src/test/panels/overflowmenu/overflowmenu.ts diff --git a/src/components/button/button-styles.ts b/src/components/button/button-styles.ts index 6f775b8..1eb5288 100644 --- a/src/components/button/button-styles.ts +++ b/src/components/button/button-styles.ts @@ -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 { diff --git a/src/components/button/button.ts b/src/components/button/button.ts index 90573e2..77d943a 100644 --- a/src/components/button/button.ts +++ b/src/components/button/button.ts @@ -49,7 +49,6 @@ export class StarButton extends LitElement { return html` diff --git a/src/components/overflowmenu/README.md b/src/components/overflowmenu/README.md new file mode 100644 index 0000000..d929b63 --- /dev/null +++ b/src/components/overflowmenu/README.md @@ -0,0 +1,35 @@ +# starwebcomponents——overflowmenu + +## 介绍 + +overflowmenu:溢出菜单。 + +属性介绍: +1. size:组件显示大小,分为四种类型:small、medium、large和extralarge,默认为medium。 + +2. type:组件类型,包括:仅图标:onlyicon;图标和标签:iconlabel + +3. disabled:禁用状态,默认为false + +4. icon:用于传递图标类型 + +5. label:标签名 + +6. (暂定) + +## 需求 +1. 自定义颜色用以外部控制,如: --test-color:XXX p{color: --test-color} +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. 主、副屏的图标定位 +5. 弹出的菜单绑定在父节点上以供调用,减少重复使用 +6. 鼠标点击其他位置后菜单自动收缩 + + +## 问题 +1. 首次点击最右侧的按钮是获取到的菜单宽度和高度与实际不符: +2. 点击空白处无法关闭菜单栏 diff --git a/src/components/overflowmenu/overflowmenu.ts b/src/components/overflowmenu/overflowmenu.ts new file mode 100644 index 0000000..ef3818b --- /dev/null +++ b/src/components/overflowmenu/overflowmenu.ts @@ -0,0 +1,109 @@ +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) + const menuleft = (buttonright - buttonleft) / 2 + buttonleft + // 通过“open”判断是否显示menu + if (this.open == true) { + for (var i = 0; i < this._evenEl.length; i++) { + // 设置div显示display状态 + mu.style.display = "block" + // 设置显示位置类型 + this._evenEl[i].style.position = 'fixed' + this.open = false + // 获取溢出菜单width及height + const menuwidth = this._evenEl[i].getBoundingClientRect().width + const menuheight = this._evenEl[i].getBoundingClientRect().height + // 弹出菜单边界,rightline和bottomline分别为是否超过右侧和下侧显示区域 + const rightline = (buttonright + menuwidth > bodywidth)?true:false + const bottomline = (buttonbottom + menuheight > bodyheight)?true:false + // 右下角边界 + if (rightline && bottomline) { + this._evenEl[i].style.right = bodywidth - buttonright + 'px' + this._evenEl[i].style.top = buttontop - menuheight + 'px' + return + } else if (rightline) { + // 右侧边界 + this._evenEl[i].style.right = bodywidth - buttonright + 'px' + this._evenEl[i].style.top = buttonbottom + 'px' + return + } else if (bottomline) { + // 下侧边界 + this._evenEl[i].style.top = buttontop - menuheight + 'px' + this._evenEl[i].style.left = menuleft + 'px' + return + } else { + // 正常情况 + this._evenEl[i].style.left = menuleft + 'px' + this._evenEl[i].style.top = buttonbottom + 'px' + 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` + + + ` + } + + render() { + return this.getBaseMenu() + } +} + +declare global { + interface HTMLElementTagNameMap { + 'star-overflowmenu': StarOverflowMenu + } +} diff --git a/src/components/overflowmenu/overflowmenustyle.ts b/src/components/overflowmenu/overflowmenustyle.ts new file mode 100644 index 0000000..4c902f3 --- /dev/null +++ b/src/components/overflowmenu/overflowmenustyle.ts @@ -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; + } +` diff --git a/src/index.ts b/src/index.ts index 9de3d75..a9b27e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ import './components/radio/radio-group' import './components/radio/radio' import './components/toast/toast' import './components/picker/picker' +import './components/overflowmenu/overflowmenu' + @customElement('settings-app') export class SettingsApp extends LitElement { @query('star-animate-section#root') private rootSection!: StarAnimateSection diff --git a/src/test/panels/overflowmenu/overflowmenu.ts b/src/test/panels/overflowmenu/overflowmenu.ts new file mode 100644 index 0000000..17b1c73 --- /dev/null +++ b/src/test/panels/overflowmenu/overflowmenu.ts @@ -0,0 +1,173 @@ +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') { + 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` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ` + } + + static styles = css` + :host, + div { + display: block; + width: 100vw; + height: 100vh; + } + ` +} + +declare global { + interface HTMLElementTagNameMap { + 'panel-overflowmenu': PanelOverflowMenu + } +} diff --git a/src/test/panels/root.ts b/src/test/panels/root.ts index 80b5c0f..60072cf 100644 --- a/src/test/panels/root.ts +++ b/src/test/panels/root.ts @@ -14,7 +14,7 @@ import './blur/use-blur' import './button/button' import './container/container' import './radio/radio' - +import './overflowmenu/overflowmenu' import './toast/toast' import './picker/picker' type SEID = string @@ -129,6 +129,14 @@ export class PanelRoot extends LitElement { href="#radio" >
+ +
Date: Thu, 8 Sep 2022 15:37:29 +0800 Subject: [PATCH 17/18] TASK #107657 OverflowMenu --- src/components/overflowmenu/overflowmenu.ts | 30 ++++++----- src/test/panels/overflowmenu/overflowmenu.ts | 56 ++++++-------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/src/components/overflowmenu/overflowmenu.ts b/src/components/overflowmenu/overflowmenu.ts index ef3818b..3b3aa7a 100644 --- a/src/components/overflowmenu/overflowmenu.ts +++ b/src/components/overflowmenu/overflowmenu.ts @@ -35,40 +35,46 @@ export class StarOverflowMenu extends LitElement { const buttonbottom = Number(buttonposition?.bottom) const buttonleft = Number(buttonposition?.left) const buttonright = Number(buttonposition?.right) - const menuleft = (buttonright - buttonleft) / 2 + buttonleft // 通过“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' + // this._evenEl[i].style.position = 'fixed' + slotelement.style.position = 'relative' this.open = false // 获取溢出菜单width及height - const menuwidth = this._evenEl[i].getBoundingClientRect().width - const menuheight = this._evenEl[i].getBoundingClientRect().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) { - this._evenEl[i].style.right = bodywidth - buttonright + 'px' - this._evenEl[i].style.top = buttontop - menuheight + 'px' + // this._evenEl[i].style.right = bodywidth - buttonright + 'px' + // this._evenEl[i].style.top = buttontop - menuheight + 'px' + // this._evenEl[i].style.right = menuwidth + (bodywidth - buttonright) - (buttonright - buttonleft) + 'px' + slotelement.style.left = -(menuwidth - (buttonright - buttonleft)) + 'px' + slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px' return } else if (rightline) { // 右侧边界 - this._evenEl[i].style.right = bodywidth - buttonright + 'px' - this._evenEl[i].style.top = buttonbottom + 'px' + // this._evenEl[i].style.right = bodywidth - buttonright + 'px' + // this._evenEl[i].style.top = buttonbottom + 'px' + slotelement.style.right = menuwidth - (buttonright - buttonleft) + 'px' return } else if (bottomline) { // 下侧边界 - this._evenEl[i].style.top = buttontop - menuheight + 'px' - this._evenEl[i].style.left = menuleft + 'px' + // this._evenEl[i].style.top = buttontop - menuheight + 'px' + // this._evenEl[i].style.left = menuleft + 'px' + slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px' return } else { // 正常情况 - this._evenEl[i].style.left = menuleft + 'px' - this._evenEl[i].style.top = buttonbottom + 'px' + // this._evenEl[i].style.left = menuleft + 'px' + // this._evenEl[i].style.top = buttonbottom + 'px' return } } diff --git a/src/test/panels/overflowmenu/overflowmenu.ts b/src/test/panels/overflowmenu/overflowmenu.ts index 17b1c73..c0a2778 100644 --- a/src/test/panels/overflowmenu/overflowmenu.ts +++ b/src/test/panels/overflowmenu/overflowmenu.ts @@ -25,6 +25,7 @@ export class PanelOverflowMenu extends LitElement { } // 如果点在空白处则关闭菜单 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 @@ -58,40 +59,11 @@ export class PanelOverflowMenu extends LitElement { return html`
- - + - - - - - - - - @@ -131,12 +103,12 @@ export class PanelOverflowMenu extends LitElement { icon="more" style="position: fixed; bottom: 0; left: 0;" > - - + + @@ -157,7 +129,11 @@ export class PanelOverflowMenu extends LitElement { } static styles = css` - :host, + :host { + display: block; + width: 100vw; + height: 100vh; + } div { display: block; width: 100vw; From 09352e1aff8e62a480741d4dedc2914dee96d4a0 Mon Sep 17 00:00:00 2001 From: yajun Date: Thu, 8 Sep 2022 15:48:30 +0800 Subject: [PATCH 18/18] =?UTF-8?q?TASK=20#107657=20OverflowMenu-=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=97=A0=E7=94=A8console?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/overflowmenu/README.md | 42 +++++++++------------ src/components/overflowmenu/overflowmenu.ts | 9 ----- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/components/overflowmenu/README.md b/src/components/overflowmenu/README.md index d929b63..c72978f 100644 --- a/src/components/overflowmenu/README.md +++ b/src/components/overflowmenu/README.md @@ -2,34 +2,28 @@ ## 介绍 -overflowmenu:溢出菜单。 +### overflowmenu:溢出菜单。 -属性介绍: -1. size:组件显示大小,分为四种类型:small、medium、large和extralarge,默认为medium。 - -2. type:组件类型,包括:仅图标:onlyicon;图标和标签:iconlabel - -3. disabled:禁用状态,默认为false - -4. icon:用于传递图标类型 - -5. label:标签名 - -6. (暂定) - -## 需求 -1. 自定义颜色用以外部控制,如: --test-color:XXX p{color: --test-color} -2. 弹出菜单时的越界判断 +## 新需求(主页面要求——罗 9.5) +1. 外部颜色控制(思路:使用自定义css样式,如: --test-color:XXX p{color: --test-color},通过修改自定义css样式的值达到从外部修改组件颜色) +2. 弹出菜单时的越界判断,包括主、副屏切换时的图标定位以及旋转屏幕时的定位 思路: (1)首先获取button在屏幕显示的left、right、top和bottom值以及menu的width和height (2)对于右侧边界:right >= width ? true则menu的left = button的left : false则menu的right = button的right (3)对于下边界:bottom >= height ? true则menu的top = button的bottom : false则menu的bottom = button的top -3. 外部控制接口,事件还是属性? -4. 主、副屏的图标定位 -5. 弹出的菜单绑定在父节点上以供调用,减少重复使用 -6. 鼠标点击其他位置后菜单自动收缩 +3. 外部控制接口,事件还是属性(暂定) +4. 弹出的菜单绑定在父节点上以供调用,减少重复使用(思路:后续通过overlay组件实现) -## 问题 -1. 首次点击最右侧的按钮是获取到的菜单宽度和高度与实际不符: -2. 点击空白处无法关闭菜单栏 +## 问题(9.6) +1. 首次点击最右侧的按钮是获取到的菜单宽度和高度与实际不符:(该问题已消失,但不知道为何消失) +2. 点击空白处无法关闭菜单栏(解决方法:将点击事件绑定在父容器中) + + +## 新要求:(9.7) +(1)将不需要修改的“var”变量声明变成“const”(已修改) +(2)变量命名要直观且有解释(已修改变量命名规范并添加对应注释) +(3)点击一个按钮后其余按钮应关闭(方法同(1)) +(4)可以将slot增加名称从而将div以删除 +(5)定位方式修改为相对定位,将将fixed改为relative,达到适应效果 +(6)控制菜单栏宽度,菜单栏中star-ul中的ul标签负责扩充大小,修改其width值 \ No newline at end of file diff --git a/src/components/overflowmenu/overflowmenu.ts b/src/components/overflowmenu/overflowmenu.ts index 3b3aa7a..83dcc96 100644 --- a/src/components/overflowmenu/overflowmenu.ts +++ b/src/components/overflowmenu/overflowmenu.ts @@ -53,28 +53,19 @@ export class StarOverflowMenu extends LitElement { const bottomline = (buttonbottom + menuheight > bodyheight)?true:false // 右下角边界 if (rightline && bottomline) { - // this._evenEl[i].style.right = bodywidth - buttonright + 'px' - // this._evenEl[i].style.top = buttontop - menuheight + 'px' - // this._evenEl[i].style.right = menuwidth + (bodywidth - buttonright) - (buttonright - buttonleft) + 'px' slotelement.style.left = -(menuwidth - (buttonright - buttonleft)) + 'px' slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px' return } else if (rightline) { // 右侧边界 - // this._evenEl[i].style.right = bodywidth - buttonright + 'px' - // this._evenEl[i].style.top = buttonbottom + 'px' slotelement.style.right = menuwidth - (buttonright - buttonleft) + 'px' return } else if (bottomline) { // 下侧边界 - // this._evenEl[i].style.top = buttontop - menuheight + 'px' - // this._evenEl[i].style.left = menuleft + 'px' slotelement.style.bottom = menuheight + (buttonbottom - buttontop) + 'px' return } else { // 正常情况 - // this._evenEl[i].style.left = menuleft + 'px' - // this._evenEl[i].style.top = buttonbottom + 'px' return } }