Constraint Scoping Across Groups and Containers
MosaicJS evaluates a drop through up to three layers of constraints, in a
fixed order: built-in constraints (including group scoping) run first,
then any user-registered group-level constraints
(MosaicOptions.constraints), and — only for a drop resolved onto a linked
peer instance — user-registered cross-container constraints
(MosaicOptions.crossContainerConstraints). Each layer only runs once the
previous one has passed; a rejection at any layer short-circuits the rest.
flowchart TD A["Built-in checkConstraints()"] -->|allowed| B["User constraints (MosaicOptions.constraints)"] A -->|rejected| R[Reject + rollback] B -->|allowed| C{"Cross-container drop?"} B -->|rejected| R C -->|no| D[Confirm] C -->|yes| E["Cross-container constraints (MosaicOptions.crossContainerConstraints)"] E -->|allowed| F["Confirm + mosaic:container:transfer"] E -->|rejected| RAll three layers share the exact same function signature —
(input: ConstraintInput) => ConstraintResult — so a constraint written for
one layer is trivially portable to another; the only difference is which
MosaicOptions array it’s registered in (or, for built-ins, that it isn’t
user-registered at all).
Layer 1: Built-In Group Scoping (default behavior)
When selectors.group is configured, every drop is checked against the
dragged node’s own origin group and the resolved target’s group. A mismatch
is rejected with reason "group-boundary" — the default is to reject
cross-group drops — unless MosaicOptions.crossGroupDrag is explicitly
enabled.
const mosaic = new Mosaic({ root, selectors: { node: ".card", group: ".lane" } // crossGroupDrag omitted — cross-group drops rejected by default});This is real, already-shipped behavior with no registration involved — see
Group Containers for the full picture, including the
crossGroupDrag option and the mosaic:group:enter/leave events fired
during a cross-group hover.
Layer 2: User-Registered Group-Level Constraints
MosaicOptions.constraints lets you register additional business rules,
evaluated after the built-in checks above have passed. A common use is
allowing specific cross-group transfers under conditions the built-in
crossGroupDrag boolean can’t express on its own (e.g. “urgent” cards may
move into any lane, but other cards may only move within their own lane).
const mosaic = new Mosaic({ root, selectors: { node: ".card", group: ".lane" }, crossGroupDrag: true, // let the built-in check pass through to this layer constraints: [ (input) => { const isUrgent = input.dragged.classList.contains("urgent"); const sameLane = input.sourceGroup === input.targetGroup;
if (isUrgent || sameLane) return { allowed: true };
return { allowed: false, reason: "lane-transfer-restricted", metadata: { sourceGroupId: input.sourceGroup?.id ?? null } }; } ]});input.sourceGroup/input.targetGroup are the same real HTMLElement | null
group-container references checkConstraints()’s own built-in
"group-boundary" check uses internally — a user constraint sees exactly
what the built-in layer saw, not a re-derived approximation.
Layer 3: Cross-Container Constraints
Cross-container constraints are a distinct concept from group-level ones:
they govern a drop moving between two entirely separate, linked Mosaic
instances — each with its own root, its own selectors, and potentially
its own group configuration — not two groups within a single instance.
Two instances must first be linked via Mosaic.link(a, b) before either can
ever receive a drop from the other — the default is to reject a drop onto
an unlinked instance outright, before any constraint (built-in or
user-registered) ever runs.
const board = new Mosaic({ root: boardRoot, selectors: { node: ".card" } });const archive = new Mosaic({ root: archiveRoot, selectors: { node: ".card" }, crossContainerConstraints: [ (input) => { // input.sourceInstanceId / input.targetInstanceId identify the two // instances involved — only populated for a cross-container drop. if (input.dragged.dataset.archivable !== "true") { return { allowed: false, reason: "not-archivable" }; } return { allowed: true }; } ]});
board.initialize();archive.initialize();Mosaic.link(board, archive);Cross-container constraints are evaluated against the receiving
(target) instance’s own configuration — archive’s crossContainerConstraints
above, not board’s — mirroring how the target instance’s own built-in and
user constraints (layers 1 and 2) are also evaluated using its own
selectors/maxNestingDepth/etc. rather than the dragging instance’s. The
instance whose DOM is receiving the drop is the one whose rules govern
whether it accepts the incoming node.
A confirmed cross-container drop fires mosaic:container:transfer with
{ sourceInstanceId, targetInstanceId, nodeId }, in addition to the usual
mosaic:mutation:confirmed.
window.addEventListener("mosaic:container:transfer", (e) => { const { sourceInstanceId, targetInstanceId, nodeId } = e.detail;});Full Evaluation Order
For a drop resolved onto a linked peer instance, the complete order is:
- Built-in constraints (
checkConstraints()) — evaluated against the target instance’s own configuration, including its own group-boundary check if it configuresselectors.group - The target instance’s own
MosaicOptions.constraints(user group-level constraints) - The target instance’s own
MosaicOptions.crossContainerConstraints
For a same-instance drop, step 3 never runs at all — ConstraintInput’s
sourceInstanceId/targetInstanceId fields are undefined, and evaluation
stops after step 2, exactly as it did before cross-container support
existed.
Every layer stops at its first rejection — a later layer never runs once an earlier one (built-in or user, group-level or cross-container) has already rejected the drop.
Related
- Group Containers — the built-in group-scoping
mechanism (layer 1) in full, including
crossGroupDragand the group-hover event pair - Constraints Design — the broader constraint evaluation model this page’s three layers build on top of
- Mosaic API Reference —
Mosaic.link()/unlink()/arePeers()/getLinkedPeers()andmosaicInstanceId, the multi-instance peer-linking primitives cross-container constraints (layer 3) depend on