HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<div id="root"></div>
/******* Styled-components *******/
const theme = {
colors: {
primary: 'hsla(317, 45%, 52%, 1)',
secondary: 'hsla(340, 95%, 50%, 1)',
primary_transparent: 'hsla(317, 45%, 52%, 0.05)',
secondary_transparent: 'hsla(340, 95%, 50%, 0.05)',
white: 'hsla(0, 100%, 100%, 1)',
black: 'hsla(0, 0%, 0%, 1)',
shadow1: 'hsla(0, 0%, 0%, 0.15)',
shadow2: 'hsla(0, 0%, 0%, 0.3)',
shadow3: 'hsla(0, 0%, 0%, 0.75)',
grey: 'hsla(9, 9%, 73%, 1)',
light_grey: 'hsla(220, 10%, 90%, 1)',
dark_grey: 'hsla(0, 0%, 20%, 1)',
turquoise: 'hsla(179, 81%, 65%, 1)',
red: 'hsla(2, 48%, 57%, 1)',
purple: 'hsla(254, 15%, 60%, 1)',
yellow: 'hsla(39, 98%, 84%, 1)',
light_beige: 'hsla(354, 11%, 92%, 1)',
dark_beige: 'hsla(338, 6%, 65%, 1)',
light_beige_shadow: 'hsla(0, 10%, 71%, 0.9)',
grey_shadow: 'hsla(9, 9%, 53%, 1)',
red_shadow: 'hsla(2, 48%, 37%, 1)',
purple_shadow: 'hsla(254, 15%, 40%, 1)',
yellow_shadow: 'hsla(34, 59%, 68%, 1)',
},
}
const Theme = ({ children }) => (
<styled.ThemeProvider theme={theme}>{children}</styled.ThemeProvider>
)
const GlobalStyle = styled.createGlobalStyle`
* {
padding: 0;
margin: 0;
}
html {
font-size: 62.5%;
scrollbar-color: ${({ theme: { colors } }) =>
`${colors.primary} ${colors.secondary}`};
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
font-family: 'Roboto', sans-serif;
color: ${({ theme: { colors } }) => colors.dark_grey};
background: linear-gradient(35deg, ${({ theme: { colors } }) =>
colors.primary}, ${({ theme: { colors } }) => colors.secondary} 100%);
user-select: none;
}
`
// App styles
const AppContainer = styled.div`
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
`
const Calculator = styled.div`
background:
/* paper roll */ linear-gradient(
${({ theme: { colors } }) => colors.white} 0% 100%
)
15.1em 2em / 8.4em 13em,
radial-gradient(
100% 100% at 50% 50%,
${({ theme: { colors } }) => colors.grey} 5%,
${({ theme: { colors } }) => colors.white} 8%,
${({ theme: { colors } }) => colors.grey} 11%,
${({ theme: { colors } }) => colors.white} 14%,
${({ theme: { colors } }) => colors.grey} 17%,
${({ theme: { colors } }) => colors.white} 20%,
${({ theme: { colors } }) => colors.grey} 23%,
${({ theme: { colors } }) => colors.white} 26%,
${({ theme: { colors } }) => colors.grey} 29%,
${({ theme: { colors } }) => colors.white} 32%,
${({ theme: { colors } }) => colors.grey} 35%,
${({ theme: { colors } }) => colors.white} 38%,
${({ theme: { colors } }) => colors.grey} 41%,
${({ theme: { colors } }) => colors.white} 44%,
${({ theme: { colors } }) => colors.grey} 47%,
transparent 50%
)
23.5em 0.8em / 3em 10.4em,
radial-gradient(
200% 200% at 100% 100%,
${({ theme: { colors } }) => colors.white} 0%,
${({ theme: { colors } }) => colors.light_grey} 49%,
transparent 52%
)
15.1em 1em / 1.2em 1.2em,
linear-gradient(
hsla(0, 0%, 93%, 1) 0%,
${({ theme: { colors } }) => colors.white} 15%,
${({ theme: { colors } }) => colors.white} 80%,
${({ theme: { colors } }) => colors.light_grey} 100%
)
16.1em 1em / 9em 10em,
/* paper slit */
linear-gradient(
${({ theme: { colors } }) => colors.grey},
${({ theme: { colors } }) => colors.black}
)
13.3em 14.8em / 12em 0.4em,
/* screen corners */
radial-gradient(
200% 200% at 100% 100%,
${({ theme: { colors } }) => colors.black} 50%,
transparent 52%
)
4em 16.1em / 1em 1em,
radial-gradient(
200% 200% at 0% 100%,
${({ theme: { colors } }) => colors.black} 50%,
transparent 52%
)
55em 16.1em / 1em 1em,
radial-gradient(
200% 200% at 100% 0%,
${({ theme: { colors } }) => colors.black} 50%,
transparent 52%
)
4em 22em / 6em 6em,
radial-gradient(
200% 200% at 0% 0%,
${({ theme: { colors } }) => colors.black} 50%,
transparent 52%
)
50em 22em / 6em 6em,
/* screen body */
linear-gradient(${({ theme: { colors } }) => colors.black} 0% 100%) 4.9em
16.1em / 50.3em 1em,
linear-gradient(${({ theme: { colors } }) => colors.black} 0% 100%) 4em 17em /
52em 6em,
linear-gradient(${({ theme: { colors } }) => colors.black} 0% 100%) 9em 16.1em /
42em 11.9em,
/* upper body rounded corners */
radial-gradient(
200% 200% at 100% 0%,
${({ theme: { colors } }) => colors.light_beige} 0em,
${({ theme: { colors } }) => colors.light_beige} 2.9em,
${({ theme: { colors } }) => colors.dark_beige} 49%,
${({ theme: { colors } }) => colors.light_beige} 51%
)
2.6em 27.1em / 3.1em 3.1em,
radial-gradient(
200% 200% at 0% 0%,
${({ theme: { colors } }) => colors.light_beige} 0em,
${({ theme: { colors } }) => colors.light_beige} 2.9em,
${({ theme: { colors } }) => colors.dark_beige} 49%,
${({ theme: { colors } }) => colors.light_beige} 51%
)
54.2em 27.1em / 3.1em 3.1em,
/* calculator upper body */
linear-gradient(
${({ theme: { colors } }) => colors.dark_beige} 0em,
${({ theme: { colors } }) => colors.light_beige} 0.2em,
${({ theme: { colors } }) => colors.light_beige} 18em,
${({ theme: { colors } }) => colors.dark_beige} 18.1em
)
2.8em 12em / 54.3em 18.2em,
linear-gradient(
90deg,
${({ theme: { colors } }) => colors.dark_beige} 0em,
${({ theme: { colors } }) => colors.light_beige} 0.2em,
${({ theme: { colors } }) => colors.light_beige} 54.5em,
${({ theme: { colors } }) => colors.dark_beige} 54.6em
)
2.6em 12em / 54.7em 18em,
/* roll stand */
radial-gradient(
200% 200% at 100% 100%,
${({ theme: { colors } }) => colors.dark_beige} 0%,
${({ theme: { colors } }) => colors.white} 25%,
${({ theme: { colors } }) => colors.dark_beige} 50%,
transparent 51%
)
11em 5.5em / 1.1em 1.1em,
linear-gradient(
${({ theme: { colors } }) => colors.dark_beige} 0%,
${({ theme: { colors } }) => colors.white} 45%,
${({ theme: { colors } }) => colors.dark_beige} 100%
)
12em 5.5em / 6em 1em,
linear-gradient(
90deg,
${({ theme: { colors } }) => colors.dark_beige} 0%,
${({ theme: { colors } }) => colors.white} 45%,
${({ theme: { colors } }) => colors.dark_beige} 100%
)
11em 6.5em / 1em 8em,
/* calculator lower body */
linear-gradient(
${({ theme: { colors } }) => colors.light_beige} 0em,
${({ theme: { colors } }) => colors.light_beige} 21em
)
2.6em 30em / 54.7em 25em,
/* very slanted sides */
linear-gradient(
-82deg,
${({ theme: { colors } }) => colors.light_beige} 59%,
${({ theme: { colors } }) => colors.dark_beige} 62%,
transparent 66%
)
1.1em 18.1em / 3em 12em,
linear-gradient(
82deg,
${({ theme: { colors } }) => colors.light_beige} 59%,
${({ theme: { colors } }) => colors.dark_beige} 62%,
transparent 66%
)
55.9em 18.1em / 3em 12em,
/* slightly slanted sides */
linear-gradient(
88deg,
${({ theme: { colors } }) => colors.light_beige} 58%,
${({ theme: { colors } }) => colors.dark_beige} 65%,
transparent 67%
)
57.2em 30em / 3em 25em,
linear-gradient(
-88deg,
${({ theme: { colors } }) => colors.light_beige} 58%,
${({ theme: { colors } }) => colors.dark_beige} 65%,
transparent 67%
) -0.2em 30em / 3em 25em,
/* right corner bottom */
radial-gradient(
200% 100% at 0% 0%,
${({ theme: { colors } }) => colors.light_beige} 4em,
${({ theme: { colors } }) => colors.dark_beige} 4.2em,
${({ theme: { colors } }) => colors.dark_beige} 4.4em,
${({ theme: { colors } }) => colors.light_beige} 4.6em,
${({ theme: { colors } }) => colors.light_beige} 4.8em,
${({ theme: { colors } }) => colors.dark_beige} 5em,
${({ theme: { colors } }) => colors.dark_beige} 50%,
transparent 51%
)
54.7em 54.8em / 5.2em 8.7em,
/* left corner bottom */
radial-gradient(
200% 100% at 100% 0%,
${({ theme: { colors } }) => colors.light_beige} 4em,
${({ theme: { colors } }) => colors.dark_beige} 4.2em,
${({ theme: { colors } }) => colors.dark_beige} 4.4em,
${({ theme: { colors } }) => colors.light_beige} 4.6em,
${({ theme: { colors } }) => colors.light_beige} 4.8em,
${({ theme: { colors } }) => colors.dark_beige} 5em,
${({ theme: { colors } }) => colors.dark_beige} 50%,
transparent 51%
)
0.1em 54.8em / 5.2em 8.7em,
/* base bottom */
radial-gradient(
200% 100% at 50% 0%,
${({ theme: { colors } }) => colors.light_beige} 41em,
${({ theme: { colors } }) => colors.dark_beige} 43em,
${({ theme: { colors } }) => colors.dark_beige} 44em,
${({ theme: { colors } }) => colors.light_beige} 46em,
${({ theme: { colors } }) => colors.light_beige} 47em,
${({ theme: { colors } }) => colors.dark_beige} 49em,
${({ theme: { colors } }) => colors.dark_beige} 50em,
${({ theme: { colors } }) => colors.dark_beige} 49%,
transparent 51%
)
5em 54.9em / 50em 10em;
background-repeat: no-repeat;
font-size: 1.2rem;
width: 60em;
height: 60em;
position: relative;
@media screen and (max-width: 57em) {
font-size: 0.55rem;
}
`
const Screen = styled.span`
font-size: 1em;
display: flex;
justify-content: flex-end;
overflow: hidden;
white-space: nowrap;
color: ${({ theme: { colors } }) => colors.turquoise};
position: absolute;
left: 5em;
top: 19em;
width: 49em;
line-height: 1.1;
& > span {
font-size: 4em;
}
`
const ScreenExpression = styled.span`
display: flex;
justify-content: flex-end;
overflow: hidden;
white-space: nowrap;
color: ${({ theme: { colors } }) => colors.turquoise};
position: absolute;
left: 7em;
top: 24em;
width: 45em;
& > span {
font-size: 2em;
}
`
const Equal = styled.span`
margin-left: 0.5em;
`
const Brand = styled.div`
position: absolute;
font-size: 1em;
font-weight: 600;
top: 28.4em;
left: 6em;
& > span {
font-size: 1.5em;
}
`
const ProductNumber = styled.div`
position: absolute;
font-size: 1em;
top: 28.7em;
left: 11.5em;
`
// MiniHistoryList styles
const MiniHistoryListContainer = styled.div`
position: absolute;
display: flex;
flex-direction: column-reverse;
align-content: flex-end;
padding: 0.5em;
width: 8.5em;
height: 13em;
top: 2em;
left: 15em;
overflow: hidden;
`
const List = styled.ul`
list-style-type: none;
padding: 0.5em;
`
// MiniHistoryItem styles
const ItemMiniHistory = styled.li`
position: relative;
display: flex;
align-items: center;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-bottom: 0.5rem;
& > span {
font-size: 0.8em;
}
`
// HistoryToggle styles
const Toggle = styled.div`
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: 12em;
width: 8.5em;
top: 3em;
left: 15em;
&:hover {
box-shadow: 0.3em 0.3em 0.3em 0 ${({ theme: { colors } }) => colors.shadow2};
cursor: pointer;
.icon {
display: inline;
}
}
`
const HistoryToggleHistoryIcon = styled.div`
display: none;
font-size: 3em;
color: ${({ theme: { colors } }) => colors.secondary};
`
// HistoryList styles
const HistoryListContainer = styled.div`
background-color: ${({ theme: { colors } }) => colors.white};
width: 50em;
min-height: 16em;
max-height: 50em;
box-sizing: border-box;
position: absolute;
top: 0;
left: 4em;
box-shadow: 0 -0.1em 1em -0.6em ${({ theme: { colors } }) => colors.black};
padding-bottom: 1em;
display: flex;
flex-direction: column;
z-index: 1;
&::before,
&::after {
content: '';
position: absolute;
width: 50em;
background-size: 1.5em 2em;
background-repeat: repeat-x;
}
&::before {
height: 1.1em;
top: -1.1em;
background-image: linear-gradient(
225deg,
transparent 50%,
${({ theme: { colors } }) => colors.light_grey} 52%,
${({ theme: { colors } }) => colors.white} 60%
),
linear-gradient(
135deg,
transparent 50%,
${({ theme: { colors } }) => colors.light_grey} 52%,
${({ theme: { colors } }) => colors.white} 60%
);
background-position: top left, top left;
}
&::after {
height: 0.8em;
bottom: -0.8em;
background-image: linear-gradient(
225deg,
${({ theme: { colors } }) => colors.white} 50%,
${({ theme: { colors } }) => colors.light_grey} 52%,
transparent 60%
),
linear-gradient(
135deg,
${({ theme: { colors } }) => colors.white} 50%,
${({ theme: { colors } }) => colors.light_grey} 52%,
transparent 60%
);
background-position: bottom left, bottom left;
}
`
const NoContent = styled.div`
font-size: 3em;
align-self: center;
`
const IconContainer = styled.div`
display: flex;
justify-content: space-between;
border-bottom: 0.1em solid ${({ theme: { colors } }) => colors.light_grey};
padding: 1em;
`
const HistoryIcon = styled.div`
font-size: 2em;
color: ${({ theme: { colors } }) => colors.primary};
&:hover {
cursor: pointer;
}
`
const TrashIcon = styled.div`
font-size: 2em;
color: ${({ theme: { colors } }) => colors.secondary};
&:hover {
cursor: pointer;
}
`
const HistoryListList = styled.ul`
position: relative;
display: flex;
flex-direction: column;
list-style-type: none;
overflow: auto;
padding: 1em;
max-height: 22.5em;
z-index: 1;
scrollbar-color: ${({ theme: { colors } }) => colors.primary} transparent;
`
// HistoryItem styles
const ItemHistoryItem = styled.li`
display: flex;
align-items: center;
position: relative;
width: 100%;
white-space: nowrap;
margin-bottom: 1em;
`
const HistoryItemEqual = styled.span`
font-size: 2em;
`
const Content = styled.div`
display: inline-block;
margin: 0 1em;
position: relative;
max-width: 50%;
border: 0.1em solid
${({ total, theme: { colors } }) =>
total ? colors.secondary : colors.primary};
border-radius: 0.5em;
color: ${({ error, theme: { colors } }) => (error ? colors.red : 'inherit')};
box-sizing: border-box;
padding: 0.6em 1.2em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:hover {
background-color: ${({ total, theme: { colors } }) =>
total ? colors.secondary_transparent : colors.primary_transparent};
cursor: pointer;
}
& > span {
font-size: 2em;
}
`
// DisplayItem styles
const Pow = styled.sup`
font-size: 0.7em;
`
// DisplayValue styles
const Paren = styled.span`
color: ${({ theme: { colors } }) => colors.light_grey};
`
// KeyList styles
const Grid = styled.div`
position: absolute;
z-index: 1;
top: 31em;
left: 5em;
display: grid;
grid-template-columns: 5em 1fr repeat(3, 5em) 1fr 5em 1fr 5em 5em;
grid-template-rows: 3.5em repeat(4, 1fr);
grid-gap: 0.8em 1.2em;
width: 51em;
height: 25em;
`
const Key = styled.button`
${(props) =>
(props.plus && `grid-row: span 3;`) ||
(props.equal && `grid-row: span 2;`) ||
(props.zero && `grid-column: span 2;`) ||
(props.dot && `grid-column: span 2;`)}
box-shadow: inset 0em -0.6em 0.1em 0.3em ${(props) =>
(props.digit && props.theme.colors.light_beige_shadow) ||
(props.red && props.theme.colors.red_shadow) ||
(props.purple && props.theme.colors.purple_shadow) ||
(props.yellow && props.theme.colors.yellow_shadow) ||
props.theme.colors.grey_shadow},
0em 0.1em 0.05em 0.1em ${({ theme: { colors } }) => colors.shadow3},
0.4em -0.2em 0.2em 0.2em ${({ theme: { colors } }) => colors.shadow1};
background-color: ${(props) =>
(props.digit && props.theme.colors.light_beige) ||
(props.red && props.theme.colors.red) ||
(props.purple && props.theme.colors.purple) ||
(props.yellow && props.theme.colors.yellow) ||
props.theme.colors.grey};
border: 1px solid hsla(0, 0%, 0%, 1);
border-top-color: ${(props) =>
(props.digit && props.theme.colors.light_beige_shadow) ||
(props.red && props.theme.colors.red_shadow) ||
(props.purple && props.theme.colors.purple_shadow) ||
(props.yellow && props.theme.colors.yellow_shadow) ||
props.theme.colors.grey_shadow};
white-space: nowrap;
cursor: pointer;
outline: none;
font-family: 'Roboto', sans-serif;
font-size: 1em;
border-radius: 1em;
user-select: none;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
::-moz-focus-inner {
border: 0;
}
&:active {
box-shadow: inset -0.3em 0.1em 0.2em 0.1em ${(props) =>
(props.digit && props.theme.colors.light_beige_shadow) ||
(props.red && props.theme.colors.red_shadow) ||
(props.purple && props.theme.colors.purple_shadow) ||
props.theme.colors.grey_shadow},0em 0.1em 0.05em 0.1em ${({
theme: { colors },
}) => colors.shadow3};
border-top-color: ${({ theme: { colors } }) => colors.dark_grey};
padding-top: 0.3em;
}
`
const KeyFont = styled.span`
font-size: ${(props) =>
(props.digit && `2.5em`) ||
(props.red && `3em`) ||
(props.purple && `2em`) ||
(props.yellow && `1.5em`) ||
`2.5em`};
`
const KeyListPow = styled.sup`
font-size: 0.7em;
`
const EmptyColumn1 = styled.div`
grid-column: 2;
grid-row: 2 / span 3;
`
const EmptyColumn2 = styled.div`
grid-column: 6;
grid-row: 2 / span 3;
`
const EmptyColumn3 = styled.div`
grid-column: 8;
grid-row: 2 / span 4;
`
const EmptyColumn4 = styled.div`
grid-column: 3 / span 6;
`
// AngleSwitch styles
const AngleSwitchContainer = styled.div`
font-size: 1em;
display: flex;
align-items: center;
justify-content: space-around;
background: ${({ theme: { colors } }) => colors.white};
border: 0.1em solid ${({ theme: { colors } }) => colors.dark_beige};
box-shadow: inset 0.1em 0.1em 0.2em
${({ theme: { colors } }) => colors.shadow2};
grid-column: 1 / span 2;
position: relative;
width: 7em;
height: 3em;
border-radius: 0.5em;
&:hover {
cursor: pointer;
}
`
const ToggleButton = styled.div`
position: absolute;
top: -0.1em;
right: -0.1em;
bottom: -0.1em;
left: 50%;
border: 0.1em solid ${({ theme: { colors } }) => colors.dark_beige};
background-image: linear-gradient(
180deg,
${({ theme: { colors } }) => colors.light_beige} 0% 25%,
${({ theme: { colors } }) => colors.dark_beige} 25% 50%,
${({ theme: { colors } }) => colors.light_beige} 50% 75%,
${({ theme: { colors } }) => colors.dark_beige} 75%
);
border-radius: 0.5em;
transition: transform 200ms ease-in-out;
${({ angle }) => angle === 'Deg' && 'transform: translate(-100%, 0);'}
`
const Angle = styled.span`
font-size: 1.6em;
`
/******* Javascript utilities *******/
// Action types
const SET_ANGLE = 'SET_ANGLE'
const ADD = 'ADD'
const CLEAR = 'CLEAR'
const EQUAL = 'EQUAL'
const UNDO = 'UNDO'
const HISTORY = 'HISTORY'
const DELETE_HISTORY = 'DELETE_HISTORY'
const isNumeric = (n) => !Number.isNaN(Number(n)) // Returns true if it's a number
const last = (a) => a[a.length - 1] // Peeks the last item in the array
const last2 = (a) => a[a.length - 2] // Peeks the second to last item in the array
const toRadians = (angle) => angle * (Math.PI / 180) // Converts from radians to degrees for trigonometric functions
// Returns the number of open parentheses in the array
const openParens = (arr) => {
return arr.reduce((acc, cur) => {
if (acc < 0) return acc
else if (cur === '(') return ++acc
else if (cur === ')') return --acc
return acc
}, 0)
}
// Fills the array with closing parentheses
const fillClosingParens = (arr) => new Array(openParens(arr)).fill(')')
// Trims the total
const trimTotal = (total) => {
const totalString = total.toString()
const totalCut = total.toPrecision(12)
// Number is equal or smaller than 12 after removing unary minus and dot
if (totalString.replace(/^-/, '').replace(/\./, '').length <= 12)
return totalString
// Exponent symbol e found
else if (totalCut.indexOf('e') !== -1) {
// Includes the exponent and trailing characters
const exponentChars = totalCut.match(/e.*$/)[0].length
// Truncate the number and remove zeros before the exponent
return total
.toPrecision(12 - exponentChars - (totalCut[0] === '0' ? 1 : 0))
.replace(/\.?0*e/, 'e')
}
// No exponent symbol e found
else {
// Null or array of length 1 or 2 if the unary minus or dot is found
const arrayChars = totalCut.match(/(^-|\.)/g)
// Reduce the string to 12 digits plus unary minus and dot
const reducedTotal = totalCut.substr(
0,
12 + (arrayChars ? arrayChars.length : 0)
)
// If decimal number remove trailing zeros
return reducedTotal.indexOf('.') !== -1
? reducedTotal.replace(/\.?0*$/, '')
: reducedTotal
}
}
// Stacks the superscript exponents
const arraySuperscript = (expression) => {
let parent = 0
let stackParens = []
const operatorsSuperscript = ['+', '-', '×', '÷', '√', 'sin', 'cos', 'tan']
const functionsSuperscript = ['√', 'sin', 'cos', 'tan']
return expression
.reduce((acc, value, idx) => {
if (value === '▢') {
parent = idx
return !expression[idx + 1]
? [...acc, { id: ++idx, parent, value, show: true }]
: [...acc, { id: ++idx, parent, value }]
}
if (value === '(' || functionsSuperscript.includes(value)) {
if (acc.length > 0 && acc[idx - 1].value !== '▢')
parent = last(stackParens) ? last(stackParens).parent : 0
if (
(acc.length > 0 && !functionsSuperscript.includes(acc[idx - 1].value)) ||
acc.length === 0
) {
stackParens = [
...stackParens,
{
id: `filler${stackParens.length + 1}`,
parent,
value: ')',
stackParens: true,
},
]
}
}
if (value === ')') {
const closingParens = [
...acc,
{ id: ++idx, parent: last(stackParens).parent, value },
]
stackParens.pop()
return closingParens
}
if (operatorsSuperscript.includes(value))
parent = last(stackParens) ? last(stackParens).parent : 0
return [...acc, { id: ++idx, parent, value }]
}, [])
.concat(stackParens)
}
// Nests recursively the children elements with the right parent when stacking exponents
const nestChildren = (expression, parent = 0) => {
return expression.reduce((acc, cur) => {
if (cur.parent === parent) {
const children = nestChildren(expression, cur.id)
if (children.length) cur.children = children
return [...acc, cur]
}
return acc
}, [])
}
// Converts the infix expression to postfix or Reversed Polish Notation using the Shunting Yard Algorithm
const postfix = (expression) => {
const ops = {
'+': 2,
'-': 2,
'×': 3,
'÷': 3,
unary: 4,
'▢': 5,
'√': 6,
'%': 6,
sin: 6,
cos: 6,
tan: 6,
}
const functions = ['%', '√', 'sin', 'cos', 'tan']
const operators = ['+', '-', '×', '÷', '▢']
const stack = []
const postfix = []
const infix = expression.reduce((acc, cur, idx) => {
// Handles implicit multiplications for example 6(6) or 5 sin(30) 8
if (
(cur === '(' ||
cur === 'sin' ||
cur === 'cos' ||
cur === 'tan' ||
cur === '√') &&
(isNumeric(expression[idx - 1]) ||
expression[idx - 1] === ')' ||
expression[idx - 1] === '%')
)
acc = [...acc, '×', cur]
else acc = [...acc, cur]
return acc
}, [])
// Conversion to postfix starts here
for (let [idx, token] of infix.entries()) {
if (Number(token) || Number(token) === 0) {
postfix.push(token)
continue
}
if (functions.includes(token)) {
stack.push(token)
continue
}
if (token === '-') {
if (
idx === 0 ||
infix[idx - 1] === '(' ||
operators.includes(infix[idx - 1])
)
token = 'unary'
}
if (token in ops) {
while (
ops[last(stack)] > ops[token] ||
(ops[last(stack)] >= ops[token] && token !== '▢')
)
postfix.push(stack.pop())
stack.push(token)
continue
}
if (token === '(') {
stack.push(token)
continue
}
if (token === ')') {
while (last(stack) !== '(') {
if (stack.length === 0) break
postfix.push(stack.pop())
}
stack.pop()
continue
}
}
while (stack.length) {
let op = stack.pop()
if (op === ')') break
postfix.push(op)
}
return postfix
}
// Parses and evaluates the postfix expression and returns the total
const evaluate = (postfix, angle) => {
return postfix
.reduce((stack, token, i) => {
if (isNumeric(token)) stack.push(token)
else if (
token === '√' ||
token === 'sin' ||
token === 'cos' ||
token === 'tan' ||
token === 'unary'
) {
const operand = Number(stack.pop())
switch (token) {
case '√':
stack.push(Math.sqrt(operand))
break
case 'sin':
stack.push(Math.sin(angle === 'Rad' ? operand : toRadians(operand)))
break
case 'cos':
stack.push(Math.cos(angle === 'Rad' ? operand : toRadians(operand)))
break
case 'tan':
stack.push(Math.tan(angle === 'Rad' ? operand : toRadians(operand)))
break
case 'unary':
stack.push(-operand)
break
default:
throw new Error('Error found in unary functions')
}
} else {
const right = Number(stack.pop())
const left = Number(stack.pop())
switch (token) {
case '-':
stack.push(left - right)
break
case '+':
stack.push(left + right)
break
case '×':
stack.push(left * right)
break
case '÷':
stack.push(left / right)
break
case '▢':
stack.push(Math.pow(left, right))
break
case '%':
if (!left) {
stack.push(right / 100)
} else if (postfix[i + 1] === '-' || postfix[i + 1] === '+') {
stack.push(left)
stack.push((left * right) / 100)
} else {
stack.push(left)
stack.push(right / 100)
}
break
default:
throw new Error('Error found in binary expressions')
}
}
return stack
}, [])
.pop()
}
const calculatorReducer = (state, action) => {
const se = state.expression
const ap = action.payload
const keyPress = (arr) => {
switch (arr) {
case ADD:
if (se[0] === 'Error') {
if (/^[÷×+%▢]$/.test(ap)) {
return ['0', ap]
} else {
return [ap]
}
} else if (!isNumeric(ap)) {
switch (ap) {
case '+':
case '×':
case '÷':
case '▢':
case '%':
if (last(se) === '-' && (last2(se) === '×' || last2(se) === '÷')) {
return se
} else if (se.length === 1 && last(se) === '-') {
return se
} else if (/^[+\-×÷▢]$/.test(last(se)) && last(se).length === 1) {
return [...se.slice(0, -1), ap]
} else if (last(se) === '(') {
return se
} else {
return [...se, ap]
}
case '.':
return /^(\+|-)?\d+$/.test(last(se))
? [...se.slice(0, -1), last(se).concat(ap)]
: se
case '-':
if (last(se) === '+' || (se.length === 1 && last(se) === '0')) {
return [...se.slice(0, -1), ap]
} else if (last(se) === '-') {
return se
} else {
return [...se, ap]
}
case '√':
case 'sin':
case 'cos':
case 'tan':
return se[0] === '0' && se.length === 1 ? [ap, '('] : [...se, ap, '(']
case '(':
return se[0] === '0' && se.length === 1 ? [ap] : [...se, ap]
case ')':
return openParens(se) > 0 && last(se) !== '(' ? [...se, ap] : se
default:
return se
}
} else {
if (ap === '00') {
if (state.totalShowing) {
return ['0']
} else if (
!isNumeric(last(se)) ||
(se[0] === '0' && se.length === 1) ||
(!isNumeric(last2(se)) && last(se) === '0')
) {
return se
} else {
return [...se.slice(0, -1), last(se).concat(ap)]
}
} else if (state.totalShowing) {
return [ap]
} else if (isNumeric(last(se)) && last(se) !== '0') {
return [...se.slice(0, -1), last(se).concat(ap)]
} else if (last(se) === ')' || last(se) === '%') {
return [...se, '×', ap]
} else if (last(se) === '0' && !isNumeric(last2(se))) {
return [...se.slice(0, -1), ap]
} else {
return [...se, ap]
}
}
case UNDO:
if (
(last(se) === '(' && last2(se) === 'sin') ||
last2(se) === 'cos' ||
last2(se) === 'tan' ||
last2(se) === '√'
) {
return se.length === 2 ? ['0'] : se.slice(0, -2)
} else {
return se.length === 1 ? ['0'] : se.slice(0, -1)
}
case CLEAR:
return ['0']
default:
return state
}
}
if (action.type === EQUAL) {
if (state.totalShowing) return state
if (/^[+\-×÷▢(]$/.test(last(se))) return state
const expressionWithParentheses = se.concat(fillClosingParens(se))
const total = Number(
evaluate(postfix(expressionWithParentheses), state.angle)
)
const trimmedTotal = Number.isNaN(total) ? 'Error' : trimTotal(total)
return {
...state,
expression: [trimmedTotal],
history: [
...state.history,
{
id: uuid(),
expression: expressionWithParentheses,
total: trimmedTotal,
},
],
totalShowing: true,
}
} else if (action.type === HISTORY) {
return {
...state,
expression: ap,
totalShowing: false,
}
} else if (action.type === DELETE_HISTORY) {
return {
...state,
history: [],
}
} else if (action.type === SET_ANGLE) {
return {
...state,
angle: state.angle === 'Rad' ? 'Deg' : 'Rad',
}
} else {
return {
...state,
expression: keyPress(action.type),
totalShowing: false,
}
}
}
const useOnClickOutside = (ref, handler) => {
React.useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
/******* React components *******/
const AngleSwitch = ({ angle, dispatch }) => (
<AngleSwitchContainer onClick={() => dispatch({ type: SET_ANGLE })}>
<ToggleButton angle={angle} />
<Angle>Rad</Angle>
<Angle>Deg</Angle>
</AngleSwitchContainer>
)
const KeysList = ({ dispatch, angle }) => {
const keyPress = (payload) => () => {
dispatch({ type: ADD, payload })
}
return (
<Grid>
<AngleSwitch angle={angle} dispatch={dispatch} />
<EmptyColumn4 />
<Key yellow onClick={keyPress('(')}>
<KeyFont yellow>(</KeyFont>
</Key>
<Key yellow onClick={keyPress(')')}>
<KeyFont yellow>)</KeyFont>
</Key>
<Key onClick={keyPress('÷')}>
<KeyFont>÷</KeyFont>
</Key>
<EmptyColumn1 />
<Key digit onClick={keyPress('7')}>
<KeyFont digit>7</KeyFont>
</Key>
<Key digit onClick={keyPress('8')}>
<KeyFont digit>8</KeyFont>
</Key>
<Key digit onClick={keyPress('9')}>
<KeyFont digit>9</KeyFont>
</Key>
<EmptyColumn2 />
<Key red onClick={keyPress('-')}>
<KeyFont red>−</KeyFont>
</Key>
<EmptyColumn3 />
<Key onClick={keyPress('%')}>
<KeyFont>%</KeyFont>
</Key>
<Key purple onClick={keyPress('▢')}>
<KeyFont purple>
x<KeyListPow>y</KeyListPow>
</KeyFont>
</Key>
<Key onClick={keyPress('×')}>
<KeyFont>×</KeyFont>
</Key>
<Key digit onClick={keyPress('4')}>
<KeyFont digit>4</KeyFont>
</Key>
<Key digit onClick={keyPress('5')}>
<KeyFont digit>5</KeyFont>
</Key>
<Key digit onClick={keyPress('6')}>
<KeyFont digit>6</KeyFont>
</Key>
<Key plus onClick={keyPress('+')}>
<KeyFont plus>+</KeyFont>
</Key>
<Key onClick={keyPress('√')}>
<KeyFont>√</KeyFont>
</Key>
<Key purple onClick={keyPress('sin')}>
<KeyFont purple>sin</KeyFont>
</Key>
<Key onClick={() => dispatch({ type: CLEAR })}>
<KeyFont>AC</KeyFont>
</Key>
<Key digit onClick={keyPress('1')}>
<KeyFont digit>1</KeyFont>
</Key>
<Key digit onClick={keyPress('2')}>
<KeyFont digit>2</KeyFont>
</Key>
<Key digit onClick={keyPress('3')}>
<KeyFont digit>3</KeyFont>
</Key>
<Key equal onClick={() => dispatch({ type: EQUAL })}>
<KeyFont equal>=</KeyFont>
</Key>
<Key purple onClick={keyPress('cos')}>
<KeyFont purple>cos</KeyFont>
</Key>
<Key onClick={() => dispatch({ type: UNDO })}>
<KeyFont>CE</KeyFont>
</Key>
<Key zero digit onClick={keyPress('0')}>
<KeyFont digit>0</KeyFont>
</Key>
<Key digit onClick={keyPress('00')}>
<KeyFont digit>00</KeyFont>
</Key>
<Key dot digit onClick={keyPress('.')}>
<KeyFont dot digit>
•
</KeyFont>
</Key>
<Key purple onClick={keyPress('tan')}>
<KeyFont purple>tan</KeyFont>
</Key>
</Grid>
)
}
const DisplayValue = ({ expression, item, idx }) => {
const { value, stackParens, show } = item
if (/^[+\-×÷]$/.test(value)) {
return ` ${value} `
} else if (
expression[idx - 1] !== '(' &&
(value === 'sin' || value === 'cos' || value === 'tan' || value === '√')
) {
return ` ${value}`
} else if (stackParens) {
return <Paren>{value}</Paren>
} else if (value === '▢') {
return show ? value : null
} else {
return value
}
}
const DisplayItem = ({ expression, item, idx }) => {
const nestedItems = (item.children || []).map((item) => {
return (
<DisplayItem
key={item.id}
expression={expression}
item={item}
idx={idx}
type="child"
/>
)
})
return (
<>
<DisplayValue expression={expression} item={item} idx={idx} />
{nestedItems.length > 0 && <Pow>{nestedItems}</Pow>}
</>
)
}
const Display = ({ expression }) => (
<>
{nestChildren(arraySuperscript(expression)).map((item, idx) => (
<DisplayItem key={item.id} expression={expression} item={item} idx={idx} />
))}
</>
)
const HistoryItem = ({ expression, total, setHistoryOpen, dispatch }) => {
const handleHistoryClick = (payload) => () => {
dispatch({ type: HISTORY, payload })
setHistoryOpen(false)
}
return (
<ItemHistoryItem>
<Content onClick={handleHistoryClick(expression)}>
<span>
<Display expression={expression} />
</span>
</Content>
<HistoryItemEqual>{` = `}</HistoryItemEqual>
{total === 'Error' ? (
<Content error>
<span>{total}</span>
</Content>
) : (
<Content total onClick={handleHistoryClick([`${total}`])}>
<span>{total}</span>
</Content>
)}
</ItemHistoryItem>
)
}
const HistoryList = ({ calcHistory, setHistoryOpen, dispatch }) => {
const ref = React.useRef()
const ULref = React.useRef()
useOnClickOutside(ref, () => setHistoryOpen((prevOpen) => !prevOpen))
React.useEffect(() => {
ULref.current.scrollTop = ULref.current.scrollTopMax
}, [])
return (
<HistoryListContainer ref={ref}>
<IconContainer>
<HistoryIcon onClick={() => setHistoryOpen((prevOpen) => !prevOpen)}>
<i className="fas fa-history" />
</HistoryIcon>
<TrashIcon onClick={() => dispatch({ type: DELETE_HISTORY })}>
<i className="fas fa-trash" />
</TrashIcon>
</IconContainer>
{calcHistory.length ? null : <NoContent>Calculations history</NoContent>}
<HistoryListList ref={ULref}>
{calcHistory.length > 0 &&
calcHistory.map((item) => {
const { id, expression, total } = item
return (
<HistoryItem
key={id}
expression={expression}
total={total}
setHistoryOpen={setHistoryOpen}
dispatch={dispatch}
/>
)
})}
</HistoryListList>
</HistoryListContainer>
)
}
const HistoryToggle = ({ historyOpen, setHistoryOpen, children }) => (
<>
<Toggle onClick={() => setHistoryOpen((prevOpen) => !prevOpen)}>
<HistoryToggleHistoryIcon className="icon">
<i className="fas fa-history" />
</HistoryToggleHistoryIcon>
</Toggle>
{historyOpen && children}
</>
)
const MiniHistoryItem = ({ expression, total }) => (
<ItemMiniHistory>
<span>
<Display expression={expression} />
{` = ${total}`}
</span>
</ItemMiniHistory>
)
const MiniHistoryList = ({ calcHistory }) => (
<MiniHistoryListContainer>
<List>
{calcHistory.length > 0 &&
calcHistory.map((item) => {
const { id, expression, total } = item
return <MiniHistoryItem key={id} expression={expression} total={total} />
})}
</List>
</MiniHistoryListContainer>
)
const App = () => {
const initialState = {
expression: ['0'],
history: [],
totalShowing: false,
angle: 'Rad',
}
const [state, dispatch] = React.useReducer(calculatorReducer, initialState)
const [historyOpen, setHistoryOpen] = React.useState(false)
const sh = state.history
return (
<Theme>
<>
<GlobalStyle />
<AppContainer>
<Calculator>
<MiniHistoryList calcHistory={sh} />
<HistoryToggle historyOpen={historyOpen} setHistoryOpen={setHistoryOpen}>
<HistoryList
calcHistory={sh}
setHistoryOpen={setHistoryOpen}
dispatch={dispatch}
/>
</HistoryToggle>
<Screen>
<span>
<Display expression={state.expression} />
</span>
</Screen>
<ScreenExpression>
{sh.length && state.totalShowing ? (
<span>
<Display expression={last(sh).expression} />
</span>
) : null}
{sh.length > 0 && state.totalShowing && <Equal>=</Equal>}
</ScreenExpression>
<Brand>
<span>SHARP</span>
</Brand>
<ProductNumber>EL-1801V</ProductNumber>
<KeysList dispatch={dispatch} angle={state.angle} />
</Calculator>
</AppContainer>
</>
</Theme>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
Also see: Tab Triggers