diff --git a/packages/dflex-core-instance/src/Node/DFlexBaseNode.ts b/packages/dflex-core-instance/src/Node/DFlexBaseNode.ts index 062cf89b5..c1e5aedab 100644 --- a/packages/dflex-core-instance/src/Node/DFlexBaseNode.ts +++ b/packages/dflex-core-instance/src/Node/DFlexBaseNode.ts @@ -1,4 +1,4 @@ -import { PointNum } from "@dflex/utils"; +import { PointNum, DFlexElmType } from "@dflex/utils"; import { DFLEX_ATTRIBUTES } from "./constants"; import type { AllowedAttributes } from "./constants"; @@ -14,19 +14,31 @@ class DFlexBaseNode { private _hasAttribute?: AttributeSet; - static getType(): string { - return "base:node"; - } + private _type: DFlexElmType; static transform(DOM: HTMLElement, x: number, y: number): void { DOM.style.transform = `translate3d(${x}px,${y}px, 0)`; } - constructor(id: string) { + constructor(id: string, type: DFlexElmType) { this.id = id; + this._type = type; this.isPaused = true; } + getType(): DFlexElmType { + return this._type; + } + + /** + * This only happens during the registration. + * + * @param type + */ + setType(type: DFlexElmType): void { + this._type = type; + } + /** * Initialize the translate AxesCoordinates as part of abstract instance and * necessary for darg only movement. @@ -42,7 +54,7 @@ class DFlexBaseNode { setAttribute( DOM: HTMLElement, key: AllowedAttributes, - value: string | number + value: "true" | "false" | DFlexElmType | number ): void { if (key === "INDEX") { DOM.setAttribute(DFLEX_ATTRIBUTES[key], `${value}`); @@ -50,6 +62,16 @@ class DFlexBaseNode { return; } + if (__DEV__) { + if (this._hasAttribute === undefined) { + throw new Error(`setAttribute: Attribute set is not initialized`); + } + + if (!DFLEX_ATTRIBUTES[key]) { + throw new Error(`setAttribute: Invalid attribute key: ${key}`); + } + } + if (this._hasAttribute!.has(key)) return; DOM.setAttribute(DFLEX_ATTRIBUTES[key], `${value}`); this._hasAttribute!.add(key); diff --git a/packages/dflex-core-instance/src/Node/DFlexCoreNode.ts b/packages/dflex-core-instance/src/Node/DFlexCoreNode.ts index a8f9c3555..47551526d 100644 --- a/packages/dflex-core-instance/src/Node/DFlexCoreNode.ts +++ b/packages/dflex-core-instance/src/Node/DFlexCoreNode.ts @@ -1,10 +1,16 @@ import { PointNum } from "@dflex/utils"; -import type { RectDimensions, Direction, Axes, AxesPoint } from "@dflex/utils"; +import type { + RectDimensions, + Direction, + Axes, + AxesPoint, + DFlexElmType, +} from "@dflex/utils"; import DFlexBaseNode from "./DFlexBaseNode"; export type SerializedDFlexCoreNode = { - type: string; + type: DFlexElmType; version: 3; id: string; translate: PointNum | null; @@ -45,7 +51,7 @@ export interface DFlexNodeInput { order: DOMGenOrder; keys: Keys; depth: number; - readonly: boolean; + type: DFlexElmType; } class DFlexCoreNode extends DFlexBaseNode { @@ -65,27 +71,20 @@ class DFlexCoreNode extends DFlexBaseNode { hasPendingTransform!: boolean; - readonly: boolean; - animatedFrame: number | null; private _translateHistory?: TransitionHistory[]; - static getType(): string { - return "core:node"; - } - static transform = DFlexBaseNode.transform; constructor(eleWithPointer: DFlexNodeInput) { - const { order, keys, depth, readonly, id } = eleWithPointer; + const { order, keys, depth, id, type } = eleWithPointer; - super(id); + super(id, type); this.order = order; this.keys = keys; this.depth = depth; - this.readonly = readonly; this.isPaused = false; this.isVisible = !this.isPaused; this.animatedFrame = null; @@ -382,7 +381,7 @@ class DFlexCoreNode extends DFlexBaseNode { getSerializedInstance(): SerializedDFlexCoreNode { return { - type: DFlexCoreNode.getType(), + type: this.getType(), version: 3, id: this.id, grid: this.grid, diff --git a/packages/dflex-core-instance/src/Node/constants.ts b/packages/dflex-core-instance/src/Node/constants.ts index eb9171c02..ec79b007c 100644 --- a/packages/dflex-core-instance/src/Node/constants.ts +++ b/packages/dflex-core-instance/src/Node/constants.ts @@ -2,6 +2,8 @@ const OUT_POS = "data-dragged-out-position"; const OUT_CONTAINER = "data-dragged-out-container"; const INDEX = "data-index"; const DRAGGED = "dragged"; +const ELM_TYPE = "data-element-type"; + // const GRID_X = "data-grid-x"; // const GRID_Y = "data-grid-y"; @@ -12,6 +14,7 @@ export const DFLEX_ATTRIBUTES = Object.freeze({ INDEX, OUT_POS, OUT_CONTAINER, + ELM_TYPE, }); export type AllowedAttributes = keyof typeof DFLEX_ATTRIBUTES; diff --git a/packages/dflex-dnd-playground/src/App.tsx b/packages/dflex-dnd-playground/src/App.tsx index b83434ded..3fb13bcbd 100644 --- a/packages/dflex-dnd-playground/src/App.tsx +++ b/packages/dflex-dnd-playground/src/App.tsx @@ -15,6 +15,7 @@ import { ContainerBasedEvent, ScrollMultiLists, ListMigration, + LayoutWithDroppable, } from "./components"; function App() { @@ -61,6 +62,7 @@ function App() { } /> } /> } /> + } /> { const taskRef = React.useRef() as React.MutableRefObject; - const { id, depth, readonly } = registerInput; + const { id, depth, type } = registerInput; React.useEffect(() => { if (taskRef.current) { - store.register({ id, depth, readonly }); + store.register({ id, depth, type }); } return () => { diff --git a/packages/dflex-dnd-playground/src/components/droppable/LayoutWithDroppable.tsx b/packages/dflex-dnd-playground/src/components/droppable/LayoutWithDroppable.tsx new file mode 100644 index 000000000..455b0be02 --- /dev/null +++ b/packages/dflex-dnd-playground/src/components/droppable/LayoutWithDroppable.tsx @@ -0,0 +1,86 @@ +import type { DFlexElmType } from "@dflex/dnd"; +import React from "react"; + +import DFlexDnDComponent from "../DFlexDnDComponent"; + +type Container = { + id: string; + content: string; + type: DFlexElmType; + style: React.CSSProperties; +}; + +const container1: Container[] = [ + { id: "inter-elm-1", type: "interactive", content: "Inter-1", style: {} }, + { + id: "inter-elm-2", + type: "interactive", + content: "Inter-2", + style: {}, + }, + { + id: "inter-elm-3", + type: "interactive", + content: "Inter-3", + style: {}, + }, + { id: "inter-elm-4", type: "interactive", content: "Inter-4", style: {} }, + { id: "inter-elm-5", type: "interactive", content: "Inter-5", style: {} }, + { id: "inter-elm-6", type: "interactive", content: "Inter-6", style: {} }, + { id: "inter-elm-7", type: "interactive", content: "Inter-7", style: {} }, + { id: "inter-elm-8", type: "interactive", content: "Inter-8", style: {} }, + { id: "inter-elm-9", type: "interactive", content: "Inter-9", style: {} }, + { id: "inter-elm-10", type: "interactive", content: "Inter-10", style: {} }, +]; + +const container2: Container = { + id: "drop-elm-1", + type: "droppable", + content: "Droppable area 1", + style: {}, +}; + +const container3: Container = { + id: "drop-elm-2", + type: "droppable", + content: "Droppable area 2", + style: {}, +}; + +const DFlexInteractive = ({ container }: { container: Container[] }) => ( +
    + {container.map(({ content, id, type, style }) => ( + + {content} + + ))} +
+); + +const DFlexDroppable = ({ content, id, style, type }: Container) => ( + +
  • {content}
  • +
    +); + +const LayoutWithDroppable = () => { + return ( +
    + + + +
    + ); +}; + +export default LayoutWithDroppable; diff --git a/packages/dflex-dnd-playground/src/components/droppable/index.ts b/packages/dflex-dnd-playground/src/components/droppable/index.ts new file mode 100644 index 000000000..3a14cb7e2 --- /dev/null +++ b/packages/dflex-dnd-playground/src/components/droppable/index.ts @@ -0,0 +1,3 @@ +import LayoutWithDroppable from "./LayoutWithDroppable"; + +export default LayoutWithDroppable; diff --git a/packages/dflex-dnd-playground/src/components/essential/List.css b/packages/dflex-dnd-playground/src/components/essential/List.css index 2502014ad..ac168957b 100644 --- a/packages/dflex-dnd-playground/src/components/essential/List.css +++ b/packages/dflex-dnd-playground/src/components/essential/List.css @@ -117,6 +117,7 @@ padding: 8px; border: 12px #ffee93 solid; border-radius: 18px; + overflow: auto; } .list-migration li { diff --git a/packages/dflex-dnd-playground/src/components/index.ts b/packages/dflex-dnd-playground/src/components/index.ts index 29ce94e40..09d0824fb 100644 --- a/packages/dflex-dnd-playground/src/components/index.ts +++ b/packages/dflex-dnd-playground/src/components/index.ts @@ -1,5 +1,6 @@ export { default as Depth1 } from "./depth"; export { default as ExtendedList } from "./extended"; +export { default as LayoutWithDroppable } from "./droppable"; export { AllRestrictedContainer, diff --git a/packages/dflex-dnd-playground/src/components/todo/TodoListWithReadonly.tsx b/packages/dflex-dnd-playground/src/components/todo/TodoListWithReadonly.tsx index 1bd9ff21c..63021b7cc 100644 --- a/packages/dflex-dnd-playground/src/components/todo/TodoListWithReadonly.tsx +++ b/packages/dflex-dnd-playground/src/components/todo/TodoListWithReadonly.tsx @@ -1,29 +1,37 @@ +import type { DFlexElmType } from "@dflex/dnd"; import React from "react"; import DFlexDnDComponent from "../DFlexDnDComponent"; +type Task = { + id: string; + type: DFlexElmType; + msg: string; + style: React.CSSProperties; +}; + const TodoListWithReadonly = () => { - const tasks = [ + const tasks: Task[] = [ { - readonly: false, + type: "interactive", id: "interactive-1", msg: "Interactive task 1", style: { height: "4.5rem" }, }, { - readonly: true, + type: "draggable", id: "readonly-1", msg: "Readonly task 1", style: { height: "4.5rem" }, }, { - readonly: false, + type: "interactive", id: "interactive-2", msg: "Interactive task 2", style: { height: "4.5rem" }, }, { - readonly: true, + type: "draggable", id: "readonly-2", msg: "Readonly task 2", style: { height: "4.5rem" }, @@ -34,10 +42,10 @@ const TodoListWithReadonly = () => {
      - {tasks.map(({ msg, id, readonly, style }) => ( + {tasks.map(({ msg, id, type, style }) => ( diff --git a/packages/dflex-dnd/src/Droppable/DFlexMechanismController.ts b/packages/dflex-dnd/src/Droppable/DFlexMechanismController.ts index 43a8053e5..2d2bd0b67 100644 --- a/packages/dflex-dnd/src/Droppable/DFlexMechanismController.ts +++ b/packages/dflex-dnd/src/Droppable/DFlexMechanismController.ts @@ -16,7 +16,7 @@ export function isIDEligible(elmID: string, draggedID: string): boolean { elmID.length > 0 && elmID !== draggedID && store.has(elmID) && - !store.registry.get(elmID)!.readonly + store.registry.get(elmID)!.getType() === "interactive" ); } diff --git a/packages/dflex-dnd/src/LayoutManager/DFlexDnDStore.ts b/packages/dflex-dnd/src/LayoutManager/DFlexDnDStore.ts index 404037a07..cf0865f27 100644 --- a/packages/dflex-dnd/src/LayoutManager/DFlexDnDStore.ts +++ b/packages/dflex-dnd/src/LayoutManager/DFlexDnDStore.ts @@ -36,6 +36,8 @@ type UpdatesQueue = Array< type Deferred = Array<() => void>; +const INTERACTIVE_ELM = "interactive"; + class DFlexDnDStore extends DFlexBaseStore { containers: Containers; @@ -89,24 +91,27 @@ class DFlexDnDStore extends DFlexBaseStore { container: DFlexParentContainer, id: string ) { - const [dflexNode, DOM] = this.getElmWithDOM(id); + const [dflexElm, DOM] = this.getElmWithDOM(id); const { scrollRect } = scroll; - dflexNode.resume(DOM, scrollRect.left, scrollRect.top); + dflexElm.resume(DOM, scrollRect.left, scrollRect.top); // Using element grid zero to know if the element has been initiated inside // container or not. - if (dflexNode.grid.x === 0) { - const { initialOffset } = dflexNode; + if (dflexElm.grid.x === 0) { + const { initialOffset } = dflexElm; container.registerNewElm( initialOffset, - this.unifiedContainerDimensions[dflexNode.depth] + this.unifiedContainerDimensions[dflexElm.depth] ); - dflexNode.grid.clone(container.grid); + dflexElm.grid.clone(container.grid); } + + dflexElm.setAttribute(DOM, "INDEX", dflexElm.order.self); + dflexElm.setAttribute(DOM, "ELM_TYPE", dflexElm.getType()); } private _initBranch(SK: string, depth: number, DOM: HTMLElement) { @@ -196,8 +201,8 @@ class DFlexDnDStore extends DFlexBaseStore { () => { const coreInput = { id, - readonly: !!element.readonly, depth: element.depth || 0, + type: element.type || INTERACTIVE_ELM, }; // Create an instance of DFlexCoreNode and gets the DOM element into the store. diff --git a/packages/dflex-dnd/src/index.ts b/packages/dflex-dnd/src/index.ts index a4985e555..f10898f06 100644 --- a/packages/dflex-dnd/src/index.ts +++ b/packages/dflex-dnd/src/index.ts @@ -11,3 +11,5 @@ export type { DFlexEventsTypes, DFlexListenerEvents, } from "./LayoutManager"; + +export type { DFlexElmType } from "@dflex/utils"; diff --git a/packages/dflex-draggable/src/DFlexDraggable.ts b/packages/dflex-draggable/src/DFlexDraggable.ts index 3de18594d..bb1ec027e 100644 --- a/packages/dflex-draggable/src/DFlexDraggable.ts +++ b/packages/dflex-draggable/src/DFlexDraggable.ts @@ -18,6 +18,15 @@ class DFlexDraggable extends DFlexBaseDraggable { } dragAt(x: number, y: number) { + if (this.draggedElm.getType() === "droppable") { + if (__DEV__) { + // eslint-disable-next-line no-console + console.warn("Droppable element can't be dragged"); + } + + return; + } + this.translate(x, y); this.draggedElm.translate.clone(this.translatePlaceholder); diff --git a/packages/dflex-draggable/src/DFlexDraggableStore.ts b/packages/dflex-draggable/src/DFlexDraggableStore.ts index 05b19a116..03f3d0b6c 100644 --- a/packages/dflex-draggable/src/DFlexDraggableStore.ts +++ b/packages/dflex-draggable/src/DFlexDraggableStore.ts @@ -6,6 +6,8 @@ declare global { var $DFlex_Draggable: DFlexDraggableStore; } +const DRAGGABLE_ELM = "draggable"; + class DFlexDraggableStore extends DFlexBaseStore { constructor() { super(); @@ -17,6 +19,7 @@ class DFlexDraggableStore extends DFlexBaseStore { const [dflexNode, DOM] = this.getElmWithDOM(id); dflexNode.resume(DOM, 0, 0); + dflexNode.setAttribute(DOM, "ELM_TYPE", DRAGGABLE_ELM); } private _initBranch(SK: string) { @@ -29,11 +32,13 @@ class DFlexDraggableStore extends DFlexBaseStore { */ // @ts-ignore register(id: string) { + if (!canUseDOM()) return; + super.register( { id, depth: 0, - readonly: false, + type: DRAGGABLE_ELM, }, this._initBranch ); diff --git a/packages/dflex-store/src/DFlexBaseStore.ts b/packages/dflex-store/src/DFlexBaseStore.ts index 4efc1e1be..a120639d4 100644 --- a/packages/dflex-store/src/DFlexBaseStore.ts +++ b/packages/dflex-store/src/DFlexBaseStore.ts @@ -2,7 +2,7 @@ import Generator, { ELmBranch } from "@dflex/dom-gen"; import { DFlexNode, DFlexNodeInput } from "@dflex/core-instance"; -import { getParentElm, Tracker } from "@dflex/utils"; +import { getParentElm, Tracker, DFlexElmType } from "@dflex/utils"; // https://github.com/microsoft/TypeScript/issues/28374#issuecomment-536521051 type DeepNonNullable = { @@ -19,11 +19,7 @@ export type RegisterInputOpts = { /** The depth of targeted element starting from zero (The default value is zero). */ depth?: number; - /** - * True for elements that won't be transformed during DnD but belongs to the - * same interactive container. - * */ - readonly?: boolean; + type?: DFlexElmType; }; export type RegisterInputBase = DeepNonNullable; @@ -110,7 +106,7 @@ class DFlexBaseStore { elm: RegisterInputBase, branchComposedCallBack: BranchComposedCallBackFunction | null ): void { - const { id, depth, readonly } = elm; + const { id, depth, type } = elm; if (!this.interactiveDOM.has(id)) { this.interactiveDOM.set(id, DOM); @@ -122,7 +118,7 @@ class DFlexBaseStore { // This is the only difference between register by default and register // with a user only. In the future if there's new options then this should // be updated. - elmInRegistry!.readonly = readonly; + elmInRegistry!.setType(type); if (__DEV__) { // eslint-disable-next-line no-console @@ -139,15 +135,13 @@ class DFlexBaseStore { order, keys, depth, - readonly, + type, }; const dflexElm = new DFlexNode(coreElement); this.registry.set(id, dflexElm); - dflexElm.setAttribute(DOM, "INDEX", dflexElm.order.self); - if (depth >= 1) { if (keys.CHK === null) { if (__DEV__) { @@ -176,7 +170,7 @@ class DFlexBaseStore { */ register( element: RegisterInputBase, - branchComposedCallBack?: BranchComposedCallBackFunction + branchComposedCallBack: BranchComposedCallBackFunction | null = null ): void { const { id, depth } = element; @@ -215,10 +209,10 @@ class DFlexBaseStore { { id: parentID, depth: parentDepth, - // Default value for inserted parent element. - readonly: true, + // Dropped elements are not interactive. + type: "droppable", }, - branchComposedCallBack || null + branchComposedCallBack ); }); diff --git a/packages/dflex-utils/src/dom/getParentElm.ts b/packages/dflex-utils/src/dom/getParentElm.ts index 3bba3af7b..714452ed9 100644 --- a/packages/dflex-utils/src/dom/getParentElm.ts +++ b/packages/dflex-utils/src/dom/getParentElm.ts @@ -9,29 +9,38 @@ function getParentElm( let current: HTMLElement | null = baseElement; - do { - iterationCounter += 1; - - if (__DEV__) { - if (iterationCounter > MAX_LOOP_ELEMENTS_TO_WARN) { - throw new Error( - `getParentElm: DFlex detects performance issues during iterating for nearest parent element.` + - `Please check your registered interactive element at id:${baseElement.id}.` - ); + try { + do { + iterationCounter += 1; + + if (__DEV__) { + if (iterationCounter > MAX_LOOP_ELEMENTS_TO_WARN) { + throw new Error( + `getParentElm: DFlex detects performance issues during iterating for nearest parent element.` + + `Please check your registered interactive element at id:${baseElement.id}.` + ); + } } - } - // Skip the same element `baseElement`. - if (iterationCounter > 1) { - // If the callback returns true, then we have found the parent element. - if (cb(current)) { - iterationCounter = 0; - return current; + // Skip the same element `baseElement`. + if (iterationCounter > 1) { + // If the callback returns true, then we have found the parent element. + if (cb(current)) { + iterationCounter = 0; + return current; + } } - } - current = current.parentElement; - } while (current !== null && !current.isSameNode(document.body)); + current = current.parentElement; + } while (current !== null && !current.isSameNode(document.body)); + } catch (e) { + if (__DEV__) { + // eslint-disable-next-line no-console + console.error(e); + } + } finally { + iterationCounter = 0; + } return null; } diff --git a/packages/dflex-utils/src/index.ts b/packages/dflex-utils/src/index.ts index 740e4fef9..bed6bff04 100644 --- a/packages/dflex-utils/src/index.ts +++ b/packages/dflex-utils/src/index.ts @@ -21,6 +21,7 @@ export type { Axes, Axis, Direction, + DFlexElmType, } from "./types"; export { combineKeys, dirtyAssignBiggestRect } from "./collections"; diff --git a/packages/dflex-utils/src/types.ts b/packages/dflex-utils/src/types.ts index e7b3117f2..fe610753a 100644 --- a/packages/dflex-utils/src/types.ts +++ b/packages/dflex-utils/src/types.ts @@ -22,3 +22,13 @@ export type Axis = "x" | "y"; /** Bi-directional Axis. */ export type Axes = Axis | "z"; + +export type DFlexElmType = + /** Interactive element can be dragged and switched its position. */ + | "interactive" + + /** To define droppable area. */ + | "droppable" + + /** For elements that won't interact with active dragging element. */ + | "draggable";