Skip to content

Commit f32faa6

Browse files
authored
Merge pull request #44 from aquaductape/scaling-feature
Scaling feature
2 parents 5b61b37 + 6ac2733 commit f32faa6

File tree

7 files changed

+361
-3
lines changed

7 files changed

+361
-3
lines changed

playground/app.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { isValidUrl } from './utils/isValidUrl';
2323

2424
import CompilerWorker from '../src/workers/compiler?worker';
2525
import FormatterWorker from '../src/workers/formatter?worker';
26+
import useZoom from '../src/hooks/useZoom';
2627

2728
(window as any).MonacoEnvironment = {
2829
getWorker(_moduleId: unknown, label: string) {
@@ -108,6 +109,26 @@ export const App = (): JSX.Element => {
108109
const actionBar = !noActionBar;
109110
const editableTabs = !noEditableTabs;
110111

112+
const { zoomState, updateZoomScale } = useZoom();
113+
114+
document.addEventListener('keydown', (e) => {
115+
const key = e.key;
116+
117+
if (!zoomState.overrideNative) return;
118+
119+
if (!((e.ctrlKey || e.metaKey) && (key === '=' || key === '-'))) {
120+
return;
121+
}
122+
123+
e.preventDefault();
124+
125+
if (key === '=') {
126+
updateZoomScale('increase');
127+
} else {
128+
updateZoomScale('decrease');
129+
}
130+
});
131+
111132
return (
112133
<div class="relative flex bg-blueGray-50 h-screen overflow-hidden text-blueGray-900 dark:text-blueGray-50 font-sans flex-col">
113134
<Show

playground/components/header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import logo from '../assets/logo.svg?url';
1414
import { processImport, Tab } from '../../src';
1515
import { exportToCsb } from '../utils/exportToCsb';
1616
import { exportToJSON } from '../utils/exportToJson';
17+
import { ZoomDropdown } from './zoomDropdown';
1718

1819
export const Header: Component<{
1920
dark: boolean;
@@ -71,7 +72,7 @@ export const Header: Component<{
7172

7273
return (
7374
<header
74-
class="p-2 flex text-sm justify-between items-center bg-brand-default text-white"
75+
class="p-2 flex text-sm justify-between items-center bg-brand-default text-white z-20"
7576
classList={{ 'md:col-span-3': !props.isHorizontal }}
7677
>
7778
<h1 class="flex items-center space-x-4 uppercase leading-0 tracking-widest">
@@ -172,6 +173,7 @@ export const Header: Component<{
172173
<Icon class="h-6" path={copy() ? link : share} />
173174
<span class="text-xs md:sr-only">{copy() ? 'Copied to clipboard' : 'Share'}</span>
174175
</button>
176+
<ZoomDropdown showMenu={showMenu()} />
175177
</div>
176178
</div>
177179
<button
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Icon } from '@amoutonbrady/solid-heroicons';
2+
import { zoomIn } from '@amoutonbrady/solid-heroicons/outline';
3+
import { Component, createSignal, Show, createEffect } from 'solid-js';
4+
import useFocusOut from '../../src/hooks/useFocusOut';
5+
import useZoom from '../../src/hooks/useZoom';
6+
7+
export const ZoomDropdown: Component<{ showMenu: boolean }> = (props) => {
8+
const [[toggle, setToggle], { onFOBlur, onFOClick, onFOFocus }] = useFocusOut();
9+
const { zoomState, updateZoomScale, updateZoomSettings } = useZoom();
10+
const popupDuration = 1250;
11+
let containerEl!: HTMLDivElement;
12+
let prevZoom = zoomState.zoom;
13+
let timeoutId: number | null = null;
14+
let btnEl!: HTMLButtonElement;
15+
let prevFocusedEl: HTMLElement | null;
16+
let stealFocus = true;
17+
18+
const onMouseMove = () => {
19+
stealFocus = true;
20+
window.clearTimeout(timeoutId!);
21+
};
22+
23+
const onKeyDownContainer = (e: KeyboardEvent) => {
24+
if (!toggle()) return;
25+
26+
if (e.key === 'Escape' && !stealFocus) {
27+
if (prevFocusedEl) {
28+
prevFocusedEl.focus();
29+
stealFocus = true;
30+
}
31+
window.clearTimeout(timeoutId!);
32+
}
33+
34+
if (!['Tab', 'Enter', 'Space'].includes(e.key)) return;
35+
stealFocus = false;
36+
prevFocusedEl = null;
37+
window.clearTimeout(timeoutId!);
38+
};
39+
40+
createEffect(() => {
41+
if (prevZoom === zoomState.zoom) return;
42+
prevZoom = zoomState.zoom;
43+
44+
if (stealFocus) {
45+
prevFocusedEl = document.activeElement as HTMLElement;
46+
btnEl.focus();
47+
stealFocus = false;
48+
}
49+
50+
setToggle(true);
51+
52+
window.clearTimeout(timeoutId!);
53+
54+
timeoutId = setTimeout(() => {
55+
setToggle(false);
56+
57+
stealFocus = true;
58+
if (prevFocusedEl) {
59+
prevFocusedEl.focus();
60+
}
61+
}, popupDuration);
62+
});
63+
64+
createEffect(() => {
65+
if (!toggle()) {
66+
if (containerEl) {
67+
containerEl.removeEventListener('mousemove', onMouseMove);
68+
}
69+
stealFocus = true;
70+
} else {
71+
if (containerEl) {
72+
containerEl.addEventListener('mousemove', onMouseMove, { once: true });
73+
}
74+
}
75+
});
76+
77+
return (
78+
<div
79+
class="relative"
80+
onFocusIn={onFOFocus}
81+
onFocusOut={onFOBlur}
82+
onKeyDown={onKeyDownContainer}
83+
onClick={() => {
84+
window.clearTimeout(timeoutId!);
85+
}}
86+
ref={containerEl}
87+
tabindex="-1"
88+
>
89+
<button
90+
type="button"
91+
class="text-black md:text-white flex flex-row space-x-2 items-center w-full md:px-3 px-2 py-2 focus:ring-1 rounded text-white opacity-80 hover:opacity-100"
92+
classList={{
93+
'bg-gray-900': toggle() && !props.showMenu,
94+
'bg-gray-300': toggle() && props.showMenu,
95+
'rounded-none active:bg-gray-300 hover:bg-gray-300 focus:outline-none focus:highlight-none active:highlight-none focus:ring-0 active:outline-none':
96+
props.showMenu,
97+
}}
98+
onClick={onFOClick}
99+
title="Scale editor to make text larger or smaller"
100+
ref={btnEl}
101+
>
102+
<Icon class="h-6" path={zoomIn} />
103+
<span class="text-xs md:sr-only">Scale Editor</span>
104+
</button>
105+
<Show when={toggle()}>
106+
<div
107+
class="absolute top-full left-1/2 bg-white text-brand-default border border-gray-900 rounded shadow p-6 -translate-x-1/2 z-10"
108+
classList={{
109+
'left-1/4': props.showMenu,
110+
}}
111+
>
112+
<div class="flex">
113+
<button
114+
class="bg-gray-500 text-white px-3 py-1 rounded-l text-sm uppercase tracking-wide hover:bg-gray-800"
115+
aria-label="decrease font size"
116+
onClick={() => updateZoomScale('decrease')}
117+
>
118+
-
119+
</button>
120+
<div class="text-black bg-gray-100 px-3 py-1 text-sm text-center w-20 uppercase tracking-wide ">
121+
{zoomState.zoom}%
122+
</div>
123+
<button
124+
class="bg-gray-500 text-white px-3 py-1 rounded-r text-sm uppercase tracking-wide mr-4 hover:bg-gray-800"
125+
aria-label="increase font size"
126+
onClick={() => updateZoomScale('increase')}
127+
>
128+
+
129+
</button>
130+
<button
131+
class="bg-gray-500 text-white px-3 py-1 rounded text-sm uppercase tracking-wide hover:bg-gray-800"
132+
aria-label="reset font size"
133+
onClick={() => updateZoomScale('reset')}
134+
>
135+
Reset
136+
</button>
137+
</div>
138+
<div className="mt-10">
139+
<label class="block my-3 cursor-pointer">
140+
<input
141+
type="checkbox"
142+
class="mr-4 cursor-pointer"
143+
checked={zoomState.overrideNative}
144+
onChange={(e) => updateZoomSettings('overrideNative', e.currentTarget.checked)}
145+
/>
146+
Override browser zoom keyboard shortcut
147+
</label>
148+
<label class="block my-3 cursor-pointer">
149+
<input
150+
type="checkbox"
151+
class="mr-4 cursor-pointer"
152+
checked={zoomState.scaleIframe}
153+
onChange={(e) => updateZoomSettings('scaleIframe', e.currentTarget.checked)}
154+
/>
155+
Scale iframe <strong>Result</strong>
156+
</label>
157+
</div>
158+
</div>
159+
</Show>
160+
</div>
161+
);
162+
};

src/components/editor/index.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ import {
1717
clipboardCheck,
1818
} from '@amoutonbrady/solid-heroicons/outline';
1919
import { liftOff } from './setupSolid';
20+
import useZoom from '../../hooks/useZoom';
2021

2122
const Editor: Component<Props> = (props) => {
2223
const finalProps = mergeProps({ showActionBar: true }, props);
2324

2425
let parent!: HTMLDivElement;
2526
let editor: mEditor.IStandaloneCodeEditor;
2627

28+
const { zoomState } = useZoom();
29+
2730
const model = () => mEditor.getModel(Uri.parse(finalProps.url));
2831

2932
const [format, setFormat] = createSignal(false);
@@ -82,7 +85,7 @@ const Editor: Component<Props> = (props) => {
8285
model: null,
8386
automaticLayout: true,
8487
readOnly: finalProps.disabled,
85-
fontSize: 15,
88+
fontSize: zoomState.fontSize,
8689
lineDecorationsWidth: 5,
8790
lineNumbersMinChars: 3,
8891
padding: { top: 15 },
@@ -112,6 +115,7 @@ const Editor: Component<Props> = (props) => {
112115
setupEditor();
113116
}
114117
});
118+
115119
onCleanup(() => editor?.dispose());
116120

117121
const updateModel = () => {
@@ -124,6 +128,13 @@ const Editor: Component<Props> = (props) => {
124128
createEffect(() => {
125129
mEditor.setTheme(finalProps.isDark ? 'vs-dark-plus' : 'vs-light-plus');
126130
});
131+
createEffect(() => {
132+
const fontSize = zoomState.fontSize;
133+
134+
if (!editor) return;
135+
136+
editor.updateOptions({ fontSize });
137+
});
127138

128139
const showActionBar = () => {
129140
const hasActions = finalProps.canFormat || props.canCopy;

src/components/preview.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Component, createEffect, createSignal, splitProps, JSX, For, Show } from 'solid-js';
22
import { Icon } from '@amoutonbrady/solid-heroicons';
33
import { chevronDown, chevronRight } from '@amoutonbrady/solid-heroicons/solid';
4+
import useZoom from '../hooks/useZoom';
45

56
export const Preview: Component<Props> = (props) => {
7+
const { zoomState } = useZoom();
68
const [internal, external] = splitProps(props, ['code', 'class', 'reloadSignal']);
79

810
let iframe!: HTMLIFrameElement;
@@ -186,11 +188,19 @@ export const Preview: Component<Props> = (props) => {
186188
</html>
187189
`;
188190

191+
const styleScale = () => {
192+
if (zoomState.scale === 100 || !zoomState.scaleIframe) return '';
193+
194+
return `width: ${zoomState.scale}%; height: ${zoomState.scale}%; transform: scale(${
195+
zoomState.zoom / 100
196+
}); transform-origin: 0 0;`;
197+
};
198+
189199
return (
190200
<div
191201
class={`grid relative ${internal.class}`}
192202
{...external}
193-
style="grid-template-rows: 1fr auto"
203+
style={`grid-template-rows: 1fr auto; ${styleScale()}`}
194204
>
195205
<iframe
196206
title="Solid REPL"

src/hooks/useFocusOut.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createEffect, createSignal, onCleanup } from 'solid-js';
2+
3+
type TCb = (t: boolean) => void;
4+
export type FocusOutToggle = TCb;
5+
6+
const useFocusOut = (
7+
props: {
8+
onToggle?: FocusOutToggle;
9+
debug?: boolean;
10+
} = {},
11+
) => {
12+
const onToggle = props.onToggle;
13+
const debug = props.debug || false;
14+
const [toggle, setToggle] = createSignal(false);
15+
let timeoutId: number | null = 0;
16+
let init = false;
17+
18+
const onKeyDown = (e: KeyboardEvent) => {
19+
if (e.key !== 'Escape') return;
20+
setToggle(false);
21+
};
22+
23+
createEffect(() => {
24+
const toggleVal = toggle();
25+
26+
if (!init) {
27+
init = true;
28+
return;
29+
}
30+
if (!toggleVal && debug) return;
31+
32+
if (toggleVal) {
33+
document.addEventListener('keydown', onKeyDown);
34+
} else {
35+
document.removeEventListener('keydown', onKeyDown);
36+
}
37+
onToggle && onToggle(toggleVal);
38+
});
39+
40+
const onFOClick = () => {
41+
clearTimeout(timeoutId!);
42+
timeoutId = null;
43+
44+
const toggleVal = toggle();
45+
setToggle(!toggleVal);
46+
};
47+
48+
const onFOBlur = () => {
49+
const newTimeout = window.setTimeout(() => {
50+
setToggle(false);
51+
});
52+
timeoutId = newTimeout;
53+
};
54+
55+
const onFOFocus = () => {
56+
clearTimeout(timeoutId!);
57+
timeoutId = null;
58+
};
59+
60+
onCleanup(() => {
61+
document.removeEventListener('keydown', onKeyDown);
62+
});
63+
64+
return [[toggle, setToggle], { onFOBlur, onFOFocus, onFOClick }] as [
65+
[() => boolean, (v: boolean) => void],
66+
{ onFOBlur: () => void; onFOFocus: () => void; onFOClick: () => void },
67+
];
68+
};
69+
70+
export default useFocusOut;

0 commit comments

Comments
 (0)