Demo
การสร้างกล่องข้อความบน Google Apps Script Web App + Bootstrap 5.3 + Google Sheet
โดยใช้ Rich Text Editor ตัวเดียวเป็นแกนหลัก แล้วเสริมเครื่องมือวิชาการเข้าไป
ผมสรุปแนวทางที่ “เหมาะกับ GAS มากที่สุด” ให้เป็นระบบนะครับ 👇
🔧 โครงสร้างที่แนะนำ (ภาพรวม)
🧠 Rich Text Editor ที่เหมาะสุด: TinyMCE 6
เหตุผล:
- ใช้งานง่าย
- รองรับ Bootstrap ดี
- เก็บข้อมูลเป็น HTML → บันทึกลง Google Sheet ได้ตรง ๆ
- มี Plugin ครบตามที่คุณต้องการ
✅ ความสามารถที่คุณต้องการ (เช็กครบทุกข้อ)
✍️ 1. จัดรูปแบบตัวอักษร
- สีตัวอักษร (กำหนดเฉพาะ: ดำ แดง น้ำเงิน เขียว เหลือง)
- ตัวหนา / ตัวเอียง
- จัดซ้าย / กลาง / ขวา
สามารถล็อกสีได้แบบนี้:
🖼️ 2. แทรกรูป
✔ จาก URL
TinyMCE รองรับโดยตรง
✔ อัปโหลด → Google Drive
ใช้ images_upload_handler
อัปโหลด → Apps Script → Drive → คืน URL
โฟลว์:
✔ วิธีนี้ นิยมมากใน GAS Web App
🔗 3. แทรกลิงก์
- ลิงก์ภายใน
- ลิงก์ภายนอก
- เปิดแท็บใหม่ (
target="_blank")
📊 4. แทรกตาราง
✔ TinyMCE ทำได้สมบูรณ์
สามารถเพิ่ม/ลบ แถว คอลัมน์ รวมเซลล์ ได้หมด
🧮 5. เครื่องมือคณิต / ฟิสิกส์ / เคมี (จุดสำคัญ)
แนะนำ ใช้ 2 ตัวร่วมกัน
🔹 A. MathLive (พิมพ์สมการ)
- มี virtual keyboard
- พิมพ์สูตรได้เหมือน Word / LaTeX
- เหมาะกับครู / นักเรียนมาก
🔹 B. MathJax (แสดงผล)
- Render LaTeX → สวย คม
รองรับ:
คณิตศาสตร์- ฟิสิกส์
- เคมี (
mhchem)
ตัวอย่าง:
✔ เก็บใน Sheet เป็น HTML + LaTeX ได้เลย
💾 การบันทึกลง Google Sheet (Best Practice)
- บันทึก content ของ TinyMCE เป็น HTML
- เวลาแสดงผล → ใช้
innerHTML - MathJax จะ render อัตโนมัติ
🏆 สรุป Stack ที่ “ดีที่สุด” สำหรับงานนี้
| ความต้องการ | เครื่องมือ |
|---|---|
| Rich Text | TinyMCE 6 |
| รูป / ตาราง / ลิงก์ | TinyMCE Plugins |
| สมการ | MathLive |
| Render สูตร | MathJax |
| เก็บข้อมูล | Google Sheet |
| รูป | Google Drive |
| UI | Bootstrap 5.3 |
ตัวอย่าง HTML + JS เต็มชุด (พร้อมใช้งานใน GAS)
code.gs
const CONFIG = {
SHEET_NAME: 'data',
DRIVE_FOLDER_ID: 'xxxxxxxxxxxxxxxxx' //'PUT_YOUR_FOLDER_ID_HERE'
};
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Rich Text Editor')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/* ===============================
INIT
================================ */
function initApp() {
const ss = SpreadsheetApp.getActive();
let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(CONFIG.SHEET_NAME);
sheet.appendRow(['timestamp', 'content_html']);
}
return { success: true };
}
/* ===============================
SAVE CONTENT
================================ */
function saveContent(html) {
const sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.SHEET_NAME);
sheet.appendRow([new Date(), html]);
return { success: true };
}
/* ===============================
IMAGE UPLOAD
================================ */
function uploadImage(base64, filename) {
const blob = Utilities.newBlob(
Utilities.base64Decode(base64),
"image/png",
filename
);
const file = DriveApp.getFolderById(CONFIG.DRIVE_FOLDER_ID).createFile(blob);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
const fileId = file.getId();
const urlImg = 'https://lh3.googleusercontent.com/d/' + fileId;
return urlImg;
}
index.html
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<base target="_top">
<!-- Bootstrap 5.3 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- TinyMCE -->
<script src="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js"></script>
<!-- MathLive -->
<link rel="stylesheet" href="https://unpkg.com/mathlive/dist/mathlive.core.css">
<script src="https://unpkg.com/mathlive"></script>
<!-- MathJax -->
<script>
window.MathJax = {
tex: {
inlineMath: [['\\(', '\\)'], ['$', '$']],
displayMath: [['$$', '$$']]
},
svg: { fontCache: 'global' }
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<!-- Style CSS -->
<style>
#previewBox {
border: 1px solid #ddd;
padding: 15px;
min-height: 200px;
background: #fafafa;
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<h4 class="mb-3">📝 Rich Text + Equation Editor</h4>
<textarea id="editor"></textarea>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-secondary" onclick="preview()">👁 Preview</button>
<button class="btn btn-primary" onclick="save()">💾 Save</button>
</div>
<h5 class="mt-4">Preview</h5>
<div id="previewBox"></div>
</div>
<script>
/* ===============================
INIT
================================ */
google.script.run.initApp();
/* ===============================
TINYMCE
================================ */
tinymce.init({
selector: '#editor',
height: 400,
plugins: 'image link table code',
toolbar:
'undo redo | bold italic | alignleft aligncenter alignright | ' +
'forecolor | link image table | mathlive | code',
forecolor_map: [
'#000000', 'ดำ',
'#FF0000', 'แดง',
'#0000FF', 'น้ำเงิน',
'#008000', 'เขียว',
'#FFD700', 'เหลือง'
],
setup: function (editor) {
editor.ui.registry.addButton('mathlive', {
text: '∑ Equation',
onAction: () => insertMath(editor)
});
},
images_upload_handler: (blobInfo) => {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
google.script.run.withSuccessHandler(url => {
resolve(url);
}).uploadImage(reader.result.split(',')[1], blobInfo.filename());
};
reader.readAsDataURL(blobInfo.blob());
});
}
});
/* ===============================
MATHLIVE
================================ */
function insertMath(editor) {
const mathfield = document.createElement('math-field');
mathfield.style.width = '100%';
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5>Insert Equation</h5>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button class="btn btn-primary">Insert</button>
</div>
</div>
</div>`;
modal.querySelector('.modal-body').appendChild(mathfield);
modal.querySelector('button').onclick = () => {
editor.insertContent(`\\(${mathfield.value}\\)`);
bootstrap.Modal.getInstance(modal).hide();
};
document.body.appendChild(modal);
new bootstrap.Modal(modal).show();
}
/* ===============================
PREVIEW
================================ */
function preview() {
const html = tinymce.get('editor').getContent();
const box = document.getElementById('previewBox');
// 1. แสดง HTML
box.innerHTML = html;
// 2. Render Equation
if (window.MathJax) {
MathJax.typesetClear([box]);
MathJax.typesetPromise([box]);
}
}
/* ===============================
SAVE
================================ */
function save() {
const html = tinymce.get('editor').getContent();
const box = document.getElementById('previewBox');
google.script.run.withSuccessHandler(() => {
alert('บันทึกเรียบร้อย');
tinymce.get('editor').setContent('');
box.innerHTML = html;
}).saveContent(html);
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
***********************************
หากถูกใจโปรดสนับสนุนเป็นกำลังใจกันด้วยนะครับ
สนับสนุนที่นี่