diff --git a/angular/doc/api/base-widget.md b/angular/doc/api/base-widget.md
index 8a81d03a5..e71850e59 100644
--- a/angular/doc/api/base-widget.md
+++ b/angular/doc/api/base-widget.md
@@ -80,8 +80,8 @@ The default implementation automatically assigns input data to component propert
```typescript
deserialize(w: NgGridStackWidget) {
- super.deserialize(w); // Call parent for basic setup
-
+ super.deserialize(w); // Call parent for basic setup
+
// Custom initialization logic
if (w.input?.complexData) {
this.processComplexData(w.input.complexData);
diff --git a/angular/doc/api/gridstack.component.md b/angular/doc/api/gridstack.component.md
index c642e0257..28263105b 100644
--- a/angular/doc/api/gridstack.component.md
+++ b/angular/doc/api/gridstack.component.md
@@ -127,7 +127,7 @@ this.gridComponent.grid.addWidget({x: 0, y: 0, w: 2, h: 1});
new GridstackComponent(elementRef): GridstackComponent;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:252](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L252)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:253](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L253)
###### Parameters
@@ -155,7 +155,7 @@ Register a list of Angular components for dynamic creation.
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `typeList` | `Type`\<`Object`\>[] | Array of component types to register |
+| `typeList` | `Type`\<`object`\>[] | Array of component types to register |
###### Returns
@@ -184,7 +184,7 @@ Extract the selector string from an Angular component type.
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `type` | `Type`\<`Object`\> | The component type to get selector from |
+| `type` | `Type`\<`object`\> | The component type to get selector from |
###### Returns
@@ -198,7 +198,7 @@ The component's selector string
ngOnInit(): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:266](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L266)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:267](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L267)
A callback method that is invoked immediately after the
default change detector has checked the directive's
@@ -222,7 +222,7 @@ OnInit.ngOnInit
ngAfterContentInit(): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:276](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L276)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:277](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L277)
wait until after all DOM is ready to init gridstack children (after angular ngFor and sub-components run first)
@@ -242,7 +242,7 @@ AfterContentInit.ngAfterContentInit
ngOnDestroy(): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:284](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L284)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:285](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L285)
A callback method that performs custom clean-up, invoked immediately
before a directive, pipe, or service instance is destroyed.
@@ -263,7 +263,7 @@ OnDestroy.ngOnDestroy
updateAll(): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:298](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L298)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:299](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L299)
called when the TEMPLATE (not recommended) list of items changes - get a list of nodes and
update the layout accordingly (which will take care of adding/removing items changed by Angular)
@@ -278,7 +278,7 @@ update the layout accordingly (which will take care of adding/removing items cha
checkEmpty(): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:309](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L309)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:310](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L310)
check if the grid is empty, if so show alternative content
@@ -292,7 +292,7 @@ check if the grid is empty, if so show alternative content
protected hookEvents(grid?): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:315](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L315)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:316](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L316)
get all known events as easy to use Outputs for convenience
@@ -312,7 +312,7 @@ get all known events as easy to use Outputs for convenience
protected unhookEvents(grid?): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:342](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L342)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:343](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L343)
###### Parameters
@@ -345,11 +345,11 @@ Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:342](https://gi
| `resizeStopCB` | `public` | `EventEmitter`\<[`elementCB`](#elementcb)\> | `undefined` | Emitted when widget resize stops | [angular/projects/lib/src/lib/gridstack.component.ts:184](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L184) |
| `ref` | `public` | \| `undefined` \| `ComponentRef`\<[`GridstackComponent`](#gridstackcomponent)\> | `undefined` | Component reference for dynamic component removal. Used internally when this component is created dynamically. | [angular/projects/lib/src/lib/gridstack.component.ts:207](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L207) |
| `selectorToType` | `static` | [`SelectorToType`](#selectortotype) | `{}` | Mapping of component selectors to their types for dynamic creation. This enables dynamic component instantiation from string selectors. Angular doesn't provide public access to this mapping, so we maintain our own. **Example** `GridstackComponent.addComponentToSelectorType([MyWidgetComponent]);` | [angular/projects/lib/src/lib/gridstack.component.ts:220](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L220) |
-| `_options?` | `protected` | `GridStackOptions` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:247](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L247) |
-| `_grid?` | `protected` | `GridStack` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:248](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L248) |
-| `_sub` | `protected` | `undefined` \| `Subscription` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:249](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L249) |
-| `loaded?` | `protected` | `boolean` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:250](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L250) |
-| `elementRef` | `readonly` | `ElementRef`\<[`GridCompHTMLElement`](#gridcomphtmlelement)\> | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:252](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L252) |
+| `_options?` | `protected` | `GridStackOptions` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:248](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L248) |
+| `_grid?` | `protected` | `GridStack` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:249](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L249) |
+| `_sub` | `protected` | `undefined` \| `Subscription` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:250](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L250) |
+| `loaded?` | `protected` | `boolean` | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:251](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L251) |
+| `elementRef` | `readonly` | `ElementRef`\<[`GridCompHTMLElement`](#gridcomphtmlelement)\> | `undefined` | - | [angular/projects/lib/src/lib/gridstack.component.ts:253](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L253) |
## Interfaces
@@ -3095,7 +3095,7 @@ function gsCreateNgComponents(
isGrid): undefined | HTMLElement;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:353](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L353)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:354](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L354)
can be used when a new item needs to be created, which we do as a Angular component, or deleted (skip)
@@ -3120,7 +3120,7 @@ can be used when a new item needs to be created, which we do as a Angular compon
function gsSaveAdditionalNgInfo(n, w): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:437](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L437)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:439](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L439)
called for each item in the grid - check if additional information needs to be saved.
Note: since this is options minus gridstack protected members using Utils.removeInternalForSave(),
@@ -3146,7 +3146,7 @@ using BaseWidget.serialize()
function gsUpdateNgComponents(n): void;
```
-Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:456](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L456)
+Defined in: [angular/projects/lib/src/lib/gridstack.component.ts:458](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/gridstack.component.ts#L458)
track when widgeta re updated (rather than created) to make sure we de-serialize them as well
@@ -3296,5 +3296,5 @@ Used for dynamic component creation based on widget selectors.
#### Index Signature
```ts
-[key: string]: Type
+[key: string]: Type
```
diff --git a/angular/doc/api/types.md b/angular/doc/api/types.md
index 0615a08dd..65322fde1 100644
--- a/angular/doc/api/types.md
+++ b/angular/doc/api/types.md
@@ -68,7 +68,7 @@ Supports Angular-specific widget definitions and nested grids.
type NgCompInputs = object;
```
-Defined in: [angular/projects/lib/src/lib/types.ts:54](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/types.ts#L54)
+Defined in: [angular/projects/lib/src/lib/types.ts:55](https://github.com/adumesny/gridstack.js/blob/master/angular/projects/lib/src/lib/types.ts#L55)
Type for component input data serialization.
Maps @Input() property names to their values for widget persistence.
diff --git a/spec/dd-base-impl-spec.ts b/spec/dd-base-impl-spec.ts
new file mode 100644
index 000000000..e56bacca7
--- /dev/null
+++ b/spec/dd-base-impl-spec.ts
@@ -0,0 +1,99 @@
+import { DDBaseImplement } from '../src/dd-base-impl';
+
+describe('DDBaseImplement', () => {
+ let baseImpl: DDBaseImplement;
+
+ beforeEach(() => {
+ baseImpl = new DDBaseImplement();
+ });
+
+ describe('constructor', () => {
+ it('should create instance with undefined disabled state', () => {
+ expect(baseImpl.disabled).toBeUndefined();
+ });
+ });
+
+ describe('enable/disable', () => {
+ it('should enable when disabled', () => {
+ baseImpl.disable();
+ expect(baseImpl.disabled).toBe(true);
+
+ baseImpl.enable();
+
+ expect(baseImpl.disabled).toBe(false);
+ });
+
+ it('should disable when enabled', () => {
+ baseImpl.enable();
+ expect(baseImpl.disabled).toBe(false);
+
+ baseImpl.disable();
+
+ expect(baseImpl.disabled).toBe(true);
+ });
+
+ it('should return undefined (no chaining)', () => {
+ expect(baseImpl.enable()).toBeUndefined();
+ expect(baseImpl.disable()).toBeUndefined();
+ });
+ });
+
+ describe('destroy', () => {
+ it('should clean up event register', () => {
+ baseImpl.enable();
+ baseImpl.on('test', () => {});
+
+ baseImpl.destroy();
+
+ // Should not throw when trying to trigger events after destroy
+ expect(() => baseImpl.triggerEvent('test', new Event('test'))).not.toThrow();
+ });
+ });
+
+ describe('event handling', () => {
+ beforeEach(() => {
+ baseImpl.enable(); // Need to enable for events to trigger
+ });
+
+ it('should handle on/off events', () => {
+ const callback = vi.fn();
+
+ baseImpl.on('test', callback);
+ baseImpl.triggerEvent('test', new Event('test'));
+
+ expect(callback).toHaveBeenCalled();
+ });
+
+ it('should remove event listeners with off', () => {
+ const callback = vi.fn();
+
+ baseImpl.on('test', callback);
+ baseImpl.off('test');
+ baseImpl.triggerEvent('test', new Event('test'));
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should only keep last listener for same event', () => {
+ const callback1 = vi.fn();
+ const callback2 = vi.fn();
+
+ baseImpl.on('test', callback1);
+ baseImpl.on('test', callback2); // This overwrites callback1
+ baseImpl.triggerEvent('test', new Event('test'));
+
+ expect(callback1).not.toHaveBeenCalled();
+ expect(callback2).toHaveBeenCalled();
+ });
+
+ it('should not trigger events when disabled', () => {
+ const callback = vi.fn();
+
+ baseImpl.on('test', callback);
+ baseImpl.disable();
+ baseImpl.triggerEvent('test', new Event('test'));
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/dd-droppable-spec.ts b/spec/dd-droppable-spec.ts
new file mode 100644
index 000000000..59fd0ecbe
--- /dev/null
+++ b/spec/dd-droppable-spec.ts
@@ -0,0 +1,311 @@
+import { DDDroppable } from '../src/dd-droppable';
+import { DDManager } from '../src/dd-manager';
+
+describe('DDDroppable', () => {
+ let element: HTMLElement;
+ let droppable: DDDroppable;
+
+ beforeEach(() => {
+ // Create a test element
+ element = document.createElement('div');
+ element.id = 'test-droppable';
+ element.style.width = '100px';
+ element.style.height = '100px';
+ document.body.appendChild(element);
+ });
+
+ afterEach(() => {
+ // Clean up
+ if (droppable) {
+ droppable.destroy();
+ }
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ // Clear DDManager state
+ delete DDManager.dragElement;
+ delete DDManager.dropElement;
+ });
+
+ describe('constructor', () => {
+ it('should create a droppable instance with default options', () => {
+ droppable = new DDDroppable(element);
+
+ expect(droppable).toBeDefined();
+ expect(droppable.el).toBe(element);
+ expect(droppable.option).toEqual({});
+ expect(element.classList.contains('ui-droppable')).toBe(true);
+ });
+
+ it('should create a droppable instance with custom options', () => {
+ const options = {
+ accept: '.draggable-item',
+ drop: vi.fn(),
+ over: vi.fn(),
+ out: vi.fn()
+ };
+
+ droppable = new DDDroppable(element, options);
+
+ expect(droppable.option).toBe(options);
+ expect(droppable.accept).toBeDefined();
+ });
+ });
+
+ describe('enable/disable', () => {
+ beforeEach(() => {
+ droppable = new DDDroppable(element);
+ });
+
+ it('should enable droppable functionality', () => {
+ droppable.disable();
+ expect(droppable.disabled).toBe(true);
+ expect(element.classList.contains('ui-droppable-disabled')).toBe(true);
+
+ droppable.enable();
+ expect(droppable.disabled).toBe(false);
+ expect(element.classList.contains('ui-droppable')).toBe(true);
+ expect(element.classList.contains('ui-droppable-disabled')).toBe(false);
+ });
+
+ it('should disable droppable functionality', () => {
+ expect(droppable.disabled).toBe(false);
+
+ droppable.disable();
+ expect(droppable.disabled).toBe(true);
+ expect(element.classList.contains('ui-droppable')).toBe(false);
+ expect(element.classList.contains('ui-droppable-disabled')).toBe(true);
+ });
+
+ it('should not enable if already enabled', () => {
+ const spy = vi.spyOn(element.classList, 'add');
+ droppable.enable(); // Already enabled
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should not disable if already disabled', () => {
+ droppable.disable();
+ const spy = vi.spyOn(element.classList, 'remove');
+ droppable.disable(); // Already disabled
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('destroy', () => {
+ it('should clean up droppable instance', () => {
+ droppable = new DDDroppable(element);
+
+ droppable.destroy();
+
+ expect(element.classList.contains('ui-droppable')).toBe(false);
+ expect(element.classList.contains('ui-droppable-disabled')).toBe(false);
+ expect(droppable.disabled).toBe(true);
+ });
+ });
+
+ describe('updateOption', () => {
+ beforeEach(() => {
+ droppable = new DDDroppable(element);
+ });
+
+ it('should update options', () => {
+ const newOptions = {
+ accept: '.new-class',
+ drop: vi.fn()
+ };
+
+ const result = droppable.updateOption(newOptions);
+
+ expect(result).toBe(droppable);
+ expect(droppable.option.accept).toBe('.new-class');
+ expect(droppable.option.drop).toBe(newOptions.drop);
+ });
+
+ it('should update accept function when accept is a string', () => {
+ droppable.updateOption({ accept: '.test-class' });
+
+ expect(droppable.accept).toBeDefined();
+
+ // Test the accept function
+ const testEl = document.createElement('div');
+ testEl.classList.add('test-class');
+ expect(droppable.accept(testEl)).toBe(true);
+
+ const otherEl = document.createElement('div');
+ expect(droppable.accept(otherEl)).toBe(false);
+ });
+
+ it('should update accept function when accept is a function', () => {
+ const acceptFn = vi.fn().mockReturnValue(true);
+ droppable.updateOption({ accept: acceptFn });
+
+ expect(droppable.accept).toBe(acceptFn);
+ });
+ });
+
+ describe('mouse events', () => {
+ beforeEach(() => {
+ droppable = new DDDroppable(element, {
+ over: vi.fn(),
+ out: vi.fn(),
+ drop: vi.fn()
+ });
+
+ // Create a mock draggable element
+ const mockDraggable = {
+ el: document.createElement('div'),
+ ui: vi.fn().mockReturnValue({
+ helper: document.createElement('div'),
+ position: { left: 0, top: 0 }
+ })
+ };
+ DDManager.dragElement = mockDraggable as any;
+ });
+
+ describe('_mouseEnter', () => {
+ it('should handle mouse enter when dragging', () => {
+ const event = new MouseEvent('mouseenter', { bubbles: true });
+ const spy = vi.spyOn(event, 'preventDefault');
+
+ droppable._mouseEnter(event);
+
+ expect(spy).toHaveBeenCalled();
+ expect(DDManager.dropElement).toBe(droppable);
+ expect(element.classList.contains('ui-droppable-over')).toBe(true);
+ expect(droppable.option.over).toHaveBeenCalled();
+ });
+
+ it('should not handle mouse enter when not dragging', () => {
+ delete DDManager.dragElement;
+ const event = new MouseEvent('mouseenter', { bubbles: true });
+
+ droppable._mouseEnter(event);
+
+ expect(DDManager.dropElement).toBeUndefined();
+ expect(element.classList.contains('ui-droppable-over')).toBe(false);
+ });
+
+ it('should not handle mouse enter when element cannot be dropped', () => {
+ droppable.updateOption({ accept: '.not-matching' });
+ const event = new MouseEvent('mouseenter', { bubbles: true });
+
+ droppable._mouseEnter(event);
+
+ expect(DDManager.dropElement).toBeUndefined();
+ expect(element.classList.contains('ui-droppable-over')).toBe(false);
+ });
+ });
+
+ describe('_mouseLeave', () => {
+ beforeEach(() => {
+ DDManager.dropElement = droppable;
+ element.classList.add('ui-droppable-over');
+ });
+
+ it('should handle mouse leave when this is the current drop element', () => {
+ const event = new MouseEvent('mouseleave', { bubbles: true });
+ const spy = vi.spyOn(event, 'preventDefault');
+
+ droppable._mouseLeave(event);
+
+ expect(spy).toHaveBeenCalled();
+ expect(DDManager.dropElement).toBeUndefined();
+ expect(droppable.option.out).toHaveBeenCalled();
+ });
+
+ it('should not handle mouse leave when not the current drop element', () => {
+ DDManager.dropElement = null as any;
+ const event = new MouseEvent('mouseleave', { bubbles: true });
+
+ droppable._mouseLeave(event);
+
+ expect(droppable.option.out).not.toHaveBeenCalled();
+ });
+
+ it('should not handle mouse leave when no drag element', () => {
+ delete DDManager.dragElement;
+ const event = new MouseEvent('mouseleave', { bubbles: true });
+
+ droppable._mouseLeave(event);
+
+ expect(droppable.option.out).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('drop', () => {
+ it('should handle drop event', () => {
+ const event = new MouseEvent('mouseup', { bubbles: true });
+ const spy = vi.spyOn(event, 'preventDefault');
+
+ droppable.drop(event);
+
+ expect(spy).toHaveBeenCalled();
+ expect(droppable.option.drop).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('_canDrop', () => {
+ beforeEach(() => {
+ droppable = new DDDroppable(element);
+ });
+
+ it('should return true when no accept filter is set', () => {
+ const testEl = document.createElement('div');
+ expect(droppable._canDrop(testEl)).toBe(true);
+ });
+
+ it('should return false when element is null', () => {
+ expect(droppable._canDrop(null as any)).toBeFalsy();
+ });
+
+ it('should use accept function when set', () => {
+ const acceptFn = vi.fn().mockReturnValue(false);
+ droppable.accept = acceptFn;
+
+ const testEl = document.createElement('div');
+ const result = droppable._canDrop(testEl);
+
+ expect(acceptFn).toHaveBeenCalledWith(testEl);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('event handling', () => {
+ beforeEach(() => {
+ droppable = new DDDroppable(element);
+ });
+
+ it('should support on/off event methods', () => {
+ const callback = vi.fn();
+
+ droppable.on('drop', callback);
+ droppable.off('drop');
+
+ // These methods should exist and not throw
+ expect(typeof droppable.on).toBe('function');
+ expect(typeof droppable.off).toBe('function');
+ });
+ });
+
+ describe('_ui helper', () => {
+ it('should create UI data object', () => {
+ droppable = new DDDroppable(element);
+
+ const dragEl = document.createElement('div');
+ const mockDraggable = {
+ el: dragEl,
+ ui: vi.fn().mockReturnValue({
+ helper: dragEl,
+ position: { left: 0, top: 0 }
+ })
+ };
+
+ const result = droppable._ui(mockDraggable as any);
+
+ expect(result.draggable).toBe(dragEl);
+ expect(result.helper).toBe(dragEl);
+ expect(result.position).toEqual({ left: 0, top: 0 });
+ });
+ });
+});
diff --git a/spec/dd-manager-spec.ts b/spec/dd-manager-spec.ts
new file mode 100644
index 000000000..32b58e476
--- /dev/null
+++ b/spec/dd-manager-spec.ts
@@ -0,0 +1,63 @@
+import { DDManager } from '../src/dd-manager';
+
+describe('DDManager', () => {
+ afterEach(() => {
+ // Clean up DDManager state
+ delete DDManager.dragElement;
+ delete DDManager.dropElement;
+ delete DDManager.pauseDrag;
+ });
+
+ describe('static properties', () => {
+ it('should have dragElement property', () => {
+ expect(DDManager.dragElement).toBeUndefined();
+
+ const mockDragElement = {} as any;
+ DDManager.dragElement = mockDragElement;
+
+ expect(DDManager.dragElement).toBe(mockDragElement);
+ });
+
+ it('should have dropElement property', () => {
+ expect(DDManager.dropElement).toBeUndefined();
+
+ const mockDropElement = {} as any;
+ DDManager.dropElement = mockDropElement;
+
+ expect(DDManager.dropElement).toBe(mockDropElement);
+ });
+
+ it('should have pauseDrag property', () => {
+ expect(DDManager.pauseDrag).toBeUndefined();
+
+ DDManager.pauseDrag = true;
+ expect(DDManager.pauseDrag).toBe(true);
+
+ DDManager.pauseDrag = 500;
+ expect(DDManager.pauseDrag).toBe(500);
+ });
+ });
+
+ describe('state management', () => {
+ it('should allow setting and clearing drag element', () => {
+ const mockElement = { id: 'test' } as any;
+
+ DDManager.dragElement = mockElement;
+ expect(DDManager.dragElement).toBe(mockElement);
+
+ delete DDManager.dragElement;
+ expect(DDManager.dragElement).toBeUndefined();
+ });
+
+ it('should allow setting and clearing drop element', () => {
+ const mockElement = { id: 'test' } as any;
+
+ DDManager.dropElement = mockElement;
+ expect(DDManager.dropElement).toBe(mockElement);
+
+ delete DDManager.dropElement;
+ expect(DDManager.dropElement).toBeUndefined();
+ });
+ });
+});
+
diff --git a/spec/dd-simple-integration-spec.ts b/spec/dd-simple-integration-spec.ts
new file mode 100644
index 000000000..7af0cfa31
--- /dev/null
+++ b/spec/dd-simple-integration-spec.ts
@@ -0,0 +1,248 @@
+import { DDDraggable } from '../src/dd-draggable';
+import { DDDroppable } from '../src/dd-droppable';
+import { DDResizable } from '../src/dd-resizable';
+import { DDElement } from '../src/dd-element';
+import { GridItemHTMLElement } from '../src/types';
+
+describe('DD Integration Tests', () => {
+ let element: GridItemHTMLElement;
+
+ beforeEach(() => {
+ element = document.createElement('div') as GridItemHTMLElement;
+ element.style.width = '100px';
+ element.style.height = '100px';
+ element.style.position = 'absolute';
+ document.body.appendChild(element);
+
+ // Mock gridstackNode
+ element.gridstackNode = {
+ id: 'test-node',
+ x: 0,
+ y: 0,
+ w: 1,
+ h: 1
+ } as any;
+ });
+
+ afterEach(() => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ });
+
+ describe('DDElement', () => {
+ it('should create DDElement instance', () => {
+ const ddElement = DDElement.init(element);
+
+ expect(ddElement).toBeDefined();
+ expect(ddElement.el).toBe(element);
+ });
+
+ it('should return same instance on multiple init calls', () => {
+ const ddElement1 = DDElement.init(element);
+ const ddElement2 = DDElement.init(element);
+
+ expect(ddElement1).toBe(ddElement2);
+ });
+
+ it('should setup draggable', () => {
+ const ddElement = DDElement.init(element);
+
+ ddElement.setupDraggable({ handle: '.drag-handle' });
+
+ expect(ddElement.ddDraggable).toBeDefined();
+ });
+
+ it('should setup droppable', () => {
+ const ddElement = DDElement.init(element);
+
+ ddElement.setupDroppable({ accept: '.draggable' });
+
+ expect(ddElement.ddDroppable).toBeDefined();
+ });
+
+ it('should setup resizable with default handles', () => {
+ const ddElement = DDElement.init(element);
+
+ ddElement.setupResizable({ handles: 'se' });
+
+ expect(ddElement.ddResizable).toBeDefined();
+ });
+
+ it('should clean up components', () => {
+ const ddElement = DDElement.init(element);
+
+ ddElement.setupDraggable({});
+ ddElement.setupDroppable({});
+ ddElement.setupResizable({ handles: 'se' });
+
+ expect(ddElement.ddDraggable).toBeDefined();
+ expect(ddElement.ddDroppable).toBeDefined();
+ expect(ddElement.ddResizable).toBeDefined();
+
+ ddElement.cleanDraggable();
+ ddElement.cleanDroppable();
+ ddElement.cleanResizable();
+
+ expect(ddElement.ddDraggable).toBeUndefined();
+ expect(ddElement.ddDroppable).toBeUndefined();
+ expect(ddElement.ddResizable).toBeUndefined();
+ });
+ });
+
+ describe('DDDraggable basic functionality', () => {
+ it('should create draggable instance', () => {
+ const draggable = new DDDraggable(element);
+
+ expect(draggable).toBeDefined();
+ expect(draggable.el).toBe(element);
+ expect(draggable.disabled).toBe(false);
+
+ draggable.destroy();
+ });
+
+ it('should enable/disable draggable', () => {
+ const draggable = new DDDraggable(element);
+
+ draggable.disable();
+ expect(draggable.disabled).toBe(true);
+
+ draggable.enable();
+ expect(draggable.disabled).toBe(false);
+
+ draggable.destroy();
+ });
+
+ it('should update options', () => {
+ const draggable = new DDDraggable(element);
+
+ const result = draggable.updateOption({ handle: '.new-handle' });
+
+ expect(result).toBe(draggable);
+ expect(draggable.option.handle).toBe('.new-handle');
+
+ draggable.destroy();
+ });
+ });
+
+ describe('DDDroppable basic functionality', () => {
+ it('should create droppable instance', () => {
+ const droppable = new DDDroppable(element);
+
+ expect(droppable).toBeDefined();
+ expect(droppable.el).toBe(element);
+ expect(droppable.disabled).toBe(false);
+ expect(element.classList.contains('ui-droppable')).toBe(true);
+
+ droppable.destroy();
+ });
+
+ it('should enable/disable droppable', () => {
+ const droppable = new DDDroppable(element);
+
+ droppable.disable();
+ expect(droppable.disabled).toBe(true);
+ expect(element.classList.contains('ui-droppable-disabled')).toBe(true);
+
+ droppable.enable();
+ expect(droppable.disabled).toBe(false);
+ expect(element.classList.contains('ui-droppable')).toBe(true);
+
+ droppable.destroy();
+ });
+
+ it('should update options', () => {
+ const droppable = new DDDroppable(element);
+
+ const result = droppable.updateOption({ accept: '.new-class' });
+
+ expect(result).toBe(droppable);
+ expect(droppable.option.accept).toBe('.new-class');
+
+ droppable.destroy();
+ });
+
+ it('should handle accept function', () => {
+ const droppable = new DDDroppable(element);
+
+ droppable.updateOption({ accept: '.test-class' });
+
+ const testEl = document.createElement('div');
+ testEl.classList.add('test-class');
+ expect(droppable._canDrop(testEl)).toBe(true);
+
+ const otherEl = document.createElement('div');
+ expect(droppable._canDrop(otherEl)).toBe(false);
+
+ droppable.destroy();
+ });
+ });
+
+ describe('DDResizable basic functionality', () => {
+ it('should create resizable instance with handles', () => {
+ const resizable = new DDResizable(element, { handles: 'se' });
+
+ expect(resizable).toBeDefined();
+ expect(resizable.el).toBe(element);
+ expect(resizable.disabled).toBe(false);
+ // Note: ui-resizable class is added by enable() which is called in constructor
+ // but the class might not be added immediately in test environment
+
+ resizable.destroy();
+ });
+
+ it('should enable/disable resizable', () => {
+ const resizable = new DDResizable(element, { handles: 'se' });
+
+ resizable.disable();
+ expect(resizable.disabled).toBe(true);
+ expect(element.classList.contains('ui-resizable-disabled')).toBe(true);
+
+ resizable.enable();
+ expect(resizable.disabled).toBe(false);
+ expect(element.classList.contains('ui-resizable-disabled')).toBe(false);
+
+ resizable.destroy();
+ });
+
+ it('should update options', () => {
+ const resizable = new DDResizable(element, { handles: 'se' });
+
+ const result = resizable.updateOption({ handles: 'n,s,e,w' });
+
+ expect(result).toBe(resizable);
+ expect(resizable.option.handles).toBe('n,s,e,w');
+
+ resizable.destroy();
+ });
+
+ it('should create resize handles', () => {
+ const resizable = new DDResizable(element, { handles: 'se,nw' });
+
+ const seHandle = element.querySelector('.ui-resizable-se');
+ const nwHandle = element.querySelector('.ui-resizable-nw');
+
+ expect(seHandle).toBeTruthy();
+ expect(nwHandle).toBeTruthy();
+
+ resizable.destroy();
+ });
+ });
+
+ describe('Event handling', () => {
+ it('should support event listeners on DDElement', () => {
+ const ddElement = DDElement.init(element);
+ const callback = vi.fn();
+
+ ddElement.setupDraggable({});
+ ddElement.on('dragstart', callback);
+ ddElement.off('dragstart');
+
+ // Should not throw
+ expect(typeof ddElement.on).toBe('function');
+ expect(typeof ddElement.off).toBe('function');
+
+ ddElement.cleanDraggable();
+ });
+ });
+});
diff --git a/spec/dd-touch-spec.ts b/spec/dd-touch-spec.ts
new file mode 100644
index 000000000..1e7cec99e
--- /dev/null
+++ b/spec/dd-touch-spec.ts
@@ -0,0 +1,515 @@
+import {
+ isTouch,
+ touchstart,
+ touchmove,
+ touchend,
+ pointerdown,
+ pointerenter,
+ pointerleave
+} from '../src/dd-touch';
+import { DDManager } from '../src/dd-manager';
+import { Utils } from '../src/utils';
+
+// Mock Utils.simulateMouseEvent
+vi.mock('../src/utils', () => ({
+ Utils: {
+ simulateMouseEvent: vi.fn()
+ }
+}));
+
+// Mock DDManager
+vi.mock('../src/dd-manager', () => ({
+ DDManager: {
+ dragElement: null
+ }
+}));
+
+// Helper function to create mock TouchList
+function createMockTouchList(touches: Touch[]): TouchList {
+ const touchList = {
+ length: touches.length,
+ item: (index: number) => touches[index] || null,
+ ...touches
+ };
+ return touchList as TouchList;
+}
+
+// Helper function to create mock TouchEvent
+function createMockTouchEvent(type: string, touches: Touch[], options: Partial = {}): TouchEvent {
+ const touchList = createMockTouchList(touches);
+ const changedTouchList = options.changedTouches ?
+ createMockTouchList(options.changedTouches as Touch[]) : touchList;
+
+ const mockEvent = {
+ touches: touchList,
+ changedTouches: changedTouchList,
+ targetTouches: touchList,
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ cancelable: true,
+ type,
+ target: document.createElement('div'),
+ currentTarget: document.createElement('div'),
+ bubbles: true,
+ composed: false,
+ defaultPrevented: false,
+ eventPhase: Event.AT_TARGET,
+ isTrusted: true,
+ timeStamp: Date.now(),
+ altKey: false,
+ ctrlKey: false,
+ metaKey: false,
+ shiftKey: false,
+ detail: 0,
+ view: window,
+ which: 0,
+ ...options
+ };
+ return mockEvent as TouchEvent;
+}
+
+// Helper function to create mock PointerEvent
+function createMockPointerEvent(type: string, pointerType: string, options: Partial = {}): PointerEvent {
+ const mockEvent = {
+ pointerId: 1,
+ pointerType,
+ target: document.createElement('div'),
+ preventDefault: vi.fn(),
+ stopPropagation: vi.fn(),
+ cancelable: true,
+ type,
+ currentTarget: document.createElement('div'),
+ bubbles: true,
+ composed: false,
+ defaultPrevented: false,
+ eventPhase: Event.AT_TARGET,
+ isTrusted: true,
+ timeStamp: Date.now(),
+ clientX: 100,
+ clientY: 200,
+ pageX: 100,
+ pageY: 200,
+ screenX: 100,
+ screenY: 200,
+ button: 0,
+ buttons: 1,
+ ctrlKey: false,
+ shiftKey: false,
+ altKey: false,
+ metaKey: false,
+ width: 1,
+ height: 1,
+ pressure: 0.5,
+ tangentialPressure: 0,
+ tiltX: 0,
+ tiltY: 0,
+ twist: 0,
+ isPrimary: true,
+ detail: 0,
+ view: window,
+ which: 0,
+ getCoalescedEvents: vi.fn(() => []),
+ getPredictedEvents: vi.fn(() => []),
+ movementX: 0,
+ movementY: 0,
+ offsetX: 0,
+ offsetY: 0,
+ relatedTarget: null,
+ ...options
+ };
+ return mockEvent as PointerEvent;
+}
+
+describe('dd-touch', () => {
+ let mockUtils: any;
+ let mockDDManager: any;
+
+ beforeEach(() => {
+ mockUtils = vi.mocked(Utils);
+ mockDDManager = vi.mocked(DDManager);
+
+ // Reset mocks
+ mockUtils.simulateMouseEvent.mockClear();
+ mockDDManager.dragElement = null;
+
+ // Mock window.clearTimeout and setTimeout
+ vi.spyOn(window, 'clearTimeout');
+ vi.spyOn(window, 'setTimeout').mockImplementation((callback: Function, delay: number) => {
+ return setTimeout(callback, delay) as any;
+ });
+
+ // Reset DDTouch state by calling touchend to reset touchHandled flag
+ // This is a workaround since we can't access DDTouch directly
+ const resetTouch = {
+ pageX: 0, pageY: 0, clientX: 0, clientY: 0, screenX: 0, screenY: 0,
+ identifier: 0, target: document.createElement('div'),
+ radiusX: 0, radiusY: 0, rotationAngle: 0, force: 0
+ } as Touch;
+ const resetEvent = createMockTouchEvent('touchend', [], { changedTouches: [resetTouch] });
+
+ // Call touchstart then touchend to reset state
+ const startEvent = createMockTouchEvent('touchstart', [resetTouch]);
+ touchstart(startEvent);
+ touchend(resetEvent);
+
+ // Clear any calls made during reset
+ mockUtils.simulateMouseEvent.mockClear();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('isTouch detection', () => {
+ it('should be a boolean value', () => {
+ expect(typeof isTouch).toBe('boolean');
+ });
+
+ it('should detect touch support in current environment', () => {
+ // Since we're in jsdom, isTouch should be false unless touch APIs are mocked
+ // This test validates that the detection logic runs without errors
+ expect(isTouch).toBeDefined();
+ });
+ });
+
+ describe('touchstart', () => {
+ let mockTouch: Touch;
+
+ beforeEach(() => {
+ mockTouch = {
+ pageX: 100,
+ pageY: 200,
+ clientX: 100,
+ clientY: 200,
+ screenX: 100,
+ screenY: 200,
+ identifier: 1,
+ target: document.createElement('div'),
+ radiusX: 10,
+ radiusY: 10,
+ rotationAngle: 0,
+ force: 1
+ } as Touch;
+ });
+
+ it('should simulate mousedown for single touch', () => {
+ const mockTouchEvent = createMockTouchEvent('touchstart', [mockTouch]);
+
+ touchstart(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'mousedown');
+ });
+
+ it('should prevent default on cancelable events', () => {
+ const mockTouchEvent = createMockTouchEvent('touchstart', [mockTouch], { cancelable: true });
+
+ touchstart(mockTouchEvent);
+
+ expect(mockTouchEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should not prevent default on non-cancelable events', () => {
+ const mockTouchEvent = createMockTouchEvent('touchstart', [mockTouch], { cancelable: false });
+
+ touchstart(mockTouchEvent);
+
+ expect(mockTouchEvent.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('should ignore multi-touch events', () => {
+ const secondTouch = { ...mockTouch, identifier: 2 };
+ const mockTouchEvent = createMockTouchEvent('touchstart', [mockTouch, secondTouch]);
+
+ touchstart(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('touchmove', () => {
+ let mockTouch: Touch;
+
+ beforeEach(() => {
+ mockTouch = {
+ pageX: 150,
+ pageY: 250,
+ clientX: 150,
+ clientY: 250,
+ screenX: 150,
+ screenY: 250,
+ identifier: 1,
+ target: document.createElement('div'),
+ radiusX: 10,
+ radiusY: 10,
+ rotationAngle: 0,
+ force: 1
+ } as Touch;
+ });
+
+ it('should simulate mousemove for single touch when touch is handled', () => {
+ // First call touchstart to set DDTouch.touchHandled = true
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear(); // Clear previous calls
+
+ const mockTouchEvent = createMockTouchEvent('touchmove', [mockTouch]);
+ touchmove(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'mousemove');
+ });
+
+ it('should ignore touchmove when touch is not handled', () => {
+ // Don't call touchstart first, so DDTouch.touchHandled remains false
+ const mockTouchEvent = createMockTouchEvent('touchmove', [mockTouch]);
+
+ touchmove(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should ignore multi-touch events', () => {
+ // First call touchstart to set DDTouch.touchHandled = true
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear(); // Clear previous calls
+
+ const secondTouch = { ...mockTouch, identifier: 2 };
+ const mockTouchEvent = createMockTouchEvent('touchmove', [mockTouch, secondTouch]);
+
+ touchmove(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('touchend', () => {
+ let mockTouch: Touch;
+
+ beforeEach(() => {
+ mockTouch = {
+ pageX: 200,
+ pageY: 300,
+ clientX: 200,
+ clientY: 300,
+ screenX: 200,
+ screenY: 300,
+ identifier: 1,
+ target: document.createElement('div'),
+ radiusX: 10,
+ radiusY: 10,
+ rotationAngle: 0,
+ force: 1
+ } as Touch;
+ });
+
+ it('should simulate mouseup when touch is handled', () => {
+ // First call touchstart to set DDTouch.touchHandled = true
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear(); // Clear previous calls
+
+ const mockTouchEvent = createMockTouchEvent('touchend', [], { changedTouches: [mockTouch] });
+ touchend(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'mouseup');
+ });
+
+ it('should simulate click when not dragging', () => {
+ // First call touchstart to set DDTouch.touchHandled = true
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear(); // Clear previous calls
+ mockDDManager.dragElement = null; // Not dragging
+
+ const mockTouchEvent = createMockTouchEvent('touchend', [], { changedTouches: [mockTouch] });
+ touchend(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'mouseup');
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'click');
+ });
+
+ it('should not simulate click when dragging', () => {
+ // First call touchstart to set DDTouch.touchHandled = true
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear(); // Clear previous calls
+ mockDDManager.dragElement = {}; // Dragging
+
+ const mockTouchEvent = createMockTouchEvent('touchend', [], { changedTouches: [mockTouch] });
+ touchend(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockTouch, 'mouseup');
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalledWith(mockTouch, 'click');
+ });
+
+ it('should ignore touchend when touch is not handled', () => {
+ // Don't call touchstart first, so DDTouch.touchHandled remains false
+ const mockTouchEvent = createMockTouchEvent('touchend', [], { changedTouches: [mockTouch] });
+ touchend(mockTouchEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should clear pointerLeaveTimeout when it exists', () => {
+ // First set up a pointerleave timeout
+ mockDDManager.dragElement = {};
+ const pointerEvent = createMockPointerEvent('pointerleave', 'touch');
+
+ let timeoutId: number;
+ vi.mocked(window.setTimeout).mockImplementation((callback: Function, delay: number) => {
+ timeoutId = 123;
+ return timeoutId as any;
+ });
+
+ pointerleave(pointerEvent);
+
+ // Now call touchstart and touchend to trigger the timeout clearing
+ const startEvent = createMockTouchEvent('touchstart', [mockTouch]);
+ touchstart(startEvent);
+
+ mockUtils.simulateMouseEvent.mockClear();
+
+ const mockTouchEvent = createMockTouchEvent('touchend', [], { changedTouches: [mockTouch] });
+ touchend(mockTouchEvent);
+
+ expect(window.clearTimeout).toHaveBeenCalledWith(123);
+ });
+ });
+
+ describe('pointerdown', () => {
+ let mockElement: HTMLElement;
+
+ beforeEach(() => {
+ mockElement = document.createElement('div');
+ mockElement.releasePointerCapture = vi.fn();
+ });
+
+ it('should release pointer capture for touch events', () => {
+ const mockPointerEvent = createMockPointerEvent('pointerdown', 'touch', { target: mockElement });
+
+ pointerdown(mockPointerEvent);
+
+ expect(mockElement.releasePointerCapture).toHaveBeenCalledWith(1);
+ });
+
+ it('should release pointer capture for pen events', () => {
+ const mockPointerEvent = createMockPointerEvent('pointerdown', 'pen', { target: mockElement });
+
+ pointerdown(mockPointerEvent);
+
+ expect(mockElement.releasePointerCapture).toHaveBeenCalledWith(1);
+ });
+
+ it('should not release pointer capture for mouse events', () => {
+ const mockPointerEvent = createMockPointerEvent('pointerdown', 'mouse', { target: mockElement });
+
+ pointerdown(mockPointerEvent);
+
+ expect(mockElement.releasePointerCapture).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('pointerenter', () => {
+ it('should ignore pointerenter when no drag element', () => {
+ mockDDManager.dragElement = null;
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'touch');
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should ignore pointerenter for mouse events', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'mouse');
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should simulate mouseenter for touch events when dragging', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'touch');
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockPointerEvent, 'mouseenter');
+ });
+
+ it('should simulate mouseenter for pen events when dragging', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'pen');
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockPointerEvent, 'mouseenter');
+ });
+
+ it('should prevent default on cancelable pointer events', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'touch', { cancelable: true });
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockPointerEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should not prevent default on non-cancelable pointer events', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerenter', 'touch', { cancelable: false });
+
+ pointerenter(mockPointerEvent);
+
+ expect(mockPointerEvent.preventDefault).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('pointerleave', () => {
+ it('should ignore pointerleave when no drag element', () => {
+ mockDDManager.dragElement = null;
+ const mockPointerEvent = createMockPointerEvent('pointerleave', 'touch');
+
+ pointerleave(mockPointerEvent);
+
+ expect(window.setTimeout).not.toHaveBeenCalled();
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should ignore pointerleave for mouse events', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerleave', 'mouse');
+
+ pointerleave(mockPointerEvent);
+
+ expect(window.setTimeout).not.toHaveBeenCalled();
+ expect(mockUtils.simulateMouseEvent).not.toHaveBeenCalled();
+ });
+
+ it('should delay mouseleave simulation for touch events when dragging', () => {
+ mockDDManager.dragElement = {};
+ const mockPointerEvent = createMockPointerEvent('pointerleave', 'touch');
+
+ // Mock setTimeout to capture the callback
+ let timeoutCallback: Function;
+ vi.mocked(window.setTimeout).mockImplementation((callback: Function, delay: number) => {
+ timeoutCallback = callback;
+ return 123 as any;
+ });
+
+ pointerleave(mockPointerEvent);
+
+ expect(window.setTimeout).toHaveBeenCalledWith(expect.any(Function), 10);
+
+ // Execute the timeout callback
+ timeoutCallback!();
+
+ expect(mockUtils.simulateMouseEvent).toHaveBeenCalledWith(mockPointerEvent, 'mouseleave');
+ });
+ });
+});
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index b6c859340..02623896f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -11,8 +11,13 @@ module.exports = {
rules: [
{
test: /\.ts$/,
- use: 'ts-loader',
- exclude: ['/node_modules/'],
+ use: {
+ loader: 'ts-loader',
+ options: {
+ configFile: 'tsconfig.build.json'
+ }
+ },
+ exclude: ['/node_modules/', '/spec/'],
},
],
},