├── .npmignore
├── .gitignore
├── lib
├── dock-offset.jsx
├── dock-item.jsx
├── dock-background.jsx
├── dock.jsx
└── index.jsx
├── LICENSE
├── package.json
├── test
└── index.html
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | lib/
2 | test/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 | dist/
4 |
--------------------------------------------------------------------------------
/lib/dock-offset.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function(props) {
4 | let style = Object.assign({
5 | width: `${props.width}px`,
6 | height: `${props.height}px`,
7 | background: "red",
8 | opacity: props.debug ? 0.5 : 0,
9 | }, (() => {
10 | switch (props.magnifyDirection) {
11 | case "up": return { alignSelf: "end", };
12 | case "down": return { alignSelf: "start", };
13 | case "center": return { alignSelf: "center", };
14 | }
15 | })());
16 |
17 | return
;
18 | }
19 |
--------------------------------------------------------------------------------
/lib/dock-item.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function(props) {
4 | return (
5 |
16 | {props.children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/lib/dock-background.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function(props) {
4 | let style = Object.assign({
5 | position: "absolute",
6 | width: "100%",
7 | height: `${props.height}px`,
8 | boxSizing: "border-box",
9 | border: props.debug ? "1px solid red" : null,
10 | zIndex: 0,
11 | }, (() => {
12 | switch (props.magnifyDirection) {
13 | case "up": return { bottom: 0, };
14 | case "down": return { top: 0, };
15 | case "center": return { top: "50%", transform: "translateY(-50%)", };
16 | }
17 | })());
18 |
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Luke Horvat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/dock.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DockItem from "./dock-item";
3 | import DockBackground from "./dock-background";
4 |
5 | export default function(props) {
6 | React.Children.forEach(props.children, item => {
7 | if (item.type !== DockItem) throw new Error("Invalid child type.");
8 | });
9 |
10 | let style = Object.assign({
11 | display: "grid",
12 | gridTemplateColumns: props.itemWidths.map(() => "auto").join(" "),
13 | position: "relative",
14 | }, (() => {
15 | switch (props.magnifyDirection) {
16 | case "up": return { alignItems: "end", };
17 | case "down": return { alignItems: "start", };
18 | case "center": return { alignItems: "center", };
19 | }
20 | })());
21 |
22 | return (
23 |
24 | {React.Children.map(props.children, (item, index) => (
25 | React.cloneElement(item, { width: props.itemWidths[index], debug: props.debug, })
26 | ))}
27 |
28 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-osx-dock",
3 | "version": "1.0.0",
4 | "description": "React component that is magnifiable like the Mac OS X dock.",
5 | "author": "Luke Horvat",
6 | "license": "MIT",
7 | "repository": "lukehorvat/react-osx-dock",
8 | "main": "dist",
9 | "scripts": {
10 | "build": "node_modules/.bin/babel lib -d dist -q",
11 | "prebuild": "node_modules/.bin/rimraf dist",
12 | "prepublish": "npm run build",
13 | "pretest": "npm run build",
14 | "test": "node_modules/.bin/electron test/index.html"
15 | },
16 | "dependencies": {},
17 | "devDependencies": {
18 | "babel-cli": "6.26.0",
19 | "babel-preset-es2015": "6.24.1",
20 | "babel-preset-react": "6.24.1",
21 | "babel-preset-stage-0": "6.24.1",
22 | "electron": "1.7.9",
23 | "react": "16.0.0",
24 | "react-dom": "16.0.0",
25 | "rimraf": "2.5.4"
26 | },
27 | "peerDependencies": {
28 | "react": ">= 16.0.0",
29 | "react-dom": ">= 16.0.0"
30 | },
31 | "keywords": [
32 | "react",
33 | "component",
34 | "osx",
35 | "mac",
36 | "dock",
37 | "magnifiable"
38 | ],
39 | "babel": {
40 | "presets": [
41 | "es2015",
42 | "react",
43 | "stage-0"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
43 |
44 |
45 |
46 |
47 |
48 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-osx-dock [](https://www.npmjs.com/package/react-osx-dock)
2 |
3 |
4 |
5 |
6 |
7 | [React](https://reactjs.org) component that is magnifiable like the Mac OS X dock.
8 |
9 | Works in any Web browser that supports CSS [grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout) and [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout) layout.
10 |
11 | View a demo [here](https://lukehorvat.github.io/react-osx-dock).
12 |
13 | ## Installation
14 |
15 | Install the package with NPM:
16 |
17 | ```bash
18 | $ npm install react-osx-dock
19 | ```
20 |
21 | ## Usage
22 |
23 | Example:
24 |
25 | ```javascript
26 | import Dock from "react-osx-dock";
27 |
28 |
29 | {["a", "b", "c", "d", "e"].map((item, index) => (
30 | console.log(item)}>
31 |
32 |
33 | ))}
34 |
35 | ```
36 |
37 | ## API
38 |
39 | ### Dock
40 |
41 | React component that accepts [Dock.Item](#dockitem)s as children, and the following props:
42 |
43 | Name | Description | Type | Required
44 | ---- | ----------- | ---- | --------
45 | `width` | The width of the dock in pixels. | number | yes
46 | `magnification` | The level of dock magnification produced on mouse-over. | number | yes
47 | `magnifyDirection` | The vertical direction that dock items grow when magnified. | string enum
(`"up"`, `"down"`, `"center"`) | yes
48 | `className` | The dock's CSS class. | string | no
default: `undefined`
49 | `backgroundClassName` | The dock background's CSS class. | string | no
default: `undefined`
50 | `debug` | Whether to render dock sub-component bounding boxes or not. Useful for debugging! | boolean | no
default: `false`
51 |
52 | ### Dock.Item
53 |
54 | React component that accepts any HTML/React elements as children, and the following props:
55 |
56 | Name | Description | Type | Required
57 | ---- | ----------- | ---- | --------
58 | `className` | The dock item's CSS class. | string | no
default: `undefined`
59 | `onClick` | The dock item's mouse click event handler. | function | no
default: `undefined`
60 | `onMouseOver` | The dock item's mouse over event handler. | function | no
default: `undefined`
61 | `onMouseOut` | The dock item's mouse out event handler. | function | no
default: `undefined`
62 |
63 | ## Contributing
64 |
65 | Pull requests are most welcome. Clone this repository and run `npm test` to test/debug your code changes.
66 |
67 | ## Credits
68 |
69 | The demo website uses a couple of free icon packs from [FlatIcon](https://flaticon.com):
70 |
71 | - [Social icons](https://flaticon.com/packs/glypho) designed by Bogdan Rosu.
72 | - [Pokémon icons](https://flaticon.com/packs/pokemon-go) designed by Roundicons Freebies.
73 |
74 | Thanks!
75 |
--------------------------------------------------------------------------------
/lib/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import Dock from "./dock";
4 | import DockItem from "./dock-item";
5 | import DockOffset from "./dock-offset";
6 |
7 | export default class extends React.Component {
8 | static Item = DockItem;
9 |
10 | state = { magnifierX: null };
11 |
12 | render() {
13 | let offsetLeft = this.state.magnifierX === null ? this.unmagnifiedDockOffsetLeft : this.magnifiedDockOffsetLeft;
14 | let offsetRight = this.state.magnifierX === null ? this.unmagnifiedDockOffsetRight : this.magnifiedDockOffsetRight;
15 | let itemWidths = this.state.magnifierX === null ? this.unmagnifiedDockItemWidths : this.magnifiedDockItemWidths;
16 |
17 | return (
18 |
22 |
28 |
35 | {this.props.children}
36 |
37 |
43 |
44 | );
45 | }
46 |
47 | onMagnify(event) {
48 | let element = ReactDOM.findDOMNode(this);
49 | let magnifierX = event.pageX - element.offsetLeft - this.unmagnifiedDockOffsetLeft;
50 |
51 | if (magnifierX >= 0 && magnifierX < this.unmagnifiedDockWidth) {
52 | this.setState({ magnifierX });
53 | } else {
54 | this.onUnmagnify(); // The mouse isn't over the dock; don't bother recording its coordinates.
55 | }
56 | }
57 |
58 | onUnmagnify() {
59 | this.setState({ magnifierX: null });
60 | }
61 |
62 | computeDockItemWidths(magnifierX = null) {
63 | return React.Children.map(this.props.children, (item, index) => {
64 | if (magnifierX === null) return this.unmagnifiedDockItemWidth;
65 |
66 | let itemCenter = this.computeDockWidth(this.unmagnifiedDockItemWidths.slice(0, index)) + (this.unmagnifiedDockItemWidth / 2);
67 | let distance = Math.abs(magnifierX - itemCenter);
68 | let distancePercent = Math.max(1 - (distance / this.magnifierRadius), 0);
69 | return this.unmagnifiedDockItemWidth + (this.unmagnifiedDockItemWidth * distancePercent * this.magnification);
70 | });
71 | }
72 |
73 | computeDockWidth(itemWidths = []) {
74 | return itemWidths.reduce((sum, itemWidth) => sum + itemWidth, 0);
75 | }
76 |
77 | get unmagnifiedDockItemWidth() {
78 | return this.props.width / React.Children.count(this.props.children);
79 | }
80 |
81 | get unmagnifiedDockItemWidths() {
82 | return this.computeDockItemWidths();
83 | }
84 |
85 | get unmagnifiedDockWidth() {
86 | return this.computeDockWidth(this.unmagnifiedDockItemWidths);
87 | }
88 |
89 | get unmagnifiedDockOffset() {
90 | return Math.abs(this.unmagnifiedDockWidth - this.maxMagnifiedDockWidth);
91 | }
92 |
93 | get unmagnifiedDockOffsetLeft() {
94 | return this.unmagnifiedDockOffset / 2;
95 | }
96 |
97 | get unmagnifiedDockOffsetRight() {
98 | return this.unmagnifiedDockOffsetLeft;
99 | }
100 |
101 | get magnifiedDockItemWidths() {
102 | return this.computeDockItemWidths(this.state.magnifierX);
103 | }
104 |
105 | get magnifiedDockWidth() {
106 | return this.computeDockWidth(this.magnifiedDockItemWidths);
107 | }
108 |
109 | get magnifiedDockOffset() {
110 | return Math.abs(this.magnifiedDockWidth - this.maxMagnifiedDockWidth);
111 | }
112 |
113 | get magnifiedDockOffsetLeft() {
114 | return this.state.magnifierX < this.unmagnifiedDockWidth / 2 ? this.magnifiedDockOffset : 0;
115 | }
116 |
117 | get magnifiedDockOffsetRight() {
118 | return this.state.magnifierX >= this.unmagnifiedDockWidth / 2 ? this.magnifiedDockOffset : 0;
119 | }
120 |
121 | get maxMagnifiedDockWidth() {
122 | // The dock's width will be maximum when the mouse is magnifying the center of it.
123 | return this.computeDockWidth(this.computeDockItemWidths(this.unmagnifiedDockWidth / 2));
124 | }
125 |
126 | get magnifierRadius() {
127 | return this.unmagnifiedDockItemWidth * 3;
128 | }
129 |
130 | get magnification() {
131 | let { magnification } = this.props;
132 |
133 | if (magnification == undefined || isNaN(magnification) || magnification < 0) {
134 | throw new Error("Invalid magnification.");
135 | }
136 |
137 | return magnification;
138 | }
139 |
140 | get magnifyDirection() {
141 | let { magnifyDirection } = this.props;
142 |
143 | if (!["up", "down", "center"].includes(magnifyDirection)) {
144 | throw new Error("Invalid magnify direction.");
145 | }
146 |
147 | return magnifyDirection;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------