├── .gitignore ├── README.md ├── demo ├── index.html ├── index.js ├── main.css ├── now.json ├── package-lock.json └── package.json ├── index.ts ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tackjs 2 | Tiny utility to position an element absolutely in relation to another element. **500b gzipped.** 3 | 4 | > What's this for? Think popovers, modals, tooltips, scroll-jacking, etc. 5 | 6 | ## Install 7 | ```bash 8 | npm i tackjs --save 9 | ``` 10 | 11 | ## Usage 12 | Usage is very straightfoward. Think, "pin *element* to *target* at the *top*": 13 | ```javascript 14 | import tack from 'tackjs' 15 | 16 | const element = document.querySelector('...') 17 | const target = document.querySelector('...') 18 | 19 | const pin = tack(element, target, 'top') 20 | ``` 21 | 22 | To update the position – say after the window resizes – use `update`: 23 | ```javascript 24 | pin.update() 25 | ``` 26 | 27 | If you need to un-pin and remove all styles: 28 | ```javascript 29 | pin.destroy() 30 | ``` 31 | 32 | But don't worry! It can be re-pinned as well: 33 | ```javascript 34 | pin.update() 35 | ``` 36 | 37 | **N.B.** `tackjs` also adds an `.is-tacked` class to all pinned elements. 38 | 39 | ### Alignment 40 | Tack supports the following coordinates relative to the passed `target` element: 41 | - `top` 42 | - `bottom` 43 | - `left` 44 | - `right` 45 | - `topLeft` 46 | - `topRight` 47 | - `bottomLeft` 48 | - `bottomRight` 49 | 50 | ## License 51 | MIT License © [Eric Bailey](https://estrattonbailey.com) 52 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tackjs 📌 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |
20 |
🍕
21 | 22 |

tackjs

23 |

A minimal toolkit to pin an element relative to another element.

24 | 25 |

👉 Open your browser console to play around.

26 | 27 |
28 |
29 | 0 30 |
31 |
32 | 1 33 |
34 |
35 | 2 36 |
37 |
38 | 3 39 |
40 |
41 |
42 | View this project on GitHub. 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', e => { 2 | window.pizza = document.getElementById('target') 3 | window.box = [ 4 | document.getElementById('box0'), 5 | document.getElementById('box1'), 6 | document.getElementById('box2'), 7 | document.getElementById('box3') 8 | ] 9 | window.tack = window.tackjs 10 | 11 | console.log(`welcome to the tackjs demo 📌 12 | 13 | attached to the window are a few objects for you to mess with: 14 | 1. pizza - a tasty piece of pizza 15 | 2. box - the array of tomato colored rectangles 16 | 3. tackjs library, available as \`window.tack\` 17 | 18 | use them like this: 19 | 20 | tack(pizza, box[1], 'top') 21 | 22 | 💡 think "tack pizza to box 1 at the top" 23 | 24 | also, tack returns some utils: 25 | 26 | const pin = tack(pizza, box[3], 'bottomLeft') 27 | 28 | pin.update() // update position, like after resize 29 | pin.destroy() // unpin 30 | 31 | bonus points: attach an event listener, like resize, and call update on each tick to maintain pinned position. 32 | `) 33 | }) 34 | -------------------------------------------------------------------------------- /demo/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | html, body { 5 | padding: 0; 6 | margin: 0; 7 | font-family: monospace; 8 | } 9 | .outer { 10 | padding: 2em; 11 | } 12 | .container { 13 | max-width: 600px; 14 | margin: auto; 15 | } 16 | .flex { 17 | display: flex; 18 | flex-wrap: wrap; 19 | margin-left: -1em; 20 | margin-right: -1em; 21 | } 22 | .box { 23 | width: 50%; 24 | padding: 1em; 25 | 26 | div { 27 | display: block; 28 | position: relative; 29 | padding-top: 100%; 30 | background-color: tomato; 31 | } 32 | span { 33 | padding: 1em; 34 | font-size: 1.5em; 35 | position: absolute; 36 | top: 0; 37 | } 38 | } 39 | #target { 40 | position: absolute; 41 | left: 0; 42 | top: 0; 43 | width: 2em; 44 | height: 2em; 45 | background: rgba(0,0,0,0.8); 46 | transition: transform 200ms cubic-bezier(0,.58,.38,1); 47 | } 48 | -------------------------------------------------------------------------------- /demo/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "alias": "tackjs.now.sh" 4 | } 5 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "tackjs-demo", 4 | "scripts": { 5 | "deploy": "now --target production" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | type Placement = 2 | | "top" 3 | | "bottom" 4 | | "left" 5 | | "right" 6 | | "topLeft" 7 | | "bottomLeft" 8 | | "topRight" 9 | | "bottomRight"; 10 | 11 | export function getCoords(element: HTMLElement) { 12 | const { 13 | left: l, 14 | right: r, 15 | top: t, 16 | bottom: b 17 | } = element.getBoundingClientRect(); 18 | const { pageYOffset: y } = window; 19 | 20 | return { 21 | height: b - t, 22 | width: r - l, 23 | top: { 24 | y: y + t, 25 | x: l + (r - l) / 2 26 | }, 27 | bottom: { 28 | y: y + b, 29 | x: l + (r - l) / 2 30 | }, 31 | left: { 32 | y: y + t + (b - t) / 2, 33 | x: l 34 | }, 35 | right: { 36 | y: y + t + (b - t) / 2, 37 | x: r 38 | }, 39 | topLeft: { 40 | y: y + t, 41 | x: l 42 | }, 43 | bottomLeft: { 44 | y: y + b, 45 | x: l 46 | }, 47 | topRight: { 48 | y: y + t, 49 | x: r 50 | }, 51 | bottomRight: { 52 | y: y + b, 53 | x: r 54 | } 55 | }; 56 | } 57 | 58 | export function position( 59 | target: HTMLElement, 60 | scope: HTMLElement, 61 | placement: Placement 62 | ) { 63 | const c = getCoords(scope)[placement]; 64 | const e = getCoords(target); 65 | const { pageYOffset: y } = window; 66 | 67 | const vp = { 68 | top: y, 69 | bottom: y + window.innerHeight, 70 | left: 0, 71 | right: window.innerWidth 72 | }; 73 | 74 | const offsets = { 75 | top: { 76 | x: e.width / 2, 77 | y: e.height 78 | }, 79 | bottom: { 80 | x: e.width / 2, 81 | y: 0 82 | }, 83 | left: { 84 | x: e.width, 85 | y: e.height / 2 86 | }, 87 | right: { 88 | x: 0, 89 | y: e.height / 2 90 | }, 91 | topLeft: { 92 | x: e.width, 93 | y: e.height 94 | }, 95 | topRight: { 96 | x: 0, 97 | y: e.height 98 | }, 99 | bottomLeft: { 100 | x: e.width, 101 | y: 0 102 | }, 103 | bottomRight: { 104 | x: 0, 105 | y: 0 106 | } 107 | }; 108 | 109 | let posx = c.x - offsets[placement].x; 110 | let posy = c.y - offsets[placement].y; 111 | 112 | if (posx < vp.left) { 113 | posx = vp.left; 114 | } else if (posx + e.width > vp.right) { 115 | posx = vp.right - e.width; 116 | } 117 | 118 | if (posy < vp.top) { 119 | posy = vp.top; 120 | } else if (posy + e.height > vp.bottom) { 121 | posy = vp.bottom - e.height; 122 | } 123 | 124 | target.style.transform = `translateX(${Math.round( 125 | posx 126 | )}px) translateY(${Math.round(posy)}px)`; 127 | } 128 | 129 | export function tack( 130 | target: HTMLElement, 131 | scope: HTMLElement, 132 | placement: Placement 133 | ) { 134 | target.classList.add("is-tacked"); 135 | position(target, scope, placement); 136 | 137 | return { 138 | update() { 139 | position(target, scope, placement); 140 | }, 141 | destroy() { 142 | target.style.transform = ""; 143 | target.classList.remove("is-tacked"); 144 | } 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tackjs", 3 | "version": "2.1.0", 4 | "description": "A simple toolkit to pin an element relative to another element.", 5 | "source": "index.ts", 6 | "module": "dist/tackjs.es.js", 7 | "main": "dist/tackjs.js", 8 | "umd:main": "dist/tackjs.umd.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "microbundle build", 15 | "watch": "microbundle watch", 16 | "test": "ava" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/estrattonbailey/tackjs.git" 21 | }, 22 | "author": "estrattonbailey", 23 | "keywords": [ 24 | "tack", 25 | "positioning" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/estrattonbailey/tackjs/issues" 30 | }, 31 | "homepage": "https://github.com/estrattonbailey/tackjs#readme", 32 | "devDependencies": { 33 | "ava": "^2.1.0", 34 | "microbundle": "^0.11.0", 35 | "ts-node": "^8.6.0" 36 | }, 37 | "ava": { 38 | "compileEnhancements": false, 39 | "extensions": [ 40 | "ts" 41 | ], 42 | "require": [ 43 | "ts-node/register" 44 | ] 45 | } 46 | } 47 | --------------------------------------------------------------------------------