@lf2com/magnet.js

Magnet.js is a JavaScript library that groups HTML elements and makes them attractable with each other

Usage no npm install needed!

<script type="module">
  import lf2comMagnetJs from 'https://cdn.skypack.dev/@lf2com/magnet.js';
</script>

README

Magnet.js

Magnet.js is a JavaScript library that groups HTML elements and makes them attractable with each other

Demo

Basic demo

jQuery version

  • Configure magnet attract distance
  • Switch to stay in parent element
  • Align to outer/inner edge of the others
  • Align to the x/y center of the others
  • Align to the x/y center of parent element

Group demo

4 magnet groups that can attract the others in their own groups or all the other group members.

Arrow key demo

Extend the Basic demo with new features:

  • Support arrow keys to move focused box (also support a/w/d/s keys)
  • Configure px unit of arrow_ (
  • unit < distance would cause the box stuck with the others when attracted

Install

Git

git clone https://github.com/lf2com/magnet.js.git
cd magnet.js
npm install .

NodeJS

CAUTION: Magnet.js is not tested on NodeJS environment. It uses document and eventListener related functions.

npm install @lf2com/magnet.js
# Or
npm install https://github.com/lf2com/magnet.js

Import

import Magnet from '@lf2com/magnet.js';
// Or
const Magnet = require('@lf2com/magnet.js');

Build

The required files are ./index.js and ./libs/*.js. All dependencies in ./package.json are only used for building a packaged/minified JS file as ./magnet.min.js. Since the code registered as window.Magnet. You can build a browser-used magnet.min.js with the following commands:

npm run build

Build jQuery Plugin

Build ./jquery-magnet.min.js

npm run jquery-build

Build All

Build both ./magnet.min.js and ./jquery-magnet.min.js

npm run all-build

Debug Build

Append -debug on any build command

npm run build-debug

# for jQuery
npm run jquery-build-debug

# for both
npm run all-build-debug

Browser

Download from this repository or use your own built: magnet.min.js

<!-- include script -->
<script src="PATH/TO/magnet.min.js"></script>

<script>
  console.log(window.Magnet); // here it is
</script>

jQuery Plugin

NOTICE: Please include jQuery library before incluing jquery-magnet.min.js

<script src="PATH/TO/jQuery.js"></script>
<script src="PATH/TO/jquery-magnet.min.js"></script>
<script>
  (function($) {
    console.log($.magnet); // here it is
  })(jQuery);
</script>

Usage of Magnet

Create Magnet Group

Create a magnet group. All the elements added into the group would be applied the attract behaviors.

let magnet = new Magnet();

jQuery

Create a new group

$.magnet(options?)

let options = {
  distance: 15,
  stayInParent: true,
};
let $magnet = $.magnet(options);

Add Elements

Add HTML elements into the group

.add(...DOMs)

magnet.add(document.querySelectorAll('.magnet')); // return this

Or add HTML element when creating a group

let magnet = new Magnet(document.querySelectorAll('.magnet'));

Flexable ways to add elements

magnet.add(
  document.querySelectorAll('.magnet'),
  document.querySelectorAll('.other-magnet'),
  document.getElementById('major-magnet')
);

// the same as above
magnet
  .add(document.querySelectorAll('.magnet'))
  .add(document.querySelectorAll('.other-magnet'))
  .add(document.getElementById('major-magnet'));

jQuery

$magnet.add(...DOMs)

Add elements to an existing group

$.fn.magnet(options?)

Add element to a new group

let $magnet = $('.magnet').magnet(options);

Remove Elements

Remove HTML elements from the group

.remove(...DOMs)

Keep the positon changed by the magnet

magnet.remove(document.querySelector('.magnet')); // return this

.removeFull(...DOMs)

Remove the positions changed by the magnet

magnet.removeFull(document.querySelector('.magnet')); // return this

Flexable ways to remove elements

magnet.remove(
  document.querySelectorAll('.magnet'),
  document.querySelectorAll('.other-magnet'),
  document.getElementById('major-magnet')
);

// the same as above
magnet
  .remove(document.querySelectorAll('.magnet'))
  .remove(document.querySelectorAll('.other-magnet'))
  .remove(document.getElementById('major-magnet'));

jQuery

$magnet.remove(...DOMs)

$magnet.removeFull(...DOMs)

Clear All Elements

Remove all the HTML elements from the group

.clear()

Keep the position changed by the magnet

magnet.clear();

.clearFull()

Remove the position changed by the magnet

magnet.clearFull();

jQuery

$magnet.clear()

$magnet.clearFull()

Distance of Attraction

Distance for elements to attract others in the same group

Default: 0 (px)

.distance(px?)

Get/set distance

magnet.distance(15); // set: unit px, return this
magnet.distance(); // get: 15

Alias

.setDistance(px)

magnet.setDistance(15); // set to 15

.getDistance()

magnet.getDistance(); // get 15

jQuery

$magnet.distance(px?)

Attractable

Attractable between group members

Default: true

NOTICE: Setting to false has the same effect as pressing ctrl key

.attractable(enabled?)

Get/set attractable

magnet.attractable(true); // set to attract members, return this
magnet.attractable(); // get: true

Alias

.setAttractable(enabled)

magnet.setAttractable(true); // set to true

.getAttractable()

magnet.getAttractable(); // get true

jQuery

$magnet.attractable(enabled?)

Allow Ctrl Key

Allow to press ctrl key to be unattractable temporarily

Default: true

NOTICE: Pressing ctrl key makes group members unattractable, any magnet related event will not be triggered

.allowCtrlKey(enabled?)

Get/set allow ctrl key

magnet.allowCtrlKey(true); // set to allow ctrl key, return this
magnet.allowCtrlKey(); // get: true

Alias

.setAllowCtrlKey(enabled)

magnet.setAllowCtrlKey(true); // set to true

.getAllowCtrlKey()

magnet.getAllowCtrlKey(); // get true

jQuery

$magnet.allowCtrlKey(enabled?)

Allow Drag Elements

Allow to drag element by mouse/touch

Default: true

.allowDrag(enabled?)

Get/set allow drag

magnet.allowDrag(true); // set to allow drag, return this
manget.allowDrag(); // get: true

Alias

.setAllowDrag(enabled)

magnet.setAllowDrag(true); // set to true

.getAllowDrag()

magnet.getAllowDrag(); // get true

jQuery

$magnet.allowDrag(enabled?)

Use Relative Unit

Use relative unit % or absolute unit px

Default: false

.useRelativeUnit(enabled?)

Get/set use relative unit

magnet.useRelativeUnit(true); // set to use relative unit, return this
magnet.useRelativeUnit(); // get: true

Alias

.setUseRelativeUnit(enabled)

magnet.setUseRelativeUnit(true); // set to true
magnet.getUseRelativeUnit(); // get true

jQuery

$magnet.useRelativeUnit(enabled?)

Alignments

Magnet supports the following alignments:

| Type | Description | Default | | :-: | :- | :-: | | outer | align edges to other edges from outside | true | | inner | align edges to other edges from inside | true | | center | align middle x/y to other's middle x/y | true | | parent center | align middle x/y to parent's middle x/y | false |

.align{Prop}(enabled?)

Get/set enabled of alignment

magnet.alignOuter(true); // set: align to element outside edges, return this
magnet.alignInner(false); // set: align to element inside edges, return this
magnet.alignCenter(true); // set: align to element middle line, return this
magnet.alignParentCenter(false); // set: alien to parent element middle line, return this

magnet.alignOuter(); // get: true

Alias

.enabledAlign{Prop}(enabled?)

magnet.enabledAlignOuter(true); // set to true
magnet.enabledAlignParentCenter(false); // set to false

magnet.enabledAlignOuter(); // get: true
magnet.enabledAlignParentCenter(); // get: false

.setEnabledAlign{Prop}(enabled)

magnet.setEnabledAlignOuter(true); // set to true

.getEnabledAlign{Prop}()

magnet.getEnabledAlignOuter(); // get true

jQuery

$magnet.align{Prop}(enabled?)

Align to Parent Inner Edges

CAUTION:

  • Parent may NOT be the 1st parentNode of the current element._
  • Parent is the first matched parentNode whose style.position is not static
  • All the top/left offset of magnet members is based on the parent element

.stayInParent(enabled?)

Force elements of group not to be out of the edge of parent element

Default: false

Get/set stay inside of the parent

magnet.stayInParent(true); // set: not to move outside of the parent element, return this
magnet.stayInParent(); // get: true

Alias

.stayInParentEdge(enabled?)

magnet.stayInParentEdge(true); // set to true
magnet.stayInParentEdge(); // get: true

.stayInParentElem(enabled?)

magnet.stayInParentElem(true); // set to true
magnet.stayInParentElem(); // get true

Another alias

.setStayInParent(enabled)

magnet.setStayInParent(true); // set to true

.getStayInParent()

magnet.getStayInParent(); // get true

jQuery

$magnet.stayInParent(enabled?)

Events of Magnet

Magnet supports the following events:

| Name | Description | Alias | | :-: | :- | :-: | | magnetstart | when the last result has no any attract but now it does | start, magnetenter, enter | | magnetend | when the last result has any attract but now it doesn't | end, magnetleave, leave | | magnetchange | when any change of attract, including start/end and the changes of attracted alignment properties | change |

Arguments of Magnet Event

Each event has the following members in the detail of event object:

| Property | Type | Description | | :-: | :-: | :- | | source | DOM | HTML element that is dragged | | x | Object | Attract info of x-axis, null if no attract | | y | Object | Attract info of y-axis, null if no attract |

.on(eventNames, functions)

Add event listener

magnet.on('magnetenter', function(evt) {
  let detail = evt.detail;
  console.log('magnetenter', detail); // detail info of attract elements
  console.log('source', detail.source); // current HTML element
  console.log('targets', detail.x, detail.y); // current attracted of both axises
});

magnet.on('magnetleave', function(evt) {
  let detail = evt.detail;
  console.log('magnetleave', detail);
  console.log('source', detail.source);
  console.log('targets', detail.x, detail.y); // the last attracted of both axises
});

magnet.on('magnetchange', function(evt) {
  let detail = evt.detail;
  console.log('magnetchange', detail);
  console.log('source', detail.source);
  console.log('targets', detail.x, detail.y); // the newest attracted of both axises
});

// the same as above
magnet.on('magnetstart', function(evt) {
  // do something
}).on('magnetchange', function(evt) {
  // do something
}).on('magnetend', function(evt) {
  // do something
});

jQuery

$magnet.on(eventNames, functions)

.off(eventNames)

Remove event listeners

magnet.off('magnetenter magnetleave magnetchange'); // remove event listeners

// the same as above
magnet
  .off('magnetenter')
  .off('magnetleave')
  .off('magnetchange');

jQuery

$magnet.off(eventNames)

Events of magnet members

Magnet members supports the following events:

| Name | Target | description | | :-: | :-: | :- | | attract | forcused | Attract to other members | | unattract | focused | Unattract from other members | | attracted | others | Attracted by the focused member | | unattracted | others | Unattracted by the focused member | | attractstart | focused | Start of dragging | | attractend | focused | End of dragging | | attractmove | focused | Moving of dragging |

Arguments of attract/unattract

Events of attract and unattract have the following members in the detail of event object:

| Property | Type | Description | | :-: | :-: | :- | | x | Object | Attract info of x-axis, null if no attract | | y | Object | Attract info of y-axis, null if no attract |

let elem = document.querySelector('.block');
magnet.add(elem);

function onAttract(evt) {
  let detail = evt.detail;
  console.log('attract', detail); // detail info of attract elements
  console.log('targets', detail.x, detail.y); // current attracted of both axises
}
function onUnattract(evt) {
  let detail = evt.detail;
  console.log('unattract', detail);
  console.log('targets', detail.x, detail.y); // the last attracted of both axises
}

// add event listener
elem.addEventListener('attract', onAttract);
elem.addEventListener('unattract', onUnattract);

// remove event listener
elem.removeEventListener('attract', onAttract);
elem.removeEventListener('unattract', onUnattract);

jQuery

// the same as above
$(elem)
  .on('attract', onAttract)
  .on('unattract', onUnattract);

$(elem)
  .off('attract unattract');

Arguments of attracted/unattracted

Events of attracted and unattracted have the target member in the detail of event object

function onAttracted(evt) {
  let dom = evt.detail;
  console.log('attracted', dom); // be attracted by dom
}
function onUnattracted(evt) {
  let dom = evt.detail;
  console.log('unattracted', dom); // be unattracted by dom
}

// add event listener
elem.addEventListener('attracted', onAttracted);
elem.addEventListener('unattracted', onUnattracted);

// remove event listener
elem.removeEventListener('attracted', onAttracted);
elem.removeEventListener('unattracted', onUnattracted);

jQuery

// the same as above
$(elem)
  .on('attracted', onAttracted)
  .on('unattracted', onUnattracted);

$(elem).off('attracted unattracted');

Arguments of attractstart/attractend

function onAttractStart(evt) {
  let rect = evt.detail;
  console.log('attract start', rect); // rectangle of dom
}
function onAttractEnd(evt) {
  let rect = evt.detail;
  console.log('attract end', rect); // rectangle of dom
}

// add event listener
elem.addEventListener('attractstart', onAttractStart);
elem.addEventListener('attractend', onAttractEnd;

// remove event listener
elem.removeEventListener('attractstart', onAttractStart);
elem.removeEventListener('attractend', onAttractEnd

jQuery

// the same as above
$(elem)
  .on('attractstart', onAttractStart)
  .on('attractend', onAttractEnd);

$(elem).off('attractstart attractend');

Arguments of attractmove

NOTICE: Call preventDefault() to ignore attraction if need

function onAttractMove(evt) {
  let { rects, attracts } = evt.detail;
  let { origin, target } = rects;
  let { current, last } = attracts;
  
  // do something
  // ...

  evt.preventDefault(); // call this to ignore attraction if need
}

elem.addEventListener('attractmove', onAttractMove); // add event listener
elem.removeEventListener('attractmove', onAttractMove); // remove event listener

jQuery

// the same as above
$(elem).on('attractmove', onAttractMove);

$(elem).off('attractmove');

Check Attracting Result

Check the relationships between source and all the other group members

.check(sourceDOM[, sourceRect[, alignments]])

Default sourceRect is the rectangle of sourceDOM

Default alignments is the outer/inner/center settings of magnet

Parameter of Check Result

| Property | Type | Description | | :-: | :-: | :- | | source | Object | Element object | | parent | Object | Element object | | targets | Array | Array of measurement result object | | results | Object | Object with alignment properties and the values are array of measurement results | | rankings | Object | Object as results but each property is sorted from near to far | | mins | Object | Object with alignment properties and the values are the minimum value of distance | | maxs | Object | Object with alignment properties and the values are the maximum value of distance |

magnet.add(elem);
magnet.check(elem, ['topToTop', 'bottomToBottom']); // get the result of 'topToTop' and 'bottomToBottom' between the other members

// the same as above
magnet.check(elem, elem.getBoundingClientRect(), ['topToTop', 'bottomToBottom']);

jQuery

$magnet.check(sourceDOM[, sourceRect[, alignments]])

Handle Rectangle Position of Element

Change the position of target member for the input position with checking the attracting relationships between source and all the other group members

.handle(sourceDOM[, sourceRect[, attractable]])

Default sourceRect is the rectangle of sourceDOM

Default attractable is the value of attractable

let { top, right, bottom, left } = elem.getBoundingClientRect();
let offset = {
  x: 15,
  y: 10
};
let rect = {
  top: (top-offset.y),
  right: (right-offset.x),
  bottom: (bottom-offset.y),
  left: (left-offset.x),
};
magnet.add(elem);
magnet.handle(elem, rect, true); // move the member to the new rectangle position with the attracting relationship, return this

jQuery

$magnet.handle(sourceDOM[, sourceRect[, attractable]])

Set Rectangle Position of Member

Directly change the position of member that is faster than .handle(...)

.setMemberRectangle(sourceDOM[, sourceRect[, useRelativeUnit]])

Default sourceRect is the rectangle of sourceDOM

Default useRelativeUnit is the value of .getUseRelativeUnit()

let { top, right, bottom, left } = elem.getBoundingClientRect();

magnet.setMemberRectangle(elem, rect);

Before/After/Do Applying Rectangle Position

The group passes the info to target function before/after/do applying the change to target element

NOTICE: The function will be called with rectangle infos and attract infos as long as dragging the target element

Rectangle Infos

| Property | Type | Description | | :-: | :-: | :- | | origin | Object | Origin rectangle object | | target | Object | Target rectangle object |

Attract Infos

| Property | Type | Description | | :-: | :-: | :- | | current | Object | Current attract infos of x/y axises | | last | Object | Last attract infos of x/y axises |

.beforeAttract = function(targetDom rectangleInfos, attractInfos)

Set to a function for confirming the change

| Value | Description | | :-: | :- | | false | Apply the original rectangle without attraction | | rectangle | Rectangle object to apply on the target element |

function beforeAttractFunc(dom, { origin, target }, { current, last }) {
  console.log(this); // manget

  if (MAKE_SOME_CHANGES) {
    // apply other rectangle info
    return {
      top: (target.top - 1),
      right: (target.right + 1),
      bottom: (target.bottom + 1),
      left: (target.left - 1),
    };
  } else if (NO_ATTRACTION) {
    return false; // ignore attraction
  } else if (STILL_NO_ATTRACTION) {
    return origin; // the same as no attraction
  }

  // if went here, it would apply default change
};

magnet.beforeAttract = beforeAttractFunc; // set function
console.log(magnet.beforeAttract); // print function

magnet.beforeAttract = null; // unset function

jQuery

$magnet.beforeAttract(function(targetDom, rectangleInfos, attractInfos)?)

$magnet.beforeAttract(beforeAttractFunc); // set function
$magnet.beforeAttract(); // get beforeAttractFunc

$magnet.beforeAttract(null); // unset function (input non-function value)

.doAttract = function(targetDom, rectangleInfos, attractInfos)

Set the displacement handly which means the user has to set the style of DOM to apply the position change if need

magnet.doAttract = function(dom, { origin, target }, { current, last }) {
  const { top, right, bottom, left } = origin;
  const { x, y } = current;
  const px = (p) => `${p}px`;

  if (x && y && x.element === y.element) {
    // attract current targets
    const elem = x.element;
    const { width, height } = x.rect;
    const move = (type) => {
      switch (type) {
        case 'topToTop': return elem.style.top = px(top);
        case 'rightToRight': return elem.style.left = px(right-width);
        case 'bottomToBottom': return elem.style.top = px(bottom-height);
        case 'leftToLeft': return elem.style.left = px(left);
        case 'topToBottom': return elem.style.top = px(top-height);
        case 'bottomToTop': return elem.style.top = px(bottom);
        case 'rightToLeft': return elem.style.left = px(right);
        case 'LeftToRight': return elem.style.left = px(left-width);
        case 'xCenter': return elem.style.left = px((right+left-width)/2);
        case 'yCenter': return elem.style.top = px((top+bottom-height)/2);
      }
    }
    move(x.type);
    move(y.type);
  }

  // keep original position
  dom.style.top = px(top);
  dom.style.left = px(left);
});

jQuery

$magnet.doAttract(function(targetDom, rectangleInfos, attractInfos)?)

.afterAttract = function(targetDom, rectangleInfos, attractInfos)

See what changed after attracting

jQuery

$magnet.afterAttract(function(targetDom, rectangleInfos, attractInfos)?)

Usage of Rectangle

Check Rectangle

Magnet.isRect(rect)

Check if rect is a rectangle like object with the following object members and rules:

Property Rule
top <= bottom
right >= left
bottom >= top
left <= right
width = right - left
height = bottom - top
x (optional) = left
y (optional) = top

NOTICE: Default use 0.0000000001 for bias of calculation

let rect = { top: 1, right: 2, bottom: 3, left: 4 };
Magnet.isRect(rect); // false: right < left
rect.right = 5;
Magnet.isRect(rect); // true

rect.x = 3;
Magnet.isRect(rect); // false: x != left
rect.x = rect.left;

rect.width = 2;
Magnet.isRect(rect); // false: width != (right - left)

Standardize Rectangle

Magnet.stdRect(rect)

Return a rectangle object if rect is a HTML element or a valid rectangle like object:

| Property | Rule | | :-: | :- | | top | Inherit from rect | | right | Inherit from rect | | bottom | Inherit from rect | | left | Inherit from rect | | width | Inherit from rect or set to right - left | | height | Inherit from rect or set to bottom - top | | x | Inherit from rect or set to left | | y | Inherit from rect or set to top |

Magnet.stdRect(rect); // get a rectangle object

Measure Distance between Rectangles

Magnet.measure(source, target[, options])

Measure distance between 2 elements/rectangles

Options

Options of measurement:

| Property | Type | Description | | :-: | :-: | :- | | alignments | Array | Array of alignment properties. Default is ALL alignment properties | | absDistance | Boolean | false to allow negative value of distance. Default is true |

let rectA = { top: 0, right: 3, bottom: 1, left: 2 };
let rectB = { top: 10, right: 13, bottom: 11, left: 12 };
Magnet.measure(rectA, rectB); // MeasureResult object

Alias

Magnet.diffRect(source, target[, options])

Magnet.diffRect(rectA, rectB);

Result of Measurement

See measurement result object

DEPRECATED Methods

Magnet.nearby(...)

To reduce the usless calculations of measurement, it's recommended to call Magnet.measure/Magnet.diffRect independently and handle the results handly to get what you really want.

References

Magnet Default Values

| Property | Type | Description | Default | | :-: | :-: | :- | :-: | | distance | Number | Distance to attract | 0 | | attractable | Boolean | Ability to attract | true | | allowCtrlKey | Boolean | Ability to use ctrl key to unattract | true | | stayInParent | Boolean | Stay in parent element | false | | alignOuter | Boolean | Align outer edges to that of the others | true | | alignInner | Boolean | Align inner edges to that of the others | true | | alignCenter | Boolean | Align x/y center to that of the others | true | | alignParentCenter | Boolean | Align x/y center to that of parent element | false |

Alignment Properties

| Value | Description | | :-: | :- | | topToTop | Source top to target top (inner) | | rightToRight | Source right to target right (inner) | | bottomToBottom | Source bottom to target bottom (inner) | | leftToLeft | Source left to target left (inner) | | topToBottom | Source top to target bottom (outer) | | bottomToTop | Source bottom to target top (outer) | | rightToLeft | Source right to target left (outer) | | leftToright | Source left to target right (outer) | | xCenter | Source x middle to target x middle (center) | | yCenter | Source y middle to target y middle (center) |

Attract Info

| Property | Type | Description | | :-: | :-: | :- | | type | String | Alignment property name | | rect | Object | Rectangle object of element | | element | DOM | HTML element | | position | Number | Absolute offset px based on window's top/left | | offset | Number | Offset px based on parent element |

Rectangle Object

| Property | Type | Description | | :-: | :-: | :- | | top | Number | The same as y | | right | Number | | | bottom | Number | | | left | Number | The same as x | | width | Number | The same as right - left | | height | Number | The same as bottom - top | | x | Number | The same as left | | y | Number | The same as top |

Element Object

| Property | Type | Description | | :-: | :-: | :- | | rect | Object | Rectangle object | | element (optional) | DOM | HTML element. undefined if the source is a pure rectangle like object |

Measurement Value Object

NOTICE: All the properties inherit from alignment properties

Value Type
topToTop (optional) Number
rightToRight (optional) Number
bottomToBottom (optional) Number
leftToLeft (optional) Number
topToBottom (optional) Number
bottomToTop (optional) Number
rightToLeft (optional) Number
leftToright (optional) Number
xCenter (optional) Number
yCenter (optional) Number

Measurement Result Object

| Property | Type | Description | | :-: | :-: | :- | | source | Object | Element object | | target | Object | Element object | | results | Object | Measurement value object. The properties follow the input alignment properties of measurement | ranking | Array | Array of alignment properties sorted from near to far | | min | String | Alignment property with minimum distance | | max | String | Alignment property with maximum distance |

NOTICE: The following properties are DEPRECATED

Property Type Replacement
topToTop Number results.topToTop
topToBottom Number results.topToBottom
rightToRight Number results.rightToRight
rightToLeft Number results.rightToLeft
bottomToTop Number results.bottomToTop
bottomToBottom Number results.bottomToBottom
xCenter Number results.xCenter
yCenter Number results.yCenter

License

MIT Copyright @ Wan Wan