<div id="root"></div>
<div id="context-menu"></div>
html,
body {
min-height: 100%;
}
body {
position: relative;
color: #666;
background-color: #a18cd1;
background-image: linear-gradient(to top, #a18cd1 0%, #fbc2eb 100%);
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.portal-toggle {
position: absolute;
display: block;
padding: 6px 10px;
bottom: 16px;
left: 50%;
outline: none;
border: none;
border-radius: 4px;
min-width: 140px;
margin-left: -70px;
cursor: pointer;
color: #666;
background-color: #eee;
box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.2),
0 4px 6px rgba(0, 0, 0, 0.1);
&:hover {
color: #fff;
background-color: #42b6f4;
outline: none;
}
}
.window {
position: fixed;
top: 18%;
left: 18%;
right: 18%;
bottom: 18%;
background: #fff;
overflow: hidden;
border-radius: 5px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
.scroll {
position: absolute;
top: 32px;
left: 0;
right: 0;
bottom: 0;
overflow: scroll;
cursor: crosshair;
}
header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 32px;
line-height: 32px;
background-color: #d6d6d6;
border-bottom: 1px solid #ddd;
z-index: 1;
}
.controls {
position: absolute;
left: 10px;
top: 0;
}
.red,
.yellow,
.green {
display: inline-block;
margin-right: 5px;
width: 12px;
height: 12px;
outline: none;
border: 0;
border-radius: 6px;
box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.1);
}
.red {
background-color: #ed5b44;
}
.yellow {
background-color: #fcc846;
}
.green {
background-color: #60d657;
}
h2 {
margin: 0;
font-size: 16px;
font-weight: normal;
text-align: center;
}
}
.context-menu {
position: fixed;
margin: 0;
background: #eee;
overflow: hidden;
border-radius: 4px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
min-width: 250px;
ul {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
line-height: 28px;
padding: 0 24px;
&:hover {
color: #fff;
background-color: #42b6f4;
}
}
.menu-item {
cursor: pointer;
}
hr {
background: #ccc;
height: 1px;
border: none;
}
}
.artboard {
position: relative;
width: 5000px;
height: 5000px;
background-color: #f3f3f3;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj4gIDxyZWN0IGZpbGw9InJnYmEoMCwgMCwgMCwgMC4wNSkiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMCIgaGVpZ2h0PSIxMCIgLz4gIDxyZWN0IGZpbGw9InJnYmEoMCwgMCwgMCwgMC4wNSkiIHg9IjEwIiB5PSIxMCIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiAvPjwvc3ZnPg==');
overflow: hidden;
}
.shape {
position: absolute;
height: 100px;
width: 100px;
box-sizing: border-box;
&.square {
height: 100px;
width: 100px;
border: solid 20px #ba8bf4;
}
&.circle {
height: 130px;
width: 130px;
border-radius: 50%;
border: solid 20px #fcec92;
&:before {
border-radius: 50%;
}
}
&.diamond {
height: 80px;
width: 80px;
border: solid 20px #67efa2;
transform: rotate(45deg);
transform-origin: 50% 50%;
&:before {
transform: rotate(45deg);
transform-origin: 50% 50%;
}
}
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #42b6f4;
animation: shape-added 0.5s ease-out forwards;
}
}
@keyframes shape-added {
0% {
transform: scale(1.5);
opacity: 0.5;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
View Compiled
/**
* -------------------------------------------------------
* Context Menu
* -------------------------------------------------------
*/
const ContextMenu = ({
addShape,
closeMenu,
portalEl,
menuX,
menuY,
artX,
artY,
}) => {
const menu = (
<nav
className="context-menu"
style={{
top: menuY,
left: menuX,
}}
>
<ul>
<li
className="menu-item"
onClick={event => {
addShape({ x: artX, y: artY, type: 'square' });
closeMenu();
}}
>
Add Shape: Square
</li>
<li
className="menu-item"
onClick={event => {
addShape({ x: artX, y: artY, type: 'circle' });
closeMenu();
}}
>
Add Shape: Circle
</li>
<li
className="menu-item"
onClick={event => {
addShape({ x: artX, y: artY, type: 'diamond' });
closeMenu();
}}
>
Add Shape: Diamond
</li>
{/*
Fake items to make the menu bigger and more likely to not fit in the window
*/}
<hr />
<li>Fake Menu Item: Foo</li>
<li>Fake Menu Item: Bar</li>
<hr />
<li>Fake Menu Item: Bat</li>
<li>Fake Menu Item: Bin</li>
<li>Fake Menu Item: Foe</li>
<hr />
<li>Fake Menu Item: Fat</li>
<li>Fake Menu Item: Bot</li>
</ul>
</nav>
);
// if the portal el is available render into it.
// This is just for demo purposes to toggle between states.
return portalEl ? ReactDOM.createPortal(menu, portalEl) : menu;
};
/**
* -------------------------------------------------------
* Window
* -------------------------------------------------------
*/
const Window = ({ children, title }) => (
<section className="window">
<header>
<h2>{title}</h2>
<div className="controls">
<span className="red" />
<span className="yellow" />
<span className="green" />
</div>
</header>
{children}
</section>
);
/**
* -------------------------------------------------------
* Shape
* -------------------------------------------------------
*/
class Shape extends React.Component {
render() {
const { x, y, type } = this.props;
return <div className={`shape ${type}`} style={{ top: y, left: x }} />;
}
}
/**
* -------------------------------------------------------
* Artboard
* -------------------------------------------------------
*/
function randomIntBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
class Artboard extends React.Component {
constructor() {
super();
this.portalEl = document.getElementById('context-menu');
}
state = {
portalActive: false,
menuOpen: false,
menuX: 0,
menuY: 0,
shapes: this.generateRandomArt(),
};
generateRandomArt() {
const mw = 5000;
const mh = 5000;
const types = ['circle', 'square', 'diamond'];
const shapes = new Array(randomIntBetween(80, 150))
.fill({})
.map(shape => ({
x: randomIntBetween(0, mw),
y: randomIntBetween(0, mh),
type: types[randomIntBetween(0, types.length)],
}));
return shapes;
}
handleClick = event => {
const { clientX: x, clientY: y } = event;
const { top, left } = this.artboard.getBoundingClientRect();
const artX = x - left;
const artY = y - top;
this.setState(({ menuOpen }) => ({
menuOpen: !menuOpen,
menuX: x,
menuY: y,
artX,
artY,
}));
};
handleCloseMenu = () => {
this.setState(() => ({
menuOpen: false,
}));
};
handleTogglePortal = () => {
this.setState(({ portalActive }) => ({
portalActive: !portalActive,
}));
};
handleAddObject = newShape => {
const shapes = [...this.state.shapes, newShape];
this.setState(() => ({ shapes }));
};
render() {
const { children } = this.props;
const {
portalActive,
menuOpen,
menuX,
menuY,
artX,
artY,
shapes,
event,
} = this.state;
return (
<React.Fragment>
<div className="scroll" onClick={this.handleClick}>
<div
className="artboard"
ref={ref => (this.artboard = ref)}
>
{shapes.map((config, i) => (
<Shape key={i} {...config} />
))}
</div>
</div>
{/*
This is our context menu that should behave as though
it's a global menu item, even though we are rendering
within the confines of the <Window>
*/}
{menuOpen && (
<ContextMenu
menuX={menuX}
menuY={menuY}
artX={artX}
artY={artY}
portalEl={portalActive ? this.portalEl : null}
addShape={this.handleAddObject}
closeMenu={this.handleCloseMenu}
/>
)}
{/*
This button and state only exists to toggle the
portal on/off for illustrative purposes.
*/}
<button
className="portal-toggle"
onClick={this.handleTogglePortal}
>
{portalActive ? 'Disable Portal' : 'Activate Portal'}
</button>
</React.Fragment>
);
}
}
const App = () => (
<main>
<Window title="Artboard Window">
<Artboard />
</Window>
</main>
);
const run = () => {
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
};
run();
View Compiled
This Pen doesn't use any external CSS resources.