use-theme-editor
Version:
Zero configuration CSS variables based theme editor
158 lines (146 loc) • 4.73 kB
JavaScript
import React, { useContext, useEffect, useRef, useState } from 'react';
import { get } from '../state';
import { ThemeEditorContext } from './ThemeEditor';
const wrapperMargin = 28;
const scale = 0.05;
const inverseScale = 1 / scale;
// Getting this component to work in any reasonable way is hard in most cases, and impossible in many.
// Some fixed or sticky elements simply don't have a "right" position to be in when you stretch out the page.
// And there are many more CSS configurations that cause all kinds of trouble.
// The result is there's only few cases where you'll actually get a matching frame beyond the top position.
export function SmallFullHeightFrame(props) {
const { src } = props;
const { width, height } = get;
const [scrollPosition, setScrollPosition] = useState(0);
const [windowDragged, setWindowDragged] = useState(false);
const [dragStartPos, setDragStartPos] = useState(0);
const [scrollAtStartDrag, setScrollAtStartDrag] = useState(0);
const [ownPosition, setOwnPosition] = useState(null);
const [shouldSmoothScroll, setShouldSmoothScroll] = useState(false);
const [windowHeight, setWindowHeight] = useState(null);
const cursorRef = useRef();
useEffect(() => {
if (ownPosition !== null) {
frameRef.current?.contentWindow.postMessage(
{
type: 'force-scroll',
payload: { position: ownPosition, shouldSmoothScroll },
},
window.location.origin
);
}
}, [ownPosition]);
const { frameRef, scrollFrameRef } = useContext(ThemeEditorContext);
useEffect(() => {
setTimeout(() => {
const listener = ({ data: { type, payload } }) => {
if (type === 'frame-scrolled') {
setScrollPosition(payload.scrollPosition);
setOwnPosition(null);
}
if (type === 'window-height') {
setWindowHeight(payload);
}
};
window.addEventListener('message', listener);
frameRef.current?.contentWindow.postMessage(
{
type: 'emit-scroll',
},
window.location.origin
);
return () => {
window.removeEventListener('message', listener);
};
}, 900);
}, []);
const top =
Math.max(
0,
(windowDragged ? ownPosition || scrollPosition : scrollPosition) * scale
) - 2;
const applyDragDelta = (event) => {
if (windowDragged) {
setOwnPosition(
scrollAtStartDrag - (dragStartPos - event.clientY) * inverseScale
);
setShouldSmoothScroll(false);
}
};
const jumpFrame = (e) => {
const diff = e.clientY - top - 1.5 * cursorRef.current.offsetHeight;
setOwnPosition(scrollPosition + diff * inverseScale);
setShouldSmoothScroll(false);
};
return (
<div
onWheel={(e) => {
setOwnPosition(Math.max(0, scrollPosition + e.deltaY * 6));
setShouldSmoothScroll(true);
}}
style={{
display: windowHeight === null ? 'none' : 'block',
position: 'relative',
width: width * scale,
}}
>
<div
className="responsive-frame-container"
style={{
transform: `scale(${scale})`,
width: `${wrapperMargin + parseInt(width)}px`,
overflow: 'visible',
// padding: '0',
// boxSizing: 'border-box',
}}
>
<iframe
className="responsive-frame"
ref={scrollFrameRef}
{...{
src,
width: parseInt(width),
height: windowHeight,
}}
/>
</div>
<div
onClick={jumpFrame}
onMouseUp={() => setWindowDragged(false)}
onMouseMove={applyDragDelta}
style={{
zIndex: 1,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
}}
></div>
<span
ref={cursorRef}
onClick={jumpFrame}
onMouseDown={(event) => {
setWindowDragged(true);
setDragStartPos(event.clientY);
setScrollAtStartDrag(scrollPosition);
}}
onMouseMove={applyDragDelta}
onMouseUp={() => setWindowDragged(false)}
style={{
userSelect: 'none',
zIndex: 2,
top,
left: -2,
position: 'absolute',
display: 'inline-block',
border: '2px solid yellow',
width: width * scale,
height: height * scale,
transition: 'top .05s ease-out',
boxSizing: 'content-box',
}}
/>
</div>
);
}