見出し画像

ChatGPT-4oでiPad用の電卓アプリ作ってみたよ中編

前編で四則演算が出来るだけのシンプルな電卓アプリが完成しました。

中編では履歴機能を追加して完成を目指します。

制作環境:
Windows11 Home。
ChatGPTはChatGPT Plus。
ブラウザはGoogle ChromeとMicrosoft Edge。
テキストエディターはBrackets。


計算履歴機能を追加する

履歴機能

ボク:
機能の追加をお願いします。 計算の履歴を記録してリストとして表示したいです。 履歴は以下のような感じ。

100+200=300
300/4=75
75+25=100
1+2=3
50*2=100
100/5=20

電卓のデザインはレスポンシブではなくてよいです。
表示領域(ディスプレイ)が縦の場合は、上に履歴、下に電卓。
表示領域が横の場合は、左に履歴、右に電卓。

履歴のリストの各行の右側にコピーボタンと削除ボタンを付けたいです。コピーアイコンとゴミ箱アイコンだと表示領域が節約できるかも。

ChatGPT:
計算の履歴を記録して表示する機能を追加し、レスポンシブデザインを考慮して、縦表示と横表示でレイアウトを変更するようにします。また、履歴の各行にコピーアイコンとゴミ箱アイコンを追加します。

1. index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>電卓アプリ</title>
    <link rel="stylesheet" href="styles.css">
    <link rel="manifest" href="manifest.json">
</head>
<body>
    <div id="container">
        <div id="history">
            <ul id="history-list"></ul>
        </div>
        <div id="calculator">
            <div id="display">
                <div id="previous-input"></div>
                <div id="current-input">0</div>
            </div>
            <div id="copy-message">コピーしました</div>
            <div id="buttons">
                <button>AC</button>
                <button>C</button>
                <button>%</button>
                <button>+</button>
                <button>1</button>
                <button>2</button>
                <button>3</button>
                <button>-</button>
                <button>4</button>
                <button>5</button>
                <button>6</button>
                <button>*</button>
                <button>7</button>
                <button>8</button>
                <button>9</button>
                <button>/</button>
                <button>0</button>
                <button>00</button>
                <button>.</button>
                <button>=</button>
            </div>
        </div>
    </div>
    <script src="script.js"></script>
    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js')
                    .then(registration => {
                        console.log('ServiceWorker registration successful with scope: ', registration.scope);
                    }, err => {
                        console.log('ServiceWorker registration failed: ', err);
                    });
            });
        }
    </script>
</body>
</html>

2. styles.css

body {
    margin: 0;
    font-family: Arial, sans-serif;
}

#container {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

#history {
    flex: 1;
    overflow-y: auto;
    background-color: #f9f9f9;
    padding: 10px;
    border-bottom: 1px solid #ccc;
}

#history-list {
    list-style-type: none;
    padding: 0;
    margin: 0;
}

.history-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 5px 0;
    border-bottom: 1px solid #ddd;
}

.history-text {
    flex: 1;
}

.history-buttons {
    display: flex;
    gap: 5px;
}

.history-buttons button {
    background: none;
    border: none;
    cursor: pointer;
}

#calculator {
    flex: 2;
    display: flex;
    flex-direction: column;
    background-color: #fff;
}

#display {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 10px 20px;
    background-color: #222;
    color: #fff;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    overflow-x: auto; /* 長い数式をスクロール可能にする */
    cursor: pointer; /* クリック可能にする */
}

#previous-input {
    font-size: 1.2em; /* 小さいフォントサイズ */
    color: #888; /* 薄い色 */
    text-align: right;
    height: 1.5em; /* 高さを固定 */
    margin-bottom: 0.5em; /* 下の行との間にスペースを追加 */
}

#current-input {
    font-size: 2em; /* 大きいフォントサイズ */
    text-align: right;
    height: 1.5em; /* 高さを固定 */
}

#copy-message {
    color: #4CAF50;
    text-align: center;
    margin: 5px 0; /* 上下の余白を追加 */
    height: 1.5em; /* 高さを固定 */
    line-height: 1.5em; /* 行の高さを中央に揃える */
    opacity: 0; /* デフォルトでは透明 */
    transition: opacity 0.3s ease; /* 透明度のトランジションを追加 */
}

#buttons {
    flex: 3;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    padding: 10px;
    overflow-y: auto; /* 縦方向のはみ出しを防ぐ */
}

button {
    font-size: 1.5em;
    padding: 15px;
    background-color: #e0e0e0;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
    display: flex;
    justify-content: center;
    align-items: center;
}

button:hover {
    background-color: #d0d0d0;
}

button:active {
    background-color: #c0c0c0;
}

@media (min-width: 600px) {
    button {
        padding: 20px;
    }
}

@media (orientation: landscape) {
    #container {
        flex-direction: row;
    }

    #history {
        flex: 1;
        border-bottom: none;
        border-right: 1px solid #ccc;
    }

    #calculator {
        flex: 2;
    }
}

3. script.js

const previousInputDisplay = document.getElementById('previous-input');
const currentInputDisplay = document.getElementById('current-input');
const copyMessage = document.getElementById('copy-message');
const historyList = document.getElementById('history-list');
const buttons = Array.from(document.querySelectorAll('button'));
let currentInput = '0'; // 初期値を'0'に設定
let previousInput = '';
let operator = '';
let resultDisplayed = false;

// ボタンのクリックイベントを処理する関数
function handleButtonClick(value) {
    try {
        if (value === 'AC') {
            currentInput = '0';
            previousInput = '';
            operator = '';
            currentInputDisplay.innerText = '0';
            previousInputDisplay.innerText = '';
        } else if (value === 'C') {
            currentInput = '0';
            currentInputDisplay.innerText = '0';
        } else if (value === '%') {
            currentInput = (parseFloat(currentInput) / 100).toString();
            currentInputDisplay.innerText = currentInput;
        } else if (value === '00') {
            // currentInputが '0' または空文字の場合は何もしない
            if (currentInput !== '0' && currentInput !== '') {
                currentInput += '00';
                currentInputDisplay.innerText = currentInput;
            }
        } else if (value === '.') {
            // 小数点が既に含まれている場合は何もしない
            if (!currentInput.includes('.')) {
                if (currentInput === '0') {
                    currentInput = '0.';
                } else {
                    currentInput += '.';
                }
                currentInputDisplay.innerText = currentInput;
            }
        } else if (['+', '-', '*', '/'].includes(value)) {
            if (operator && previousInput && currentInput) {
                previousInput = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                currentInput = '';
            } else if (!previousInput) {
                previousInput = currentInput;
                currentInput = '';
            }
            operator = value;
            resultDisplayed = false;
            currentInputDisplay.innerText = previousInput + ' ' + operator;
            previousInputDisplay.innerText = '';
        } else if (value === '=') {
            if (operator && previousInput && currentInput) {
                currentInput = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInputDisplay.innerText} = ${currentInput}`);
                previousInputDisplay.innerText = '';
                currentInputDisplay.innerText = currentInput;
                previousInput = '';
                operator = '';
                resultDisplayed = true;
            }
        } else {
            if (resultDisplayed) {
                currentInput = value;
                resultDisplayed = false;
            } else {
                if (currentInput === '0') {
                    currentInput = value;
                } else {
                    currentInput += value;
                }
            }
            currentInputDisplay.innerText = currentInput;

            // オペレーターがある場合は上の行に移動
            if (operator) {
                previousInputDisplay.innerText = previousInput + ' ' + operator;
            }
        }
    } catch (error) {
        currentInputDisplay.innerText = 'Error';
        previousInputDisplay.innerText = '';
        currentInput = '0';
        previousInput = '';
        operator = '';
    }
}

// 計算履歴を追加する関数
function addHistory(entry) {
    const li = document.createElement('li');
    li.classList.add('history-item');

    const span = document.createElement('span');
    span.classList.add('history-text');
    span.textContent = entry;

    const buttonsDiv = document.createElement('div');
    buttonsDiv.classList.add('history-buttons');

    const copyButton = document.createElement('button');
    copyButton.innerHTML = '📋'; // コピーアイコン
    copyButton.addEventListener('click', () => {
        navigator.clipboard.writeText(entry).then(() => {
            copyMessage.style.opacity = '1'; // メッセージを表示
            setTimeout(() => {
                copyMessage.style.opacity = '0'; // メッセージを隠す
            }, 2000);
        }).catch(err => {
            console.error('Error copying to clipboard: ', err);
        });
    });

    const deleteButton = document.createElement('button');
    deleteButton.innerHTML = '🗑️'; // ゴミ箱アイコン
    deleteButton.addEventListener('click', () => {
        historyList.removeChild(li);
    });

    buttonsDiv.appendChild(copyButton);
    buttonsDiv.appendChild(deleteButton);

    li.appendChild(span);
    li.appendChild(buttonsDiv);

    historyList.appendChild(li);
}

// ボタンのクリックイベントリスナーを追加
buttons.forEach(button => {
    button.addEventListener('click', () => handleButtonClick(button.innerText));
});

// キーボードのキー入力を処理するイベントリスナーを追加
document.addEventListener('keydown', (event) => {
    const key = event.key;
    if (key >= '0' && key <= '9') {
        handleButtonClick(key);
    } else if (key === '.') {
        handleButtonClick('.');
    } else if (key === '+') {
        handleButtonClick('+');
    } else if (key === '-') {
        handleButtonClick('-');
    } else if (key === '*') {
        handleButtonClick('*');
    } else if (key === '/') {
        handleButtonClick('/');
    } else if (key === 'Enter') {
        handleButtonClick('=');
    } else if (key === 'Escape') {
        handleButtonClick('AC');
    } else if (key === 'Backspace') {
        handleButtonClick('C');
    } else if (key === '%') {
        handleButtonClick('%');
    }
});

// クリップボードに結果をコピー
currentInputDisplay.addEventListener('click', () => {
    let textToCopy = currentInput;

    // 演算子を除外してコピー
    if (['+', '-', '*', '/'].includes(operator)) {
        textToCopy = currentInputDisplay.innerText.replace(/[+\-*/]/g, '').trim();
    }

    // クリップボードにコピー
    navigator.clipboard.writeText(textToCopy).then(() => {
        copyMessage.style.opacity = '1'; // メッセージを表示
        setTimeout(() => {
            copyMessage.style.opacity = '0'; // メッセージを隠す
        }, 2000);
    }).catch(err => {
        console.error('Error copying to clipboard: ', err);
    });
});

変更点の説明

  1. 履歴表示領域の追加:

    • #history を追加し、計算履歴を表示する #history-list を内包しています。

    • 計算結果を表示するためにリスト項目(li)を生成し、それにコピーアイコンとゴミ箱アイコンのボタンを追加します。

  2. レスポンシブデザイン:

    • 縦表示では、履歴が上部に、電卓が下部に配置されます。

    • 横表示では、履歴が左側に、電卓が右側に配置されます。

  3. 履歴の追加:

    • 計算結果が出たときに addHistory 関数を呼び出し、履歴リストに計算履歴を追加します。

  4. コピーと削除の機能:

    • 各履歴項目にコピーアイコンとゴミ箱アイコンのボタンを追加し、クリックでコピーまたは削除ができるようにします。

ChatGPT:
この修正により、計算の履歴が表示され、各履歴項目をコピーしたり削除したりできるようになります。

チェーン計算に対応させる

ボク:
「=」が入力された時だけでなく、「=」の代わりに「+, -, *, /」が入力された時も履歴に追加してください。
その場合は、

100+200/4=75
の場合、
100+200=300
300/4=75
のように「=結果」を追加してください。

ChatGPT:
了解しました。「=」だけでなく「+, -, *, /」が入力された際にも履歴に追加するように修正します。この場合、演算子を入力する際に現在の計算結果を履歴に追加し、新しい計算を開始します。

修正後の script.js

const previousInputDisplay = document.getElementById('previous-input');
const currentInputDisplay = document.getElementById('current-input');
const copyMessage = document.getElementById('copy-message');
const historyList = document.getElementById('history-list');
const buttons = Array.from(document.querySelectorAll('button'));
let currentInput = '0'; // 初期値を'0'に設定
let previousInput = '';
let operator = '';
let resultDisplayed = false;

// ボタンのクリックイベントを処理する関数
function handleButtonClick(value) {
    try {
        if (value === 'AC') {
            currentInput = '0';
            previousInput = '';
            operator = '';
            currentInputDisplay.innerText = '0';
            previousInputDisplay.innerText = '';
        } else if (value === 'C') {
            currentInput = '0';
            currentInputDisplay.innerText = '0';
        } else if (value === '%') {
            currentInput = (parseFloat(currentInput) / 100).toString();
            currentInputDisplay.innerText = currentInput;
        } else if (value === '00') {
            // currentInputが '0' または空文字の場合は何もしない
            if (currentInput !== '0' && currentInput !== '') {
                currentInput += '00';
                currentInputDisplay.innerText = currentInput;
            }
        } else if (value === '.') {
            // 小数点が既に含まれている場合は何もしない
            if (!currentInput.includes('.')) {
                if (currentInput === '0') {
                    currentInput = '0.';
                } else {
                    currentInput += '.';
                }
                currentInputDisplay.innerText = currentInput;
            }
        } else if (['+', '-', '*', '/'].includes(value)) {
            if (operator && previousInput && currentInput) {
                const result = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInput} = ${result}`);
                previousInput = result;
                currentInput = '';
            } else if (!previousInput) {
                previousInput = currentInput;
                currentInput = '';
            }
            operator = value;
            resultDisplayed = false;
            currentInputDisplay.innerText = previousInput + ' ' + operator;
            previousInputDisplay.innerText = '';
        } else if (value === '=') {
            if (operator && previousInput && currentInput) {
                currentInput = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInputDisplay.innerText} = ${currentInput}`);
                previousInputDisplay.innerText = '';
                currentInputDisplay.innerText = currentInput;
                previousInput = '';
                operator = '';
                resultDisplayed = true;
            }
        } else {
            if (resultDisplayed) {
                currentInput = value;
                resultDisplayed = false;
            } else {
                if (currentInput === '0') {
                    currentInput = value;
                } else {
                    currentInput += value;
                }
            }
            currentInputDisplay.innerText = currentInput;

            // オペレーターがある場合は上の行に移動
            if (operator) {
                previousInputDisplay.innerText = previousInput + ' ' + operator;
            }
        }
    } catch (error) {
        currentInputDisplay.innerText = 'Error';
        previousInputDisplay.innerText = '';
        currentInput = '0';
        previousInput = '';
        operator = '';
    }
}

// 計算履歴を追加する関数
function addHistory(entry) {
    const li = document.createElement('li');
    li.classList.add('history-item');

    const span = document.createElement('span');
    span.classList.add('history-text');
    span.textContent = entry;

    const buttonsDiv = document.createElement('div');
    buttonsDiv.classList.add('history-buttons');

    const copyButton = document.createElement('button');
    copyButton.innerHTML = '📋'; // コピーアイコン
    copyButton.addEventListener('click', () => {
        navigator.clipboard.writeText(entry).then(() => {
            copyMessage.style.opacity = '1'; // メッセージを表示
            setTimeout(() => {
                copyMessage.style.opacity = '0'; // メッセージを隠す
            }, 2000);
        }).catch(err => {
            console.error('Error copying to clipboard: ', err);
        });
    });

    const deleteButton = document.createElement('button');
    deleteButton.innerHTML = '🗑️'; // ゴミ箱アイコン
    deleteButton.addEventListener('click', () => {
        historyList.removeChild(li);
    });

    buttonsDiv.appendChild(copyButton);
    buttonsDiv.appendChild(deleteButton);

    li.appendChild(span);
    li.appendChild(buttonsDiv);

    historyList.appendChild(li);
}

// ボタンのクリックイベントリスナーを追加
buttons.forEach(button => {
    button.addEventListener('click', () => handleButtonClick(button.innerText));
});

// キーボードのキー入力を処理するイベントリスナーを追加
document.addEventListener('keydown', (event) => {
    const key = event.key;
    if (key >= '0' && key <= '9') {
        handleButtonClick(key);
    } else if (key === '.') {
        handleButtonClick('.');
    } else if (key === '+') {
        handleButtonClick('+');
    } else if (key === '-') {
        handleButtonClick('-');
    } else if (key === '*') {
        handleButtonClick('*');
    } else if (key === '/') {
        handleButtonClick('/');
    } else if (key === 'Enter') {
        handleButtonClick('=');
    } else if (key === 'Escape') {
        handleButtonClick('AC');
    } else if (key === 'Backspace') {
        handleButtonClick('C');
    } else if (key === '%') {
        handleButtonClick('%');
    }
});

// クリップボードに結果をコピー
currentInputDisplay.addEventListener('click', () => {
    let textToCopy = currentInput;

    // 演算子を除外してコピー
    if (['+', '-', '*', '/'].includes(operator)) {
        textToCopy = currentInputDisplay.innerText.replace(/[+\-*/]/g, '').trim();
    }

    // クリップボードにコピー
    navigator.clipboard.writeText(textToCopy).then(() => {
        copyMessage.style.opacity = '1'; // メッセージを表示
        setTimeout(() => {
            copyMessage.style.opacity = '0'; // メッセージを隠す
        }, 2000);
    }).catch(err => {
        console.error('Error copying to clipboard: ', err);
    });
});

変更点の説明

  1. 履歴の追加:

    • 「+」、「-」、「*」、「/」が入力されたときに、現在の計算結果を履歴に追加するようにしました。

    • 「=」が入力されたときも同様に、現在の計算結果を履歴に追加します。

ChatGPT:
これにより、たとえば「100+200/4=75」の場合、「100+200=300」と「300/4=75」のように履歴に追加されます。


履歴から再入力できるように変更

ボク:
コピーボタンの動作を変更します。
コピーボタンを押した場合は、その行の結果を電卓に入力してください。
・電卓が結果が表示された後の場合は履歴の結果を電卓に入力。
・電卓が数値と「+, -, *, /」の場合は、演算子の後ろに履歴の結果を入力。

ChatGPT:
了解しました。コピーボタンの動作を変更し、コピーした結果を電卓に入力するようにします。

修正後の script.js

const previousInputDisplay = document.getElementById('previous-input');
const currentInputDisplay = document.getElementById('current-input');
const copyMessage = document.getElementById('copy-message');
const historyList = document.getElementById('history-list');
const buttons = Array.from(document.querySelectorAll('button'));
let currentInput = '0'; // 初期値を'0'に設定
let previousInput = '';
let operator = '';
let resultDisplayed = false;

// ボタンのクリックイベントを処理する関数
function handleButtonClick(value) {
    try {
        if (value === 'AC') {
            currentInput = '0';
            previousInput = '';
            operator = '';
            currentInputDisplay.innerText = '0';
            previousInputDisplay.innerText = '';
        } else if (value === 'C') {
            currentInput = '0';
            currentInputDisplay.innerText = '0';
        } else if (value === '%') {
            currentInput = (parseFloat(currentInput) / 100).toString();
            currentInputDisplay.innerText = currentInput;
        } else if (value === '00') {
            // currentInputが '0' または空文字の場合は何もしない
            if (currentInput !== '0' && currentInput !== '') {
                currentInput += '00';
                currentInputDisplay.innerText = currentInput;
            }
        } else if (value === '.') {
            // 小数点が既に含まれている場合は何もしない
            if (!currentInput.includes('.')) {
                if (currentInput === '0') {
                    currentInput = '0.';
                } else {
                    currentInput += '.';
                }
                currentInputDisplay.innerText = currentInput;
            }
        } else if (['+', '-', '*', '/'].includes(value)) {
            if (operator && previousInput && currentInput) {
                const result = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInput} = ${result}`);
                previousInput = result;
                currentInput = '';
            } else if (!previousInput) {
                previousInput = currentInput;
                currentInput = '';
            }
            operator = value;
            resultDisplayed = false;
            currentInputDisplay.innerText = previousInput + ' ' + operator;
            previousInputDisplay.innerText = '';
        } else if (value === '=') {
            if (operator && previousInput && currentInput) {
                currentInput = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInputDisplay.innerText} = ${currentInput}`);
                previousInputDisplay.innerText = '';
                currentInputDisplay.innerText = currentInput;
                previousInput = '';
                operator = '';
                resultDisplayed = true;
            }
        } else {
            if (resultDisplayed) {
                currentInput = value;
                resultDisplayed = false;
            } else {
                if (currentInput === '0') {
                    currentInput = value;
                } else {
                    currentInput += value;
                }
            }
            currentInputDisplay.innerText = currentInput;

            // オペレーターがある場合は上の行に移動
            if (operator) {
                previousInputDisplay.innerText = previousInput + ' ' + operator;
            }
        }
    } catch (error) {
        currentInputDisplay.innerText = 'Error';
        previousInputDisplay.innerText = '';
        currentInput = '0';
        previousInput = '';
        operator = '';
    }
}

// 計算履歴を追加する関数
function addHistory(entry) {
    const li = document.createElement('li');
    li.classList.add('history-item');

    const span = document.createElement('span');
    span.classList.add('history-text');
    span.textContent = entry;

    const buttonsDiv = document.createElement('div');
    buttonsDiv.classList.add('history-buttons');

    const copyButton = document.createElement('button');
    copyButton.innerHTML = '📋'; // コピーアイコン
    copyButton.addEventListener('click', () => {
        const result = entry.split('=')[1].trim();
        if (resultDisplayed || (!operator && currentInput === '0')) {
            currentInput = result;
        } else if (operator) {
            currentInput += result;
        } else {
            currentInput = result;
        }
        currentInputDisplay.innerText = currentInput;
    });

    const deleteButton = document.createElement('button');
    deleteButton.innerHTML = '🗑️'; // ゴミ箱アイコン
    deleteButton.addEventListener('click', () => {
        historyList.removeChild(li);
    });

    buttonsDiv.appendChild(copyButton);
    buttonsDiv.appendChild(deleteButton);

    li.appendChild(span);
    li.appendChild(buttonsDiv);

    historyList.appendChild(li);
}

// ボタンのクリックイベントリスナーを追加
buttons.forEach(button => {
    button.addEventListener('click', () => handleButtonClick(button.innerText));
});

// キーボードのキー入力を処理するイベントリスナーを追加
document.addEventListener('keydown', (event) => {
    const key = event.key;
    if (key >= '0' && key <= '9') {
        handleButtonClick(key);
    } else if (key === '.') {
        handleButtonClick('.');
    } else if (key === '+') {
        handleButtonClick('+');
    } else if (key === '-') {
        handleButtonClick('-');
    } else if (key === '*') {
        handleButtonClick('*');
    } else if (key === '/') {
        handleButtonClick('/');
    } else if (key === 'Enter') {
        handleButtonClick('=');
    } else if (key === 'Escape') {
        handleButtonClick('AC');
    } else if (key === 'Backspace') {
        handleButtonClick('C');
    } else if (key === '%') {
        handleButtonClick('%');
    }
});

// クリップボードに結果をコピー
currentInputDisplay.addEventListener('click', () => {
    let textToCopy = currentInput;

    // 演算子を除外してコピー
    if (['+', '-', '*', '/'].includes(operator)) {
        textToCopy = currentInputDisplay.innerText.replace(/[+\-*/]/g, '').trim();
    }

    // クリップボードにコピー
    navigator.clipboard.writeText(textToCopy).then(() => {
        copyMessage.style.opacity = '1'; // メッセージを表示
        setTimeout(() => {
            copyMessage.style.opacity = '0'; // メッセージを隠す
        }, 2000);
    }).catch(err => {
        console.error('Error copying to clipboard: ', err);
    });
});

変更点の説明

  1. コピーアイコンの動作変更:

    • コピーアイコンをクリックした際に、その履歴行の結果を電卓に入力します。

    • resultDisplayed が true または現在の入力が「0」である場合は、コピーした結果を新しい入力として設定します。

    • 現在の入力に演算子が含まれている場合は、演算子の後にコピーした結果を追加します。

ChatGPT:
これにより、履歴の結果を電卓に簡単に再入力できるようになります。

ボク:
良好です。

一か所、変更したいです。
電卓が数字プラス演算子の時に履歴のコピーボタンが押された場合、数字プラス演算子を電卓の上の行に表示したいです。

ChatGPT:
了解しました。電卓が数字プラス演算子の状態で履歴のコピーボタンが押された場合、その数字プラス演算子を電卓の上の行に表示するように修正します。

修正後の script.js

const previousInputDisplay = document.getElementById('previous-input');
const currentInputDisplay = document.getElementById('current-input');
const copyMessage = document.getElementById('copy-message');
const historyList = document.getElementById('history-list');
const buttons = Array.from(document.querySelectorAll('button'));
let currentInput = '0'; // 初期値を'0'に設定
let previousInput = '';
let operator = '';
let resultDisplayed = false;

// ボタンのクリックイベントを処理する関数
function handleButtonClick(value) {
    try {
        if (value === 'AC') {
            currentInput = '0';
            previousInput = '';
            operator = '';
            currentInputDisplay.innerText = '0';
            previousInputDisplay.innerText = '';
        } else if (value === 'C') {
            currentInput = '0';
            currentInputDisplay.innerText = '0';
        } else if (value === '%') {
            currentInput = (parseFloat(currentInput) / 100).toString();
            currentInputDisplay.innerText = currentInput;
        } else if (value === '00') {
            // currentInputが '0' または空文字の場合は何もしない
            if (currentInput !== '0' && currentInput !== '') {
                currentInput += '00';
                currentInputDisplay.innerText = currentInput;
            }
        } else if (value === '.') {
            // 小数点が既に含まれている場合は何もしない
            if (!currentInput.includes('.')) {
                if (currentInput === '0') {
                    currentInput = '0.';
                } else {
                    currentInput += '.';
                }
                currentInputDisplay.innerText = currentInput;
            }
        } else if (['+', '-', '*', '/'].includes(value)) {
            if (operator && previousInput && currentInput) {
                const result = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInput} = ${result}`);
                previousInput = result;
                currentInput = '';
            } else if (!previousInput) {
                previousInput = currentInput;
                currentInput = '';
            }
            operator = value;
            resultDisplayed = false;
            currentInputDisplay.innerText = previousInput + ' ' + operator;
            previousInputDisplay.innerText = '';
        } else if (value === '=') {
            if (operator && previousInput && currentInput) {
                currentInput = eval(`${previousInput} ${operator} ${currentInput}`).toString();
                addHistory(`${previousInput} ${operator} ${currentInputDisplay.innerText} = ${currentInput}`);
                previousInputDisplay.innerText = '';
                currentInputDisplay.innerText = currentInput;
                previousInput = '';
                operator = '';
                resultDisplayed = true;
            }
        } else {
            if (resultDisplayed) {
                currentInput = value;
                resultDisplayed = false;
            } else {
                if (currentInput === '0') {
                    currentInput = value;
                } else {
                    currentInput += value;
                }
            }
            currentInputDisplay.innerText = currentInput;

            // オペレーターがある場合は上の行に移動
            if (operator) {
                previousInputDisplay.innerText = previousInput + ' ' + operator;
            }
        }
    } catch (error) {
        currentInputDisplay.innerText = 'Error';
        previousInputDisplay.innerText = '';
        currentInput = '0';
        previousInput = '';
        operator = '';
    }
}

// 計算履歴を追加する関数
function addHistory(entry) {
    const li = document.createElement('li');
    li.classList.add('history-item');

    const span = document.createElement('span');
    span.classList.add('history-text');
    span.textContent = entry;

    const buttonsDiv = document.createElement('div');
    buttonsDiv.classList.add('history-buttons');

    const copyButton = document.createElement('button');
    copyButton.innerHTML = '📋'; // コピーアイコン
    copyButton.addEventListener('click', () => {
        const result = entry.split('=')[1].trim();
        if (resultDisplayed || (!operator && currentInput === '0')) {
            currentInput = result;
        } else if (operator) {
            previousInputDisplay.innerText = currentInputDisplay.innerText;
            currentInput = result;
        } else {
            currentInput = result;
        }
        currentInputDisplay.innerText = currentInput;
    });

    const deleteButton = document.createElement('button');
    deleteButton.innerHTML = '🗑️'; // ゴミ箱アイコン
    deleteButton.addEventListener('click', () => {
        historyList.removeChild(li);
    });

    buttonsDiv.appendChild(copyButton);
    buttonsDiv.appendChild(deleteButton);

    li.appendChild(span);
    li.appendChild(buttonsDiv);

    historyList.appendChild(li);
}

// ボタンのクリックイベントリスナーを追加
buttons.forEach(button => {
    button.addEventListener('click', () => handleButtonClick(button.innerText));
});

// キーボードのキー入力を処理するイベントリスナーを追加
document.addEventListener('keydown', (event) => {
    const key = event.key;
    if (key >= '0' && key <= '9') {
        handleButtonClick(key);
    } else if (key === '.') {
        handleButtonClick('.');
    } else if (key === '+') {
        handleButtonClick('+');
    } else if (key === '-') {
        handleButtonClick('-');
    } else if (key === '*') {
        handleButtonClick('*');
    } else if (key === '/') {
        handleButtonClick('/');
    } else if (key === 'Enter') {
        handleButtonClick('=');
    } else if (key === 'Escape') {
        handleButtonClick('AC');
    } else if (key === 'Backspace') {
        handleButtonClick('C');
    } else if (key === '%') {
        handleButtonClick('%');
    }
});

// クリップボードに結果をコピー
currentInputDisplay.addEventListener('click', () => {
    let textToCopy = currentInput;

    // 演算子を除外してコピー
    if (['+', '-', '*', '/'].includes(operator)) {
        textToCopy = currentInputDisplay.innerText.replace(/[+\-*/]/g, '').trim();
    }

    // クリップボードにコピー
    navigator.clipboard.writeText(textToCopy).then(() => {
        copyMessage.style.opacity = '1'; // メッセージを表示
        setTimeout(() => {
            copyMessage.style.opacity = '0'; // メッセージを隠す
        }, 2000);
    }).catch(err => {
        console.error('Error copying to clipboard: ', err);
    });
});

変更点の説明

  1. コピーボタンの動作変更:

    • 現在の入力に演算子が含まれている場合、その演算子を含む数字を電卓の上の行に表示し、履歴の結果を新しい入力として設定します。

ChatGPT:
これにより、電卓が数字プラス演算子の状態で履歴のコピーボタンが押された場合、その数字プラス演算子を電卓の上の行に表示し、履歴の結果を電卓に入力するようになります。

不具合修正

ボク:
いっけん上手くいっていますが、次のような結果になりました。

「100+200/4=75」と計算し、履歴1行目「100+200=300」のコピーボタンを押す。そうすると電卓の下の行に「300」と表示される。さらに「/」を入力し、「2」を入力。最後に「=」を押すと、履歴には「300/2=150」と表示されますが、電卓の下の行は「300」と表示されます。

履歴はちゃんと記録されていますが、電卓の表示が間違っているようです。

ChatGPT:
ご指摘ありがとうございます。handleButtonClick 関数での演算子入力時や結果計算時の currentInputDisplay と previousInputDisplay の更新が適切に行われるように修正します。また、履歴からコピーした結果の処理も見直します。

修正後の script.js

……(中略)……

変更点の説明

  1. handleButtonClick 関数の修正:

    • 演算子が入力された場合、演算結果を計算し、previousInputDisplay と currentInputDisplay を正しく更新します。

    • = が入力された場合も、同様に演算結果を計算し、ディスプレイを更新します。

  2. 履歴の結果を電卓に入力する処理の修正:

    • 履歴のコピーボタンを押した場合の処理を修正し、現在の入力に演算子が含まれている場合には、上の行にその演算子を含む数字を表示し、結果を新しい入力として設定します。

ChatGPT:
これにより、履歴の結果をコピーした際に電卓の表示が正しく更新されるようになります。

ここまでの成果画面

ボク:
うおおお、ちゃんと動きました!すごい。

見た目の調整、その他

レイアウトの調整

機能は実装できたので、ここから最後のレイアウト調整を……と思っていたのですが、ここからが大変でした。
指示を出して、ChatGPTが修正という繰り返しで、一見上手くいっているように見えますが、後半に行くにしたがって、1つ直すと他がずれたり、指示してないところが変更されてたり……。堂々巡りでいつまでたっても完成しない……。

ボク:
あとは履歴の表示サイズと電卓の表示サイズのバランスを調整したいです。 画面が縦の場合は今のままでよいので、横の場合を、電卓より履歴が横に広くなるように調節してください。

ChatGPT:
了解しました。画面が横向きの場合に、履歴の表示が電卓よりも横に広くなるように調整します。

修正後の styles.css

……(中略)……

変更点の説明

  1. 横向き表示のバランス調整:

    • #history の flex プロパティを 3 に設定し、横向き表示時に履歴の幅を広くしました。

    • #calculator の flex プロパティを 2 に設定し、横向き表示時に電卓の幅を狭くしました。

ChatGPT:
この変更により、画面が横向きの場合に履歴が電卓よりも横に広く表示されるようになります。縦向きの場合は従来通りのバランスが保たれます。

ボク:
電卓部分の幅と高さの最大値を、
画面が横の時は幅400px、高さ700px、
縦の時は幅400px、高さ640px
に固定することはできますか?

ChatGPT:
もちろん、電卓部分の幅と高さの最大値を指定のサイズに固定するようにCSSを調整します。

ボク:
ボタンがピッタリ下まであると打ちづらいので、上の表示部分の高さを少し削って、一番下に10pxくらい余白を足してください。

ChatGPT:
了解しました。上の表示部分の高さを少し削り、一番下に10pxの余白を追加します。

ボク:
画面を縦にした時に、ボタンの表示領域の縦が足りなくて、ボタンがはみ出してスクロールしてしまいます。

ChatGPT:
了解しました。ボタンがはみ出してスクロールしないように、縦画面での電卓の高さを調整します。表示部分の高さをもう少し削り、ボタンの表示領域を確保します。

ボク:
たいてい、iPadなどで表示すると、上にバーがある分、表示領域が狭くなるのではないでしょうか。履歴の分を減らして、電卓がもう少し上に表示されるようにしてください。

ChatGPT:
了解しました。履歴の表示領域を減らし、電卓がもう少し上に表示されるように調整します。

ボク:
電卓部分の横幅は最大が400pxですが、表示領域の幅が400px未満の場合は表示領域いっぱいに、表示領域の幅が400px以上の場合は400pxになるようにしてください。 電卓の数値表示部分はなるべく固定の高さにしてください。

ChatGPT:
了解しました。電卓部分の横幅を最大400pxにしつつ、表示領域が400px未満の場合には表示領域いっぱいにするように調整します。また、電卓の数値表示部分の高さを固定します。

ボク:
電卓部分の高さは縦画面でも横画面でも最大600pxとし、電卓部分の下に高さが最低40pxの空白スペースを入れてください。このスペースの高さは、縦画面の場合は40px、横画面の場合は最低40pxで、必要があれば40px以上になります。

ChatGPT:
了解しました。電卓部分の高さを縦画面でも横画面でも最大600pxに設定し、電卓部分の下に高さが最低40pxの空白スペースを追加します。横画面の場合、この空白スペースは最低40pxで、必要に応じて40px以上の高さになります。

ボク:
まず先ほど一番下に追加した10pxを削除。 今回追加した#spacerが反映されていないので、反映されるように修正をお願いします。

ChatGPT:
了解しました。先ほど追加した10pxの余白を削除し、#spacer が反映されるように修正します。

ボク:
今回追加した#spacer要素が、横画面の時に電卓の下ではなく、横に表示されます。

ChatGPT:
#spacerが電卓の下に表示されるように修正します。#spacer 要素が電卓の下に常に表示されるようにするために、#calculator と #spacer をコンテナ要素内で一緒に管理します。

ボク:
iPadで表示した時に、電卓部分の高さが、縦画面だと560px、横画面だと524pxで表示されるのはどうしてでしょうか。

ChatGPT:
電卓部分の高さが指定した最大高さ(600px)よりも小さく表示される理由は、CSSのflexプロパティと高さの制約が影響している可能性があります。特に、flexコンテナの高さが親要素の高さを超えないように調整されるためです。

・flex: 1.3やflex: 1の指定が、電卓部分の高さに影響を与えている可能性があります。
・#container、#calculator-container、#calculatorのflex設定を見直して、電卓部分の高さが適切に600pxまで表示されるように調整します。

また、min-heightの指定を利用して、電卓部分の高さを適切に確保します。

ボク:
うーん、さらにボタン部分が低くなりました。 表示部分、コピーメッセージ部分、ボタン部分を合わせて高さ600px、スペーサー部分を高さ40px。を基本として、画面の高さが足りないときはボタン部分の高さを低くしたいのですが。

ChatGPT:
了解しました。表示部分、コピーメッセージ部分、ボタン部分を合わせて高さ600pxにし、スペーサー部分を高さ40pxに設定します。画面の高さが足りない場合はボタン部分の高さを調整します。

ボク:
なんかうまくいっていません。
まず、電卓部分(表示部分、コピーメッセージ部分、ボタン部分)のサイズを幅400px、高さ600pxに固定しましょう。電卓部分の下には幅400px、高さ最低40pxのスペーサーがあります。

ChatGPT:
了解しました。電卓部分(表示部分、コピーメッセージ部分、ボタン部分)のサイズを幅400px、高さ600pxに固定し、電卓部分の下に幅400px、高さ最低40pxのスペーサーを追加します。

ボク:
script.jsにバグがあるようです。一度、内容を確認してください。

ChatGPT:
script.js の内容を確認し、電卓の機能が正しく動作するように修正します。以下は、基本的な電卓機能とコピー機能を備えた script.js の修正版です。

そして後編へ……

ここまでぐるぐるとやり取りをして力尽きました。

後編では、新規にChatGPTを開き、ここまでに完成したファイルを読み込んで仕切り直しした様子をお伝えします。


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?