diff --git a/examples/allowFrom-callback/index.html b/examples/allowFrom-callback/index.html new file mode 100644 index 000000000..8e6167616 --- /dev/null +++ b/examples/allowFrom-callback/index.html @@ -0,0 +1,321 @@ + + + + + + InteractJS - allowFrom Callback Example + + + +

InteractJS - allowFrom with Callback Function

+ +
+

Demonstration of the `allowFrom` option with callback:

+ +
+ +
+ Your role: + + + +
+ +
+
+
✋ Handle Only
+
+

This container can only be moved by grabbing the blue handle above.

+ +

Try clicking here... it won't work!

+
+
+ +
+
🔐 Role-based Access
+
+ 👑 Admin Only Zone - Drag allowed if role = Admin +
+
+ ✏️ Editor+ Zone - Drag allowed if role = Admin or Editor +
+
+ ⛔ Forbidden Zone - No dragging possible +
+
+ +
+
🧠 Advanced Logic
+
+
📝 Conditional zone (role-based)
+ +
+ 🔧 Admin special feature +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/examples/combined-callbacks/index.html b/examples/combined-callbacks/index.html new file mode 100644 index 000000000..6bcf9418e --- /dev/null +++ b/examples/combined-callbacks/index.html @@ -0,0 +1,436 @@ + + + + + + InteractJS - allowFrom & ignoreFrom Callbacks Combined + + + +

InteractJS - allowFrom & ignoreFrom with Callbacks

+ +
+

Advanced demonstration combining allowFrom and ignoreFrom:

+

This example shows how to use both options together with callback functions to create sophisticated access control logic.

+

Logic: allowFrom determines allowed zones, then ignoreFrom can override and forbid specific zones.

+
+ +
+
+
+ Allowed zone (allowFrom) +
+
+
+ Ignored zone (ignoreFrom) +
+
+
+ Neutral zone +
+
+
+ Priority zone +
+
+ +
+
+
🎯 Complex Access Control
+ +
+ ✅ Basic allowed zone + +
+ +
+ ❌ Always ignored zone +
+ Even if nested in allowed, it's ignored +
+
+ +
+ ⚪ Neutral zone (not allowed by default) +
+ +
+ 🔥 Priority management zone +
+ ✨ High priority (allowed) +
+
+ 🔧 Under maintenance (ignored) +
+
+
+ +
+
👥 Role-based Permissions
+ +
+ 👤 User zone (all roles) +
+ +
+ 👑 Admin only zone +
+ 🔒 Sensitive data (always ignored) +
+
+ 🛠️ Admin tools +
+
+ + +
+
+ +
+

📊 Decision Log

+
+
+ + + + + \ No newline at end of file diff --git a/examples/ignoreFrom-callback/index.html b/examples/ignoreFrom-callback/index.html new file mode 100644 index 000000000..b6b8754fc --- /dev/null +++ b/examples/ignoreFrom-callback/index.html @@ -0,0 +1,136 @@ + + + + + + InteractJS - ignoreFrom Callback Example + + + +

InteractJS - ignoreFrom with Callback Function

+ +
+

This example shows how to use a callback function with the ignoreFrom option.

+

Try dragging the elements below. Some areas are configured to be ignored.

+
+ +
+
✋ Drag Handle
+

Basic draggable area

+
⛔ Ignored zone (no-drag class)
+ +
+ +
+
✋ Drag Handle
+

Draggable area with advanced logic

+
+ 📝 Zone with data-ignore (ignored) +
+ + +
+ + + + + \ No newline at end of file diff --git a/examples/ignoreFrom-callback/test.js b/examples/ignoreFrom-callback/test.js new file mode 100644 index 000000000..1b309f2c4 --- /dev/null +++ b/examples/ignoreFrom-callback/test.js @@ -0,0 +1,60 @@ +// Test simple pour vérifier que la nouvelle fonctionnalité compile +// et fonctionne avec la syntaxe JavaScript moderne + +// Simuler un environnement interactjs +const interact = require('../../packages/interactjs/index.ts'); + +// Test 1: Usage avec string (fonctionnalité existante) +const configWithString = { + ignoreFrom: '.ignore-class', + listeners: { + move: (event) => console.log('move') + } +}; + +// Test 2: Usage avec fonction callback (nouvelle fonctionnalité) +const configWithCallback = { + ignoreFrom: function(targetNode, eventTarget) { + // Logique personnalisée + if (eventTarget instanceof Element) { + return eventTarget.hasAttribute('data-ignore'); + } + return false; + }, + listeners: { + move: (event) => console.log('callback move') + } +}; + +// Test 3: Usage avec fonction callback plus complexe +const configWithComplexCallback = { + ignoreFrom: (targetNode, eventTarget) => { + if (eventTarget instanceof Element) { + // Ignorer les inputs + if (eventTarget.tagName === 'INPUT' || eventTarget.tagName === 'TEXTAREA') { + return true; + } + + // Ignorer si parent a classe 'no-drag' + let parent = eventTarget.parentElement; + while (parent && parent !== targetNode) { + if (parent.classList && parent.classList.contains('no-drag')) { + return true; + } + parent = parent.parentElement; + } + } + return false; + } +}; + +console.log('Configurations créées avec succès:'); +console.log('- Config avec string:', typeof configWithString.ignoreFrom); +console.log('- Config avec callback:', typeof configWithCallback.ignoreFrom); +console.log('- Config avec callback complexe:', typeof configWithComplexCallback.ignoreFrom); + +module.exports = { + configWithString, + configWithCallback, + configWithComplexCallback +}; \ No newline at end of file diff --git a/packages/@interactjs/auto-start/base.ts b/packages/@interactjs/auto-start/base.ts index 442225457..c30ab42f9 100644 --- a/packages/@interactjs/auto-start/base.ts +++ b/packages/@interactjs/auto-start/base.ts @@ -59,8 +59,8 @@ declare module '@interactjs/core/options' { manualStart?: boolean max?: number maxPerElement?: number - allowFrom?: string | Element - ignoreFrom?: string | Element + allowFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) + ignoreFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) cursorChecker?: CursorChecker // only allow left button by default diff --git a/packages/@interactjs/core/Interactable.allowFrom.spec.ts b/packages/@interactjs/core/Interactable.allowFrom.spec.ts new file mode 100644 index 000000000..a679b3f76 --- /dev/null +++ b/packages/@interactjs/core/Interactable.allowFrom.spec.ts @@ -0,0 +1,157 @@ +import * as helpers from '@interactjs/core/tests/_helpers' + +import type { Interactable } from './Interactable' + +describe('Interactable.testAllow with callback function', () => { + let interactable: Interactable + let targetNode: HTMLElement + let eventTarget: HTMLElement + + beforeEach(() => { + // Create DOM elements for testing + targetNode = document.createElement('div') + targetNode.className = 'target' + + eventTarget = document.createElement('span') + eventTarget.className = 'event-target' + + targetNode.appendChild(eventTarget) + + // Create interactable using testEnv + const { interactable: testInteractable } = helpers.testEnv({ target: targetNode }) + interactable = testInteractable + }) + + test('should work with string selector (existing functionality)', () => { + const allowFrom = '.allow-class' + eventTarget.className = 'allow-class' + + const result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with element (existing functionality)', () => { + const allowElement = document.createElement('div') + allowElement.appendChild(eventTarget) + + const result = interactable.testAllow(allowElement, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with callback function returning true', () => { + const allowFrom = (_targetNode: Node, eventTarget: Node) => { + return eventTarget instanceof Element && eventTarget.hasAttribute('data-allow') + } + + eventTarget.setAttribute('data-allow', 'true') + + const result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with callback function returning false', () => { + const allowFrom = (_targetNode: Node, eventTarget: Node) => { + return eventTarget instanceof Element && eventTarget.hasAttribute('data-allow') + } + + // No data-allow attribute, so should return false + + const result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(false) + }) + + test('should work with complex callback logic for drag handles', () => { + const allowFrom = (_targetNode: Node, eventTarget: Node) => { + if (eventTarget instanceof Element) { + // Only allow dragging from elements with 'drag-handle' class + if (eventTarget.classList.contains('drag-handle')) { + return true + } + + // Or allow dragging from specific data attributes + if (eventTarget.hasAttribute('data-draggable')) { + return true + } + + // Check parent elements for drag handle + let parent = eventTarget.parentElement + while (parent && parent !== targetNode) { + if (parent.classList.contains('drag-handle')) { + return true + } + parent = parent.parentElement + } + } + return false + } + + // Test with drag handle element + const handleElement = document.createElement('div') + handleElement.className = 'drag-handle' + targetNode.appendChild(handleElement) + + let result = interactable.testAllow(allowFrom, targetNode, handleElement) + expect(result).toBe(true) + + // Test with element having data-draggable + const draggableElement = document.createElement('span') + draggableElement.setAttribute('data-draggable', 'true') + targetNode.appendChild(draggableElement) + + result = interactable.testAllow(allowFrom, targetNode, draggableElement) + expect(result).toBe(true) + + // Test with child of drag handle + const childElement = document.createElement('span') + handleElement.appendChild(childElement) + + result = interactable.testAllow(allowFrom, targetNode, childElement) + expect(result).toBe(true) + + // Test with normal element (should not allow) + const normalElement = document.createElement('div') + targetNode.appendChild(normalElement) + + result = interactable.testAllow(allowFrom, targetNode, normalElement) + expect(result).toBe(false) + }) + + test('should return true for undefined allowFrom (default behavior)', () => { + const result = interactable.testAllow(undefined, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should return false for non-element eventTarget', () => { + const allowFrom = () => true + const textNode = document.createTextNode('text') + + const result = interactable.testAllow(allowFrom, targetNode, textNode) + expect(result).toBe(false) + }) + + test('should work with role-based access control', () => { + const allowFrom = (_targetNode: Node, eventTarget: Node) => { + if (eventTarget instanceof Element) { + const role = eventTarget.getAttribute('data-role') + const allowedRoles = ['admin', 'editor'] + return allowedRoles.includes(role || '') + } + return false + } + + // Test with allowed role + eventTarget.setAttribute('data-role', 'admin') + let result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(true) + + // Test with forbidden role + eventTarget.setAttribute('data-role', 'viewer') + result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(false) + + // Test with no role + eventTarget.removeAttribute('data-role') + result = interactable.testAllow(allowFrom, targetNode, eventTarget) + expect(result).toBe(false) + }) +}) diff --git a/packages/@interactjs/core/Interactable.ignoreFrom.spec.ts b/packages/@interactjs/core/Interactable.ignoreFrom.spec.ts new file mode 100644 index 000000000..e976c9fd5 --- /dev/null +++ b/packages/@interactjs/core/Interactable.ignoreFrom.spec.ts @@ -0,0 +1,120 @@ +import * as helpers from '@interactjs/core/tests/_helpers' + +import type { Interactable } from './Interactable' + +describe('Interactable.testIgnore with callback function', () => { + let interactable: Interactable + let targetNode: HTMLElement + let eventTarget: HTMLElement + + beforeEach(() => { + // Create DOM elements for testing + targetNode = document.createElement('div') + targetNode.className = 'target' + + eventTarget = document.createElement('span') + eventTarget.className = 'event-target' + + targetNode.appendChild(eventTarget) + + // Create interactable using testEnv + const { interactable: testInteractable } = helpers.testEnv({ target: targetNode }) + interactable = testInteractable + }) + + test('should work with string selector (existing functionality)', () => { + const ignoreFrom = '.ignore-class' + eventTarget.className = 'ignore-class' + + const result = interactable.testIgnore(ignoreFrom, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with element (existing functionality)', () => { + const ignoreElement = document.createElement('div') + ignoreElement.appendChild(eventTarget) + + const result = interactable.testIgnore(ignoreElement, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with callback function returning true', () => { + const ignoreFrom = (_targetNode: Node, eventTarget: Node) => { + return eventTarget instanceof Element && eventTarget.hasAttribute('data-ignore') + } + + eventTarget.setAttribute('data-ignore', 'true') + + const result = interactable.testIgnore(ignoreFrom, targetNode, eventTarget) + expect(result).toBe(true) + }) + + test('should work with callback function returning false', () => { + const ignoreFrom = (_targetNode: Node, eventTarget: Node) => { + return eventTarget instanceof Element && eventTarget.hasAttribute('data-ignore') + } + + // No data-ignore attribute, so should return false + + const result = interactable.testIgnore(ignoreFrom, targetNode, eventTarget) + expect(result).toBe(false) + }) + + test('should work with complex callback logic', () => { + const ignoreFrom = (_targetNode: Node, eventTarget: Node) => { + if (eventTarget instanceof Element) { + // Ignore input elements + if (eventTarget.tagName === 'INPUT' || eventTarget.tagName === 'TEXTAREA') { + return true + } + + // Ignore elements with no-drag class in parent chain + let parent = eventTarget.parentElement + while (parent && parent !== targetNode) { + if (parent.classList.contains('no-drag')) { + return true + } + parent = parent.parentElement + } + } + return false + } + + // Test with input element + const inputElement = document.createElement('input') + targetNode.appendChild(inputElement) + + let result = interactable.testIgnore(ignoreFrom, targetNode, inputElement) + expect(result).toBe(true) + + // Test with element having no-drag parent + const noDragParent = document.createElement('div') + noDragParent.className = 'no-drag' + const childElement = document.createElement('span') + noDragParent.appendChild(childElement) + targetNode.appendChild(noDragParent) + + result = interactable.testIgnore(ignoreFrom, targetNode, childElement) + expect(result).toBe(true) + + // Test with normal element (should not ignore) + const normalElement = document.createElement('div') + targetNode.appendChild(normalElement) + + result = interactable.testIgnore(ignoreFrom, targetNode, normalElement) + expect(result).toBe(false) + }) + + test('should return false for undefined ignoreFrom', () => { + const result = interactable.testIgnore(undefined, targetNode, eventTarget) + expect(result).toBe(false) + }) + + test('should return false for non-element eventTarget', () => { + const ignoreFrom = () => true + const textNode = document.createTextNode('text') + + const result = interactable.testIgnore(ignoreFrom, targetNode, textNode) + expect(result).toBe(false) + }) +}) diff --git a/packages/@interactjs/core/Interactable.ts b/packages/@interactjs/core/Interactable.ts index 9616423d9..dae357b57 100644 --- a/packages/@interactjs/core/Interactable.ts +++ b/packages/@interactjs/core/Interactable.ts @@ -27,7 +27,8 @@ import type { import { Eventable } from './Eventable' import type { ActionDefaults, Defaults, OptionsArg, PerActionDefaults, Options } from './options' -type IgnoreValue = string | Element | boolean +type IgnoreValue = string | Element | boolean | ((targetNode: Node, eventTarget: Node) => boolean) +type AllowValue = string | Element | boolean | ((targetNode: Node, eventTarget: Node) => boolean) type DeltaSource = 'page' | 'client' const enum OnOffMethod { @@ -291,7 +292,7 @@ export class Interactable implements Partial { /** @internal */ testIgnoreAllow( this: Interactable, - options: { ignoreFrom?: IgnoreValue; allowFrom?: IgnoreValue }, + options: { ignoreFrom?: IgnoreValue; allowFrom?: AllowValue }, targetNode: Node, eventTarget: Node, ) { @@ -302,7 +303,7 @@ export class Interactable implements Partial { } /** @internal */ - testAllow(this: Interactable, allowFrom: IgnoreValue | undefined, targetNode: Node, element: Node) { + testAllow(this: Interactable, allowFrom: AllowValue | undefined, targetNode: Node, element: Node) { if (!allowFrom) { return true } @@ -315,6 +316,8 @@ export class Interactable implements Partial { return matchesUpTo(element, allowFrom, targetNode) } else if (is.element(allowFrom)) { return nodeContains(allowFrom, element) + } else if (is.func(allowFrom)) { + return allowFrom(targetNode, element) } return false @@ -330,6 +333,8 @@ export class Interactable implements Partial { return matchesUpTo(element, ignoreFrom, targetNode) } else if (is.element(ignoreFrom)) { return nodeContains(ignoreFrom, element) + } else if (is.func(ignoreFrom)) { + return ignoreFrom(targetNode, element) } return false diff --git a/packages/@interactjs/core/options.ts b/packages/@interactjs/core/options.ts index 553829939..745bbd65e 100644 --- a/packages/@interactjs/core/options.ts +++ b/packages/@interactjs/core/options.ts @@ -20,8 +20,8 @@ export interface PerActionDefaults { enabled?: boolean origin?: Point | string | Element listeners?: Listeners - allowFrom?: string | Element - ignoreFrom?: string | Element + allowFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) + ignoreFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) } export type Options = Partial & diff --git a/packages/@interactjs/core/types.ts b/packages/@interactjs/core/types.ts index 3d65d508c..35ff8037d 100644 --- a/packages/@interactjs/core/types.ts +++ b/packages/@interactjs/core/types.ts @@ -122,8 +122,8 @@ export type OriginFunction = (target: Element) => Rect export interface PointerEventsOptions { holdDuration?: number - allowFrom?: string - ignoreFrom?: string + allowFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) + ignoreFrom?: string | Element | ((targetNode: Node, eventTarget: Node) => boolean) origin?: Rect | Point | string | Element | OriginFunction }