星期二, 2月 24, 2026

[Docs] Markdown 功能

Google Docs 內有 Markdown 功能可以使用,必須手動去開啟,路徑為 工具 => 偏好設定 => 啟用 Markdowm

開啟後右鍵選單上就會有 Markdown 功能

另外編輯時也支援 Markdown 語法輸入,詳見官方文章介紹 - 在 Google 文件、簡報和繪圖中使用 Markdown 

星期五, 2月 20, 2026

[GAS] 這個應用程式是由 Google Apps Script 的使用者建立

把 [網頁應用程式] 部屬出去執行後,在網頁上方匯出出現 [這個應用程式是由 Google Apps Script 的使用者建立] 安全性警告,這個沒有辦法直接移除,常見繞道隱藏方式為把網頁內嵌在 Google Site 內

直接拿 [JS] 基礎練習 UI 畫面來呈現
XFrameOptionsMode
  • ALLOWALL:No X-Frame-Options header will be set. This will let any site iframe the page, so the developer should implement their own protection against clickjacking.
  • DEFAULT:Sets the default value for the X-Frame-Options header, which preserves normal security assumptions. If a script does not set an X-Frame-Options mode, Apps Script uses this mode as the default.
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('圖書館館藏管理系統')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
一開始嚐試把 GAS 網頁應用程式內嵌在 Google Site 時,並沒有指定 ALLOWALL,但一樣可以正常顯示,大概是 DEFAULT 會把 Google 相關服務設定為白名單或信任服務之類,所以不需要特別指定,萬一是要把 GAS 網頁應用程式內嵌在其他平台,就一定要明確設定 ALLOWALL

GAS 警語隱藏是 Goolge Site 標誌,下面則是內崁的 GAS 網頁應用程式,已經不會出現官方制式警語   

星期三, 2月 18, 2026

[JS] 基礎練習

該筆記會在 GAS 上練習 js
  • 資料來源:陣列、物件概念
  • 基礎函示:push、filter 使用
  • DOM API:使用 API 來顯示資料,而非使用 innerHTML
  • addEventListener 註冊事件:DOMContentLoaded 和 Click 事件

gs code
/**
 * 處理 HTTP GET 請求
 * @return {HtmlOutput}
 */
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('圖書館館藏管理系統');
}

/**
 * 嵌入 HTML 檔案內容
 * @param {string} filename 檔案名稱
 * @return {string} 檔案內容
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>新增書籍館藏</h2>
    <div class="input-group">
      <select class="bookCategory">
        <option value="程式語言">程式語言</option>
        <option value="文學小說">文學小說</option>
        <option value="商業理財">商業理財</option>
      </select>
      <input type="text" placeholder="書籍名稱" class="bookTitle">
      <input type="text" placeholder="作者" class="bookAuthor">
      <input type="button" class="addBtn" value="加入館藏">
    </div>

    <h2>館藏查詢與篩選</h2>
    <div class="filterGroup">
      <input type="button" value="全部">
      <input type="button" value="程式語言">
      <input type="button" value="文學小說">
      <input type="button" value="商業理財">
    </div>

    <ul class="bookList"></ul>

    <?!= include('javascript'); ?>
  </body>
</html>

js.html
<script>
/**
 * 書籍測試資料
 */
let libraryData = [
  { title: "深入淺出 JavaScript", author: "Eric Freeman", category: "程式語言" },
  { title: "哈利波特", author: "J.K. Rowling", category: "文學小說" },
  { title: "原子習慣", author: "James Clear", category: "商業理財" },
  { title: "Clean Code", author: "Robert C. Martin", category: "程式語言" }
];

// 選取 DOM 元素
const bookList = document.querySelector('.bookList');
const bookTitle = document.querySelector('.bookTitle');
const bookAuthor = document.querySelector('.bookAuthor');
const bookCategory = document.querySelector('.bookCategory');
const addBtn = document.querySelector('.addBtn');
const filterGroup = document.querySelector('.filterGroup');

/**
 * 使用純 DOM API 產生書籍列表
 * @param {Array} data 欲顯示的書籍陣列
 */
function renderLibrary(data) {
  // 清空目前列表
  while (bookList.firstChild) {
    bookList.removeChild(bookList.firstChild);
  }

  // 建立新節點
  data.forEach(function(book) {
    const li = document.createElement('li');
    
    // 建立書籍資訊容器
    const infoSpan = document.createElement('span');
    infoSpan.textContent = `[${book.category}] 《${book.title}》 - 作者:${book.author}`;
    
    li.appendChild(infoSpan);
    bookList.appendChild(li);
  });
}

/**
 * 新增書籍
 */
addBtn.addEventListener('click', function() {
  const title = bookTitle.value.trim();
  const author = bookAuthor.value.trim();
  const category = bookCategory.value;

  if (title === "" || author === "") {
    alert("請完整填寫書名與作者!");
    return;
  }

  const newBook = {
    title: title,
    author: author,
    category: category
  };

  libraryData.push(newBook);
  
  // 重新渲染並清空輸入框
  renderLibrary(libraryData);
  bookTitle.value = "";
  bookAuthor.value = "";
});

/**
 * 篩選資料功能
 */
filterGroup.addEventListener('click', function(e) {
  if (e.target.type !== "button") return;

  const filterType = e.target.value;
  let result;

  if (filterType === "全部") {
    result = libraryData;
  } else {
    result = libraryData.filter(function(book) {
      return book.category === filterType;
    });
  }

  renderLibrary(result);
});

/**
 * 初始化頁面
 */
document.addEventListener('DOMContentLoaded', function() {
  renderLibrary(libraryData);
});

</script>

星期一, 2月 09, 2026

[GAS] Library - 最新程式碼快照 (開發人員模式)

[GAS] 資料庫 (Library) 筆記時有提到版本內有 [最新程式碼快照 (開發人員模式)] 可以使用,但實際應用時發現,原來要存取 [最新程式碼快照 (開發人員模式)] 必須具備 [編輯者] 權限,當初分享 library 給別人使用,因為只授予 [檢視者],就無法存取

星期四, 2月 05, 2026

[GAS] Slide 合併資料

Slide 常見應用場景,在一個範本 Slide 上設定多個 {{文字說明}} 符號,並從 Google Sheet 內抓取資料後,根據實務需求取代 {{文字說明}},該筆記以課程結業證書為例筆記

Slide 範本

gs Code

把 [課程結業證書範本 Slide] 複製至新位置,該筆記是以根目錄內的日期資料夾 (EX:2026-02-05) 去,並把學員資料透過 replaceAllText() 更新至 Slide {{文字說明}}

{{文字說明}} 基本上在 Slide 內是唯一的識別文字就行,前後有沒有符號包起來不是重點,但既然官方教學都使用 {{}} 包起來,就延續該風格囉,Slide 內有三個文字說明,分別為 {{學員姓名}}、{{課程名稱}}、{{日期}}

replaceAllText(findText, replaceText) 函式
  • findText:以該筆記為例,是指 {{文字說明}}
  • replaceText:取代 {{文字說明}} 的內容
function SlideMergeData() {

  const presentation = SlidesApp.getActivePresentation();
  const timeZone = Session.getScriptTimeZone();
  const dateString = Utilities.formatDate(new Date(), timeZone, "yyyy-MM-dd");

  // 設定根 FolderID
  const rootFolderId = GetFolderID();
  const rootFolder = DriveApp.getFolderById(rootFolderId);

  // 檢查並刪除同名的日期資料夾
  const existingFolders = rootFolder.getFoldersByName(dateString);
  while (existingFolders.hasNext()) {
    const oldFolder = existingFolders.next();
    oldFolder.setTrashed(true); // 將舊資料夾移至垃圾桶
  }

  // 建立日期資料夾
  const dateFolder = rootFolder.createFolder(dateString);

  // 複製範本 Slide 來使用
  const templateFile = DriveApp.getFileById(presentation.getId());
  const studentName = "王小強";
  const copyFile = templateFile.makeCopy(studentName, dateFolder);

  // 開啟新 Slide 並進行文字替換
  const newPresentation = SlidesApp.openById(copyFile.getId());
  newPresentation.replaceAllText('{{學員姓名}}', studentName);
  newPresentation.replaceAllText('{{課程名稱}}', "Google Apps Script");
  newPresentation.replaceAllText('{{日期}}', dateString);
  newPresentation.saveAndClose();
}

執行結果

該筆記只有使用 replaceAllText() 取代文字,還有兩個主題分別為
  1. replaceAllShapesWithImage 取代圖片
  2. BatchUpdate:有效能考量使用,需要開啟 Google Drive API 服務
目前沒有使用到,單純紀錄