สร้างกล่องข้อความ + Rich Text Editor

 


Demo


การสร้างกล่องข้อความบน Google Apps Script Web App + Bootstrap 5.3 + Google Sheet
โดยใช้ Rich Text Editor ตัวเดียวเป็นแกนหลัก แล้วเสริมเครื่องมือวิชาการเข้าไป

ผมสรุปแนวทางที่ “เหมาะกับ GAS มากที่สุด” ให้เป็นระบบนะครับ 👇


🔧 โครงสร้างที่แนะนำ (ภาพรวม)

Bootstrap 5.3 → UI TinyMCE → Rich Text Editor หลัก MathLive → พิมพ์สมการ (คณิต / ฟิสิกส์ / เคมี) MathJax → แสดงผลสมการ Google Drive → เก็บรูปที่อัปโหลด Google Sheet → เก็บ HTML Content

🧠 Rich Text Editor ที่เหมาะสุด: TinyMCE 6

เหตุผล:

  • ใช้งานง่าย
  • รองรับ Bootstrap ดี
  • เก็บข้อมูลเป็น HTML → บันทึกลง Google Sheet ได้ตรง ๆ
  • มี Plugin ครบตามที่คุณต้องการ


✅ ความสามารถที่คุณต้องการ (เช็กครบทุกข้อ)

✍️ 1. จัดรูปแบบตัวอักษร

  • สีตัวอักษร (กำหนดเฉพาะ: ดำ แดง น้ำเงิน เขียว เหลือง)
  • ตัวหนา / ตัวเอียง
  • จัดซ้าย / กลาง / ขวา
toolbar: 'bold italic forecolor alignleft aligncenter alignright'


สามารถล็อกสีได้แบบนี้:

color_map: [ "000000", "ดำ", "ff0000", "แดง", "0000ff", "น้ำเงิน", "008000", "เขียว", "ffff00", "เหลือง" ]

🖼️ 2. แทรกรูป

✔ จาก URL

TinyMCE รองรับโดยตรง

✔ อัปโหลด → Google Drive

ใช้ images_upload_handler
อัปโหลด → Apps Script → Drive → คืน URL

โฟลว์:

TinyMCE → base64 image → GAS → Drive → public URL → insert


✔ วิธีนี้ นิยมมากใน GAS Web App


🔗 3. แทรกลิงก์

  • ลิงก์ภายใน
  • ลิงก์ภายนอก
  • เปิดแท็บใหม่ (target="_blank")

plugins: 'link', link_target_list: [ {title: 'หน้าปัจจุบัน', value: '_self'}, {title: 'หน้าต่างใหม่', value: '_blank'} ]

📊 4. แทรกตาราง

✔ TinyMCE ทำได้สมบูรณ์

plugins: 'table', toolbar: 'table'


สามารถเพิ่ม/ลบ แถว คอลัมน์ รวมเซลล์ ได้หมด


🧮 5. เครื่องมือคณิต / ฟิสิกส์ / เคมี (จุดสำคัญ)

แนะนำ ใช้ 2 ตัวร่วมกัน

🔹 A. MathLive (พิมพ์สมการ)

  • มี virtual keyboard
  • พิมพ์สูตรได้เหมือน Word / LaTeX
  • เหมาะกับครู / นักเรียนมาก

🔹 B. MathJax (แสดงผล)

  • Render LaTeX → สวย คม
    • รองรับ:

      คณิตศาสตร์
    • ฟิสิกส์
    • เคมี (mhchem)

ตัวอย่าง:

\frac{a^2+b^2}{c^2} \vec{F}=m\vec{a} \ce{H2SO4 + 2NaOH -> Na2SO4 + 2H2O}


✔ เก็บใน Sheet เป็น HTML + LaTeX ได้เลย


💾 การบันทึกลง Google Sheet (Best Practice)

  • บันทึก content ของ TinyMCE เป็น HTML
tinymce.get("editor").getContent()

  • เวลาแสดงผล → ใช้ innerHTML
  • MathJax จะ render อัตโนมัติ


🏆 สรุป Stack ที่ “ดีที่สุด” สำหรับงานนี้

ความต้องการเครื่องมือ
Rich TextTinyMCE 6
รูป / ตาราง / ลิงก์TinyMCE Plugins
สมการMathLive
Render สูตรMathJax
เก็บข้อมูลGoogle Sheet
รูปGoogle Drive
UIBootstrap 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>

    

    



***********************************

หากถูกใจโปรดสนับสนุนเป็นกำลังใจกันด้วยนะครับ

สนับสนุนที่นี่

 

แสดงความคิดเห็น (0)
ใหม่กว่า เก่ากว่า