Calculator ❤️
Last updated
Last updated
🔸 心得┊ 1. Table layout ┊ 2. Grid layout ┊ 3. Web Component ❤️
NewbeDev ⟩ How to make font-size relative to parent div? - use React.
MDN ⟩
Error - error.message
Google Fonts ⟩ Orbitron
<!-- 📁 index.html -->
<script type="module" src="calc-app/calc-app.js"></script>
由 module 透過 customElements.define( ) 讀進來的 class CalcApp,可以直接用嗎❓例如:const app = new CalcApp( );
答案是:可以❗️但 export/import 還是要先做❗️ 👉🏻 ES Modules
// ⭐️ 1. export class (📁 calc-app.js)
export default class CalcApp extends HTMLElement { ... }
// ⭐️ 2. import class (📁 main.js)
import CalcApp from './calc-app/calc-app.js';
如果我們有:
<template>
<div></div>
</template>
這時注意❗️ template.querySelector('div') === null ❗️ 💾 codepen
const {log} = console;
const template = $('template');
const div1 = $('div', template);
const div2 = $('div', template.content);
log(`template: ${template}`); // HTMLTemplateElement
log(`div1: ${div1}`); // ⭐️ null ❗️
log(`div2: ${div2}`); // HTMLDivElement
// ⭐️ $(): select first element
function $(selector, parent = document){
return parent.querySelector(selector);
}
HTMLTemplateElement has a content property, which is a read-only DocumentFragment containing the DOM subtree which the template represents. 📘MDN ⟩ <template> ⟩ Attributes
換句話說:template.content 才有 DOM subtree,template 本身並沒有❗️
練習時遇到的怪事:
<input onclick="clear()">
onclick="..." 裡面竟然不能寫 "clear( )"❗️就算寫了,也沒用❗️ 👉🏻 參看上方「🗣 討論」頁。
⭐️ 解釋: 自己寫的 clear() 是 global function,事實上放在 window.clear(),所以當 "click" event 發生時,透過 event bubbling 的機制,document.clear() 會先收到 "click" event,所以就把 window.clear() 也屏蔽掉了。
Document.clear() does nothing, but doesn't raise any error. 📘 Document.clear() - Deprecated❗️
// State
const State = {
good: 0,
bad: 1,
error: 2,
};
// Calculator
class Calculator {
// init
constructor(){
this._memory = '';
this._lastInput = undefined;
this._debugMode = true;
}
// enter key into memory
input(key){
if (key == "×") key = '*';
if (key == "+") key = '+';
if (key == "-") key = '-';
if (key == "÷") key = '/';
// if (this.debugMode) console.log(`key: ${key}`);
switch(key){
// clear
case 'C':
this._memory = '';
break;
// execute
case '=':
this._memory = this.isCurrentResultValid
? `${this._currentResult}`
: '💥 Error';
break;
// in the middle of entering
default:
this._memory += key;
if (this.state == State.error) this._memory = key;
}
this._lastInput = key;
this._calcCurrentResult();
if (this.debugMode) this.log();
}
get memory(){
return this._memory;
}
get debugMode(){
return this._debugMode;
}
set debugMode(bool){
this._debugMode = bool;
}
get isCurrentResultValid(){
return Number.isFinite(this._currentResult);
}
get currentResult(){
return this._currentResult;
}
log(){
console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
}
// calculate current result based on memory
_calcCurrentResult(){
// empty memory
if(this._memory == ''){
this._currentResult = 0;
return;
}
// memory not empty, try to calculate current result
try {
// if success, return the result
const result = eval(this._memory);
this._currentResult = result;
} catch(e) {
// else set current result `undefined`
if (this.debugMode) console.log(`${e.name}: ${e.message}`);
this._currentResult = undefined;
}
}
get state(){
if (this.isCurrentResultValid) return State.good;
if (this._lastInput == '=') return State.error;
return State.bad;
}
}
const { log } = console;
const display = $('#display');
const calcUI = $('.calculator');
const calculator = new Calculator();
// calculator.debugMode = false;
// event listener ---------------------
calcUI.addEventListener('click', e => {
// make sure target is <input>
let t = e.target;
if (t.nodeName !== 'INPUT') return;
// entering `key` into calculator
let key = t.value;
calculator.input(key);
display.value = calculator.memory;
// update UI state
let state = calculator.state;
display.classList.toggle('bad', state === State.bad);
display.classList.toggle('error', state === State.error);
});
// helpers ------------------------------
// ⭐️ $(): select first element
function $(selector, parent = document){
return parent.querySelector(selector);
}
/* Google Fonts: Orbitron */
@import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
/* css reset */
* {
box-sizing: border-box;
}
.calculator {
width: 400px;
height: 380px;
padding: 10px;
border-radius: 1em;
margin: auto;
background: #191b28;
box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
}
/* highlight cells */
/* tr {
background: hsl(60, 85%, 70%);
} */
.display-box,
.button {
font-family: 'Orbitron', sans-serif;
font-size: 2em;
width: 100%;
height: 100%;
border: 0.5px solid black;
border-radius: 5px;
}
.display-box {
background: #dcdbe1;
color: black;
text-align: right;
padding: 0 .5em;
}
.display-box.bad {
background: hsl(45, 80%, 80%);
}
.display-box.error {
background: hsl(0, 80%, 50%);
color: yellow;
}
.button {
background: hsl(270, 80%, 40%);
color: white;
}
.button:active {
background: hsl(270, 80%, 50%);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
}
<table class="calculator">
<tr>
<td colspan="3">
<input class="display-box" type="text" id="display" value="" disabled />
</td>
<td>
<input class="button" type="button" value="C" style="background-color: #fb0066;" />
</td>
</tr>
<tr>
<td>
<input class="button" type="button" value="1" />
</td>
<td>
<input class="button" type="button" value="2" />
</td>
<td>
<input class="button" type="button" value="3" />
</td>
<td>
<input class="button" type="button" value="+" />
</td>
</tr>
<tr>
<td>
<input class="button" type="button" value="4" />
</td>
<td>
<input class="button" type="button" value="5" />
</td>
<td>
<input class="button" type="button" value="6" />
</td>
<td>
<input class="button" type="button" value="-" />
</td>
</tr>
<tr>
<td>
<input class="button" type="button" value="7" />
</td>
<td>
<input class="button" type="button" value="8" />
</td>
<td>
<input class="button" type="button" value="9" />
</td>
<td>
<input class="button" type="button" value="×" />
</td>
</tr>
<tr>
<td>
<input class="button" type="button" value="." />
</td>
<td>
<input class="button" type="button" value="0" />
</td>
<td>
<input class="button" type="button" value="=" style="background-color: #fb0066;" />
</td>
<td>
<input class="button" type="button" value="÷" />
</td>
</tr>
</table>
replit ⟩ calculator v.2
codepen ⟩ calculator
// Calculator State
const State = {
good : 0,
bad : 1,
error: 2,
};
// Model: Calculator
class Calculator {
// init
constructor(){
this._memory = '';
this._lastInput = undefined;
this._debugMode = true;
}
// enter `key` into memory
input(key){
switch(key){
case "×": key = '*'; break;
case "+": key = '+'; break;
case "−": key = '-'; break;
case "÷": key = '/'; break;
}
switch(key){
// clear
case 'C':
this._memory = '';
break;
// execute
case '=':
this._memory = this.isCurrentResultValid
? `${this._currentResult}`
: '💥 Error';
break;
// in the middle of entering
default:
// append `key` to memory
this._memory += key;
// ⭐️ if in error state, clear memory and start over.
if (this.state == State.error) {
this._memory = key;
}
}
this._lastInput = key;
this._calcCurrentResult();
if (this.debugMode) this.log();
}
// this.memory
get memory(){
return this._memory;
}
// this.debugMode
get debugMode(){
return this._debugMode;
}
// this.debugMode = true | false
set debugMode(bool){
this._debugMode = bool;
}
// this.isCurrentResultValid
get isCurrentResultValid(){
return Number.isFinite(this._currentResult);
}
// this.currentResult
get currentResult(){
return this._currentResult;
}
// this.log()
log(){
console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
}
// calculate current result based on `memory`
_calcCurrentResult(){
// empty memory
if (this._memory == '') {
this._currentResult = 0;
return;
}
// memory not empty, try to calculate current result
try {
// if success, return the result
const result = eval(this._memory);
this._currentResult = result;
} catch(e) {
// else set current result `undefined`
if (this.debugMode) console.log(`${e.name}: ${e.message}`);
this._currentResult = undefined;
}
}
// this.state
get state(){
// result is OK
if (this.isCurrentResultValid) return State.good;
// hit '=' and result is Not OK
if (this._lastInput == '=') return State.error;
// still in the middle of entering something
return State.bad;
}
// this.inGoodState
get inGoodState() {
return this.state === State.good;
}
// this.inBadState
get inBadState() {
return this.state === State.bad;
}
// this.inErrorState
get inErrorState() {
return this.state === State.error;
}
}
const { log } = console;
const buttonLabels = [
'C', '1', '2', '3', '+',
'4', '5', '6', '−',
'7', '8', '9', '×',
'.', '0', '=', '÷',
];
// views
const display = $('.display-box');
const calcUI = $('.calculator');
// for each button label, create new button
buttonLabels.forEach(lbl => {
// append <button> to calculator
let btn = tag('button', {
textContent: lbl,
parentNode: calcUI
});
// highlight "command" buttons
if (lbl === 'C' || lbl === '=') {
btn.classList.add('command');
}
});
// model
const calculator = new Calculator();
// calculator.debugMode = false;
// event listener ---------------------
calcUI.addEventListener('click', e => {
// make sure target is <button>
let t = e.target;
if (t.nodeName !== 'BUTTON') return;
// get `key` from UI
let key = t.textContent;
// update model
calculator.input(key);
// update UI
display.value = calculator.memory;
display.classList.toggle('bad', calculator.inBadState);
display.classList.toggle('error', calculator.inErrorState);
});
// helpers ------------------------------
// ⭐️ $(): select first element
function $(selector, parent = document){
return parent.querySelector(selector);
}
// ⭐️ tag()
function tag(name, {
textContent,
attributes = {},
parentNode,
}={}){
// create element
let elem = document.createElement(name);
// set text content if necessary
if (textContent) {
elem.textContent = textContent;
}
// set attributes
for (const [key, value] of Object.entries(attributes)) {
elem.setAttribute(key, value);
}
// append to parent node if present
if(parentNode){
parentNode.appendChild(elem);
}
// return element
return elem;
}
/* Google Fonts: Orbitron */
@import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
/* css reset */
* {
box-sizing: border-box;
}
/* calculator */
.calculator {
/* ⭐️ configurable css vars */
--calculator-user-width: 80vw;
--calculator-user-height: 80vh;
--calculator-padding: 10px;
/* ⭐️ computed css vars */
--calculator-width: max(var(--calculator-user-width), 115px);
--calculator-height: max(var(--calculator-user-height), 120px);
--calculator-cell-min-side: min(
calc((var(--calculator-width) - 2 * var(--calculator-padding))/4),
calc((var(--calculator-height) - 2 * var(--calculator-padding))/5)
);
width: var(--calculator-width);
height: var(--calculator-height);
padding: var(--calculator-padding);
border-radius: 10px;
margin: auto;
background: #191b28;
box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
}
.display-box,
button {
font-family: 'Orbitron', sans-serif;
font-size: calc(var(--calculator-cell-min-side) * 0.4);
font-weight: bold;
/* font-stretch: ultra-expanded; */
width: 100%;
height: 100%;
border: 0.5px solid black;
border-radius: 5px;
}
.display-box {
background: #dcdbe1;
color: black;
text-align: right;
padding: 0 .5em;
}
.display-box.bad {
background: hsl(45, 80%, 80%);
}
.display-box.error {
background: hsl(0, 80%, 50%);
color: yellow;
}
button {
background: hsl(270, 80%, 40%);
color: white;
}
button.command {
background-color: #fb0066;
}
button:active {
background: hsl(270, 80%, 50%);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
}
/* Grid layout */
.calculator {
display: grid;
/* rows / cols */
grid-template: repeat(5, 1fr) / repeat(3, auto) 1fr;
}
.calculator .display-box {
/* r1 / c1 / r2 / c2 */
/* row / col / height / width */
grid-area: 1 / 1 / 1 / span 3;
}
<div class="calculator">
<!-- display-box -->
<input class="display-box" type="text" value="" disabled />
<!-- ⭐️ other buttons added by script -->
</div>
<div class="container centered-element">
<!-- web component (custom element) -->
<calc-app width="200px" height="240px"></calc-app>
<calc-app></calc-app>
</div>
<!-- scripts -->
<script type="module" src="calc-app/calc-app.js"></script>
<script type="module" src="main.js"></script>
import Calculator from './Calculator.js';
const { log } = console;
// ⭐️ 1. custom web component
export default class CalcApp extends HTMLElement {
// constructor
constructor(width, height, debugMode=true) {
// tell HTMLElement to initialize itself.
super();
// ⭐️ get `width`, `height`
// from elem attributes or function arguments
const w = this.getAttribute('width') || width || '120px';
const h = this.getAttribute('height') || height || '150px';
// ⭐️ set user's preferred size
this.style.setProperty('--calculator-user-width', w);
this.style.setProperty('--calculator-user-height', h);
// model
this.calculator = new Calculator();
this.calculator.debugMode = debugMode;
}
// connected to document
connectedCallback() {
// attach shadow root
let root = this.attachShadow({mode: 'open'});
// ⭐️ clone nodes from <template>
let template = CalcApp.template;
let clone = CalcApp.template.content.cloneNode(true);
root.append( clone );
// connect views
this.display = $('.display-box', root);
this.UI = $('.calculator', root);
// add event listener ---------------------
this.UI.addEventListener('click', e => {
// make sure target is <button>
let t = e.target;
if (t.nodeName !== 'BUTTON') return;
// get `key` from UI
let key = t.textContent;
// update model
this.calculator.input(key);
// update UI
this.display.value = this.calculator.memory;
this.display.classList.toggle('bad', this.calculator.inBadState);
this.display.classList.toggle('error', this.calculator.inErrorState);
});
}
// template HTML
static get template() {
if (!CalcApp._template) {
// create <template> element
const template = document.createElement('template');
template.innerHTML = `
${CalcApp.styles}
<div class="calculator">
<input class="display-box" type="text" value="" disabled />
</div>
`;
// append buttons to template
const buttonLabels = [
'C', '1', '2', '3', '+',
'4', '5', '6', '−',
'7', '8', '9', '×',
'.', '0', '=', '÷',
];
const calcUI = $('.calculator', template.content);
// for each button label
buttonLabels.forEach(lbl => {
// append <button> to calculator
let btn = tag('button', {
textContent: lbl,
parentNode: calcUI
});
// highlight "command" buttons
if (lbl === 'C' || lbl === '=') {
btn.classList.add('command');
}
});
// save template in a static private var
CalcApp._template = template;
}
return CalcApp._template;
}
// template styles
static get styles(){
return `<style>
/* Google Fonts: Orbitron */
@import url('https://fonts.googleapis.com/css2?family=Orbitron&display=swap');
/* css reset */
.calculator {
box-sizing: border-box;
}
.calculator * {
box-sizing: inherit;
}
/* calculator */
.calculator {
/* ⭐️ configurable css vars */
--calculator-padding: 10px;
/* ⭐️ computed css vars */
--calculator-width: max(var(--calculator-user-width), 115px);
--calculator-height: max(var(--calculator-user-height), 120px);
--calculator-cell-min-side: min(
calc((var(--calculator-width) - 2 * var(--calculator-padding))/4),
calc((var(--calculator-height) - 2 * var(--calculator-padding))/5)
);
width: var(--calculator-width);
height: var(--calculator-height);
padding: var(--calculator-padding);
border-radius: 10px;
margin: auto;
background: #191b28;
box-shadow: rgba(0, 0, 0, 0.19) 0 10px 20px, rgba(0, 0, 0, 0.23) 0 6px 6px;
}
.display-box,
button {
font-family: 'Orbitron', sans-serif;
font-size: calc(var(--calculator-cell-min-side) * 0.4);
font-weight: bold;
/* font-stretch: ultra-expanded; */
width: 100%;
height: 100%;
border: 0.5px solid black;
border-radius: 5px;
}
.display-box {
background: #dcdbe1;
color: black;
text-align: right;
padding: 0 .5em;
}
.display-box.bad {
background: hsl(45, 80%, 80%);
}
.display-box.error {
background: hsl(0, 80%, 50%);
color: yellow;
}
button {
background: hsl(270, 80%, 40%);
color: white;
}
button.command {
background-color: #fb0066;
}
button:active {
background: hsl(270, 80%, 50%);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.9) inset;
}
/* Grid layout */
.calculator {
display: grid;
/* rows / cols */
grid-template: repeat(5, 1fr) / repeat(3, auto) 1fr;
}
.calculator .display-box {
/* r1 / c1 / r2 / c2 */
/* row / col / height / width */
grid-area: 1 / 1 / 1 / span 3;
}
</style>`;
}
}
// ⭐️ 2. custom tag <calc-app>
// ⭐️⭐️ custom element names must contain a "hyphen".
customElements.define('calc-app', CalcApp);
// helpers ------------------------------
// ⭐️ $(): select first element
function $(selector, parent = document){
return parent.querySelector(selector);
}
// ⭐️ tag()
function tag(name, {
textContent,
attributes = {},
parentNode,
}={}){
// create element
let elem = document.createElement(name);
// set text content if necessary
if (textContent) {
elem.textContent = textContent;
}
// set attributes
for (const [key, value] of Object.entries(attributes)) {
elem.setAttribute(key, value);
}
// append to parent node if present
if(parentNode){
parentNode.appendChild(elem);
}
// return element
return elem;
}
// Calculator State
const State = {
good : 0,
bad : 1,
error: 2,
};
// Model: Calculator
export default class Calculator {
// init
constructor(){
this._memory = '';
this._lastInput = undefined;
this._debugMode = true;
}
// enter `key` into memory
input(key){
switch(key){
case "×": key = '*'; break;
case "+": key = '+'; break;
case "−": key = '-'; break;
case "÷": key = '/'; break;
}
switch(key){
// clear
case 'C':
this._memory = '';
break;
// execute
case '=':
this._memory = this.isCurrentResultValid
? `${this._currentResult}`
: '💥 Error';
break;
// in the middle of entering
default:
// append `key` to memory
this._memory += key;
// ⭐️ if in error state, clear memory and start over.
if (this.state == State.error) {
this._memory = key;
}
}
this._lastInput = key;
this._calcCurrentResult();
if (this.debugMode) this.log();
}
// this.memory
get memory(){
return this._memory;
}
// this.debugMode
get debugMode(){
return this._debugMode;
}
// this.debugMode = true | false
set debugMode(bool){
this._debugMode = bool;
}
// this.isCurrentResultValid
get isCurrentResultValid(){
return Number.isFinite(this._currentResult);
}
// this.currentResult
get currentResult(){
return this._currentResult;
}
// this.log()
log(){
console.log(`memory: "${this.memory}", state: ${this.state}, result: ${this.currentResult}`);
}
// calculate current result based on `memory`
_calcCurrentResult(){
// empty memory
if (this._memory == '') {
this._currentResult = 0;
return;
}
// memory not empty, try to calculate current result
try {
// if success, return the result
const result = eval(this._memory);
this._currentResult = result;
} catch(e) {
// else set current result `undefined`
if (this.debugMode) console.log(`${e.name}: ${e.message}`);
this._currentResult = undefined;
}
}
// this.state
get state(){
// result is OK
if (this.isCurrentResultValid) return State.good;
// hit '=' and result is Not OK
if (this._lastInput == '=') return State.error;
// still in the middle of entering something
return State.bad;
}
// this.inGoodState
get inGoodState() {
return this.state === State.good;
}
// this.inBadState
get inBadState() {
return this.state === State.bad;
}
// this.inErrorState
get inErrorState() {
return this.state === State.error;
}
}
// ⭐️ import CalcApp first
import CalcApp from './calc-app.js';
// ⭐️ new <calc-app>
const app = new CalcApp('160px', '200px');
// calculator apps container
const div = $('.container');
div.append(app);
// helpers ------------------------------
// ⭐️ $(): select first element
function $(selector, parent = document) {
return parent.querySelector(selector);
}
.container {
display: inline-flex;
gap: 10px;
}
.centered-element {
margin: 0;
/* border: 1px solid black; */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
replit ⟩ calculator (componet)